| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:typed_data'; |
| |
| import 'package:fidl_fuchsia_xi_session/fidl_async.dart' as fidl_xi_session; |
| import 'package:fuchsia_modular/agent.dart'; |
| import 'package:fuchsia_logger/logger.dart'; |
| import 'package:zircon/zircon.dart'; |
| |
| import 'package:xi_fuchsia_client/client.dart'; |
| import 'package:xi_client/client.dart'; |
| |
| /// ignore_for_file: avoid_annotating_with_dynamic |
| |
| class PendingNotification { |
| final String method; |
| final dynamic params; |
| PendingNotification(this.method, this.params); |
| } |
| |
| class XiSessionManagerImpl extends fidl_xi_session.XiSessionManager |
| implements XiRpcHandler { |
| int _sessionId = 0; |
| final XiFuchsiaClient xiCore; |
| |
| /// A map of session identifiers to view identifiers. These are sessions |
| /// that a client can connect to. |
| final Map<String, String> _availableSessions = {}; |
| |
| /// A map of in-use sessions to view identifiers. |
| final Map<String, String> _activeSessions = {}; |
| |
| /// A map of core view identifiers to active connections |
| final Map<String, ViewForwarder> _activeConnections = {}; |
| final Map<String, List<PendingNotification>> _pending = {}; |
| |
| XiSessionManagerImpl() : xiCore = XiFuchsiaClient(null) { |
| xiCore.handler = this; |
| xiCore.init().then((_) => xiCore.sendNotification('client_started', {})); |
| } |
| |
| @override |
| Future<String> newSession() async { |
| int id = _sessionId++; |
| String idString = 'session-$id'; |
| //ignore: unawaited_futures |
| xiCore.sendRpc('new_view', {}).then((viewId) { |
| _availableSessions[idString] = viewId; |
| log.info('associated session $idString with view $viewId'); |
| }); |
| return idString; |
| } |
| |
| @override |
| Future<void> closeSession(String sessionId) async { |
| final viewId = _activeSessions.remove(sessionId); |
| if (viewId == null) { |
| log.warning('attempted to close unknown session $sessionId'); |
| } |
| _activeConnections.remove(viewId); |
| await xiCore.sendRpc('close_view', {'view_id': viewId}); |
| } |
| |
| @override |
| Future<void> connectSession(String sessionId, Socket socket) async { |
| final viewId = _availableSessions.remove(sessionId); |
| if (viewId == null) { |
| log.warning('attempted to connect to unknown session $sessionId'); |
| } |
| //ignore: unawaited_futures |
| final forwarder = ViewForwarder(socket, viewId, xiCore)..init(); |
| _activeConnections[viewId] = forwarder; |
| _activeSessions[sessionId] = viewId; |
| log.info('connecting session $sessionId to view $viewId'); |
| final pending = _pending.remove(viewId); |
| if (pending != null) { |
| for (var notif in pending) { |
| forwarder.sendNotification(notif.method, notif.params); |
| } |
| } |
| } |
| |
| @override |
| Future<Vmo> getContents(String sessionId) async { |
| final viewId = _activeSessions[sessionId]; |
| if (viewId == null) { |
| log.warning('getContents called for unknown session $sessionId'); |
| return SizedVmo.fromUint8List(utf8.encode('unknown session id?? o_O')); |
| } |
| final contents = |
| await xiCore.sendRpc('debug_get_contents', {'view_id': viewId}); |
| return SizedVmo.fromUint8List(utf8.encode(contents)); |
| } |
| |
| // handle a notification from xi-core, forwarding it to some session |
| @override |
| Future<void> handleNotification(String method, dynamic params) async { |
| String viewId = params.remove('view_id'); |
| if (viewId == null) { |
| // we ignore things that aren't for a view |
| log.warning('ignoring notification $method $params'); |
| return; |
| } |
| |
| final session = _activeConnections[viewId]; |
| if (session == null) { |
| log.info( |
| 'no session active for view $viewId. Active connections: $_activeConnections'); |
| _pending.putIfAbsent(viewId, () => []); |
| _pending[viewId].add(PendingNotification(method, params)); |
| } else { |
| session.sendNotification(method, params); |
| } |
| } |
| |
| @override |
| dynamic handleRpc(String method, dynamic params) { |
| return 'session requests not implemeneted'; |
| } |
| } |
| |
| /// Main entry point to the example parent module. |
| void main(List<String> args) { |
| setupLogger(name: '[xi_session_agent]'); |
| log.info('agent started'); |
| Agent().exposeService(XiSessionManagerImpl()); |
| } |
| |
| class ViewForwarder extends XiClient implements XiRpcHandler { |
| final Socket _socket; |
| final SocketReader _reader = new SocketReader(); |
| final String viewId; |
| final XiClient core; |
| |
| ViewForwarder(this._socket, this.viewId, this.core) |
| : assert(_socket != null), |
| assert(core != null), |
| assert(viewId != null) { |
| handler = this; |
| } |
| |
| @override |
| Future<Null> init() async { |
| _reader |
| ..bind(_socket) |
| ..onReadable = handleRead; |
| } |
| |
| @override |
| void send(String data) { |
| final List<int> ints = utf8.encode('$data\n'); |
| final Uint8List bytes = new Uint8List.fromList(ints); |
| final ByteData buffer = bytes.buffer.asByteData(); |
| |
| final WriteResult result = _reader.socket.write(buffer); |
| |
| if (result.status != ZX.OK) { |
| StateError error = new StateError('ERROR WRITING: $result'); |
| streamController |
| ..addError(error) |
| ..close(); |
| } |
| } |
| |
| void handleRead() { |
| // TODO(pylaligand): the number of bytes below is bogus. |
| final ReadResult result = _reader.socket.read(1000); |
| |
| if (result.status != ZX.OK) { |
| StateError error = new StateError('Socket read error: ${result.status}'); |
| streamController |
| ..addError(error) |
| ..close(); |
| return; |
| } |
| |
| String resultAsString = result.bytesAsUTF8String(); |
| // TODO: use string directly, avoid re-roundtrip |
| List<int> fragment = utf8.encode(resultAsString); |
| streamController.add(fragment); |
| } |
| |
| @override |
| void handleNotification(String method, dynamic params) { |
| Map<String, dynamic> outer = { |
| 'view_id': viewId, |
| 'method': method, |
| 'params': params, |
| }; |
| core.sendNotification('edit', outer); |
| } |
| |
| @override |
| Future<dynamic> handleRpc(String method, dynamic params) { |
| Map<String, dynamic> outer = { |
| 'view_id': viewId, |
| 'method': method, |
| 'params': params, |
| }; |
| return core.sendRpc('edit', outer); |
| } |
| } |