| // Copyright 2014 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. |
| |
| // @dart = 2.8 |
| |
| import 'dart:async'; |
| |
| import 'package:meta/meta.dart' show required, visibleForTesting; |
| import 'package:vm_service/vm_service.dart' as vm_service; |
| |
| import 'base/common.dart'; |
| import 'base/context.dart'; |
| import 'base/io.dart' as io; |
| import 'base/logger.dart'; |
| import 'base/utils.dart'; |
| import 'convert.dart'; |
| import 'device.dart'; |
| import 'version.dart'; |
| |
| const String kGetSkSLsMethod = '_flutter.getSkSLs'; |
| const String kSetAssetBundlePathMethod = '_flutter.setAssetBundlePath'; |
| const String kFlushUIThreadTasksMethod = '_flutter.flushUIThreadTasks'; |
| const String kRunInViewMethod = '_flutter.runInView'; |
| const String kListViewsMethod = '_flutter.listViews'; |
| const String kScreenshotSkpMethod = '_flutter.screenshotSkp'; |
| const String kScreenshotMethod = '_flutter.screenshot'; |
| |
| /// The error response code from an unrecoverable compilation failure. |
| const int kIsolateReloadBarred = 1005; |
| |
| /// Override `WebSocketConnector` in [context] to use a different constructor |
| /// for [WebSocket]s (used by tests). |
| typedef WebSocketConnector = Future<io.WebSocket> Function(String url, {io.CompressionOptions compression, @required Logger logger}); |
| |
| typedef PrintStructuredErrorLogMethod = void Function(vm_service.Event); |
| |
| WebSocketConnector _openChannel = _defaultOpenChannel; |
| |
| /// A testing only override of the WebSocket connector. |
| /// |
| /// Provide a `null` value to restore the original connector. |
| @visibleForTesting |
| set openChannelForTesting(WebSocketConnector connector) { |
| _openChannel = connector ?? _defaultOpenChannel; |
| } |
| |
| /// The error codes for the JSON-RPC standard, including VM service specific |
| /// error codes. |
| /// |
| /// See also: https://www.jsonrpc.org/specification#error_object |
| abstract class RPCErrorCodes { |
| /// The method does not exist or is not available. |
| static const int kMethodNotFound = -32601; |
| |
| /// Invalid method parameter(s), such as a mismatched type. |
| static const int kInvalidParams = -32602; |
| |
| /// Internal JSON-RPC error. |
| static const int kInternalError = -32603; |
| |
| /// Application specific error codes. |
| static const int kServerError = -32000; |
| |
| /// Non-standard JSON-RPC error codes: |
| |
| /// The VM service or extension service has disappeared. |
| static const int kServiceDisappeared = 112; |
| } |
| |
| /// A function that reacts to the invocation of the 'reloadSources' service. |
| /// |
| /// The VM Service Protocol allows clients to register custom services that |
| /// can be invoked by other clients through the service protocol itself. |
| /// |
| /// Clients like Observatory use external 'reloadSources' services, |
| /// when available, instead of the VM internal one. This allows these clients to |
| /// invoke Flutter HotReload when connected to a Flutter Application started in |
| /// hot mode. |
| /// |
| /// See: https://github.com/dart-lang/sdk/issues/30023 |
| typedef ReloadSources = Future<void> Function( |
| String isolateId, { |
| bool force, |
| bool pause, |
| }); |
| |
| typedef Restart = Future<void> Function({ bool pause }); |
| |
| typedef CompileExpression = Future<String> Function( |
| String isolateId, |
| String expression, |
| List<String> definitions, |
| List<String> typeDefinitions, |
| String libraryUri, |
| String klass, |
| bool isStatic, |
| ); |
| |
| /// A method that pulls an SkSL shader from the device and writes it to a file. |
| /// |
| /// The name of the file returned as a result. |
| typedef GetSkSLMethod = Future<String> Function(); |
| |
| Future<io.WebSocket> _defaultOpenChannel(String url, { |
| io.CompressionOptions compression = io.CompressionOptions.compressionDefault, |
| @required Logger logger, |
| }) async { |
| Duration delay = const Duration(milliseconds: 100); |
| int attempts = 0; |
| io.WebSocket socket; |
| |
| Future<void> handleError(dynamic e) async { |
| void Function(String) printVisibleTrace = logger.printTrace; |
| if (attempts == 10) { |
| logger.printStatus('Connecting to the VM Service is taking longer than expected...'); |
| } else if (attempts == 20) { |
| logger.printStatus('Still attempting to connect to the VM Service...'); |
| logger.printStatus( |
| 'If you do NOT see the Flutter application running, it might have ' |
| 'crashed. The device logs (e.g. from adb or XCode) might have more ' |
| 'details.'); |
| logger.printStatus( |
| 'If you do see the Flutter application running on the device, try ' |
| 're-running with --host-vmservice-port to use a specific port known to ' |
| 'be available.'); |
| } else if (attempts % 50 == 0) { |
| printVisibleTrace = logger.printStatus; |
| } |
| |
| printVisibleTrace('Exception attempting to connect to the VM Service: $e'); |
| printVisibleTrace('This was attempt #$attempts. Will retry in $delay.'); |
| |
| // Delay next attempt. |
| await Future<void>.delayed(delay); |
| |
| // Back off exponentially, up to 1600ms per attempt. |
| if (delay < const Duration(seconds: 1)) { |
| delay *= 2; |
| } |
| } |
| |
| final WebSocketConnector constructor = context.get<WebSocketConnector>() ?? (String url, { |
| io.CompressionOptions compression = io.CompressionOptions.compressionDefault, |
| @required Logger logger, |
| }) => io.WebSocket.connect(url, compression: compression); |
| |
| while (socket == null) { |
| attempts += 1; |
| try { |
| socket = await constructor(url, compression: compression); |
| } on io.WebSocketException catch (e) { |
| await handleError(e); |
| } on io.SocketException catch (e) { |
| await handleError(e); |
| } |
| } |
| return socket; |
| } |
| |
| /// Override `VMServiceConnector` in [context] to return a different VMService |
| /// from [VMService.connect] (used by tests). |
| typedef VMServiceConnector = Future<FlutterVmService> Function(Uri httpUri, { |
| ReloadSources reloadSources, |
| Restart restart, |
| CompileExpression compileExpression, |
| GetSkSLMethod getSkSLMethod, |
| PrintStructuredErrorLogMethod printStructuredErrorLogMethod, |
| io.CompressionOptions compression, |
| Device device, |
| @required Logger logger, |
| }); |
| |
| /// Set up the VM Service client by attaching services for each of the provided |
| /// callbacks. |
| /// |
| /// All parameters besides [vmService] may be null. |
| Future<vm_service.VmService> setUpVmService( |
| ReloadSources reloadSources, |
| Restart restart, |
| CompileExpression compileExpression, |
| Device device, |
| GetSkSLMethod skSLMethod, |
| PrintStructuredErrorLogMethod printStructuredErrorLogMethod, |
| vm_service.VmService vmService |
| ) async { |
| // Each service registration requires a request to the attached VM service. Since the |
| // order of these requests does not mattter, store each future in a list and await |
| // all at the end of this method. |
| final List<Future<vm_service.Success>> registrationRequests = <Future<vm_service.Success>>[]; |
| if (reloadSources != null) { |
| vmService.registerServiceCallback('reloadSources', (Map<String, dynamic> params) async { |
| final String isolateId = _validateRpcStringParam('reloadSources', params, 'isolateId'); |
| final bool force = _validateRpcBoolParam('reloadSources', params, 'force'); |
| final bool pause = _validateRpcBoolParam('reloadSources', params, 'pause'); |
| |
| await reloadSources(isolateId, force: force, pause: pause); |
| |
| return <String, dynamic>{ |
| 'result': <String, Object>{ |
| 'type': 'Success', |
| } |
| }; |
| }); |
| registrationRequests.add(vmService.registerService('reloadSources', 'Flutter Tools')); |
| } |
| |
| if (restart != null) { |
| vmService.registerServiceCallback('hotRestart', (Map<String, dynamic> params) async { |
| final bool pause = _validateRpcBoolParam('compileExpression', params, 'pause'); |
| await restart(pause: pause); |
| return <String, dynamic>{ |
| 'result': <String, Object>{ |
| 'type': 'Success', |
| } |
| }; |
| }); |
| registrationRequests.add(vmService.registerService('hotRestart', 'Flutter Tools')); |
| } |
| |
| vmService.registerServiceCallback('flutterVersion', (Map<String, dynamic> params) async { |
| final FlutterVersion version = context.get<FlutterVersion>() ?? FlutterVersion(); |
| final Map<String, Object> versionJson = version.toJson(); |
| versionJson['frameworkRevisionShort'] = version.frameworkRevisionShort; |
| versionJson['engineRevisionShort'] = version.engineRevisionShort; |
| return <String, dynamic>{ |
| 'result': <String, Object>{ |
| 'type': 'Success', |
| ...versionJson, |
| } |
| }; |
| }); |
| registrationRequests.add(vmService.registerService('flutterVersion', 'Flutter Tools')); |
| |
| if (compileExpression != null) { |
| vmService.registerServiceCallback('compileExpression', (Map<String, dynamic> params) async { |
| final String isolateId = _validateRpcStringParam('compileExpression', params, 'isolateId'); |
| final String expression = _validateRpcStringParam('compileExpression', params, 'expression'); |
| final List<String> definitions = List<String>.from(params['definitions'] as List<dynamic>); |
| final List<String> typeDefinitions = List<String>.from(params['typeDefinitions'] as List<dynamic>); |
| final String libraryUri = params['libraryUri'] as String; |
| final String klass = params['klass'] as String; |
| final bool isStatic = _validateRpcBoolParam('compileExpression', params, 'isStatic'); |
| |
| final String kernelBytesBase64 = await compileExpression(isolateId, |
| expression, definitions, typeDefinitions, libraryUri, klass, |
| isStatic); |
| return <String, dynamic>{ |
| 'type': 'Success', |
| 'result': <String, dynamic>{'kernelBytes': kernelBytesBase64}, |
| }; |
| }); |
| registrationRequests.add(vmService.registerService('compileExpression', 'Flutter Tools')); |
| } |
| if (device != null) { |
| vmService.registerServiceCallback('flutterMemoryInfo', (Map<String, dynamic> params) async { |
| final MemoryInfo result = await device.queryMemoryInfo(); |
| return <String, dynamic>{ |
| 'result': <String, Object>{ |
| 'type': 'Success', |
| ...result.toJson(), |
| } |
| }; |
| }); |
| registrationRequests.add(vmService.registerService('flutterMemoryInfo', 'Flutter Tools')); |
| } |
| if (skSLMethod != null) { |
| vmService.registerServiceCallback('flutterGetSkSL', (Map<String, dynamic> params) async { |
| final String filename = await skSLMethod(); |
| return <String, dynamic>{ |
| 'result': <String, Object>{ |
| 'type': 'Success', |
| 'filename': filename, |
| } |
| }; |
| }); |
| registrationRequests.add(vmService.registerService('flutterGetSkSL', 'Flutter Tools')); |
| } |
| if (printStructuredErrorLogMethod != null) { |
| vmService.onExtensionEvent.listen(printStructuredErrorLogMethod); |
| // It is safe to ignore this error because we expect an error to be |
| // thrown if we're already subscribed. |
| registrationRequests.add(vmService |
| .streamListen(vm_service.EventStreams.kExtension) |
| .catchError((dynamic error) {}, test: (dynamic error) => error is vm_service.RPCError) |
| ); |
| } |
| |
| try { |
| await Future.wait(registrationRequests); |
| } on vm_service.RPCError catch (e) { |
| throwToolExit('Failed to register service methods on attached VM Service: $e'); |
| } |
| return vmService; |
| } |
| |
| /// Connect to a Dart VM Service at [httpUri]. |
| /// |
| /// If the [reloadSources] parameter is not null, the 'reloadSources' service |
| /// will be registered. The VM Service Protocol allows clients to register |
| /// custom services that can be invoked by other clients through the service |
| /// protocol itself. |
| /// |
| /// See: https://github.com/dart-lang/sdk/commit/df8bf384eb815cf38450cb50a0f4b62230fba217 |
| Future<FlutterVmService> connectToVmService( |
| Uri httpUri, { |
| ReloadSources reloadSources, |
| Restart restart, |
| CompileExpression compileExpression, |
| GetSkSLMethod getSkSLMethod, |
| PrintStructuredErrorLogMethod printStructuredErrorLogMethod, |
| io.CompressionOptions compression = io.CompressionOptions.compressionDefault, |
| Device device, |
| @required Logger logger, |
| }) async { |
| final VMServiceConnector connector = context.get<VMServiceConnector>() ?? _connect; |
| return connector(httpUri, |
| reloadSources: reloadSources, |
| restart: restart, |
| compileExpression: compileExpression, |
| compression: compression, |
| device: device, |
| getSkSLMethod: getSkSLMethod, |
| printStructuredErrorLogMethod: printStructuredErrorLogMethod, |
| logger: logger, |
| ); |
| } |
| |
| Future<vm_service.VmService> createVmServiceDelegate( |
| Uri wsUri, { |
| io.CompressionOptions compression = io.CompressionOptions.compressionDefault, |
| @required Logger logger, |
| }) async { |
| final io.WebSocket channel = await _openChannel(wsUri.toString(), compression: compression, logger: logger); |
| return vm_service.VmService( |
| channel, |
| channel.add, |
| log: null, |
| disposeHandler: () async { |
| await channel.close(); |
| }, |
| ); |
| } |
| |
| Future<FlutterVmService> _connect( |
| Uri httpUri, { |
| ReloadSources reloadSources, |
| Restart restart, |
| CompileExpression compileExpression, |
| GetSkSLMethod getSkSLMethod, |
| PrintStructuredErrorLogMethod printStructuredErrorLogMethod, |
| io.CompressionOptions compression = io.CompressionOptions.compressionDefault, |
| Device device, |
| @required Logger logger, |
| }) async { |
| final Uri wsUri = httpUri.replace(scheme: 'ws', path: urlContext.join(httpUri.path, 'ws')); |
| final vm_service.VmService delegateService = await createVmServiceDelegate( |
| wsUri, compression: compression, logger: logger, |
| ); |
| |
| final vm_service.VmService service = await setUpVmService( |
| reloadSources, |
| restart, |
| compileExpression, |
| device, |
| getSkSLMethod, |
| printStructuredErrorLogMethod, |
| delegateService, |
| ); |
| |
| // This call is to ensure we are able to establish a connection instead of |
| // keeping on trucking and failing farther down the process. |
| await delegateService.getVersion(); |
| return FlutterVmService(service, httpAddress: httpUri, wsAddress: wsUri); |
| } |
| |
| String _validateRpcStringParam(String methodName, Map<String, dynamic> params, String paramName) { |
| final dynamic value = params[paramName]; |
| if (value is! String || (value as String).isEmpty) { |
| throw vm_service.RPCError( |
| methodName, |
| RPCErrorCodes.kInvalidParams, |
| "Invalid '$paramName': $value", |
| ); |
| } |
| return value as String; |
| } |
| |
| bool _validateRpcBoolParam(String methodName, Map<String, dynamic> params, String paramName) { |
| final dynamic value = params[paramName]; |
| if (value != null && value is! bool) { |
| throw vm_service.RPCError( |
| methodName, |
| RPCErrorCodes.kInvalidParams, |
| "Invalid '$paramName': $value", |
| ); |
| } |
| return (value as bool) ?? false; |
| } |
| |
| /// Peered to an Android/iOS FlutterView widget on a device. |
| class FlutterView { |
| FlutterView({ |
| @required this.id, |
| @required this.uiIsolate, |
| }); |
| |
| factory FlutterView.parse(Map<String, Object> json) { |
| final Map<String, Object> rawIsolate = json['isolate'] as Map<String, Object>; |
| vm_service.IsolateRef isolate; |
| if (rawIsolate != null) { |
| rawIsolate['number'] = rawIsolate['number']?.toString(); |
| isolate = vm_service.IsolateRef.parse(rawIsolate); |
| } |
| return FlutterView( |
| id: json['id'] as String, |
| uiIsolate: isolate, |
| ); |
| } |
| |
| final vm_service.IsolateRef uiIsolate; |
| final String id; |
| |
| bool get hasIsolate => uiIsolate != null; |
| |
| @override |
| String toString() => id; |
| |
| Map<String, Object> toJson() { |
| return <String, Object>{ |
| 'id': id, |
| 'isolate': uiIsolate?.toJson(), |
| }; |
| } |
| } |
| |
| /// Flutter specific VM Service functionality. |
| class FlutterVmService { |
| FlutterVmService(this.service, {this.wsAddress, this.httpAddress}); |
| |
| final vm_service.VmService service; |
| final Uri wsAddress; |
| final Uri httpAddress; |
| |
| Future<vm_service.Response> callMethodWrapper( |
| String method, { |
| String isolateId, |
| Map<String, dynamic> args |
| }) async { |
| try { |
| return await service.callMethod(method, isolateId: isolateId, args: args); |
| } on vm_service.RPCError catch (e) { |
| // If the service disappears mid-request the tool is unable to recover |
| // and should begin to shutdown due to the service connection closing. |
| // Swallow the exception here and let the shutdown logic elsewhere deal |
| // with cleaning up. |
| if (e.code == RPCErrorCodes.kServiceDisappeared) { |
| return null; |
| } |
| rethrow; |
| } |
| } |
| |
| /// Set the asset directory for the an attached Flutter view. |
| Future<void> setAssetDirectory({ |
| @required Uri assetsDirectory, |
| @required String viewId, |
| @required String uiIsolateId, |
| }) async { |
| assert(assetsDirectory != null); |
| await callMethodWrapper(kSetAssetBundlePathMethod, |
| isolateId: uiIsolateId, |
| args: <String, dynamic>{ |
| 'viewId': viewId, |
| 'assetDirectory': assetsDirectory.toFilePath(windows: false), |
| }); |
| } |
| |
| /// Retrieve the cached SkSL shaders from an attached Flutter view. |
| /// |
| /// This method will only return data if `--cache-sksl` was provided as a |
| /// flutter run argument, and only then on physical devices. |
| Future<Map<String, Object>> getSkSLs({ |
| @required String viewId, |
| }) async { |
| final vm_service.Response response = await callMethodWrapper( |
| kGetSkSLsMethod, |
| args: <String, String>{ |
| 'viewId': viewId, |
| }, |
| ); |
| if (response == null) { |
| return null; |
| } |
| return response.json['SkSLs'] as Map<String, Object>; |
| } |
| |
| /// Flush all tasks on the UI thread for an attached Flutter view. |
| /// |
| /// This method is currently used only for benchmarking. |
| Future<void> flushUIThreadTasks({ |
| @required String uiIsolateId, |
| }) async { |
| await callMethodWrapper( |
| kFlushUIThreadTasksMethod, |
| args: <String, String>{ |
| 'isolateId': uiIsolateId, |
| }, |
| ); |
| } |
| |
| /// Launch the Dart isolate with entrypoint [main] in the Flutter engine [viewId] |
| /// with [assetsDirectory] as the devFS. |
| /// |
| /// This method is used by the tool to hot restart an already running Flutter |
| /// engine. |
| Future<void> runInView({ |
| @required String viewId, |
| @required Uri main, |
| @required Uri assetsDirectory, |
| }) async { |
| try { |
| await service.streamListen(vm_service.EventStreams.kIsolate); |
| } on vm_service.RPCError { |
| // Do nothing, since the tool is already subscribed. |
| } |
| final Future<void> onRunnable = service.onIsolateEvent.firstWhere((vm_service.Event event) { |
| return event.kind == vm_service.EventKind.kIsolateRunnable; |
| }); |
| await callMethodWrapper( |
| kRunInViewMethod, |
| args: <String, Object>{ |
| 'viewId': viewId, |
| 'mainScript': main.toString(), |
| 'assetDirectory': assetsDirectory.toString(), |
| }, |
| ); |
| await onRunnable; |
| } |
| |
| Future<String> flutterDebugDumpApp({ |
| @required String isolateId, |
| }) async { |
| final Map<String, Object> response = await invokeFlutterExtensionRpcRaw( |
| 'ext.flutter.debugDumpApp', |
| isolateId: isolateId, |
| ); |
| return response != null ? response['data']?.toString() : ''; |
| } |
| |
| Future<String> flutterDebugDumpRenderTree({ |
| @required String isolateId, |
| }) async { |
| final Map<String, Object> response = await invokeFlutterExtensionRpcRaw( |
| 'ext.flutter.debugDumpRenderTree', |
| isolateId: isolateId, |
| args: <String, Object>{} |
| ); |
| return response != null ? response['data']?.toString() : ''; |
| } |
| |
| Future<String> flutterDebugDumpLayerTree({ |
| @required String isolateId, |
| }) async { |
| final Map<String, Object> response = await invokeFlutterExtensionRpcRaw( |
| 'ext.flutter.debugDumpLayerTree', |
| isolateId: isolateId, |
| ); |
| return response != null ? response['data']?.toString() : ''; |
| } |
| |
| Future<String> flutterDebugDumpSemanticsTreeInTraversalOrder({ |
| @required String isolateId, |
| }) async { |
| final Map<String, Object> response = await invokeFlutterExtensionRpcRaw( |
| 'ext.flutter.debugDumpSemanticsTreeInTraversalOrder', |
| isolateId: isolateId, |
| ); |
| return response != null ? response['data']?.toString() : ''; |
| } |
| |
| Future<String> flutterDebugDumpSemanticsTreeInInverseHitTestOrder({ |
| @required String isolateId, |
| }) async { |
| final Map<String, Object> response = await invokeFlutterExtensionRpcRaw( |
| 'ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder', |
| isolateId: isolateId, |
| ); |
| return response != null ? response['data']?.toString() : ''; |
| } |
| |
| Future<Map<String, dynamic>> _flutterToggle(String name, { |
| @required String isolateId, |
| }) async { |
| Map<String, dynamic> state = await invokeFlutterExtensionRpcRaw( |
| 'ext.flutter.$name', |
| isolateId: isolateId, |
| ); |
| if (state != null && state.containsKey('enabled') && state['enabled'] is String) { |
| state = await invokeFlutterExtensionRpcRaw( |
| 'ext.flutter.$name', |
| isolateId: isolateId, |
| args: <String, dynamic>{ |
| 'enabled': state['enabled'] == 'true' ? 'false' : 'true', |
| }, |
| ); |
| } |
| |
| return state; |
| } |
| |
| Future<Map<String, dynamic>> flutterToggleDebugPaintSizeEnabled({ |
| @required String isolateId, |
| }) => _flutterToggle('debugPaint', isolateId: isolateId); |
| |
| Future<Map<String, dynamic>> flutterToggleDebugCheckElevationsEnabled({ |
| @required String isolateId, |
| }) => _flutterToggle('debugCheckElevationsEnabled', isolateId: isolateId); |
| |
| Future<Map<String, dynamic>> flutterTogglePerformanceOverlayOverride({ |
| @required String isolateId, |
| }) => _flutterToggle('showPerformanceOverlay', isolateId: isolateId); |
| |
| Future<Map<String, dynamic>> flutterToggleWidgetInspector({ |
| @required String isolateId, |
| }) => _flutterToggle('inspector.show', isolateId: isolateId); |
| |
| Future<Map<String,dynamic>> flutterToggleInvertOversizedImages({ |
| @required String isolateId, |
| }) => _flutterToggle('invertOversizedImages', isolateId: isolateId); |
| |
| Future<Map<String, dynamic>> flutterToggleProfileWidgetBuilds({ |
| @required String isolateId, |
| }) => _flutterToggle('profileWidgetBuilds', isolateId: isolateId); |
| |
| Future<Map<String, dynamic>> flutterDebugAllowBanner(bool show, { |
| @required String isolateId, |
| }) { |
| return invokeFlutterExtensionRpcRaw( |
| 'ext.flutter.debugAllowBanner', |
| isolateId: isolateId, |
| args: <String, dynamic>{'enabled': show ? 'true' : 'false'}, |
| ); |
| } |
| |
| Future<Map<String, dynamic>> flutterReassemble({ |
| @required String isolateId, |
| }) { |
| return invokeFlutterExtensionRpcRaw( |
| 'ext.flutter.reassemble', |
| isolateId: isolateId, |
| ); |
| } |
| |
| Future<Map<String, dynamic>> flutterFastReassemble({ |
| @required String isolateId, |
| @required String className, |
| }) { |
| return invokeFlutterExtensionRpcRaw( |
| 'ext.flutter.fastReassemble', |
| isolateId: isolateId, |
| args: <String, Object>{ |
| 'className': className, |
| }, |
| ); |
| } |
| |
| Future<bool> flutterAlreadyPaintedFirstUsefulFrame({ |
| @required String isolateId, |
| }) async { |
| final Map<String, dynamic> result = await invokeFlutterExtensionRpcRaw( |
| 'ext.flutter.didSendFirstFrameRasterizedEvent', |
| isolateId: isolateId, |
| ); |
| // result might be null when the service extension is not initialized |
| return result != null && result['enabled'] == 'true'; |
| } |
| |
| Future<Map<String, dynamic>> uiWindowScheduleFrame({ |
| @required String isolateId, |
| }) { |
| return invokeFlutterExtensionRpcRaw( |
| 'ext.ui.window.scheduleFrame', |
| isolateId: isolateId, |
| ); |
| } |
| |
| Future<Map<String, dynamic>> flutterEvictAsset(String assetPath, { |
| @required String isolateId, |
| }) { |
| return invokeFlutterExtensionRpcRaw( |
| 'ext.flutter.evict', |
| isolateId: isolateId, |
| args: <String, dynamic>{ |
| 'value': assetPath, |
| }, |
| ); |
| } |
| |
| /// Exit the application by calling [exit] from `dart:io`. |
| /// |
| /// This method is only supported by certain embedders. This is |
| /// described by [Device.supportsFlutterExit]. |
| Future<bool> flutterExit({ |
| @required String isolateId, |
| }) async { |
| try { |
| final Map<String, Object> result = await invokeFlutterExtensionRpcRaw( |
| 'ext.flutter.exit', |
| isolateId: isolateId, |
| ); |
| // A response of `null` indicates that `invokeFlutterExtensionRpcRaw` caught an RPCError |
| // with a missing method code. This can happen when attempting to quit a flutter app |
| // that never registered the methods in the bindings. |
| if (result == null) { |
| return false; |
| } |
| } on vm_service.SentinelException { |
| // Do nothing on sentinel, the isolate already exited. |
| } on vm_service.RPCError { |
| // Do nothing on RPCError, the isolate already exited. |
| } |
| return true; |
| } |
| |
| /// Return the current platform override for the flutter view running with |
| /// the main isolate [isolateId]. |
| /// |
| /// If a non-null value is provided for [platform], the platform override |
| /// is updated with this value. |
| Future<String> flutterPlatformOverride({ |
| String platform, |
| @required String isolateId, |
| }) async { |
| final Map<String, dynamic> result = await invokeFlutterExtensionRpcRaw( |
| 'ext.flutter.platformOverride', |
| isolateId: isolateId, |
| args: platform != null |
| ? <String, dynamic>{'value': platform} |
| : <String, String>{}, |
| ); |
| if (result != null && result['value'] is String) { |
| return result['value'] as String; |
| } |
| return 'unknown'; |
| } |
| |
| /// Return the current brightness value for the flutter view running with |
| /// the main isolate [isolateId]. |
| /// |
| /// If a non-null value is provided for [brightness], the brightness override |
| /// is updated with this value. |
| Future<Brightness> flutterBrightnessOverride({ |
| Brightness brightness, |
| @required String isolateId, |
| }) async { |
| final Map<String, dynamic> result = await invokeFlutterExtensionRpcRaw( |
| 'ext.flutter.brightnessOverride', |
| isolateId: isolateId, |
| args: brightness != null |
| ? <String, dynamic>{'value': brightness.toString()} |
| : <String, String>{}, |
| ); |
| if (result != null && result['value'] is String) { |
| return (result['value'] as String) == 'Brightness.light' |
| ? Brightness.light |
| : Brightness.dark; |
| } |
| return null; |
| } |
| |
| Future<vm_service.Response> _checkedCallServiceExtension( |
| String method, { |
| Map<String, dynamic> args, |
| }) async { |
| try { |
| return await service.callServiceExtension(method, args: args); |
| } on vm_service.RPCError catch (err) { |
| // If an application is not using the framework or the VM service |
| // disappears while handling a request, return null. |
| if ((err.code == RPCErrorCodes.kMethodNotFound) |
| || (err.code == RPCErrorCodes.kServiceDisappeared)) { |
| return null; |
| } |
| rethrow; |
| } |
| } |
| |
| /// Invoke a flutter extension method, if the flutter extension is not |
| /// available, returns null. |
| Future<Map<String, dynamic>> invokeFlutterExtensionRpcRaw( |
| String method, { |
| @required String isolateId, |
| Map<String, dynamic> args, |
| }) async { |
| final vm_service.Response response = await _checkedCallServiceExtension( |
| method, |
| args: <String, Object>{ |
| 'isolateId': isolateId, |
| ...?args, |
| }, |
| ); |
| return response?.json; |
| } |
| |
| /// List all [FlutterView]s attached to the current VM. |
| /// |
| /// If this returns an empty list, it will poll forever unless [returnEarly] |
| /// is set to true. |
| /// |
| /// By default, the poll duration is 50 milliseconds. |
| Future<List<FlutterView>> getFlutterViews({ |
| bool returnEarly = false, |
| Duration delay = const Duration(milliseconds: 50), |
| }) async { |
| while (true) { |
| final vm_service.Response response = await callMethodWrapper( |
| kListViewsMethod, |
| ); |
| if (response == null) { |
| // The service may have disappeared mid-request. |
| // Return an empty list now, and let the shutdown logic elsewhere deal |
| // with cleaning up. |
| return <FlutterView>[]; |
| } |
| final List<Object> rawViews = response.json['views'] as List<Object>; |
| final List<FlutterView> views = <FlutterView>[ |
| for (final Object rawView in rawViews) |
| FlutterView.parse(rawView as Map<String, Object>) |
| ]; |
| if (views.isNotEmpty || returnEarly) { |
| return views; |
| } |
| await Future<void>.delayed(delay); |
| } |
| } |
| |
| /// Waits for a signal from the VM service that [extensionName] is registered. |
| /// |
| /// Looks at the list of loaded extensions for first Flutter view, as well as |
| /// the stream of added extensions to avoid races. |
| /// |
| /// If [webIsolate] is true, this uses the VM Service isolate list instead of |
| /// the `_flutter.listViews` method, which is not implemented by DWDS. |
| /// |
| /// Throws a [VmServiceDisappearedException] should the VM Service disappear |
| /// while making calls to it. |
| Future<vm_service.IsolateRef> findExtensionIsolate(String extensionName, {bool webIsolate = false}) async { |
| try { |
| await service.streamListen(vm_service.EventStreams.kIsolate); |
| } on vm_service.RPCError { |
| // Do nothing, since the tool is already subscribed. |
| } |
| |
| final Completer<vm_service.IsolateRef> extensionAdded = Completer<vm_service.IsolateRef>(); |
| StreamSubscription<vm_service.Event> isolateEvents; |
| isolateEvents = service.onIsolateEvent.listen((vm_service.Event event) { |
| if (event.kind == vm_service.EventKind.kServiceExtensionAdded |
| && event.extensionRPC == extensionName) { |
| isolateEvents.cancel(); |
| extensionAdded.complete(event.isolate); |
| } |
| }); |
| |
| try { |
| final List<vm_service.IsolateRef> refs = await _getIsolateRefs(webIsolate); |
| for (final vm_service.IsolateRef ref in refs) { |
| final vm_service.Isolate isolate = await getIsolateOrNull(ref.id); |
| if (isolate != null && isolate.extensionRPCs.contains(extensionName)) { |
| return ref; |
| } |
| } |
| return await extensionAdded.future; |
| } finally { |
| await isolateEvents.cancel(); |
| try { |
| await service.streamCancel(vm_service.EventStreams.kIsolate); |
| } on vm_service.RPCError { |
| // It's ok for cleanup to fail, such as when the service disappears. |
| } |
| } |
| } |
| |
| Future<List<vm_service.IsolateRef>> _getIsolateRefs(bool webIsolate) async { |
| if (webIsolate) { |
| final List<vm_service.IsolateRef> refs = (await service.getVM()).isolates; |
| if (refs.isEmpty) { |
| throw VmServiceDisappearedException(); |
| } |
| return refs; |
| } |
| final List<FlutterView> flutterViews = await getFlutterViews(); |
| if (flutterViews.isEmpty) { |
| throw VmServiceDisappearedException(); |
| } |
| |
| final List<vm_service.IsolateRef> refs = <vm_service.IsolateRef>[]; |
| for (final FlutterView flutterView in flutterViews) { |
| if (flutterView.uiIsolate != null) { |
| refs.add(flutterView.uiIsolate); |
| } |
| } |
| return refs; |
| } |
| |
| /// Attempt to retrieve the isolate with id [isolateId], or `null` if it has |
| /// been collected. |
| Future<vm_service.Isolate> getIsolateOrNull(String isolateId) { |
| return service.getIsolate(isolateId) |
| .catchError((dynamic error, StackTrace stackTrace) { |
| return null; |
| }, test: (dynamic error) { |
| return (error is vm_service.SentinelException) || |
| (error is vm_service.RPCError && error.code == RPCErrorCodes.kServiceDisappeared); |
| }); |
| } |
| |
| /// Create a new development file system on the device. |
| Future<vm_service.Response> createDevFS(String fsName) { |
| // Call the unchecked version of `callServiceExtension` because the caller |
| // has custom handling of certain RPCErrors. |
| return service.callServiceExtension( |
| '_createDevFS', |
| args: <String, dynamic>{'fsName': fsName}, |
| ); |
| } |
| |
| /// Delete an existing file system. |
| Future<void> deleteDevFS(String fsName) async { |
| await _checkedCallServiceExtension( |
| '_deleteDevFS', |
| args: <String, dynamic>{'fsName': fsName}, |
| ); |
| } |
| |
| Future<vm_service.Response> screenshot() { |
| return _checkedCallServiceExtension(kScreenshotMethod); |
| } |
| |
| Future<vm_service.Response> screenshotSkp() { |
| return _checkedCallServiceExtension(kScreenshotSkpMethod); |
| } |
| |
| /// Set the VM timeline flags. |
| Future<void> setTimelineFlags(List<String> recordedStreams) async { |
| assert(recordedStreams != null); |
| await _checkedCallServiceExtension( |
| 'setVMTimelineFlags', |
| args: <String, dynamic>{ |
| 'recordedStreams': recordedStreams, |
| }, |
| ); |
| } |
| |
| Future<vm_service.Response> getTimeline() { |
| return _checkedCallServiceExtension('getVMTimeline'); |
| } |
| |
| Future<void> dispose() async { |
| await service.dispose(); |
| } |
| } |
| |
| /// Thrown when the VM Service disappears while calls are being made to it. |
| class VmServiceDisappearedException implements Exception {} |
| |
| /// Whether the event attached to an [Isolate.pauseEvent] should be considered |
| /// a "pause" event. |
| bool isPauseEvent(String kind) { |
| return kind == vm_service.EventKind.kPauseStart || |
| kind == vm_service.EventKind.kPauseExit || |
| kind == vm_service.EventKind.kPauseBreakpoint || |
| kind == vm_service.EventKind.kPauseInterrupted || |
| kind == vm_service.EventKind.kPauseException || |
| kind == vm_service.EventKind.kPausePostRequest || |
| kind == vm_service.EventKind.kNone; |
| } |
| |
| /// A brightness enum that matches the values https://github.com/flutter/engine/blob/3a96741247528133c0201ab88500c0c3c036e64e/lib/ui/window.dart#L1328 |
| /// Describes the contrast of a theme or color palette. |
| enum Brightness { |
| /// The color is dark and will require a light text color to achieve readable |
| /// contrast. |
| /// |
| /// For example, the color might be dark grey, requiring white text. |
| dark, |
| |
| /// The color is light and will require a dark text color to achieve readable |
| /// contrast. |
| /// |
| /// For example, the color might be bright white, requiring black text. |
| light, |
| } |
| |
| /// Process a VM service log event into a string message. |
| String processVmServiceMessage(vm_service.Event event) { |
| final String message = utf8.decode(base64.decode(event.bytes)); |
| // Remove extra trailing newlines appended by the vm service. |
| if (message.endsWith('\n')) { |
| return message.substring(0, message.length - 1); |
| } |
| return message; |
| } |