Unverified Commit 3b884aa6 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Catch VM Service disappearance from run/attach handler code (#75298)

parent 998d1c65
......@@ -7,6 +7,7 @@
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:vm_service/vm_service.dart';
import '../android/android_device.dart';
import '../artifacts.dart';
......@@ -29,6 +30,7 @@ import '../resident_runner.dart';
import '../run_cold.dart';
import '../run_hot.dart';
import '../runner/flutter_command.dart';
import '../vmservice.dart';
/// A Flutter-command that attaches to applications that have been launched
/// without `flutter run`.
......@@ -379,6 +381,11 @@ known, it can be explicitly provided to attach via the command-line, e.g.
}
globals.printStatus('Waiting for a new connection from Flutter on ${device.name}...');
}
} on RPCError catch (err) {
if (err.code == RPCErrorCodes.kServiceDisappeared) {
throwToolExit('Lost connection to device.');
}
rethrow;
} finally {
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
for (final ForwardedPort port in ports) {
......
......@@ -6,6 +6,9 @@
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:vm_service/vm_service.dart';
import '../android/android_device.dart';
import '../base/common.dart';
import '../base/file_system.dart';
......@@ -22,6 +25,7 @@ import '../run_cold.dart';
import '../run_hot.dart';
import '../runner/flutter_command.dart';
import '../tracing.dart';
import '../vmservice.dart';
import '../web/web_runner.dart';
import 'daemon.dart';
......@@ -456,6 +460,52 @@ class RunCommand extends RunCommandBase {
await devices.single.targetPlatform == TargetPlatform.web_javascript;
}
@visibleForTesting
Future<ResidentRunner> createRunner({
@required bool hotMode,
@required List<FlutterDevice> flutterDevices,
@required String applicationBinaryPath,
@required FlutterProject flutterProject,
}) async {
if (hotMode && !webMode) {
return HotRunner(
flutterDevices,
target: targetFile,
debuggingOptions: await createDebuggingOptions(webMode),
benchmarkMode: boolArg('benchmark'),
applicationBinary: applicationBinaryPath == null
? null
: globals.fs.file(applicationBinaryPath),
projectRootPath: stringArg('project-root'),
dillOutputPath: stringArg('output-dill'),
stayResident: stayResident,
ipv6: ipv6,
);
} else if (webMode) {
return webRunnerFactory.createWebRunner(
flutterDevices.single,
target: targetFile,
flutterProject: flutterProject,
ipv6: ipv6,
debuggingOptions: await createDebuggingOptions(webMode),
stayResident: stayResident,
urlTunneller: null,
);
}
return ColdRunner(
flutterDevices,
target: targetFile,
debuggingOptions: await createDebuggingOptions(webMode),
traceStartup: traceStartup,
awaitFirstFrameWhenTracing: awaitFirstFrameWhenTracing,
applicationBinary: applicationBinaryPath == null
? null
: globals.fs.file(applicationBinaryPath),
ipv6: ipv6,
stayResident: stayResident,
);
}
@override
Future<FlutterCommandResult> runCommand() async {
// Enable hot mode by default if `--no-hot` was not passed and we are in
......@@ -557,45 +607,12 @@ class RunCommand extends RunCommandBase {
),
];
ResidentRunner runner;
if (hotMode && !webMode) {
runner = HotRunner(
flutterDevices,
target: targetFile,
debuggingOptions: await createDebuggingOptions(webMode),
benchmarkMode: boolArg('benchmark'),
applicationBinary: applicationBinaryPath == null
? null
: globals.fs.file(applicationBinaryPath),
projectRootPath: stringArg('project-root'),
dillOutputPath: stringArg('output-dill'),
stayResident: stayResident,
ipv6: ipv6,
);
} else if (webMode) {
runner = webRunnerFactory.createWebRunner(
flutterDevices.single,
target: targetFile,
flutterProject: flutterProject,
ipv6: ipv6,
debuggingOptions: await createDebuggingOptions(webMode),
stayResident: stayResident,
urlTunneller: null,
);
} else {
runner = ColdRunner(
flutterDevices,
target: targetFile,
debuggingOptions: await createDebuggingOptions(webMode),
traceStartup: traceStartup,
awaitFirstFrameWhenTracing: awaitFirstFrameWhenTracing,
applicationBinary: applicationBinaryPath == null
? null
: globals.fs.file(applicationBinaryPath),
ipv6: ipv6,
stayResident: stayResident,
);
}
final ResidentRunner runner = await createRunner(
applicationBinaryPath: applicationBinaryPath,
flutterDevices: flutterDevices,
flutterProject: flutterProject,
hotMode: hotMode,
);
DateTime appStartedTime;
// Sync completer so the completing agent attaching to the resident doesn't
......@@ -620,13 +637,20 @@ class RunCommand extends RunCommandBase {
}
));
final int result = await runner.run(
appStartedCompleter: appStartedTimeRecorder,
enableDevTools: stayResident && boolArg(FlutterCommand.kEnableDevTools),
route: route,
);
if (result != 0) {
throwToolExit(null, exitCode: result);
try {
final int result = await runner.run(
appStartedCompleter: appStartedTimeRecorder,
enableDevTools: stayResident && boolArg(FlutterCommand.kEnableDevTools),
route: route,
);
if (result != 0) {
throwToolExit(null, exitCode: result);
}
} on RPCError catch (err) {
if (err.code == RPCErrorCodes.kServiceDisappeared) {
throwToolExit('Lost connection to device.');
}
rethrow;
}
return FlutterCommandResult(
ExitStatus.success,
......
......@@ -642,6 +642,84 @@ void main() {
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('Catches service disappeared error', () async {
final MockAndroidDevice device = MockAndroidDevice();
final MockHotRunner mockHotRunner = MockHotRunner();
final MockHotRunnerFactory mockHotRunnerFactory = MockHotRunnerFactory();
when(device.portForwarder).thenReturn(const NoOpDevicePortForwarder());
when(mockHotRunner.attach(
appStartedCompleter: anyNamed('appStartedCompleter'),
allowExistingDdsInstance: true,
enableDevTools: anyNamed('enableDevTools'),
)).thenAnswer((_) async {
await null;
throw vm_service.RPCError('flutter._listViews', RPCErrorCodes.kServiceDisappeared, '');
});
when(mockHotRunnerFactory.build(
any,
target: anyNamed('target'),
debuggingOptions: anyNamed('debuggingOptions'),
packagesFilePath: anyNamed('packagesFilePath'),
flutterProject: anyNamed('flutterProject'),
ipv6: false,
)).thenReturn(mockHotRunner);
testDeviceManager.addDevice(device);
when(device.getLogReader(includePastLogs: anyNamed('includePastLogs')))
.thenAnswer((_) {
return NoOpDeviceLogReader('test');
});
testFileSystem.file('lib/main.dart').createSync();
final AttachCommand command = AttachCommand(hotRunnerFactory: mockHotRunnerFactory);
await expectLater(createTestCommandRunner(command).run(<String>[
'attach',
]), throwsToolExit(message: 'Lost connection to device.'));
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('Does not catch generic RPC error', () async {
final MockAndroidDevice device = MockAndroidDevice();
final MockHotRunner mockHotRunner = MockHotRunner();
final MockHotRunnerFactory mockHotRunnerFactory = MockHotRunnerFactory();
when(device.portForwarder).thenReturn(const NoOpDevicePortForwarder());
when(mockHotRunner.attach(
appStartedCompleter: anyNamed('appStartedCompleter'),
allowExistingDdsInstance: true,
enableDevTools: anyNamed('enableDevTools'),
)).thenAnswer((_) async {
await null;
throw vm_service.RPCError('flutter._listViews', RPCErrorCodes.kInvalidParams, '');
});
when(mockHotRunnerFactory.build(
any,
target: anyNamed('target'),
debuggingOptions: anyNamed('debuggingOptions'),
packagesFilePath: anyNamed('packagesFilePath'),
flutterProject: anyNamed('flutterProject'),
ipv6: false,
)).thenReturn(mockHotRunner);
testDeviceManager.addDevice(device);
when(device.getLogReader(includePastLogs: anyNamed('includePastLogs')))
.thenAnswer((_) {
return NoOpDeviceLogReader('test');
});
testFileSystem.file('lib/main.dart').createSync();
final AttachCommand command = AttachCommand(hotRunnerFactory: mockHotRunnerFactory);
await expectLater(createTestCommandRunner(command).run(<String>[
'attach',
]), throwsA(isA<vm_service.RPCError>()));
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
});
}
......
......@@ -22,9 +22,14 @@ import 'package:flutter_tools/src/commands/run.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:flutter_tools/src/vmservice.dart';
import 'package:meta/meta.dart';
import 'package:mockito/mockito.dart';
import 'package:vm_service/vm_service.dart';
import '../../src/common.dart';
import '../../src/context.dart';
......@@ -542,6 +547,30 @@ void main() {
expect(dartDefines, <String>['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=false']);
});
});
testUsingContext('Flutter run catches service has disappear errors and throws a tool exit', () async {
final FakeResidentRunner residentRunner = FakeResidentRunner();
residentRunner.rpcError = RPCError('flutter._listViews', RPCErrorCodes.kServiceDisappeared, '');
final TestRunCommandWithFakeResidentRunner command = TestRunCommandWithFakeResidentRunner();
command.fakeResidentRunner = residentRunner;
await expectToolExitLater(createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
]), contains('Lost connection to device.'));
});
testUsingContext('Flutter run does not catch other RPC errors', () async {
final FakeResidentRunner residentRunner = FakeResidentRunner();
residentRunner.rpcError = RPCError('flutter._listViews', RPCErrorCodes.kInvalidParams, '');
final TestRunCommandWithFakeResidentRunner command = TestRunCommandWithFakeResidentRunner();
command.fakeResidentRunner = residentRunner;
await expectLater(() => createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
]), throwsA(isA<RPCError>()));
});
}
class MockCache extends Mock implements Cache {}
......@@ -585,7 +614,7 @@ class FakeDevice extends Fake implements Device {
bool supportsRuntimeMode(BuildMode mode) => true;
@override
bool get supportsHotReload => false;
bool supportsHotReload = false;
@override
bool get supportsFastStart => false;
......@@ -641,7 +670,7 @@ class FakeDevice extends Fake implements Device {
}
}
class FakeApplicationPackageFactory extends Fake implements ApplicationPackageFactory {
class FakeApplicationPackageFactory extends Fake implements ApplicationPackageFactory {
ApplicationPackage package;
@override
......@@ -653,3 +682,41 @@ class FakeApplicationPackageFactory extends Fake implements ApplicationPackageF
return package;
}
}
class TestRunCommandWithFakeResidentRunner extends RunCommand {
FakeResidentRunner fakeResidentRunner;
@override
Future<ResidentRunner> createRunner({
@required bool hotMode,
@required List<FlutterDevice> flutterDevices,
@required String applicationBinaryPath,
@required FlutterProject flutterProject,
}) async {
return fakeResidentRunner;
}
@override
// ignore: must_call_super
Future<void> validateCommand() async {
devices = <Device>[FakeDevice()..supportsHotReload = true];
}
}
class FakeResidentRunner extends Fake implements ResidentRunner {
RPCError rpcError;
@override
Future<int> run({
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
bool enableDevTools = false,
String route,
}) async {
await null;
if (rpcError != null) {
throw rpcError;
}
return 0;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment