Handle "Service connection disposed" error from VmService disconnecting while requests are outstanding (#153714)

Fixes umbrella issue https://github.com/flutter/flutter/issues/153471, including its children: https://github.com/flutter/flutter/issues/153472, https://github.com/flutter/flutter/issues/153473, and https://github.com/flutter/flutter/issues/153474.

The VM service can be disposed at any time during requests (e.g. the user closes the app or stops debugging in VSCode)[^1]. a479f91e80 and a7d8707a59 updated package:vm_service to throw new `RPCError`s when the service disconnects while requests are inflight. Therefore, we need to handle these exceptions in the tool. See umbrella issue for more details.

I plan on cherry-picking this change to the stable channel.

[^1]: https://github.com/flutter/flutter/issues/153471#issuecomment-2296294221
This commit is contained in:
Andrew Kolos 2024-08-20 09:39:19 -07:00 committed by GitHub
parent 8bf1757b9e
commit 4a03b76c68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 48 additions and 5 deletions

View File

@ -414,7 +414,8 @@ known, it can be explicitly provided to attach via the command-line, e.g.
_logger.printStatus('Waiting for a new connection from Flutter on ${device.name}...');
}
} on RPCError catch (err) {
if (err.code == RPCErrorCodes.kServiceDisappeared) {
if (err.code == RPCErrorCodes.kServiceDisappeared ||
err.message.contains('Service connection disposed')) {
throwToolExit('Lost connection to device.');
}
rethrow;

View File

@ -516,7 +516,8 @@ class DevFS {
final vm_service.Response response = await _vmService.createDevFS(fsName);
_baseUri = Uri.parse(response.json!['uri'] as String);
} on vm_service.RPCError catch (rpcException) {
if (rpcException.code == RPCErrorCodes.kServiceDisappeared) {
if (rpcException.code == RPCErrorCodes.kServiceDisappeared ||
rpcException.message.contains('Service connection disposed')) {
// This can happen if the device has been disconnected, so translate to
// a DevFSException, which the caller will handle.
throw DevFSException('Service disconnected', rpcException);

View File

@ -503,7 +503,8 @@ class FlutterVmService {
// 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) {
if (e.code == RPCErrorCodes.kServiceDisappeared ||
e.message.contains('Service connection disposed')) {
return null;
}
rethrow;
@ -873,8 +874,9 @@ class FlutterVmService {
} 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)) {
if ((err.code == RPCErrorCodes.kMethodNotFound) ||
(err.code == RPCErrorCodes.kServiceDisappeared) ||
(err.message.contains('Service connection disposed'))) {
return null;
}
rethrow;

View File

@ -1125,6 +1125,45 @@ void main() {
DeviceManager: () => testDeviceManager,
});
testUsingContext('Catches "Service connection disposed" error', () async {
final FakeAndroidDevice device = FakeAndroidDevice(id: '1')
..portForwarder = const NoOpDevicePortForwarder()
..onGetLogReader = () => NoOpDeviceLogReader('test');
final FakeHotRunner hotRunner = FakeHotRunner();
final FakeHotRunnerFactory hotRunnerFactory = FakeHotRunnerFactory()
..hotRunner = hotRunner;
hotRunner.onAttach = (
Completer<DebugConnectionInfo>? connectionInfoCompleter,
Completer<void>? appStartedCompleter,
bool allowExistingDdsInstance,
bool enableDevTools,
) async {
await null;
throw vm_service.RPCError('flutter._listViews', RPCErrorCodes.kServerError, 'Service connection disposed');
};
testDeviceManager.devices = <Device>[device];
testFileSystem.file('lib/main.dart').createSync();
final AttachCommand command = AttachCommand(
hotRunnerFactory: hotRunnerFactory,
stdio: stdio,
logger: logger,
terminal: terminal,
signals: signals,
platform: platform,
processInfo: processInfo,
fileSystem: testFileSystem,
);
await expectLater(createTestCommandRunner(command).run(<String>[
'attach',
]), throwsToolExit(message: 'Lost connection to device.'));
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
DeviceManager: () => testDeviceManager,
});
testUsingContext('Does not catch generic RPC error', () async {
final FakeAndroidDevice device = FakeAndroidDevice(id: '1')
..portForwarder = const NoOpDevicePortForwarder()