diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 24596bd17ea..d27dbbd9ebf 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -364,10 +364,16 @@ class FlutterDevice { } for (final FlutterView view in views) { if (view != null && view.uiIsolate != null) { - // If successful, there will be no response from flutterExit. + // If successful, there will be no response from flutterExit. If the exit + // method is not registered, this will complete with `false`. unawaited(vmService.flutterExit( isolateId: view.uiIsolate.id, - )); + ).then((bool exited) async { + // If exiting the app failed, fall back to stopApp + if (!exited) { + await device.stopApp(package, userIdentifier: userIdentifier); + } + })); } } return vmService.service.onDone @@ -378,10 +384,6 @@ class FlutterDevice { ); }) .timeout(timeoutDelay, onTimeout: () { - // TODO(jonahwilliams): this only seems to fail on CI in the - // flutter_attach_android_test. This log should help verify this - // is where the tool is getting stuck. - globals.logger.printTrace('error: vm service shutdown failed'); return device.stopApp(package, userIdentifier: userIdentifier); }); } diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart index 475462f0986..f6472cc0fa5 100644 --- a/packages/flutter_tools/lib/src/vmservice.dart +++ b/packages/flutter_tools/lib/src/vmservice.dart @@ -545,7 +545,7 @@ class FlutterVmService { 'ext.flutter.debugDumpApp', isolateId: isolateId, ); - return response['data']?.toString(); + return response != null ? response['data']?.toString() : ''; } Future flutterDebugDumpRenderTree({ @@ -556,7 +556,7 @@ class FlutterVmService { isolateId: isolateId, args: {} ); - return response['data']?.toString(); + return response != null ? response['data']?.toString() : ''; } Future flutterDebugDumpLayerTree({ @@ -566,7 +566,7 @@ class FlutterVmService { 'ext.flutter.debugDumpLayerTree', isolateId: isolateId, ); - return response['data']?.toString(); + return response != null ? response['data']?.toString() : ''; } Future flutterDebugDumpSemanticsTreeInTraversalOrder({ @@ -576,7 +576,7 @@ class FlutterVmService { 'ext.flutter.debugDumpSemanticsTreeInTraversalOrder', isolateId: isolateId, ); - return response['data']?.toString(); + return response != null ? response['data']?.toString() : ''; } Future flutterDebugDumpSemanticsTreeInInverseHitTestOrder({ @@ -586,7 +586,7 @@ class FlutterVmService { 'ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder', isolateId: isolateId, ); - return response['data']?.toString(); + return response != null ? response['data']?.toString() : ''; } Future> _flutterToggle(String name, { @@ -701,15 +701,26 @@ class FlutterVmService { /// /// This method is only supported by certain embedders. This is /// described by [Device.supportsFlutterExit]. - Future flutterExit({ + Future flutterExit({ @required String isolateId, - }) { - return invokeFlutterExtensionRpcRaw( - 'ext.flutter.exit', - isolateId: isolateId, - ).catchError((dynamic error, StackTrace stackTrace) { - // Do nothing on sentinel or exception, the isolate already exited. - }, test: (dynamic error) => error is vm_service.SentinelException || error is vm_service.RPCError); + }) async { + try { + final Map 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 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 14ed3a3df7e..1f05d905fd4 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart @@ -1674,6 +1674,42 @@ void main() { expect(fakeVmServiceHost.hasRemainingExpectations, false); })); + testUsingContext('FlutterDevice will exit an isolate that did not register the exit extension method', () => testbed.run(() async { + fakeVmServiceHost = FakeVmServiceHost(requests: [ + FakeVmServiceRequest( + method: '_flutter.listViews', + jsonResponse: { + 'views': [ + fakeFlutterView.toJson(), + ], + }, + ), + FakeVmServiceRequest( + method: 'getIsolate', + args: { + 'isolateId': fakeUnpausedIsolate.id, + }, + jsonResponse: fakeUnpausedIsolate.toJson(), + ), + FakeVmServiceRequest( + method: 'ext.flutter.exit', + args: { + 'isolateId': fakeUnpausedIsolate.id, + }, + errorCode: RPCErrorCodes.kMethodNotFound, + ), + ]); + final TestFlutterDevice flutterDevice = TestFlutterDevice( + mockDevice, + ); + flutterDevice.vmService = fakeVmServiceHost.vmService; + + await flutterDevice.exitApps(timeoutDelay: Duration.zero); + + expect(mockDevice.appStopped, true); + expect(fakeVmServiceHost.hasRemainingExpectations, false); + })); + testUsingContext('FlutterDevice can exit from a release mode isolate with no VmService', () => testbed.run(() async { final TestFlutterDevice flutterDevice = TestFlutterDevice( mockDevice, diff --git a/packages/flutter_tools/test/general.shard/vmservice_test.dart b/packages/flutter_tools/test/general.shard/vmservice_test.dart index a19e0cfd17e..9aff9041c53 100644 --- a/packages/flutter_tools/test/general.shard/vmservice_test.dart +++ b/packages/flutter_tools/test/general.shard/vmservice_test.dart @@ -331,6 +331,101 @@ void main() { expect(fakeVmServiceHost.hasRemainingExpectations, false); }); + testWithoutContext('flutterDebugDumpSemanticsTreeInTraversalOrder handles missing method', () async { + final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost( + requests: [ + const FakeVmServiceRequest( + method: 'ext.flutter.debugDumpSemanticsTreeInTraversalOrder', + args: { + 'isolateId': '1' + }, + errorCode: RPCErrorCodes.kMethodNotFound, + ), + ] + ); + + expect(await fakeVmServiceHost.vmService.flutterDebugDumpSemanticsTreeInTraversalOrder( + isolateId: '1', + ), ''); + expect(fakeVmServiceHost.hasRemainingExpectations, false); + }); + + testWithoutContext('flutterDebugDumpSemanticsTreeInInverseHitTestOrder handles missing method', () async { + final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost( + requests: [ + const FakeVmServiceRequest( + method: 'ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder', + args: { + 'isolateId': '1' + }, + errorCode: RPCErrorCodes.kMethodNotFound, + ), + ] + ); + + expect(await fakeVmServiceHost.vmService.flutterDebugDumpSemanticsTreeInInverseHitTestOrder( + isolateId: '1', + ), ''); + expect(fakeVmServiceHost.hasRemainingExpectations, false); + }); + + testWithoutContext('flutterDebugDumpLayerTree handles missing method', () async { + final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost( + requests: [ + const FakeVmServiceRequest( + method: 'ext.flutter.debugDumpLayerTree', + args: { + 'isolateId': '1' + }, + errorCode: RPCErrorCodes.kMethodNotFound, + ), + ] + ); + + expect(await fakeVmServiceHost.vmService.flutterDebugDumpLayerTree( + isolateId: '1', + ), ''); + expect(fakeVmServiceHost.hasRemainingExpectations, false); + }); + + testWithoutContext('flutterDebugDumpRenderTree handles missing method', () async { + final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost( + requests: [ + const FakeVmServiceRequest( + method: 'ext.flutter.debugDumpRenderTree', + args: { + 'isolateId': '1' + }, + errorCode: RPCErrorCodes.kMethodNotFound, + ), + ] + ); + + expect(await fakeVmServiceHost.vmService.flutterDebugDumpRenderTree( + isolateId: '1', + ), ''); + expect(fakeVmServiceHost.hasRemainingExpectations, false); + }); + + testWithoutContext('flutterDebugDumpApp handles missing method', () async { + final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost( + requests: [ + const FakeVmServiceRequest( + method: 'ext.flutter.debugDumpApp', + args: { + 'isolateId': '1' + }, + errorCode: RPCErrorCodes.kMethodNotFound, + ), + ] + ); + + expect(await fakeVmServiceHost.vmService.flutterDebugDumpApp( + isolateId: '1', + ), ''); + expect(fakeVmServiceHost.hasRemainingExpectations, false); + }); + testWithoutContext('Framework service extension invocations return null if service disappears ', () async { final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost( requests: [