dart_vm.dart 8.42 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7
// 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';

8
import 'package:vm_service/vm_service.dart' as vms;
9 10 11

import '../common/logging.dart';

12
const Duration _kConnectTimeout = Duration(seconds: 3);
13
final Logger _log = Logger('DartVm');
14

15
/// Signature of an asynchronous function for establishing a [vms.VmService]
16
/// connection to a [Uri].
17
typedef RpcPeerConnectionFunction = Future<vms.VmService> Function(
18
  Uri uri, {
19
  required Duration timeout,
20
});
21 22 23 24 25 26 27 28 29

/// [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.
///
30
/// Gives up after `timeout` has elapsed.
31
Future<vms.VmService> _waitAndConnect(
32 33 34
  Uri uri, {
  Duration timeout = _kConnectTimeout,
}) async {
35
  int attempts = 0;
36
  late WebSocket socket;
37
  while (true) {
38
    try {
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
      socket = await WebSocket.connect(uri.toString());
      final StreamController<dynamic> controller = StreamController<dynamic>();
      final Completer<void> streamClosedCompleter = Completer<void>();
      socket.listen(
        (dynamic data) => controller.add(data),
        onDone: () => streamClosedCompleter.complete(),
      );
      final vms.VmService service = vms.VmService(
        controller.stream,
        socket.add,
        disposeHandler: () => socket.close(),
        streamClosed: streamClosedCompleter.future
      );
      // This call is to ensure we are able to establish a connection instead of
      // keeping on trucking and failing farther down the process.
      await service.getVersion();
      return service;
56
    } catch (e) {
57 58
      // We should not be catching all errors arbitrarily here, this might hide real errors.
      // TODO(ianh): Determine which exceptions to catch here.
59
      await socket.close();
60 61
      if (attempts > 5) {
        _log.warning('It is taking an unusually long time to connect to the VM...');
62
      }
63 64
      attempts += 1;
      await Future<void>.delayed(timeout);
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
    }
  }
}

/// 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 {
96
  DartVm._(this._vmService, this.uri);
97

98
  final vms.VmService _vmService;
99

100
  /// The URL through which this DartVM instance is connected.
101 102
  final Uri uri;

103 104 105
  /// Attempts to connect to the given [Uri].
  ///
  /// Throws an error if unable to connect.
106 107 108 109
  static Future<DartVm> connect(
    Uri uri, {
    Duration timeout = _kConnectTimeout,
  }) async {
110 111 112
    if (uri.scheme == 'http') {
      uri = uri.replace(scheme: 'ws', path: '/ws');
    }
113 114 115

    final vms.VmService service = await fuchsiaVmServiceConnectionFunction(uri, timeout: timeout);
    return DartVm._(service, uri);
116 117 118 119
  }

  /// Returns a [List] of [IsolateRef] objects whose name matches `pattern`.
  ///
120
  /// This is not limited to Isolates running Flutter, but to any Isolate on the
121 122
  /// VM. Therefore, the [pattern] argument should be written to exclude
  /// matching unintended isolates.
123
  Future<List<IsolateRef>> getMainIsolatesByPattern(Pattern pattern) async {
124
    final vms.VM vmRef = await _vmService.getVM();
125
    final List<IsolateRef> result = <IsolateRef>[];
126 127
    for (final vms.IsolateRef isolateRef in vmRef.isolates!) {
      if (pattern.matchAsPrefix(isolateRef.name!) != null) {
128
        _log.fine('Found Isolate matching "$pattern": "${isolateRef.name}"');
129
        result.add(IsolateRef._fromJson(isolateRef.json!, this));
130 131 132
      }
    }
    return result;
133 134 135 136 137 138 139 140 141
  }


  /// 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.
142
  Future<List<FlutterView>> getAllFlutterViews() async {
143
    final List<FlutterView> views = <FlutterView>[];
144
    final vms.Response rpcResponse = await _vmService.callMethod('_flutter.listViews');
145 146
    for (final Map<String, dynamic> jsonView in (rpcResponse.json!['views'] as List<dynamic>).cast<Map<String, dynamic>>()) {
      views.add(FlutterView._fromJson(jsonView));
147 148 149 150
    }
    return views;
  }

151 152 153 154 155 156
  /// Tests that the connection to the [vms.VmService] is valid.
  Future<void> ping() async {
    final vms.Version version = await _vmService.getVersion();
    _log.fine('DartVM($uri) version check result: $version');
  }

157 158 159
  /// Disconnects from the Dart VM Service.
  ///
  /// After this function completes this object is no longer usable.
160
  Future<void> stop() async {
161
    await _vmService.dispose();
162
    await _vmService.onDone;
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
  }
}

/// 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) {
179 180 181 182 183 184 185
    final Map<String, dynamic>? isolate = json['isolate'] as Map<String, dynamic>?;
    final String? id = json['id'] as String?;
    String? name;
    if (id == null) {
      throw RpcFormatError(
          'Unable to find view name for the following JSON structure "$json"');
    }
186
    if (isolate != null) {
187
      name = isolate['name'] as String?;
188
      if (name == null) {
189
        throw RpcFormatError('Unable to find name for isolate "$isolate"');
190 191
      }
    }
192
    return FlutterView._(name, id);
193 194 195 196
  }

  /// 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.
197
  final String? _name;
198 199 200 201 202 203 204 205 206 207

  /// 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.
208
  String? get name => _name;
209
}
210 211 212 213

/// This is a wrapper class for the `@Isolate` RPC object.
///
/// See:
214
/// https://github.com/dart-lang/sdk/blob/main/runtime/vm/service/service.md#isolate
215 216 217 218 219 220 221
///
/// 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) {
222 223 224
    final String? number = json['number'] as String?;
    final String? name = json['name'] as String?;
    final String? type = json['type'] as String?;
225
    if (type == null) {
226
      throw RpcFormatError('Unable to find type within JSON "$json"');
227 228
    }
    if (type != '@Isolate') {
229
      throw RpcFormatError('Type "$type" does not match for IsolateRef');
230 231
    }
    if (number == null) {
232
      throw RpcFormatError(
233 234 235
          'Unable to find number for isolate ref within JSON "$json"');
    }
    if (name == null) {
236
      throw RpcFormatError(
237 238
          'Unable to find name for isolate ref within JSON "$json"');
    }
239
    return IsolateRef._(name, int.parse(number), dartVm);
240 241 242 243 244 245 246 247 248 249 250
  }

  /// 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;
}