| // Copyright 2018 The Chromium 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 = Duration(seconds: 9); |
| |
| const Duration _kReconnectAttemptInterval = Duration(seconds: 3); |
| |
| const Duration _kRpcTimeout = Duration(seconds: 5); |
| |
| final Logger _log = Logger('DartVm'); |
| |
| /// Signature of an asynchronous function for establishing a JSON RPC-2 |
| /// connection to a [Uri]. |
| typedef RpcPeerConnectionFunction = Future<json_rpc.Peer> Function( |
| Uri uri, { |
| Duration timeout, |
| }); |
| |
| /// [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; |
| |
| /// The JSON RPC 2 spec says that a notification from a client must not respond |
| /// to the client. It's possible the client sent a notification as a "ping", but |
| /// the service isn't set up yet to respond. |
| /// |
| /// For example, if the client sends a notification message to the server for |
| /// 'streamNotify', but the server has not finished loading, it will throw an |
| /// exception. Since the message is a notification, the server follows the |
| /// specification and does not send a response back, but is left with an |
| /// unhandled exception. That exception is safe for us to ignore - the client |
| /// is signaling that it will try again later if it doesn't get what it wants |
| /// here by sending a notification. |
| // This may be ignoring too many exceptions. It would be best to rewrite |
| // the client code to not use notifications so that it gets error replies back |
| // and can decide what to do from there. |
| // TODO(dnfield): https://github.com/flutter/flutter/issues/31813 |
| bool _ignoreRpcError(dynamic error) { |
| if (error is json_rpc.RpcException) { |
| final json_rpc.RpcException exception = error; |
| return exception.data == null || exception.data['id'] == null; |
| } else if (error is String && error.startsWith('JSON-RPC error -32601')) { |
| return true; |
| } |
| return false; |
| } |
| |
| |
| void _unhandledJsonRpcError(dynamic error, dynamic stack) { |
| if (_ignoreRpcError(error)) { |
| return; |
| } |
| _log.fine('Error in internalimplementation of JSON RPC.\n$error\n$stack'); |
| } |
| |
| /// Attempts to connect to a Dart VM service. |
| /// |
| /// Gives up after `timeout` has elapsed. |
| Future<json_rpc.Peer> _waitAndConnect( |
| Uri uri, { |
| Duration timeout = _kConnectTimeout, |
| }) async { |
| final Stopwatch timer = Stopwatch()..start(); |
| |
| Future<json_rpc.Peer> attemptConnection(Uri uri) async { |
| WebSocket socket; |
| json_rpc.Peer peer; |
| try { |
| socket = await WebSocket.connect(uri.toString()).timeout(timeout); |
| peer = json_rpc.Peer(IOWebSocketChannel(socket).cast(), onUnhandledError: _unhandledJsonRpcError)..listen(); |
| return peer; |
| } on HttpException catch (e) { |
| // This is a fine warning as this most likely means the port is stale. |
| _log.fine('$e: ${e.message}'); |
| await peer?.close(); |
| await socket?.close(); |
| rethrow; |
| } catch (e) { |
| _log.fine('Dart VM connection failed $e: ${e.message}'); |
| // Other unknown errors will be handled with reconnects. |
| await peer?.close(); |
| await socket?.close(); |
| if (timer.elapsed < timeout) { |
| _log.info('Attempting to reconnect'); |
| await Future<void>.delayed(_kReconnectAttemptInterval); |
| return attemptConnection(uri); |
| } else { |
| _log.warning('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, this.uri); |
| |
| final json_rpc.Peer _peer; |
| |
| /// The URI through which this DartVM instance is connected. |
| final Uri uri; |
| |
| /// Attempts to connect to the given [Uri]. |
| /// |
| /// Throws an error if unable to connect. |
| static Future<DartVm> connect( |
| Uri uri, { |
| Duration timeout = _kConnectTimeout, |
| }) async { |
| if (uri.scheme == 'http') { |
| uri = uri.replace(scheme: 'ws', path: '/ws'); |
| } |
| final json_rpc.Peer peer = |
| await fuchsiaVmServiceConnectionFunction(uri, timeout: timeout); |
| if (peer == null) { |
| return null; |
| } |
| return DartVm._(peer, uri); |
| } |
| |
| /// Returns a [List] of [IsolateRef] objects whose name matches `pattern`. |
| /// |
| /// This is not limited to Isolates running Flutter, but to any Isolate on the |
| /// VM. Therefore, the [pattern] argument should be written to exclude |
| /// matching unintended isolates. |
| Future<List<IsolateRef>> getMainIsolatesByPattern( |
| Pattern pattern, { |
| Duration timeout = _kRpcTimeout, |
| }) async { |
| final Map<String, dynamic> jsonVmRef = |
| await invokeRpc('getVM', timeout: timeout); |
| final List<IsolateRef> result = <IsolateRef>[]; |
| for (Map<String, dynamic> jsonIsolate in jsonVmRef['isolates']) { |
| final String name = jsonIsolate['name']; |
| if (pattern.matchAsPrefix(name) != null) { |
| _log.fine('Found Isolate matching "$pattern": "$name"'); |
| result.add(IsolateRef._fromJson(jsonIsolate, this)); |
| } |
| } |
| return result; |
| } |
| |
| /// 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 = _kRpcTimeout, |
| }) async { |
| final Map<String, dynamic> result = await _peer |
| .sendRequest(function, params ?? <String, dynamic>{}) |
| .timeout(timeout, onTimeout: () { |
| throw TimeoutException( |
| 'Peer connection timed out during RPC call', |
| timeout, |
| ); |
| }); |
| return result; |
| } |
| |
| /// 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({ |
| Duration timeout = _kRpcTimeout, |
| }) async { |
| final List<FlutterView> views = <FlutterView>[]; |
| final Map<String, dynamic> rpcResponse = |
| await invokeRpc('_flutter.listViews', timeout: timeout); |
| for (Map<String, dynamic> jsonView in rpcResponse['views']) { |
| final FlutterView flutterView = 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<void> 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 RpcFormatError('Unable to find name for isolate "$isolate"'); |
| } |
| } |
| if (id == null) { |
| throw RpcFormatError( |
| 'Unable to find view name for the following JSON structure "$json"'); |
| } |
| return 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; |
| } |
| |
| /// This is a wrapper class for the `@Isolate` RPC object. |
| /// |
| /// See: |
| /// https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#isolate |
| /// |
| /// This class contains information about the Isolate like its name and ID, as |
| /// well as a reference to the parent DartVM on which it is running. |
| class IsolateRef { |
| IsolateRef._(this.name, this.number, this.dartVm); |
| |
| factory IsolateRef._fromJson(Map<String, dynamic> json, DartVm dartVm) { |
| final String number = json['number']; |
| final String name = json['name']; |
| final String type = json['type']; |
| if (type == null) { |
| throw RpcFormatError('Unable to find type within JSON "$json"'); |
| } |
| if (type != '@Isolate') { |
| throw RpcFormatError('Type "$type" does not match for IsolateRef'); |
| } |
| if (number == null) { |
| throw RpcFormatError( |
| 'Unable to find number for isolate ref within JSON "$json"'); |
| } |
| if (name == null) { |
| throw RpcFormatError( |
| 'Unable to find name for isolate ref within JSON "$json"'); |
| } |
| return IsolateRef._(name, int.parse(number), dartVm); |
| } |
| |
| /// The full name of this Isolate (not guaranteed to be unique). |
| final String name; |
| |
| /// The unique number ID of this isolate. |
| final int number; |
| |
| /// The parent [DartVm] on which this Isolate lives. |
| final DartVm dartVm; |
| } |