[flutter_tool] Handle RPCErrorKind.kConnectionDisposed (#164299)

There's currently a lot of code that handles RPC Errors that contain the
text "Service connection disposed" because the error originally did not
have a unique error code.

A new error code was added in
https://dart-review.googlesource.com/c/sdk/+/381501 but it's not
currently used because it won't be caught by existing code.

This change updates all places that check for this text, and now also
handle the new error code in preperation for the code changing in
future.

See https://github.com/flutter/flutter/issues/153471

cc @bkonyi 

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
Issue listed, but this change does not directly fix it, it just prepares
for a related future change that will simplify handling these errors
without string checks
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].
This commit is contained in:
Danny Tuppeny
2025-03-27 08:51:38 +00:00
committed by GitHub
parent c575638443
commit 53b87635b0
7 changed files with 129 additions and 5 deletions

View File

@@ -430,6 +430,7 @@ known, it can be explicitly provided to attach via the command-line, e.g.
}
} on RPCError catch (err) {
if (err.code == RPCErrorKind.kServiceDisappeared.code ||
err.code == RPCErrorKind.kConnectionDisposed.code ||
err.message.contains('Service connection disposed')) {
throwToolExit('Lost connection to device.');
}

View File

@@ -868,6 +868,7 @@ class RunCommand extends RunCommandBase {
}
} on RPCError catch (error) {
if (error.code == RPCErrorKind.kServiceDisappeared.code ||
error.code == RPCErrorKind.kConnectionDisposed.code ||
error.message.contains('Service connection disposed')) {
throwToolExit('Lost connection to device.');
}

View File

@@ -519,6 +519,7 @@ class DevFS {
_baseUri = Uri.parse(response.json!['uri'] as String);
} on vm_service.RPCError catch (rpcException) {
if (rpcException.code == vm_service.RPCErrorKind.kServiceDisappeared.code ||
rpcException.code == vm_service.RPCErrorKind.kConnectionDisposed.code ||
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.

View File

@@ -486,6 +486,7 @@ class FlutterVmService {
return await service.getVM();
} on vm_service.RPCError catch (err) {
if (err.code == vm_service.RPCErrorKind.kServiceDisappeared.code ||
err.code == vm_service.RPCErrorKind.kConnectionDisposed.code ||
err.message.contains('Service connection disposed')) {
globals.printTrace('VmService.getVm call failed: $err');
return null;
@@ -507,6 +508,7 @@ class FlutterVmService {
// Swallow the exception here and let the shutdown logic elsewhere deal
// with cleaning up.
if (e.code == vm_service.RPCErrorKind.kServiceDisappeared.code ||
e.code == vm_service.RPCErrorKind.kConnectionDisposed.code ||
e.message.contains('Service connection disposed')) {
return null;
}
@@ -792,6 +794,7 @@ class FlutterVmService {
// disappears while handling a request, return null.
if ((err.code == vm_service.RPCErrorKind.kMethodNotFound.code) ||
(err.code == vm_service.RPCErrorKind.kServiceDisappeared.code) ||
(err.code == vm_service.RPCErrorKind.kConnectionDisposed.code) ||
(err.message.contains('Service connection disposed'))) {
return null;
}

View File

@@ -1385,7 +1385,55 @@ void main() {
);
testUsingContext(
'Catches "Service connection disposed" error',
'Catches "Service connection disposed" error by code',
() 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',
vm_service.RPCErrorKind.kConnectionDisposed.code,
'dummy text not matched',
);
};
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(
'Catches "Service connection disposed" error by text',
() async {
final FakeAndroidDevice device =
FakeAndroidDevice(id: '1')

View File

@@ -1156,7 +1156,7 @@ void main() {
});
testUsingContext(
'Flutter run catches catches errors due to vm service disconnection and throws a tool exit',
'Flutter run catches catches errors due to vm service disconnection by text and throws a tool exit',
() async {
final FakeResidentRunner residentRunner = FakeResidentRunner();
residentRunner.rpcError = RPCError(
@@ -1190,6 +1190,41 @@ void main() {
},
);
testUsingContext(
'Flutter run catches catches errors due to vm service disconnection by code and throws a tool exit',
() async {
final FakeResidentRunner residentRunner = FakeResidentRunner();
residentRunner.rpcError = RPCError(
'flutter._listViews',
RPCErrorKind.kServiceDisappeared.code,
'',
);
final TestRunCommandWithFakeResidentRunner command = TestRunCommandWithFakeResidentRunner();
command.fakeResidentRunner = residentRunner;
await expectToolExitLater(
createTestCommandRunner(command).run(<String>['run', '--no-pub']),
contains('Lost connection to device.'),
);
residentRunner.rpcError = RPCError(
'flutter._listViews',
RPCErrorKind.kConnectionDisposed.code,
'dummy text not matched.',
);
await expectToolExitLater(
createTestCommandRunner(command).run(<String>['run', '--no-pub']),
contains('Lost connection to device.'),
);
},
overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
},
);
testUsingContext(
'Flutter run does not catch other RPC errors',
() async {

View File

@@ -499,10 +499,10 @@ Uptime: 441088659 Realtime: 521464097
});
testUsingContext(
'AdbLogReader.provideVmService catches any RPCError due to VM service disconnection',
'AdbLogReader.provideVmService catches any RPCError due to VM service disconnection by text',
() async {
final BufferLogger logger = globals.logger as BufferLogger;
final FlutterVmService vmService = FlutterVmService(_MyFakeVmService());
final FlutterVmService vmService = FlutterVmService(_MyFakeVmServiceConnectionDisposedText());
final AdbLogReader logReader = AdbLogReader.test(FakeProcess(), 'foo', logger);
await logReader.provideVmService(vmService);
expect(
@@ -518,15 +518,50 @@ Uptime: 441088659 Realtime: 521464097
},
overrides: <Type, Generator>{Logger: () => BufferLogger.test()},
);
testUsingContext(
'AdbLogReader.provideVmService catches any RPCError due to VM service disconnection by code',
() async {
final BufferLogger logger = globals.logger as BufferLogger;
final FlutterVmService vmService = FlutterVmService(_MyFakeVmServiceConnectionDisposedCode());
final AdbLogReader logReader = AdbLogReader.test(FakeProcess(), 'foo', logger);
await logReader.provideVmService(vmService);
expect(
logger.traceText,
'VmService.getVm call failed: null: (-32010) '
'Dummy text not matched\n',
);
expect(
logger.errorText,
'An error occurred when setting up filtering for adb logs. '
'Unable to communicate with the VM service.\n',
);
},
overrides: <Type, Generator>{Logger: () => BufferLogger.test()},
);
}
class _MyFakeVmService extends Fake implements VmService {
/// A mock VM Service that throws a generic [RPCErrorKind.kServerError] error
/// with the text "Service connection disposed".
///
/// This is the way these errors are currently sent (as of Feb 2025) but are
/// planned to be migrated to their own error code (see
/// [_MyFakeVmServiceConnectionDisposedCode]) soon.
class _MyFakeVmServiceConnectionDisposedText extends Fake implements VmService {
@override
Future<VM> getVM() async {
throw RPCError(null, RPCErrorKind.kServerError.code, 'Service connection disposed');
}
}
/// A mock VM Service that throws a [RPCErrorKind.kConnectionDisposed] error.
class _MyFakeVmServiceConnectionDisposedCode extends Fake implements VmService {
@override
Future<VM> getVM() async {
throw RPCError(null, RPCErrorKind.kConnectionDisposed.code, 'Dummy text not matched');
}
}
AndroidDevice setUpAndroidDevice({
String? id,
AndroidSdk? androidSdk,