// Copyright 2018 The Flutter 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:io'; import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc; import 'package:web_socket_channel/io.dart'; import '../common/logging.dart'; const Duration _kConnectTimeout = const Duration(seconds: 30); const Duration _kReconnectAttemptInterval = const Duration(seconds: 3); const Duration _kRpcTimeout = const Duration(seconds: 5); final Logger _log = new Logger('DartVm'); /// Signature of an asynchronous function for astablishing a JSON RPC-2 /// connection to a [Uri]. typedef Future<json_rpc.Peer> RpcPeerConnectionFunction(Uri uri); /// [DartVm] uses this function to connect to the Dart VM on Fuchsia. /// /// This function can be assigned to a different one in the event that a /// custom connection function is needed. RpcPeerConnectionFunction fuchsiaVmServiceConnectionFunction = _waitAndConnect; /// Attempts to connect to a Dart VM service. /// /// Gives up after `_kConnectTimeout` has elapsed. Future<json_rpc.Peer> _waitAndConnect(Uri uri) async { final Stopwatch timer = new Stopwatch()..start(); Future<json_rpc.Peer> attemptConnection(Uri uri) async { WebSocket socket; json_rpc.Peer peer; try { socket = await WebSocket.connect(uri.toString()); peer = new json_rpc.Peer(new IOWebSocketChannel(socket).cast())..listen(); return peer; } catch (e) { await peer?.close(); await socket?.close(); if (timer.elapsed < _kConnectTimeout) { _log.info('Attempting to reconnect'); await new Future<Null>.delayed(_kReconnectAttemptInterval); return attemptConnection(uri); } else { _log.severe('Connection to Fuchsia\'s Dart VM timed out at ' '${uri.toString()}'); rethrow; } } } return attemptConnection(uri); } /// Restores the VM service connection function to the default implementation. void restoreVmServiceConnectionFunction() { fuchsiaVmServiceConnectionFunction = _waitAndConnect; } /// An error raised when a malformed RPC response is received from the Dart VM. /// /// A more detailed description of the error is found within the [message] /// field. class RpcFormatError extends Error { /// Basic constructor outlining the reason for the format error. RpcFormatError(this.message); /// The reason for format error. final String message; @override String toString() { return '$RpcFormatError: $message\n${super.stackTrace}'; } } /// Handles JSON RPC-2 communication with a Dart VM service. /// /// Either wraps existing RPC calls to the Dart VM service, or runs raw RPC /// function calls via [invokeRpc]. class DartVm { DartVm._(this._peer); final json_rpc.Peer _peer; /// Attempts to connect to the given [Uri]. /// /// Throws an error if unable to connect. static Future<DartVm> connect(Uri uri) async { if (uri.scheme == 'http') { uri = uri.replace(scheme: 'ws', path: '/ws'); } final json_rpc.Peer peer = await fuchsiaVmServiceConnectionFunction(uri); if (peer == null) { return null; } return new DartVm._(peer); } /// Invokes a raw JSON RPC command with the VM service. /// /// When `timeout` is set and reached, throws a [TimeoutException]. /// /// If the function returns, it is with a parsed JSON response. Future<Map<String, dynamic>> invokeRpc( String function, { Map<String, dynamic> params, Duration timeout, }) async { final Future<Map<String, dynamic>> future = _peer.sendRequest( function, params ?? <String, dynamic>{}, ); if (timeout == null) { return future; } return future.timeout(timeout, onTimeout: () { throw new TimeoutException( 'Peer connection timed out during RPC call', timeout, ); }); } /// Returns a list of [FlutterView] objects running across all Dart VM's. /// /// If there is no associated isolate with the flutter view (used to determine /// the flutter view's name), then the flutter view's ID will be added /// instead. If none of these things can be found (isolate has no name or the /// flutter view has no ID), then the result will not be added to the list. Future<List<FlutterView>> getAllFlutterViews() async { final List<FlutterView> views = <FlutterView>[]; final Map<String, dynamic> rpcResponse = await invokeRpc('_flutter.listViews', timeout: _kRpcTimeout); final List<Map<String, dynamic>> flutterViewsJson = rpcResponse['views']; for (Map<String, dynamic> jsonView in flutterViewsJson) { final FlutterView flutterView = new FlutterView._fromJson(jsonView); if (flutterView != null) { views.add(flutterView); } } return views; } /// Disconnects from the Dart VM Service. /// /// After this function completes this object is no longer usable. Future<Null> stop() async { await _peer?.close(); } } /// Represents an instance of a Flutter view running on a Fuchsia device. class FlutterView { FlutterView._(this._name, this._id); /// Attempts to construct a [FlutterView] from a json representation. /// /// If there is no isolate and no ID for the view, throws an [RpcFormatError]. /// If there is an associated isolate, and there is no name for said isolate, /// also throws an [RpcFormatError]. /// /// All other cases return a [FlutterView] instance. The name of the /// view may be null, but the id will always be set. factory FlutterView._fromJson(Map<String, dynamic> json) { final Map<String, dynamic> isolate = json['isolate']; final String id = json['id']; String name; if (isolate != null) { name = isolate['name']; if (name == null) { throw new RpcFormatError('Unable to find name for isolate "$isolate"'); } } if (id == null) { throw new RpcFormatError( 'Unable to find view name for the following JSON structure "$json"'); } return new FlutterView._(name, id); } /// Determines the name of the isolate associated with this view. If there is /// no associated isolate, this will be set to the view's ID. final String _name; /// The ID of the Flutter view. final String _id; /// The ID of the [FlutterView]. String get id => _id; /// Returns the name of the [FlutterView]. /// /// May be null if there is no associated isolate. String get name => _name; }