diff --git a/packages/_flutter_web_build_script/pubspec.yaml b/packages/_flutter_web_build_script/pubspec.yaml index d9ebe20d9fc..545dab259df 100644 --- a/packages/_flutter_web_build_script/pubspec.yaml +++ b/packages/_flutter_web_build_script/pubspec.yaml @@ -14,9 +14,9 @@ dependencies: test_api: 0.2.15 test_core: 0.3.3 - build_runner: 1.8.1 + build_runner: 1.8.0 build_test: 1.0.0 - build_runner_core: 5.0.0 + build_runner_core: 4.5.3 dart_style: 1.3.3 code_builder: 3.2.1 build: 1.2.2 @@ -95,4 +95,4 @@ dartdoc: # Exclude this package from the hosted API docs. nodoc: true -# PUBSPEC CHECKSUM: 7c61 +# PUBSPEC CHECKSUM: e267 diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart index cf12e58b563..11171ab2e0d 100644 --- a/packages/flutter_tools/lib/src/devfs.dart +++ b/packages/flutter_tools/lib/src/devfs.dart @@ -4,8 +4,8 @@ import 'dart:async'; +import 'package:json_rpc_2/json_rpc_2.dart' as rpc; import 'package:meta/meta.dart'; -import 'package:vm_service/vm_service.dart' as vmservice; import 'asset.dart'; import 'base/context.dart'; @@ -423,7 +423,7 @@ class DevFS { globals.printTrace('DevFS: Creating new filesystem on the device ($_baseUri)'); try { _baseUri = await _operations.create(fsName); - } on vmservice.RPCError catch (rpcException) { + } on rpc.RpcException catch (rpcException) { // 1001 is kFileSystemAlreadyExists in //dart/runtime/vm/json_stream.h if (rpcException.code != 1001) { rethrow; diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart index 11c2d32b858..b540cdf5e3e 100644 --- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart +++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart @@ -620,7 +620,7 @@ class FuchsiaDevice extends Device { // loopback (::1). final Uri uri = Uri.parse('http://[$_ipv6Loopback]:$port'); final VMService vmService = await VMService.connect(uri); - await vmService.getVMOld(); + await vmService.getVM(); await vmService.refreshViews(); for (final FlutterView flutterView in vmService.vm.views) { if (flutterView.uiIsolate == null) { @@ -717,7 +717,7 @@ class FuchsiaIsolateDiscoveryProtocol { continue; } } - await service.getVMOld(); + await service.getVM(); await service.refreshViews(); for (final FlutterView flutterView in service.vm.views) { if (flutterView.uiIsolate == null) { diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 5a34d94f3b8..f08c66af968 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -7,11 +7,11 @@ import 'dart:math' as math; import 'package:meta/meta.dart'; import 'package:platform/platform.dart'; -import 'package:vm_service/vm_service.dart' as vm_service; import 'package:process/process.dart'; import '../application_package.dart'; import '../artifacts.dart'; +import '../base/common.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/logger.dart'; @@ -514,7 +514,7 @@ class IOSDeviceLogReader extends DeviceLogReader { // and "Flutter". The regex tries to strike a balance between not producing // false positives and not producing false negatives. _anyLineRegex = RegExp(r'\w+(\([^)]*\))?\[\d+\] <[A-Za-z]+>: '); - _loggingSubscriptions = >[]; + _loggingSubscriptions = >[]; } /// Create a new [IOSDeviceLogReader]. @@ -554,7 +554,7 @@ class IOSDeviceLogReader extends DeviceLogReader { RegExp _anyLineRegex; StreamController _linesController; - List> _loggingSubscriptions; + List> _loggingSubscriptions; @override Stream get logLines => _linesController.stream; @@ -575,20 +575,18 @@ class IOSDeviceLogReader extends DeviceLogReader { if (_majorSdkVersion < _minimumUniversalLoggingSdkVersion) { return; } - try { - await connectedVmService.streamListen('Stdout'); - } on vm_service.RPCError { - // Do nothing, since the tool is already subscribed. - } - _loggingSubscriptions.add(connectedVmService.onStdoutEvent.listen((vm_service.Event event) { - final String message = utf8.decode(base64.decode(event.bytes)); - if (message.isNotEmpty) { - _linesController.add(message); + // The VM service will not publish logging events unless the debug stream is being listened to. + // onDebugEvent listens to this stream as a side effect. + unawaited(connectedVmService.onDebugEvent); + _loggingSubscriptions.add((await connectedVmService.onStdoutEvent).listen((ServiceEvent event) { + final String logMessage = event.message; + if (logMessage.isNotEmpty) { + _linesController.add(logMessage); } })); } - void _listenToSysLog() { + void _listenToSysLog () { // syslog is not written on iOS 13+. if (_majorSdkVersion >= _minimumUniversalLoggingSdkVersion) { return; @@ -643,7 +641,7 @@ class IOSDeviceLogReader extends DeviceLogReader { @override void dispose() { - for (final StreamSubscription loggingSubscription in _loggingSubscriptions) { + for (final StreamSubscription loggingSubscription in _loggingSubscriptions) { loggingSubscription.cancel(); } _idevicesyslogProcess?.kill(); diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 45b7d41331d..47553f80183 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -233,7 +233,7 @@ class FlutterDevice { : vmService.vm.views).toList(); } - Future getVMs() => vmService.getVMOld(); + Future getVMs() => vmService.getVM(); Future exitApps() async { if (!device.supportsFlutterExit) { diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index 8047c8d1915..727e062e1b0 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -3,8 +3,10 @@ // found in the LICENSE file. import 'dart:async'; -import 'package:vm_service/vm_service.dart' as vm_service; + import 'package:platform/platform.dart'; +import 'package:json_rpc_2/error_code.dart' as rpc_error_code; +import 'package:json_rpc_2/json_rpc_2.dart' as rpc; import 'package:meta/meta.dart'; import 'package:pool/pool.dart'; import 'base/async_guard.dart'; @@ -108,10 +110,9 @@ class HotRunner extends ResidentRunner { // TODO(cbernaschina): check that isolateId is the id of the UI isolate. final OperationResult result = await restart(pause: pause); if (!result.isOk) { - throw vm_service.RPCError( + throw rpc.RpcException( + rpc_error_code.INTERNAL_ERROR, 'Unable to reload sources', - RPCErrorCodes.kInternalError, - '', ); } } @@ -120,10 +121,9 @@ class HotRunner extends ResidentRunner { final OperationResult result = await restart(fullRestart: true, pause: pause); if (!result.isOk) { - throw vm_service.RPCError( + throw rpc.RpcException( + rpc_error_code.INTERNAL_ERROR, 'Unable to restart', - RPCErrorCodes.kInternalError, - '', ); } } @@ -564,15 +564,16 @@ class HotRunner extends ResidentRunner { if (benchmarkMode) { final List> isolateNotifications = >[]; for (final FlutterDevice device in flutterDevices) { - try { - await device.vmService.streamListen('Isolate'); - } on vm_service.RPCError { - // Do nothing, we're already subcribed. - } for (final FlutterView view in device.views) { isolateNotifications.add( - view.owner.vm.vmService.onIsolateEvent.firstWhere((vm_service.Event event) { - return event.kind == vm_service.EventKind.kServiceExtensionAdded; + view.owner.vm.vmService.onIsolateEvent + .then((Stream serviceEvents) async { + await for (final ServiceEvent serviceEvent in serviceEvents) { + if (serviceEvent.owner.name.contains('_spawn') + && serviceEvent.kind == ServiceEvent.kIsolateExit) { + return; + } + } }), ); } @@ -719,12 +720,9 @@ class HotRunner extends ResidentRunner { if (!result.isOk) { restartEvent = 'restart-failed'; } - } on vm_service.SentinelException catch (err, st) { + } on rpc.RpcException { restartEvent = 'exception'; - return OperationResult(1, 'hot restart failed to complete: $err\n$st', fatal: true); - } on vm_service.RPCError catch (err, st) { - restartEvent = 'exception'; - return OperationResult(1, 'hot restart failed to complete: $err\n$st', fatal: true); + return OperationResult(1, 'hot restart failed to complete', fatal: true); } finally { HotEvent(restartEvent, targetPlatform: targetPlatform, @@ -766,7 +764,7 @@ class HotRunner extends ResidentRunner { ); }, ); - } on vm_service.RPCError { + } on rpc.RpcException { HotEvent('exception', targetPlatform: targetPlatform, sdkName: sdkName, diff --git a/packages/flutter_tools/lib/src/test/coverage_collector.dart b/packages/flutter_tools/lib/src/test/coverage_collector.dart index 9523d645e39..e1fa2cf15c5 100644 --- a/packages/flutter_tools/lib/src/test/coverage_collector.dart +++ b/packages/flutter_tools/lib/src/test/coverage_collector.dart @@ -189,7 +189,7 @@ Future> collect(Uri serviceUri, bool Function(String) libra Future Function(Uri) connector = _defaultConnect, }) async { final VMService vmService = await connector(serviceUri); - await vmService.getVMOld(); + await vmService.getVM(); final Map result = await _getAllCoverage( vmService, libraryPredicate); await vmService.close(); @@ -197,7 +197,7 @@ Future> collect(Uri serviceUri, bool Function(String) libra } Future> _getAllCoverage(VMService service, bool Function(String) libraryPredicate) async { - await service.getVMOld(); + await service.getVM(); final List> coverage = >[]; for (final Isolate isolateRef in service.vm.isolates) { await isolateRef.load(); diff --git a/packages/flutter_tools/lib/src/tracing.dart b/packages/flutter_tools/lib/src/tracing.dart index 5ea6cfb1ede..25402b87693 100644 --- a/packages/flutter_tools/lib/src/tracing.dart +++ b/packages/flutter_tools/lib/src/tracing.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'package:vm_service/vm_service.dart' as vm_service; import 'base/file_system.dart'; import 'base/logger.dart'; @@ -46,8 +45,7 @@ class Tracing { ); try { final Completer whenFirstFrameRendered = Completer(); - await vmService.streamListen('Extension'); - vmService.onExtensionEvent.listen((vm_service.Event event) { + (await vmService.onExtensionEvent).listen((ServiceEvent event) { if (event.extensionKind == 'Flutter.FirstFrame') { whenFirstFrameRendered.complete(); } diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart index 8459df0beca..fbcc92af359 100644 --- a/packages/flutter_tools/lib/src/vmservice.dart +++ b/packages/flutter_tools/lib/src/vmservice.dart @@ -5,14 +5,18 @@ import 'dart:async'; import 'dart:math' as math; +import 'package:json_rpc_2/error_code.dart' as rpc_error_code; +import 'package:json_rpc_2/json_rpc_2.dart' as rpc; import 'package:meta/meta.dart' show required; -import 'package:vm_service/vm_service.dart' as vm_service; +import 'package:stream_channel/stream_channel.dart'; +import 'package:web_socket_channel/io.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; import 'base/common.dart'; import 'base/context.dart'; import 'base/io.dart' as io; import 'base/utils.dart'; -import 'convert.dart' show base64, json, utf8; +import 'convert.dart' show base64, utf8; import 'device.dart'; import 'globals.dart' as globals; import 'version.dart'; @@ -21,24 +25,10 @@ import 'version.dart'; /// for [WebSocket]s (used by tests). typedef WebSocketConnector = Future Function(String url, {io.CompressionOptions compression}); -WebSocketConnector _openChannel = _defaultOpenChannel; +/// A function that opens a two-way communication channel to the specified [uri]. +typedef _OpenChannel = Future> Function(Uri uri, {io.CompressionOptions compression}); -/// The error codes for the JSON-RPC standard. -/// -/// 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.s - static const int kServerError = -32000; -} +_OpenChannel _openChannel = _defaultOpenChannel; /// A function that reacts to the invocation of the 'reloadSources' service. /// @@ -74,9 +64,7 @@ typedef ReloadMethod = Future Function({ String libraryId, }); -Future _defaultOpenChannel(String url, { - io.CompressionOptions compression = io.CompressionOptions.compressionDefault -}) async { +Future> _defaultOpenChannel(Uri uri, {io.CompressionOptions compression = io.CompressionOptions.compressionDefault}) async { Duration delay = const Duration(milliseconds: 100); int attempts = 0; io.WebSocket socket; @@ -102,14 +90,14 @@ Future _defaultOpenChannel(String url, { while (socket == null) { attempts += 1; try { - socket = await constructor(url, compression: compression); + socket = await constructor(uri.toString(), compression: compression); } on io.WebSocketException catch (e) { await handleError(e); } on io.SocketException catch (e) { await handleError(e); } } - return socket; + return IOWebSocketChannel(socket).cast(); } /// Override `VMServiceConnector` in [context] to return a different VMService @@ -124,10 +112,10 @@ typedef VMServiceConnector = Future Function(Uri httpUri, { }); /// A connection to the Dart VM Service. -/// -/// This also implements the package:vm_service API to enable a gradual migration. -class VMService implements vm_service.VmService { +// TODO(mklim): Test this, https://github.com/flutter/flutter/issues/23031 +class VMService { VMService( + this._peer, this.httpAddress, this.wsAddress, ReloadSources reloadSources, @@ -135,49 +123,38 @@ class VMService implements vm_service.VmService { CompileExpression compileExpression, Device device, ReloadMethod reloadMethod, - this._delegateService, - this.streamClosedCompleter, - Stream secondary, ) { _vm = VM._empty(this); + _peer.listen().catchError(_connectionError.completeError); - // TODO(jonahwilliams): this is temporary to support the current vm_service - // semantics of update-in-place. - secondary.listen((dynamic rawData) { - final String message = rawData as String; - final dynamic map = json.decode(message); - if (map != null && map['method'] == 'streamNotify') { - _handleStreamNotify(map['params'] as Map); - } + _peer.registerMethod('streamNotify', (rpc.Parameters event) { + _handleStreamNotify(event.asMap.cast()); }); if (reloadSources != null) { - _delegateService.registerServiceCallback('reloadSources', (Map params) async { + _peer.registerMethod('reloadSources', (rpc.Parameters params) async { final String isolateId = params['isolateId'].value as String; - final bool force = params['force'] as bool ?? false; - final bool pause = params['pause'] as bool ?? false; + final bool force = params.asMap['force'] as bool ?? false; + final bool pause = params.asMap['pause'] as bool ?? false; if (isolateId.isEmpty) { - throw vm_service.RPCError( - "Invalid 'isolateId': $isolateId", - RPCErrorCodes.kInvalidParams, - '', - ); + throw rpc.RpcException.invalidParams("Invalid 'isolateId': $isolateId"); } try { await reloadSources(isolateId, force: force, pause: pause); return {'type': 'Success'}; - } on vm_service.RPCError { + } on rpc.RpcException { rethrow; } on Exception catch (e, st) { - throw vm_service.RPCError( - 'Error during Sources Reload: $e\n$st', - RPCErrorCodes.kServerError, - '', - ); + throw rpc.RpcException(rpc_error_code.SERVER_ERROR, + 'Error during Sources Reload: $e\n$st'); } }); - _delegateService.registerService('reloadSources', 'Flutter Tools'); + + _peer.sendNotification('registerService', { + 'service': 'reloadSources', + 'alias': 'Flutter Tools', + }); } @@ -191,23 +168,15 @@ class VMService implements vm_service.VmService { // if the build method of a StatelessWidget is updated, this is the name of class. // If the build method of a StatefulWidget is updated, then this is the name // of the Widget class that created the State object. - _delegateService.registerServiceCallback('reloadMethod', (Map params) async { - final String libraryId = params['library'] as String; - final String classId = params['class'] as String; + _peer.registerMethod('reloadMethod', (rpc.Parameters params) async { + final String libraryId = params['library'].value as String; + final String classId = params['class'].value as String; if (libraryId.isEmpty) { - throw vm_service.RPCError( - "Invalid 'libraryId': $libraryId", - RPCErrorCodes.kInvalidParams, - '', - ); + throw rpc.RpcException.invalidParams("Invalid 'libraryId': $libraryId"); } if (classId.isEmpty) { - throw vm_service.RPCError( - "Invalid 'classId': $classId", - RPCErrorCodes.kInvalidParams, - '', - ); + throw rpc.RpcException.invalidParams("Invalid 'classId': $classId"); } globals.printTrace('reloadMethod not yet supported, falling back to hot reload'); @@ -218,97 +187,113 @@ class VMService implements vm_service.VmService { classId: classId, ); return {'type': 'Success'}; - } on vm_service.RPCError { + } on rpc.RpcException { rethrow; } on Exception catch (e, st) { - throw vm_service.RPCError('Error during Sources Reload: $e\n$st', -32000, ''); + throw rpc.RpcException(rpc_error_code.SERVER_ERROR, + 'Error during Sources Reload: $e\n$st'); } }); - _delegateService.registerService('reloadMethod', 'Flutter Tools'); + _peer.sendNotification('registerService', { + 'service': 'reloadMethod', + 'alias': 'Flutter Tools', + }); } if (restart != null) { - _delegateService.registerServiceCallback('hotRestart', (Map params) async { - final bool pause = params['pause'] as bool ?? false; + _peer.registerMethod('hotRestart', (rpc.Parameters params) async { + final bool pause = params.asMap['pause'] as bool ?? false; + + if (pause is! bool) { + throw rpc.RpcException.invalidParams("Invalid 'pause': $pause"); + } + try { await restart(pause: pause); return {'type': 'Success'}; - } on vm_service.RPCError { + } on rpc.RpcException { rethrow; } on Exception catch (e, st) { - throw vm_service.RPCError( - 'Error during Hot Restart: $e\n$st', - RPCErrorCodes.kServerError, - '', - ); + throw rpc.RpcException(rpc_error_code.SERVER_ERROR, + 'Error during Hot Restart: $e\n$st'); } }); - _delegateService.registerService('hotRestart', 'Flutter Tools'); + + _peer.sendNotification('registerService', { + 'service': 'hotRestart', + 'alias': 'Flutter Tools', + }); } - _delegateService.registerServiceCallback('flutterVersion', (Map params) async { + _peer.registerMethod('flutterVersion', (rpc.Parameters params) async { final FlutterVersion version = context.get() ?? FlutterVersion(); final Map versionJson = version.toJson(); versionJson['frameworkRevisionShort'] = version.frameworkRevisionShort; versionJson['engineRevisionShort'] = version.engineRevisionShort; return versionJson; }); - _delegateService.registerService('flutterVersion', 'Flutter Tools'); + + _peer.sendNotification('registerService', { + 'service': 'flutterVersion', + 'alias': 'Flutter Tools', + }); if (compileExpression != null) { - _delegateService.registerServiceCallback('compileExpression', (Map params) async { - final String isolateId = params['isolateId'] as String; + _peer.registerMethod('compileExpression', (rpc.Parameters params) async { + final String isolateId = params['isolateId'].asString; if (isolateId is! String || isolateId.isEmpty) { - throw throw vm_service.RPCError( - "Invalid 'isolateId': $isolateId", - RPCErrorCodes.kInvalidParams, - '', - ); + throw rpc.RpcException.invalidParams( + "Invalid 'isolateId': $isolateId"); } - final String expression = params['expression'] as String; + final String expression = params['expression'].asString; if (expression is! String || expression.isEmpty) { - throw throw vm_service.RPCError( - "Invalid 'expression': $expression", - RPCErrorCodes.kInvalidParams, - '', - ); + throw rpc.RpcException.invalidParams( + "Invalid 'expression': $expression"); } - final List definitions = List.from(params['definitions'] as List); - final List typeDefinitions = List.from(params['typeDefinitions'] as List); - final String libraryUri = params['libraryUri'] as String; - final String klass = params['klass'] as String; - final bool isStatic = params['isStatic'] as bool ?? false; + final List definitions = + List.from(params['definitions'].asList); + final List typeDefinitions = + List.from(params['typeDefinitions'].asList); + final String libraryUri = params['libraryUri'].asString; + final String klass = params['klass'].exists ? params['klass'].asString : null; + final bool isStatic = params['isStatic'].asBoolOr(false); + try { final String kernelBytesBase64 = await compileExpression(isolateId, expression, definitions, typeDefinitions, libraryUri, klass, isStatic); - return { - 'type': 'Success', - 'result': { - 'result': {'kernelBytes': kernelBytesBase64}, - }, - }; - } on vm_service.RPCError { + return {'type': 'Success', + 'result': {'kernelBytes': kernelBytesBase64}}; + } on rpc.RpcException { rethrow; } on Exception catch (e, st) { - throw vm_service.RPCError( - 'Error during expression compilation: $e\n$st', - RPCErrorCodes.kServerError, - '', - ); + throw rpc.RpcException(rpc_error_code.SERVER_ERROR, + 'Error during expression compilation: $e\n$st'); } }); - _delegateService.registerService('compileExpression', 'Flutter Tools'); + + _peer.sendNotification('registerService', { + 'service': 'compileExpression', + 'alias': 'Flutter Tools', + }); } if (device != null) { - _delegateService.registerServiceCallback('flutterMemoryInfo', (Map params) async { + _peer.registerMethod('flutterMemoryInfo', (rpc.Parameters params) async { final MemoryInfo result = await device.queryMemoryInfo(); return result.toJson(); }); - _delegateService.registerService('flutterMemoryInfo', 'Flutter Tools'); + _peer.sendNotification('registerService', { + 'service': 'flutterMemoryInfo', + 'alias': 'Flutter Tools', + }); } } + static void _unhandledError(dynamic error, dynamic stack) { + globals.logger.printTrace('Error in internal implementation of JSON RPC.\n$error\n$stack'); + assert(false); + } + /// Connect to a Dart VM Service at [httpUri]. /// /// If the [reloadSources] parameter is not null, the 'reloadSources' service @@ -346,36 +331,11 @@ class VMService implements vm_service.VmService { io.CompressionOptions compression = io.CompressionOptions.compressionDefault, Device device, }) async { - // Create an instance of the package:vm_service API in addition to the flutter - // tool's to allow gradual migration. - final Completer streamClosedCompleter = Completer(); - final Uri wsUri = httpUri.replace(scheme: 'ws', path: globals.fs.path.join(httpUri.path, 'ws')); - final io.WebSocket channel = await _openChannel(wsUri.toString(), compression: compression); - final StreamController primary = StreamController(); - final StreamController secondary = StreamController(); - channel.listen((dynamic data) { - primary.add(data); - secondary.add(data); - }, onDone: () { - primary.close(); - secondary.close(); - streamClosedCompleter.complete(); - }, onError: (dynamic error, StackTrace stackTrace) { - primary.addError(error, stackTrace); - secondary.addError(error, stackTrace); - }); - - final vm_service.VmService delegateService = vm_service.VmService( - primary.stream, - channel.add, - log: null, - disposeHandler: () async { - streamClosedCompleter.complete(); - }, - ); - + final StreamChannel channel = await _openChannel(wsUri, compression: compression); + final rpc.Peer peer = rpc.Peer.withoutJson(jsonDocument.bind(channel), onUnhandledError: _unhandledError); final VMService service = VMService( + peer, httpUri, wsUri, reloadSources, @@ -383,21 +343,17 @@ class VMService implements vm_service.VmService { compileExpression, device, reloadMethod, - delegateService, - streamClosedCompleter, - secondary.stream, ); - // 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(); + await service._sendRequest('getVersion', const {}); return service; } - final vm_service.VmService _delegateService; final Uri httpAddress; final Uri wsAddress; - final Completer streamClosedCompleter; + final rpc.Peer _peer; + final Completer> _connectionError = Completer>(); VM _vm; /// The singleton [VM] object. Owns [Isolate] and [FlutterView] objects. @@ -406,36 +362,46 @@ class VMService implements vm_service.VmService { final Map> _eventControllers = >{}; + final Set _listeningFor = {}; + /// Whether our connection to the VM service has been closed; - bool get isClosed => streamClosedCompleter.isCompleted; + bool get isClosed => _peer.isClosed; Future get done async { - return streamClosedCompleter.future; + await _peer.done; } - @override - Stream get onDebugEvent => onEvent('Debug'); + // Events + Future> get onDebugEvent => onEvent('Debug'); + Future> get onExtensionEvent => onEvent('Extension'); + // IsolateStart, IsolateRunnable, IsolateExit, IsolateUpdate, ServiceExtensionAdded + Future> get onIsolateEvent => onEvent('Isolate'); + Future> get onTimelineEvent => onEvent('Timeline'); + Future> get onStdoutEvent => onEvent('Stdout'); // WriteEvent - @override - Stream get onExtensionEvent => onEvent('Extension'); + // TODO(johnmccutchan): Add FlutterView events. - @override - Stream get onIsolateEvent => onEvent('Isolate'); - - @override - Stream get onTimelineEvent => onEvent('Timeline'); - - @override - Stream get onStdoutEvent => onEvent('Stdout'); - - @override - Future streamListen(String streamId) { - return _delegateService.streamListen(streamId); + /// Returns a stream of VM service events. + /// + /// This purposely returns a `Future>` rather than a `Stream` + /// because it first registers with the VM to receive events on the stream, + /// and only once the VM has acknowledged that the stream has started will + /// we return the associated stream. Any attempt to streamline this API into + /// returning `Stream` should take that into account to avoid race + /// conditions. + Future> onEvent(String streamId) async { + await _streamListen(streamId); + return _getEventController(streamId).stream; } - @override - Stream onEvent(String streamId) { - return _delegateService.onEvent(streamId); + Future> _sendRequest( + String method, + Map params, + ) { + return Future.any>(>>[ + _peer.sendRequest(method, params).then>(castStringKeyedMap), + _connectionError.future, + ]); } StreamController _getEventController(String eventName) { @@ -475,20 +441,19 @@ class VMService implements vm_service.VmService { _getEventController(streamId).add(event); } + Future _streamListen(String streamId) async { + if (!_listeningFor.contains(streamId)) { + _listeningFor.add(streamId); + await _sendRequest('streamListen', {'streamId': streamId}); + } + } + /// Reloads the VM. - Future getVMOld() async => await vm.reload(); + Future getVM() async => await vm.reload(); Future refreshViews({ bool waitForViews = false }) => vm.refreshViews(waitForViews: waitForViews); - Future close() async { - _delegateService?.dispose(); - } - - // To enable a gradual migration to package:vm_service - @override - dynamic noSuchMethod(Invocation invocation) { - throw UnsupportedError('${invocation.memberName} is not currently supported'); - } + Future close() async => await _peer.close(); } /// An error that is thrown when constructing/updating a service object. @@ -971,15 +936,36 @@ class VM extends ServiceObjectOwner { return Future.value(_isolateCache[isolateId]); } + static String _truncate(String message, int width, String ellipsis) { + assert(ellipsis.length < width); + if (message.length <= width) { + return message; + } + return message.substring(0, width - ellipsis.length) + ellipsis; + } + /// Invoke the RPC and return the raw response. Future> invokeRpcRaw( String method, { Map params = const {}, bool truncateLogs = true, }) async { - final vm_service.Response response = await _vmService - ._delegateService.callServiceExtension(method, args: params); - return response.json; + globals.printTrace('Sending to VM service: $method($params)'); + assert(params != null); + try { + final Map result = await _vmService._sendRequest(method, params); + final String resultString = + truncateLogs ? _truncate(result.toString(), 250, '...') : result.toString(); + globals.printTrace('Result: $resultString'); + return result; + } on WebSocketChannelException catch (error) { + throwToolExit('Error connecting to observatory: $error'); + return null; + } on rpc.RpcException catch (error) { + globals.printError('Error ${error.code} received from application: ${error.message}'); + globals.printTrace('${error.data}'); + rethrow; + } } /// Invoke the RPC and return a [ServiceObject] response. @@ -1278,17 +1264,13 @@ class Isolate extends ServiceObjectOwner { } final Map response = await invokeRpcRaw('_reloadSources', params: arguments); return response; - } on vm_service.RPCError catch (e) { + } on rpc.RpcException catch (e) { return Future>.value({ 'code': e.code, 'message': e.message, 'data': e.data, }); - } on vm_service.SentinelException catch (e) { - throwToolExit('Unexpected Sentinel while reloading sources: $e'); } - assert(false); - return null; } Future> getObject(Map objectRef) { @@ -1311,9 +1293,9 @@ class Isolate extends ServiceObjectOwner { }) async { try { return await invokeRpcRaw(method, params: params); - } on vm_service.RPCError catch (err) { + } on rpc.RpcException catch (e) { // If an application is not using the framework - if (err.code == RPCErrorCodes.kMethodNotFound) { + if (e.code == rpc_error_code.METHOD_NOT_FOUND) { return null; } rethrow; @@ -1509,13 +1491,10 @@ class FlutterView extends ServiceObject { final String viewId = id; // When this completer completes the isolate is running. final Completer completer = Completer(); - try { - await owner.vm.vmService.streamListen('Isolate'); - } on vm_service.RPCError { - // Do nothing, since the tool is already subscribed. - } - final StreamSubscription subscription = - owner.vm.vmService.onIsolateEvent.listen((vm_service.Event event) { + final StreamSubscription subscription = + (await owner.vm.vmService.onIsolateEvent).listen((ServiceEvent event) { + // TODO(johnmccutchan): Listen to the debug stream and catch initial + // launch errors. if (event.kind == ServiceEvent.kIsolateRunnable) { globals.printTrace('Isolate is runnable.'); if (!completer.isCompleted) { diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index d32a41ae651..590e377d531 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: flutter_template_images: 1.0.0 http: 0.12.0+4 intl: 0.16.1 + json_rpc_2: 2.1.0 meta: 1.1.8 multicast_dns: 0.2.2 mustache_template: 1.0.0+1 @@ -27,8 +28,10 @@ dependencies: process: 3.0.12 quiver: 2.1.3 stack_trace: 1.9.3 + stream_channel: 2.0.0 usage: 3.4.1 webdriver: 2.1.2 + web_socket_channel: 1.1.0 webkit_inspection_protocol: 0.5.0+1 xml: 3.6.1 yaml: 2.2.0 @@ -85,7 +88,6 @@ dependencies: source_maps: 0.10.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.7.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sse: 3.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - stream_channel: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" stream_transform: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -93,7 +95,6 @@ dependencies: typed_data: 1.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" uuid: 2.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 0.9.7+14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: collection: 1.14.12 @@ -111,4 +112,4 @@ dartdoc: # Exclude this package from the hosted API docs. nodoc: true -# PUBSPEC CHECKSUM: a585 +# PUBSPEC CHECKSUM: 53c2 diff --git a/packages/flutter_tools/test/general.shard/devfs_test.dart b/packages/flutter_tools/test/general.shard/devfs_test.dart index a635a76558b..401075885bb 100644 --- a/packages/flutter_tools/test/general.shard/devfs_test.dart +++ b/packages/flutter_tools/test/general.shard/devfs_test.dart @@ -14,8 +14,8 @@ import 'package:flutter_tools/src/base/net.dart'; import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/vmservice.dart'; +import 'package:json_rpc_2/json_rpc_2.dart' as rpc; import 'package:mockito/mockito.dart'; -import 'package:vm_service/vm_service.dart' as vm_service; import '../src/common.dart'; import '../src/context.dart'; @@ -389,7 +389,7 @@ class MockVM implements VM { Future> createDevFS(String fsName) async { _service.messages.add('create $fsName'); if (_devFSExists) { - throw vm_service.RPCError('File system already exists', kFileSystemAlreadyExists, ''); + throw rpc.RpcException(kFileSystemAlreadyExists, 'File system already exists'); } _devFSExists = true; return {'uri': '$_baseUri'}; diff --git a/packages/flutter_tools/test/general.shard/fuchsia/fuchsia_device_test.dart b/packages/flutter_tools/test/general.shard/fuchsia/fuchsia_device_test.dart index 43e2854f1d3..fd6e8a70585 100644 --- a/packages/flutter_tools/test/general.shard/fuchsia/fuchsia_device_test.dart +++ b/packages/flutter_tools/test/general.shard/fuchsia/fuchsia_device_test.dart @@ -596,7 +596,7 @@ void main() { .thenAnswer((Invocation invocation) async => [1]); when(portForwarder.forward(1)) .thenAnswer((Invocation invocation) async => 2); - when(vmService.getVMOld()) + when(vmService.getVM()) .thenAnswer((Invocation invocation) => Future.value(null)); when(vmService.refreshViews()) .thenAnswer((Invocation invocation) => Future.value(null)); diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart index 415a3eb5e5f..0510ef3154c 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart @@ -2,18 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/build_info.dart'; -import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/ios/devices.dart'; import 'package:flutter_tools/src/ios/mac.dart'; -import 'package:flutter_tools/src/vmservice.dart'; import 'package:mockito/mockito.dart'; -import 'package:vm_service/vm_service.dart'; import '../../src/common.dart'; import '../../src/context.dart'; @@ -142,39 +137,6 @@ Runner(libsystem_asl.dylib)[297] : libMobileGestalt ' with a non-Flutter log message following it.', ]); }); - - testWithoutContext('IOSDeviceLogReader can listen to VM Service logs', () async { - final MockVmService vmService = MockVmService(); - final DeviceLogReader logReader = IOSDeviceLogReader.test( - useSyslog: false, - iMobileDevice: IMobileDevice( - artifacts: artifacts, - processManager: processManager, - cache: fakeCache, - logger: logger, - ), - ); - final StreamController controller = StreamController(); - final Completer stdoutCompleter = Completer(); - when(vmService.streamListen('Stdout')).thenAnswer((Invocation invocation) { - return stdoutCompleter.future; - }); - when(vmService.onStdoutEvent).thenAnswer((Invocation invocation) { - return controller.stream; - }); - logReader.connectedVMService = vmService; - - stdoutCompleter.complete(Success()); - controller.add(Event( - kind: 'Stdout', - timestamp: 0, - bytes: base64.encode(utf8.encode(' This is a message ')), - )); - - // Wait for stream listeners to fire. - await expectLater(logReader.logLines, emits(' This is a message ')); - }); } class MockArtifacts extends Mock implements Artifacts {} -class MockVmService extends Mock implements VMService, VmService {} diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart index 993cb0ffefe..991f7725e05 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart @@ -23,8 +23,8 @@ import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/run_cold.dart'; import 'package:flutter_tools/src/run_hot.dart'; import 'package:flutter_tools/src/vmservice.dart'; +import 'package:json_rpc_2/json_rpc_2.dart'; import 'package:mockito/mockito.dart'; -import 'package:vm_service/vm_service.dart' as vm_service; import '../src/common.dart'; import '../src/context.dart'; @@ -222,7 +222,7 @@ void main() { pathToReload: anyNamed('pathToReload'), invalidatedFiles: anyNamed('invalidatedFiles'), dillOutputPath: anyNamed('dillOutputPath'), - )).thenThrow(vm_service.RPCError('something bad happened', 666, '')); + )).thenThrow(RpcException(666, 'something bad happened')); final OperationResult result = await residentRunner.restart(fullRestart: false); expect(result.fatal, true); @@ -327,7 +327,7 @@ void main() { pathToReload: anyNamed('pathToReload'), invalidatedFiles: anyNamed('invalidatedFiles'), dillOutputPath: anyNamed('dillOutputPath'), - )).thenThrow(vm_service.RPCError('something bad happened', 666, '')); + )).thenThrow(RpcException(666, 'something bad happened')); final OperationResult result = await residentRunner.restart(fullRestart: true); expect(result.fatal, true); diff --git a/packages/flutter_tools/test/general.shard/vmservice_test.dart b/packages/flutter_tools/test/general.shard/vmservice_test.dart index 7320373dc53..3af20ff3e75 100644 --- a/packages/flutter_tools/test/general.shard/vmservice_test.dart +++ b/packages/flutter_tools/test/general.shard/vmservice_test.dart @@ -3,218 +3,362 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; -import 'package:vm_service/vm_service.dart' as vm_service; -import 'package:mockito/mockito.dart'; +import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/device.dart'; +import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/version.dart'; import 'package:flutter_tools/src/vmservice.dart'; +import 'package:json_rpc_2/json_rpc_2.dart' as rpc; +import 'package:mockito/mockito.dart'; +import 'package:platform/platform.dart'; +import 'package:quiver/testing/async.dart'; import '../src/common.dart'; import '../src/context.dart'; +import '../src/mocks.dart'; -final Map vm = { - 'type': 'VM', - 'name': 'vm', - 'architectureBits': 64, - 'targetCPU': 'x64', - 'hostCPU': ' Intel(R) Xeon(R) CPU E5-1650 v2 @ 3.50GHz', - 'version': '2.1.0-dev.7.1.flutter-45f9462398 (Fri Oct 19 19:27:56 2018 +0000) on "linux_x64"', - '_profilerMode': 'Dart', - '_nativeZoneMemoryUsage': 0, - 'pid': 103707, - 'startTime': 1540426121876, - '_embedder': 'Flutter', - '_maxRSS': 312614912, - '_currentRSS': 33091584, - 'isolates': [ - { - 'type': '@Isolate', - 'fixedId': true, - 'id': 'isolates/242098474', - 'name': 'main.dart:main()', - 'number': 242098474, - }, - ], -}; +class MockPeer implements rpc.Peer { -final vm_service.Isolate isolate = vm_service.Isolate.parse( - { - 'type': 'Isolate', - 'fixedId': true, - 'id': 'isolates/242098474', - 'name': 'main.dart:main()', - 'number': 242098474, - '_originNumber': 242098474, - 'startTime': 1540488745340, - '_heaps': { - 'new': { - 'used': 0, - 'capacity': 0, - 'external': 0, - 'collections': 0, - 'time': 0.0, - 'avgCollectionPeriodMillis': 0.0, - }, - 'old': { - 'used': 0, - 'capacity': 0, - 'external': 0, - 'collections': 0, - 'time': 0.0, - 'avgCollectionPeriodMillis': 0.0, - }, - }, + Function _versionFn = (dynamic _) => null; + + @override + rpc.ErrorCallback get onUnhandledError => null; + + @override + Future get done async { + throw 'unexpected call to done'; } -); -final Map listViews = { - 'type': 'FlutterViewList', - 'views': [ - { - 'type': 'FlutterView', - 'id': '_flutterView/0x4a4c1f8', - 'isolate': { - 'type': '@Isolate', + @override + bool get isClosed => _isClosed; + + @override + Future close() async { + _isClosed = true; + } + + @override + Future listen() async { + // this does get called + } + + @override + void registerFallback(dynamic callback(rpc.Parameters parameters)) { + throw 'unexpected call to registerFallback'; + } + + @override + void registerMethod(String name, Function callback) { + registeredMethods.add(name); + if (name == 'flutterVersion') { + _versionFn = callback; + } + } + + @override + void sendNotification(String method, [ dynamic parameters ]) { + // this does get called + sentNotifications.putIfAbsent(method, () => []).add(parameters); + } + + Map> sentNotifications = >{}; + List registeredMethods = []; + + bool isolatesEnabled = false; + bool _isClosed = false; + + Future _getVMLatch; + Completer _currentGetVMLatchCompleter; + + void tripGetVMLatch() { + final Completer lastCompleter = _currentGetVMLatchCompleter; + _currentGetVMLatchCompleter = Completer(); + _getVMLatch = _currentGetVMLatchCompleter.future; + lastCompleter?.complete(); + } + + int returnedFromSendRequest = 0; + + @override + Future sendRequest(String method, [ dynamic parameters ]) async { + if (method == 'getVM') { + await _getVMLatch; + } + await Future.delayed(Duration.zero); + returnedFromSendRequest += 1; + if (method == 'getVM') { + return { + 'type': 'VM', + 'name': 'vm', + 'architectureBits': 64, + 'targetCPU': 'x64', + 'hostCPU': ' Intel(R) Xeon(R) CPU E5-1650 v2 @ 3.50GHz', + 'version': '2.1.0-dev.7.1.flutter-45f9462398 (Fri Oct 19 19:27:56 2018 +0000) on "linux_x64"', + '_profilerMode': 'Dart', + '_nativeZoneMemoryUsage': 0, + 'pid': 103707, + 'startTime': 1540426121876, + '_embedder': 'Flutter', + '_maxRSS': 312614912, + '_currentRSS': 33091584, + 'isolates': isolatesEnabled ? [ + { + 'type': '@Isolate', + 'fixedId': true, + 'id': 'isolates/242098474', + 'name': 'main.dart:main()', + 'number': 242098474, + }, + ] : [], + }; + } + if (method == 'getIsolate') { + return { + 'type': 'Isolate', 'fixedId': true, 'id': 'isolates/242098474', 'name': 'main.dart:main()', 'number': 242098474, - }, - }, - ] -}; + '_originNumber': 242098474, + 'startTime': 1540488745340, + '_heaps': { + 'new': { + 'used': 0, + 'capacity': 0, + 'external': 0, + 'collections': 0, + 'time': 0.0, + 'avgCollectionPeriodMillis': 0.0, + }, + 'old': { + 'used': 0, + 'capacity': 0, + 'external': 0, + 'collections': 0, + 'time': 0.0, + 'avgCollectionPeriodMillis': 0.0, + }, + }, + }; + } + if (method == '_flutter.listViews') { + return { + 'type': 'FlutterViewList', + 'views': isolatesEnabled ? [ + { + 'type': 'FlutterView', + 'id': '_flutterView/0x4a4c1f8', + 'isolate': { + 'type': '@Isolate', + 'fixedId': true, + 'id': 'isolates/242098474', + 'name': 'main.dart:main()', + 'number': 242098474, + }, + }, + ] : [], + }; + } + if (method == 'flutterVersion') { + return _versionFn(parameters); + } + return null; + } -typedef ServiceCallback = Future> Function(Map); + @override + dynamic withBatch(dynamic callback()) { + throw 'unexpected call to withBatch'; + } +} void main() { - testUsingContext('VMService can refreshViews', () async { - final MockVMService mockVmService = MockVMService(); - final VMService vmService = VMService( - null, - null, - null, - null, - null, - null, - null, - mockVmService, - Completer(), - const Stream.empty(), - ); + MockStdio mockStdio; + final MockFlutterVersion mockVersion = MockFlutterVersion(); + group('VMService', () { - verify(mockVmService.registerService('flutterVersion', 'Flutter Tools')).called(1); - - when(mockVmService.callServiceExtension('getVM', - args: anyNamed('args'), // Empty - isolateId: null - )).thenAnswer((Invocation invocation) async { - return vm_service.Response.parse(vm); + setUp(() { + mockStdio = MockStdio(); }); - await vmService.getVMOld(); - - when(mockVmService.callServiceExtension('_flutter.listViews', - args: anyNamed('args'), - isolateId: anyNamed('isolateId') - )).thenAnswer((Invocation invocation) async { - return vm_service.Response.parse(listViews); + testUsingContext('fails connection eagerly in the connect() method', () async { + FakeAsync().run((FakeAsync time) { + bool failed = false; + final Future future = VMService.connect(Uri.parse('http://host.invalid:9999/')); + future.whenComplete(() { + failed = true; + }); + time.elapse(const Duration(seconds: 5)); + expect(failed, isFalse); + expect(mockStdio.writtenToStdout.join(''), ''); + expect(mockStdio.writtenToStderr.join(''), ''); + time.elapse(const Duration(seconds: 5)); + expect(failed, isFalse); + expect(mockStdio.writtenToStdout.join(''), 'This is taking longer than expected...\n'); + expect(mockStdio.writtenToStderr.join(''), ''); + }); + }, overrides: { + Logger: () => StdoutLogger( + outputPreferences: OutputPreferences.test(), + stdio: mockStdio, + terminal: AnsiTerminal( + stdio: mockStdio, + platform: const LocalPlatform(), + ), + timeoutConfiguration: const TimeoutConfiguration(), + ), + WebSocketConnector: () => (String url, {CompressionOptions compression}) async => throw const SocketException('test'), }); - await vmService.refreshViews(waitForViews: true); - expect(vmService.vm.name, 'vm'); - expect(vmService.vm.views.single.id, '_flutterView/0x4a4c1f8'); - }, overrides: { - Logger: () => BufferLogger.test() - }); + testUsingContext('closing VMService closes Peer', () async { + final MockPeer mockPeer = MockPeer(); + final VMService vmService = VMService(mockPeer, null, null, null, null, null, MockDevice(), null); + expect(mockPeer.isClosed, equals(false)); + await vmService.close(); + expect(mockPeer.isClosed, equals(true)); + }); - testUsingContext('VmService registers reloadSources', () { - Future reloadSources(String isolateId, { bool pause, bool force}) async {} - final MockVMService mockVMService = MockVMService(); - VMService( - null, - null, - reloadSources, - null, - null, - null, - null, - mockVMService, - Completer(), - const Stream.empty(), - ); + testUsingContext('refreshViews', () { + FakeAsync().run((FakeAsync time) { + bool done = false; + final MockPeer mockPeer = MockPeer(); + expect(mockPeer.returnedFromSendRequest, 0); + final VMService vmService = VMService(mockPeer, null, null, null, null, null, null, null); + expect(mockPeer.sentNotifications, contains('registerService')); + final List registeredServices = + mockPeer.sentNotifications['registerService'] + .map((dynamic service) => (service as Map)['service']) + .toList(); + expect(registeredServices, contains('flutterVersion')); + vmService.getVM().then((void value) { done = true; }); + expect(done, isFalse); + expect(mockPeer.returnedFromSendRequest, 0); + time.elapse(Duration.zero); + expect(done, isTrue); + expect(mockPeer.returnedFromSendRequest, 1); - verify(mockVMService.registerService('reloadSources', 'Flutter Tools')).called(1); - }, overrides: { - Logger: () => BufferLogger.test() - }); + done = false; + mockPeer.tripGetVMLatch(); // this blocks the upcoming getVM call + final Future ready = vmService.refreshViews(waitForViews: true); + ready.then((void value) { done = true; }); + expect(mockPeer.returnedFromSendRequest, 1); + time.elapse(Duration.zero); // this unblocks the listViews call which returns nothing + expect(mockPeer.returnedFromSendRequest, 2); + time.elapse(const Duration(milliseconds: 50)); // the last listViews had no views, so it waits 50ms, then calls getVM + expect(done, isFalse); + expect(mockPeer.returnedFromSendRequest, 2); + mockPeer.tripGetVMLatch(); // this unblocks the getVM call + expect(mockPeer.returnedFromSendRequest, 2); + time.elapse(Duration.zero); // here getVM returns with no isolates and listViews returns no views + expect(mockPeer.returnedFromSendRequest, 4); + time.elapse(const Duration(milliseconds: 50)); // so refreshViews waits another 50ms + expect(done, isFalse); + expect(mockPeer.returnedFromSendRequest, 4); + mockPeer.tripGetVMLatch(); // this unblocks the getVM call + expect(mockPeer.returnedFromSendRequest, 4); + time.elapse(Duration.zero); // here getVM returns with no isolates and listViews returns no views + expect(mockPeer.returnedFromSendRequest, 6); + time.elapse(const Duration(milliseconds: 50)); // so refreshViews waits another 50ms + expect(done, isFalse); + expect(mockPeer.returnedFromSendRequest, 6); + mockPeer.tripGetVMLatch(); // this unblocks the getVM call + expect(mockPeer.returnedFromSendRequest, 6); + time.elapse(Duration.zero); // here getVM returns with no isolates and listViews returns no views + expect(mockPeer.returnedFromSendRequest, 8); + time.elapse(const Duration(milliseconds: 50)); // so refreshViews waits another 50ms + expect(done, isFalse); + expect(mockPeer.returnedFromSendRequest, 8); + mockPeer.tripGetVMLatch(); // this unblocks the getVM call + expect(mockPeer.returnedFromSendRequest, 8); + time.elapse(Duration.zero); // here getVM returns with no isolates and listViews returns no views + expect(mockPeer.returnedFromSendRequest, 10); + const String message = 'Flutter is taking longer than expected to report its views. Still trying...\n'; + expect(mockStdio.writtenToStdout.join(''), message); + expect(mockStdio.writtenToStderr.join(''), ''); + time.elapse(const Duration(milliseconds: 50)); // so refreshViews waits another 50ms + expect(done, isFalse); + expect(mockPeer.returnedFromSendRequest, 10); + mockPeer.isolatesEnabled = true; + mockPeer.tripGetVMLatch(); // this unblocks the getVM call + expect(mockPeer.returnedFromSendRequest, 10); + time.elapse(Duration.zero); // now it returns an isolate and the listViews call returns views + expect(mockPeer.returnedFromSendRequest, 13); + expect(done, isTrue); + expect(mockStdio.writtenToStdout.join(''), message); + expect(mockStdio.writtenToStderr.join(''), ''); + }); + }, overrides: { + Logger: () => StdoutLogger( + outputPreferences: globals.outputPreferences, + terminal: AnsiTerminal( + stdio: mockStdio, + platform: const LocalPlatform(), + ), + stdio: mockStdio, + timeoutConfiguration: const TimeoutConfiguration(), + ), + }); - testUsingContext('VmService registers reloadMethod', () { - Future reloadMethod({ String classId, String libraryId,}) async {} - final MockVMService mockVMService = MockVMService(); - VMService( - null, - null, - null, - null, - null, - null, - reloadMethod, - mockVMService, - Completer(), - const Stream.empty(), - ); + testUsingContext('registers hot UI method', () { + FakeAsync().run((FakeAsync time) { + final MockPeer mockPeer = MockPeer(); + Future reloadMethod({ String classId, String libraryId }) async {} + VMService(mockPeer, null, null, null, null, null, null, reloadMethod); - verify(mockVMService.registerService('reloadMethod', 'Flutter Tools')).called(1); - }, overrides: { - Logger: () => BufferLogger.test() - }); + expect(mockPeer.registeredMethods, contains('reloadMethod')); + }); + }, overrides: { + Logger: () => StdoutLogger( + outputPreferences: globals.outputPreferences, + terminal: AnsiTerminal( + stdio: mockStdio, + platform: const LocalPlatform(), + ), + stdio: mockStdio, + timeoutConfiguration: const TimeoutConfiguration(), + ), + }); - testUsingContext('VmService registers flutterMemoryInfo service', () { - final MockDevice mockDevice = MockDevice(); - final MockVMService mockVMService = MockVMService(); - VMService( - null, - null, - null, - null, - null, - mockDevice, - null, - mockVMService, - Completer(), - const Stream.empty(), - ); + testUsingContext('registers flutterMemoryInfo service', () { + FakeAsync().run((FakeAsync time) { + final MockDevice mockDevice = MockDevice(); + final MockPeer mockPeer = MockPeer(); + Future reloadSources(String isolateId, { bool pause, bool force}) async {} + VMService(mockPeer, null, null, reloadSources, null, null, mockDevice, null); - verify(mockVMService.registerService('flutterMemoryInfo', 'Flutter Tools')).called(1); - }, overrides: { - Logger: () => BufferLogger.test() - }); + expect(mockPeer.registeredMethods, contains('flutterMemoryInfo')); + }); + }, overrides: { + Logger: () => StdoutLogger( + outputPreferences: globals.outputPreferences, + terminal: AnsiTerminal( + stdio: mockStdio, + platform: const LocalPlatform(), + ), + stdio: mockStdio, + timeoutConfiguration: const TimeoutConfiguration(), + ), + }); - testUsingContext('VMService returns correct FlutterVersion', () async { - final MockVMService mockVMService = MockVMService(); - VMService( - null, - null, - null, - null, - null, - null, - null, - mockVMService, - Completer(), - const Stream.empty(), - ); + testUsingContext('returns correct FlutterVersion', () { + FakeAsync().run((FakeAsync time) async { + final MockPeer mockPeer = MockPeer(); + VMService(mockPeer, null, null, null, null, null, MockDevice(), null); - verify(mockVMService.registerService('flutterVersion', 'Flutter Tools')).called(1); - }, overrides: { - FlutterVersion: () => MockFlutterVersion(), + expect(mockPeer.registeredMethods, contains('flutterVersion')); + expect(await mockPeer.sendRequest('flutterVersion'), equals(mockVersion.toJson())); + }); + }, overrides: { + FlutterVersion: () => mockVersion, + }); }); } class MockDevice extends Mock implements Device {} -class MockVMService extends Mock implements vm_service.VmService {} + class MockFlutterVersion extends Mock implements FlutterVersion { @override Map toJson() => const {'Mock': 'Version'};