diff --git a/packages/flutter_tools/lib/src/resident_devtools_handler.dart b/packages/flutter_tools/lib/src/resident_devtools_handler.dart index 240487e4c60..2f8afb9516b 100644 --- a/packages/flutter_tools/lib/src/resident_devtools_handler.dart +++ b/packages/flutter_tools/lib/src/resident_devtools_handler.dart @@ -7,7 +7,6 @@ import 'dart:async'; import 'package:meta/meta.dart'; -import 'package:vm_service/vm_service.dart' as vm_service; import 'base/logger.dart'; import 'resident_runner.dart'; @@ -66,12 +65,12 @@ class FlutterResidentDevtoolsHandler implements ResidentDevtoolsHandler { // report their URLs yet. Do so now. _residentRunner.printDebuggerList(includeObservatory: false); } - await _waitForExtensions(flutterDevices); + final List devicesWithExtension = await _devicesWithExtensions(flutterDevices); await _maybeCallDevToolsUriServiceExtension( - flutterDevices, + devicesWithExtension, ); await _callConnectedVmServiceUriExtension( - flutterDevices, + devicesWithExtension, ); } @@ -107,12 +106,28 @@ class FlutterResidentDevtoolsHandler implements ResidentDevtoolsHandler { } } - Future _waitForExtensions(List flutterDevices) async { - await Future.wait(>[ + Future> _devicesWithExtensions(List flutterDevices) async { + final List devices = await Future.wait(>[ for (final FlutterDevice device in flutterDevices) - if (device.vmService != null) - waitForExtension(device.vmService.service, 'ext.flutter.connectedVmServiceUri'), + _waitForExtensionsForDevice(device) ]); + return devices.where((FlutterDevice device) => device != null).toList(); + } + + /// Returns null if the service extension cannot be found on the device. + Future _waitForExtensionsForDevice(FlutterDevice flutterDevice) async { + const String extension = 'ext.flutter.connectedVmServiceUri'; + try { + await flutterDevice.vmService?.findExtensionIsolate(extension); + return flutterDevice; + } on VmServiceDisappearedException { + _logger.printTrace( + 'The VM Service for ${flutterDevice.device} disappeared while trying to' + ' find the $extension service extension. Skipping subsequent DevTools ' + 'setup for this device.', + ); + return null; + } } Future _callConnectedVmServiceUriExtension(List flutterDevices) async { @@ -164,10 +179,10 @@ class FlutterResidentDevtoolsHandler implements ResidentDevtoolsHandler { @override Future hotRestart(List flutterDevices) async { - await _waitForExtensions(flutterDevices); + final List devicesWithExtension = await _devicesWithExtensions(flutterDevices); await Future.wait(>[ - _maybeCallDevToolsUriServiceExtension(flutterDevices), - _callConnectedVmServiceUriExtension(flutterDevices), + _maybeCallDevToolsUriServiceExtension(devicesWithExtension), + _callConnectedVmServiceUriExtension(devicesWithExtension), ]); } @@ -181,37 +196,6 @@ class FlutterResidentDevtoolsHandler implements ResidentDevtoolsHandler { } } - -@visibleForTesting -Future waitForExtension(vm_service.VmService vmService, String extension) async { - final Completer completer = Completer(); - try { - await vmService.streamListen(vm_service.EventStreams.kExtension); - } on Exception { - // do nothing - } - StreamSubscription extensionStream; - extensionStream = vmService.onExtensionEvent.listen((vm_service.Event event) { - if (event.json['extensionKind'] == 'Flutter.FrameworkInitialization') { - // The 'Flutter.FrameworkInitialization' event is sent on hot restart - // as well, so make sure we don't try to complete this twice. - if (!completer.isCompleted) { - completer.complete(); - extensionStream.cancel(); - } - } - }); - final vm_service.VM vm = await vmService.getVM(); - if (vm.isolates.isNotEmpty) { - final vm_service.IsolateRef isolateRef = vm.isolates.first; - final vm_service.Isolate isolate = await vmService.getIsolate(isolateRef.id); - if (isolate.extensionRPCs.contains(extension)) { - return; - } - } - await completer.future; -} - @visibleForTesting NoOpDevtoolsHandler createNoOpHandler(DevtoolsLauncher launcher, ResidentRunner runner, Logger logger) { return NoOpDevtoolsHandler(); diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart index f98bc115a4e..7e83c63fe41 100644 --- a/packages/flutter_tools/lib/src/vmservice.dart +++ b/packages/flutter_tools/lib/src/vmservice.dart @@ -4,6 +4,8 @@ // @dart = 2.8 +import 'dart:async'; + import 'package:file/file.dart'; import 'package:meta/meta.dart' show required; import 'package:vm_service/vm_service.dart' as vm_service; @@ -479,7 +481,7 @@ class FlutterVmService { @required Uri assetsDirectory, }) async { try { - await service.streamListen('Isolate'); + await service.streamListen(vm_service.EventStreams.kIsolate); } on vm_service.RPCError { // Do nothing, since the tool is already subscribed. } @@ -784,6 +786,58 @@ class FlutterVmService { } } + /// 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. + /// + /// Throws a [VmServiceDisappearedException] should the VM Service disappear + /// while making calls to it. + Future findExtensionIsolate(String extensionName) async { + try { + await service.streamListen(vm_service.EventStreams.kIsolate); + } on vm_service.RPCError { + // Do nothing, since the tool is already subscribed. + } + + final Completer extensionAdded = Completer(); + StreamSubscription 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 flutterViews = await getFlutterViews(); + if (flutterViews.isEmpty) { + throw VmServiceDisappearedException(); + } + + for (final FlutterView flutterView in flutterViews) { + final vm_service.IsolateRef isolateRef = flutterView.uiIsolate; + if (isolateRef == null) { + continue; + } + + final vm_service.Isolate isolate = await service.getIsolate(isolateRef.id); + if (isolate.extensionRPCs.contains(extensionName)) { + return isolateRef; + } + } + 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. + } + } + } + /// Attempt to retrieve the isolate with id [isolateId], or `null` if it has /// been collected. Future getIsolateOrNull(String isolateId) { @@ -845,6 +899,9 @@ class FlutterVmService { } } +/// 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) { diff --git a/packages/flutter_tools/test/general.shard/resident_devtools_handler_test.dart b/packages/flutter_tools/test/general.shard/resident_devtools_handler_test.dart index bb4949dd04e..19ed7b37ad5 100644 --- a/packages/flutter_tools/test/general.shard/resident_devtools_handler_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_devtools_handler_test.dart @@ -5,6 +5,7 @@ // @dart = 2.8 import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/devtools_launcher.dart'; import 'package:flutter_tools/src/vmservice.dart'; import 'package:vm_service/vm_service.dart' as vm_service; @@ -17,7 +18,7 @@ import 'package:test/fake.dart'; import '../src/common.dart'; import '../src/context.dart'; - final vm_service.Isolate isolate = vm_service.Isolate( +final vm_service.Isolate isolate = vm_service.Isolate( id: '1', pauseEvent: vm_service.Event( kind: vm_service.EventKind.kResume, @@ -40,60 +41,17 @@ import '../src/context.dart'; startTime: 0, isSystemIsolate: false, isolateFlags: [], - extensionRPCs: ['foo'] -); - -final vm_service.Isolate fakeUnpausedIsolate = vm_service.Isolate( - id: '1', - pauseEvent: vm_service.Event( - kind: vm_service.EventKind.kResume, - timestamp: 0 - ), - breakpoints: [], - exceptionPauseMode: null, - extensionRPCs: [], - libraries: [ - vm_service.LibraryRef( - id: '1', - uri: 'file:///hello_world/main.dart', - name: '', - ), - ], - livePorts: 0, - name: 'test', - number: '1', - pauseOnExit: false, - runnable: true, - startTime: 0, - isSystemIsolate: false, - isolateFlags: [], -); - -final vm_service.VM fakeVM = vm_service.VM( - isolates: [fakeUnpausedIsolate], - pid: 1, - hostCPU: '', - isolateGroups: [], - targetCPU: '', - startTime: 0, - name: 'dart', - architectureBits: 64, - operatingSystem: '', - version: '', - systemIsolateGroups: [], - systemIsolates: [], -); - -final FlutterView fakeFlutterView = FlutterView( - id: 'a', - uiIsolate: fakeUnpausedIsolate, + extensionRPCs: ['ext.flutter.connectedVmServiceUri'], ); final FakeVmServiceRequest listViews = FakeVmServiceRequest( method: kListViewsMethod, jsonResponse: { 'views': [ - fakeFlutterView.toJson(), + FlutterView( + id: 'a', + uiIsolate: isolate, + ).toJson() ], }, ); @@ -173,10 +131,10 @@ void main() { const FakeVmServiceRequest( method: 'streamListen', args: { - 'streamId': 'Extension', + 'streamId': 'Isolate', } ), - FakeVmServiceRequest(method: 'getVM', jsonResponse: fakeVM.toJson()), + listViews, FakeVmServiceRequest( method: 'getIsolate', jsonResponse: isolate.toJson(), @@ -184,13 +142,11 @@ void main() { 'isolateId': '1', }, ), - FakeVmServiceStreamResponse( - streamId: 'Extension', - event: vm_service.Event( - timestamp: 0, - extensionKind: 'Flutter.FrameworkInitialization', - kind: 'test', - ), + const FakeVmServiceRequest( + method: 'streamCancel', + args: { + 'streamId': 'Isolate', + }, ), listViews, const FakeVmServiceRequest( @@ -218,15 +174,55 @@ void main() { ); }); - testWithoutContext('wait for extension handles an immediate extension', () { + testWithoutContext('serveAndAnnounceDevTools with skips calling service extensions when VM service disappears', () async { + final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler( + FakeDevtoolsLauncher()..activeDevToolsServer = DevToolsServerAddress('localhost', 8080), + FakeResidentRunner(), + BufferLogger.test(), + ); final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: [ const FakeVmServiceRequest( method: 'streamListen', args: { - 'streamId': 'Extension', - } + 'streamId': 'Isolate', + }, ), - FakeVmServiceRequest(method: 'getVM', jsonResponse: fakeVM.toJson()), + const FakeVmServiceRequest( + method: kListViewsMethod, + errorCode: RPCErrorCodes.kServiceDisappeared, + ), + const FakeVmServiceRequest( + method: 'streamCancel', + args: { + 'streamId': 'Isolate', + }, + errorCode: RPCErrorCodes.kServiceDisappeared, + ), + ], httpAddress: Uri.parse('http://localhost:1234')); + + final FakeFlutterDevice device = FakeFlutterDevice() + ..vmService = fakeVmServiceHost.vmService; + + await handler.serveAndAnnounceDevTools( + flutterDevices: [device], + ); + }); + + testWithoutContext('serveAndAnnounceDevTools with multiple devices and VM service disappears on one', () async { + final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler( + FakeDevtoolsLauncher()..activeDevToolsServer = DevToolsServerAddress('localhost', 8080), + FakeResidentRunner(), + BufferLogger.test(), + ); + + final FakeVmServiceHost vmServiceHost = FakeVmServiceHost(requests: [ + const FakeVmServiceRequest( + method: 'streamListen', + args: { + 'streamId': 'Isolate', + }, + ), + listViews, FakeVmServiceRequest( method: 'getIsolate', jsonResponse: isolate.toJson(), @@ -234,46 +230,61 @@ void main() { 'isolateId': '1', }, ), - ]); - waitForExtension(fakeVmServiceHost.vmService.service, 'foo'); - }); + const FakeVmServiceRequest( + method: 'streamCancel', + args: { + 'streamId': 'Isolate', + }, + ), + listViews, + const FakeVmServiceRequest( + method: 'ext.flutter.activeDevToolsServerAddress', + args: { + 'isolateId': '1', + 'value': 'http://localhost:8080', + }, + ), + listViews, + const FakeVmServiceRequest( + method: 'ext.flutter.connectedVmServiceUri', + args: { + 'isolateId': '1', + 'value': 'http://localhost:1234', + }, + ), + ], httpAddress: Uri.parse('http://localhost:1234')); - testWithoutContext('wait for extension handles no isolates', () { - final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: [ + final FakeVmServiceHost vmServiceHostThatDisappears = FakeVmServiceHost(requests: [ const FakeVmServiceRequest( method: 'streamListen', args: { - 'streamId': 'Extension', - } + 'streamId': 'Isolate', + }, ), - FakeVmServiceRequest(method: 'getVM', jsonResponse: vm_service.VM( - isolates: [], - pid: 1, - hostCPU: '', - isolateGroups: [], - targetCPU: '', - startTime: 0, - name: 'dart', - architectureBits: 64, - operatingSystem: '', - version: '', - systemIsolateGroups: [], - systemIsolates: [], - ).toJson()), - FakeVmServiceStreamResponse( - streamId: 'Extension', - event: vm_service.Event( - timestamp: 0, - extensionKind: 'Flutter.FrameworkInitialization', - kind: 'test', - ), + const FakeVmServiceRequest( + method: kListViewsMethod, + errorCode: RPCErrorCodes.kServiceDisappeared, ), - ]); - waitForExtension(fakeVmServiceHost.vmService.service, 'foo'); + const FakeVmServiceRequest( + method: 'streamCancel', + args: { + 'streamId': 'Isolate', + }, + errorCode: RPCErrorCodes.kServiceDisappeared, + ), + ], httpAddress: Uri.parse('http://localhost:5678')); + + await handler.serveAndAnnounceDevTools( + flutterDevices: [ + FakeFlutterDevice() + ..vmService = vmServiceHostThatDisappears.vmService, + FakeFlutterDevice() + ..vmService = vmServiceHost.vmService, + ], + ); }); } - class FakeDevtoolsLauncher extends Fake implements DevtoolsLauncher { @override DevToolsServerAddress activeDevToolsServer; @@ -296,6 +307,11 @@ class FakeResidentRunner extends Fake implements ResidentRunner { } class FakeFlutterDevice extends Fake implements FlutterDevice { + @override + final Device device = FakeDevice(); + @override FlutterVmService vmService; } + +class FakeDevice extends Fake implements Device {} diff --git a/packages/flutter_tools/test/general.shard/vmservice_test.dart b/packages/flutter_tools/test/general.shard/vmservice_test.dart index 236ef24fd82..1dc6007cf44 100644 --- a/packages/flutter_tools/test/general.shard/vmservice_test.dart +++ b/packages/flutter_tools/test/general.shard/vmservice_test.dart @@ -45,52 +45,47 @@ final Map vm = { ], }; -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, - }, - }, - } +const String kExtensionName = 'ext.flutter.test.interestingExtension'; + +final vm_service.Isolate isolate = vm_service.Isolate( + id: '1', + pauseEvent: vm_service.Event( + kind: vm_service.EventKind.kResume, + timestamp: 0 + ), + breakpoints: [], + exceptionPauseMode: null, + libraries: [ + vm_service.LibraryRef( + id: '1', + uri: 'file:///hello_world/main.dart', + name: '', + ), + ], + livePorts: 0, + name: 'test', + number: '1', + pauseOnExit: false, + runnable: true, + startTime: 0, + isSystemIsolate: false, + isolateFlags: [], + extensionRPCs: [kExtensionName], ); -final Map listViews = { - 'type': 'FlutterViewList', - 'views': [ - { - 'type': 'FlutterView', - 'id': '_flutterView/0x4a4c1f8', - 'isolate': { - 'type': '@Isolate', - 'fixedId': true, - 'id': 'isolates/242098474', - 'name': 'main.dart:main()', - 'number': 242098474, - }, - }, - ] -}; +final FlutterView fakeFlutterView = FlutterView( + id: 'a', + uiIsolate: isolate, +); + +final FakeVmServiceRequest listViewsRequest = FakeVmServiceRequest( + method: kListViewsMethod, + jsonResponse: { + 'views': [ + fakeFlutterView.toJson(), + ], + }, +); typedef ServiceCallback = Future> Function(Map); @@ -408,17 +403,7 @@ void main() { 'views': [], }, ), - const FakeVmServiceRequest( - method: kListViewsMethod, - jsonResponse: { - 'views': [ - { - 'id': 'a', - 'isolate': {}, - }, - ], - }, - ), + listViewsRequest, ] ); @@ -452,6 +437,187 @@ void main() { expect(fakeVmServiceHost.hasRemainingExpectations, false); }); + group('findExtensionIsolate', () { + + testWithoutContext('returns an isolate with the registered extensionRPC', () async { + final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: [ + const FakeVmServiceRequest( + method: 'streamListen', + args: { + 'streamId': 'Isolate', + }, + ), + listViewsRequest, + FakeVmServiceRequest( + method: 'getIsolate', + jsonResponse: isolate.toJson(), + args: { + 'isolateId': '1', + }, + ), + const FakeVmServiceRequest( + method: 'streamCancel', + args: { + 'streamId': 'Isolate', + }, + ), + ]); + + final vm_service.IsolateRef isolateRef = await fakeVmServiceHost.vmService.findExtensionIsolate(kExtensionName); + expect(isolateRef.id, '1'); + }); + + testWithoutContext('returns the isolate with the registered extensionRPC when there are multiple FlutterViews', () async { + const String otherExtensionName = 'ext.flutter.test.otherExtension'; + + // Copy the other isolate and change a few fields. + final vm_service.Isolate isolate2 = vm_service.Isolate.parse( + isolate.toJson() + ..['id'] = '2' + ..['extensionRPCs'] = [otherExtensionName], + ); + + final FlutterView fakeFlutterView2 = FlutterView( + id: '2', + uiIsolate: isolate2, + ); + + final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: [ + const FakeVmServiceRequest( + method: 'streamListen', + args: { + 'streamId': 'Isolate', + }, + ), + FakeVmServiceRequest( + method: kListViewsMethod, + jsonResponse: { + 'views': [ + fakeFlutterView.toJson(), + fakeFlutterView2.toJson(), + ], + }, + ), + FakeVmServiceRequest( + method: 'getIsolate', + jsonResponse: isolate.toJson(), + args: { + 'isolateId': '1', + }, + ), + FakeVmServiceRequest( + method: 'getIsolate', + jsonResponse: isolate2.toJson(), + args: { + 'isolateId': '2', + }, + ), + const FakeVmServiceRequest( + method: 'streamCancel', + args: { + 'streamId': 'Isolate', + }, + ), + ]); + + final vm_service.IsolateRef isolateRef = await fakeVmServiceHost.vmService.findExtensionIsolate(otherExtensionName); + expect(isolateRef.id, '2'); + }); + + testWithoutContext('when the isolate stream is already subscribed, returns an isolate with the registered extensionRPC', () async { + final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: [ + const FakeVmServiceRequest( + method: 'streamListen', + args: { + 'streamId': 'Isolate', + }, + // Stream already subscribed - https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#streamlisten + errorCode: 103, + ), + listViewsRequest, + FakeVmServiceRequest( + method: 'getIsolate', + jsonResponse: isolate.toJson()..['extensionRPCs'] = [kExtensionName], + args: { + 'isolateId': '1', + }, + ), + const FakeVmServiceRequest( + method: 'streamCancel', + args: { + 'streamId': 'Isolate', + }, + ), + ]); + + final vm_service.IsolateRef isolateRef = await fakeVmServiceHost.vmService.findExtensionIsolate(kExtensionName); + expect(isolateRef.id, '1'); + }); + + testWithoutContext('returns an isolate with a extensionRPC that is registered later', () async { + final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: [ + const FakeVmServiceRequest( + method: 'streamListen', + args: { + 'streamId': 'Isolate', + }, + ), + listViewsRequest, + FakeVmServiceRequest( + method: 'getIsolate', + jsonResponse: isolate.toJson(), + args: { + 'isolateId': '1', + }, + ), + FakeVmServiceStreamResponse( + streamId: 'Isolate', + event: vm_service.Event( + kind: vm_service.EventKind.kServiceExtensionAdded, + extensionRPC: kExtensionName, + timestamp: 1, + ), + ), + const FakeVmServiceRequest( + method: 'streamCancel', + args: { + 'streamId': 'Isolate', + }, + ), + ]); + + final vm_service.IsolateRef isolateRef = await fakeVmServiceHost.vmService.findExtensionIsolate(kExtensionName); + expect(isolateRef.id, '1'); + }); + + testWithoutContext('throws when the service disappears', () async { + final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: [ + const FakeVmServiceRequest( + method: 'streamListen', + args: { + 'streamId': 'Isolate', + }, + ), + const FakeVmServiceRequest( + method: kListViewsMethod, + errorCode: RPCErrorCodes.kServiceDisappeared, + ), + const FakeVmServiceRequest( + method: 'streamCancel', + args: { + 'streamId': 'Isolate', + }, + errorCode: RPCErrorCodes.kServiceDisappeared, + ), + ]); + + expect( + () => fakeVmServiceHost.vmService.findExtensionIsolate(kExtensionName), + throwsA(isA()), + ); + }); + }); + testWithoutContext('Can process log events from the vm service', () { final vm_service.Event event = vm_service.Event( bytes: base64.encode(utf8.encode('Hello There\n')),