1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'dart:async'; |
6 | import 'dart:io'; |
7 | |
8 | import 'package:vm_service/vm_service.dart' as vms; |
9 | |
10 | import '../common/logging.dart'; |
11 | |
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 = |
18 | Future<vms.VmService> Function(Uri uri, {required Duration timeout}); |
19 | |
20 | /// [DartVm] uses this function to connect to the Dart VM on Fuchsia. |
21 | /// |
22 | /// This function can be assigned to a different one in the event that a |
23 | /// custom connection function is needed. |
24 | RpcPeerConnectionFunction fuchsiaVmServiceConnectionFunction = _waitAndConnect; |
25 | |
26 | /// Attempts to connect to a Dart VM service. |
27 | /// |
28 | /// Gives up after `timeout` has elapsed. |
29 | Future<vms.VmService> _waitAndConnect(Uri uri, {Duration timeout = _kConnectTimeout}) async { |
30 | int attempts = 0; |
31 | late WebSocket socket; |
32 | while (true) { |
33 | try { |
34 | socket = await WebSocket.connect(uri.toString()); |
35 | final StreamController<dynamic> controller = StreamController<dynamic>(); |
36 | final Completer<void> streamClosedCompleter = Completer<void>(); |
37 | socket.listen( |
38 | (dynamic data) => controller.add(data), |
39 | onDone: () => streamClosedCompleter.complete(), |
40 | ); |
41 | final vms.VmService service = vms.VmService( |
42 | controller.stream, |
43 | socket.add, |
44 | disposeHandler: () => socket.close(), |
45 | streamClosed: streamClosedCompleter.future, |
46 | ); |
47 | // This call is to ensure we are able to establish a connection instead of |
48 | // keeping on trucking and failing farther down the process. |
49 | await service.getVersion(); |
50 | return service; |
51 | } catch (e) { |
52 | // We should not be catching all errors arbitrarily here, this might hide real errors. |
53 | // TODO(ianh): Determine which exceptions to catch here. |
54 | await socket.close(); |
55 | if (attempts > 5) { |
56 | _log.warning('It is taking an unusually long time to connect to the VM...' ); |
57 | } |
58 | attempts += 1; |
59 | await Future<void>.delayed(timeout); |
60 | } |
61 | } |
62 | } |
63 | |
64 | /// Restores the VM service connection function to the default implementation. |
65 | void restoreVmServiceConnectionFunction() { |
66 | fuchsiaVmServiceConnectionFunction = _waitAndConnect; |
67 | } |
68 | |
69 | /// An error raised when a malformed RPC response is received from the Dart VM. |
70 | /// |
71 | /// A more detailed description of the error is found within the [message] |
72 | /// field. |
73 | class RpcFormatError extends Error { |
74 | /// Basic constructor outlining the reason for the format error. |
75 | RpcFormatError(this.message); |
76 | |
77 | /// The reason for format error. |
78 | final String message; |
79 | |
80 | @override |
81 | String toString() { |
82 | return ' $RpcFormatError: $message\n ${super.stackTrace}' ; |
83 | } |
84 | } |
85 | |
86 | /// Handles JSON RPC-2 communication with a Dart VM service. |
87 | /// |
88 | /// Wraps existing RPC calls to the Dart VM service. |
89 | class DartVm { |
90 | DartVm._(this._vmService, this.uri); |
91 | |
92 | final vms.VmService _vmService; |
93 | |
94 | /// The URL through which this DartVM instance is connected. |
95 | final Uri uri; |
96 | |
97 | /// Attempts to connect to the given [Uri]. |
98 | /// |
99 | /// Throws an error if unable to connect. |
100 | static Future<DartVm> connect(Uri uri, {Duration timeout = _kConnectTimeout}) async { |
101 | if (uri.scheme == 'http' ) { |
102 | uri = uri.replace(scheme: 'ws' , path: '/ws' ); |
103 | } |
104 | |
105 | final vms.VmService service = await fuchsiaVmServiceConnectionFunction(uri, timeout: timeout); |
106 | return DartVm._(service, uri); |
107 | } |
108 | |
109 | /// Returns a [List] of [IsolateRef] objects whose name matches `pattern`. |
110 | /// |
111 | /// This is not limited to Isolates running Flutter, but to any Isolate on the |
112 | /// VM. Therefore, the [pattern] argument should be written to exclude |
113 | /// matching unintended isolates. |
114 | Future<List<IsolateRef>> getMainIsolatesByPattern(Pattern pattern) async { |
115 | final vms.VM vmRef = await _vmService.getVM(); |
116 | final List<IsolateRef> result = <IsolateRef>[]; |
117 | for (final vms.IsolateRef isolateRef in vmRef.isolates!) { |
118 | if (pattern.matchAsPrefix(isolateRef.name!) != null) { |
119 | _log.fine('Found Isolate matching " $pattern": " ${isolateRef.name}"' ); |
120 | result.add(IsolateRef._fromJson(isolateRef.json!, this)); |
121 | } |
122 | } |
123 | return result; |
124 | } |
125 | |
126 | /// Returns a list of [FlutterView] objects running across all Dart VM's. |
127 | /// |
128 | /// If there is no associated isolate with the flutter view (used to determine |
129 | /// the flutter view's name), then the flutter view's ID will be added |
130 | /// instead. If none of these things can be found (isolate has no name or the |
131 | /// flutter view has no ID), then the result will not be added to the list. |
132 | Future<List<FlutterView>> getAllFlutterViews() async { |
133 | final List<FlutterView> views = <FlutterView>[]; |
134 | final vms.Response rpcResponse = await _vmService.callMethod('_flutter.listViews' ); |
135 | for (final Map<String, dynamic> jsonView |
136 | in (rpcResponse.json!['views' ] as List<dynamic>).cast<Map<String, dynamic>>()) { |
137 | views.add(FlutterView._fromJson(jsonView)); |
138 | } |
139 | return views; |
140 | } |
141 | |
142 | /// Tests that the connection to the [vms.VmService] is valid. |
143 | Future<void> ping() async { |
144 | final vms.Version version = await _vmService.getVersion(); |
145 | _log.fine('DartVM( $uri) version check result: $version' ); |
146 | } |
147 | |
148 | /// Disconnects from the Dart VM Service. |
149 | /// |
150 | /// After this function completes this object is no longer usable. |
151 | Future<void> stop() async { |
152 | await _vmService.dispose(); |
153 | await _vmService.onDone; |
154 | } |
155 | } |
156 | |
157 | /// Represents an instance of a Flutter view running on a Fuchsia device. |
158 | class FlutterView { |
159 | FlutterView._(this._name, this._id); |
160 | |
161 | /// Attempts to construct a [FlutterView] from a json representation. |
162 | /// |
163 | /// If there is no isolate and no ID for the view, throws an [RpcFormatError]. |
164 | /// If there is an associated isolate, and there is no name for said isolate, |
165 | /// also throws an [RpcFormatError]. |
166 | /// |
167 | /// All other cases return a [FlutterView] instance. The name of the |
168 | /// view may be null, but the id will always be set. |
169 | factory FlutterView._fromJson(Map<String, dynamic> json) { |
170 | final Map<String, dynamic>? isolate = json['isolate' ] as Map<String, dynamic>?; |
171 | final String? id = json['id' ] as String?; |
172 | String? name; |
173 | if (id == null) { |
174 | throw RpcFormatError('Unable to find view name for the following JSON structure " $json"' ); |
175 | } |
176 | if (isolate != null) { |
177 | name = isolate['name' ] as String?; |
178 | if (name == null) { |
179 | throw RpcFormatError('Unable to find name for isolate " $isolate"' ); |
180 | } |
181 | } |
182 | return FlutterView._(name, id); |
183 | } |
184 | |
185 | /// Determines the name of the isolate associated with this view. If there is |
186 | /// no associated isolate, this will be set to the view's ID. |
187 | final String? _name; |
188 | |
189 | /// The ID of the Flutter view. |
190 | final String _id; |
191 | |
192 | /// The ID of the [FlutterView]. |
193 | String get id => _id; |
194 | |
195 | /// Returns the name of the [FlutterView]. |
196 | /// |
197 | /// May be null if there is no associated isolate. |
198 | String? get name => _name; |
199 | } |
200 | |
201 | /// This is a wrapper class for the `@Isolate` RPC object. |
202 | /// |
203 | /// See: |
204 | /// https://github.com/dart-lang/sdk/blob/main/runtime/vm/service/service.md#isolate |
205 | /// |
206 | /// This class contains information about the Isolate like its name and ID, as |
207 | /// well as a reference to the parent DartVM on which it is running. |
208 | class IsolateRef { |
209 | IsolateRef._(this.name, this.number, this.dartVm); |
210 | |
211 | factory IsolateRef._fromJson(Map<String, dynamic> json, DartVm dartVm) { |
212 | final String? number = json['number' ] as String?; |
213 | final String? name = json['name' ] as String?; |
214 | final String? type = json['type' ] as String?; |
215 | if (type == null) { |
216 | throw RpcFormatError('Unable to find type within JSON " $json"' ); |
217 | } |
218 | if (type != '@Isolate' ) { |
219 | throw RpcFormatError('Type " $type" does not match for IsolateRef' ); |
220 | } |
221 | if (number == null) { |
222 | throw RpcFormatError('Unable to find number for isolate ref within JSON " $json"' ); |
223 | } |
224 | if (name == null) { |
225 | throw RpcFormatError('Unable to find name for isolate ref within JSON " $json"' ); |
226 | } |
227 | return IsolateRef._(name, int.parse(number), dartVm); |
228 | } |
229 | |
230 | /// The full name of this Isolate (not guaranteed to be unique). |
231 | final String name; |
232 | |
233 | /// The unique number ID of this isolate. |
234 | final int number; |
235 | |
236 | /// The parent [DartVm] on which this Isolate lives. |
237 | final DartVm dartVm; |
238 | } |
239 | |