Unverified Commit 21881961 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] ensure flutter daemon can exit correctly when app fails to start (#60615)

The flutter daemon unconditionally waits for the appFinished signal, even if startup failed. Ensure this future is correctly completed if there is a failure in ResidentRunner.run and not just ResidentRunner.attach. Adds regression tests for run release, debug, debug web, and release web. Adds missing try catch in cold runner startup.

Manually tested with release/debug on Android and release/debug on web.

Fixes #60613
parent cf8fbc36
......@@ -410,6 +410,7 @@ class _ResidentWebRunner extends ResidentWebRunner {
'\nConsider using the -t option to specify the Dart file to start.';
}
globals.printError(message);
appFailedToStart();
return 1;
}
final String modeName = debuggingOptions.buildInfo.friendlyModeName;
......@@ -458,6 +459,7 @@ class _ResidentWebRunner extends ResidentWebRunner {
final UpdateFSReport report = await _updateDevFS(fullRestart: true);
if (!report.success) {
globals.printError('Failed to compile application.');
appFailedToStart();
return 1;
}
device.generator.accept();
......@@ -485,13 +487,20 @@ class _ResidentWebRunner extends ResidentWebRunner {
);
});
} on WebSocketException {
appFailedToStart();
throwToolExit(kExitMessage);
} on ChromeDebugException {
appFailedToStart();
throwToolExit(kExitMessage);
} on AppConnectionException {
appFailedToStart();
throwToolExit(kExitMessage);
} on SocketException {
appFailedToStart();
throwToolExit(kExitMessage);
} on Exception {
appFailedToStart();
rethrow;
}
return 0;
}
......
......@@ -1226,6 +1226,12 @@ abstract class ResidentRunner {
_finished.complete(0);
}
void appFailedToStart() {
if (!_finished.isCompleted) {
_finished.complete(1);
}
}
Future<int> waitForAppToFinish() async {
final int exitCode = await _finished.future;
assert(exitCode != null);
......
......@@ -59,14 +59,21 @@ class ColdRunner extends ResidentRunner {
}
}
for (final FlutterDevice device in flutterDevices) {
final int result = await device.runCold(
coldRunner: this,
route: route,
);
if (result != 0) {
return result;
try {
for (final FlutterDevice device in flutterDevices) {
final int result = await device.runCold(
coldRunner: this,
route: route,
);
if (result != 0) {
appFailedToStart();
return result;
}
}
} on Exception catch (err) {
globals.printError(err.toString());
appFailedToStart();
return 1;
}
// Connect to observatory.
......@@ -75,6 +82,7 @@ class ColdRunner extends ResidentRunner {
await connectToServiceProtocol();
} on String catch (message) {
globals.printError(message);
appFailedToStart();
return 2;
}
}
......
......@@ -373,11 +373,13 @@ class HotRunner extends ResidentRunner {
try {
final List<bool> results = await Future.wait(startupTasks);
if (!results.every((bool passed) => passed)) {
appFailedToStart();
return 1;
}
cacheInitialDillCompilation();
} on Exception catch (err) {
globals.printError(err.toString());
appFailedToStart();
return 1;
}
......
......@@ -251,6 +251,89 @@ void main() {
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}));
// Regression test for https://github.com/flutter/flutter/issues/60613
testUsingContext('ResidentRunner calls appFailedToStart if initial compilation fails', () => testbed.run(() async {
globals.fs.file(globals.fs.path.join('lib', 'main.dart'))
.createSync(recursive: true);
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
final MockResidentCompiler residentCompiler = MockResidentCompiler();
residentRunner = HotRunner(
<FlutterDevice>[
mockFlutterDevice,
],
stayResident: false,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
);
when(mockFlutterDevice.generator).thenReturn(residentCompiler);
when(residentCompiler.recompile(
any,
any,
outputPath: anyNamed('outputPath'),
packageConfig: anyNamed('packageConfig'),
suppressErrors: true,
)).thenAnswer((Invocation invocation) async {
return const CompilerOutput('foo', 1 ,<Uri>[]);
});
when(mockFlutterDevice.runHot(
hotRunner: anyNamed('hotRunner'),
route: anyNamed('route'),
)).thenAnswer((Invocation invocation) async {
return 0;
});
expect(await residentRunner.run(), 1);
// Completing this future ensures that the daemon can exit correctly.
expect(await residentRunner.waitForAppToFinish(), 1);
}));
// Regression test for https://github.com/flutter/flutter/issues/60613
testUsingContext('ResidentRunner calls appFailedToStart if initial compilation fails - cold mode', () => testbed.run(() async {
globals.fs.file(globals.fs.path.join('lib', 'main.dart'))
.createSync(recursive: true);
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
residentRunner = ColdRunner(
<FlutterDevice>[
mockFlutterDevice,
],
stayResident: false,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.release),
);
when(mockFlutterDevice.runCold(
coldRunner: anyNamed('coldRunner'),
route: anyNamed('route'),
)).thenAnswer((Invocation invocation) async {
return 1;
});
expect(await residentRunner.run(), 1);
// Completing this future ensures that the daemon can exit correctly.
expect(await residentRunner.waitForAppToFinish(), 1);
}));
// Regression test for https://github.com/flutter/flutter/issues/60613
testUsingContext('ResidentRunner calls appFailedToStart if exception is thrown - cold mode', () => testbed.run(() async {
globals.fs.file(globals.fs.path.join('lib', 'main.dart'))
.createSync(recursive: true);
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
residentRunner = ColdRunner(
<FlutterDevice>[
mockFlutterDevice,
],
stayResident: false,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.release),
);
when(mockFlutterDevice.runCold(
coldRunner: anyNamed('coldRunner'),
route: anyNamed('route'),
)).thenAnswer((Invocation invocation) async {
throw Exception('BAD STUFF');
});
expect(await residentRunner.run(), 1);
// Completing this future ensures that the daemon can exit correctly.
expect(await residentRunner.waitForAppToFinish(), 1);
}));
testUsingContext('ResidentRunner does not suppressErrors if running with an applicationBinary', () => testbed.run(() async {
globals.fs.file(globals.fs.path.join('lib', 'main.dart'))
.createSync(recursive: true);
......
......@@ -92,6 +92,32 @@ void main() {
)),
}));
// Regression test for https://github.com/flutter/flutter/issues/60613
test('ResidentWebRunner calls appFailedToStart if initial compilation fails', () => testbed.run(() async {
_setupMocks();
when(mockBuildSystem.build(any, any)).thenAnswer((Invocation invocation) async {
return BuildResult(success: false);
});
expect(() async => await residentWebRunner.run(), throwsToolExit());
expect(await residentWebRunner.waitForAppToFinish(), 1);
}, overrides: <Type, Generator>{
BuildSystem: () => mockBuildSystem,
}));
// Regression test for https://github.com/flutter/flutter/issues/60613
test('ResidentWebRunner calls appFailedToStart if error is thrown during startup', () => testbed.run(() async {
_setupMocks();
when(mockBuildSystem.build(any, any)).thenAnswer((Invocation invocation) async {
throw Exception('foo');
});
expect(() async => await residentWebRunner.run(), throwsA(isA<Exception>()));
expect(await residentWebRunner.waitForAppToFinish(), 1);
}, overrides: <Type, Generator>{
BuildSystem: () => mockBuildSystem,
}));
test('Can full restart after attaching', () => testbed.run(() async {
_setupMocks();
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
......
......@@ -329,19 +329,54 @@ void main() {
Platform: () => FakePlatform(operatingSystem: 'linux', environment: <String, String>{}),
});
// Regression test for https://github.com/flutter/flutter/issues/60613
testUsingContext('ResidentWebRunner calls appFailedToStart if initial compilation fails', () async {
_setupMocks();
final ResidentRunner residentWebRunner = setUpResidentRunner(mockFlutterDevice);
fileSystem.file(globals.fs.path.join('lib', 'main.dart'))
.createSync(recursive: true);
fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList());
when(mockWebDevFS.update(
mainUri: anyNamed('mainUri'),
target: anyNamed('target'),
bundle: anyNamed('bundle'),
firstBuildTime: anyNamed('firstBuildTime'),
bundleFirstUpload: anyNamed('bundleFirstUpload'),
generator: anyNamed('generator'),
fullRestart: anyNamed('fullRestart'),
dillOutputPath: anyNamed('dillOutputPath'),
projectRootPath: anyNamed('projectRootPath'),
pathToReload: anyNamed('pathToReload'),
invalidatedFiles: anyNamed('invalidatedFiles'),
trackWidgetCreation: anyNamed('trackWidgetCreation'),
packageConfig: anyNamed('packageConfig'),
)).thenAnswer((Invocation _) async {
return UpdateFSReport(success: false, syncedBytes: 0)..invalidatedModules = <String>[];
});
expect(await residentWebRunner.run(), 1);
// Completing this future ensures that the daemon can exit correctly.
expect(await residentWebRunner.waitForAppToFinish(), 1);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
Pub: () => MockPub(),
Platform: () => FakePlatform(operatingSystem: 'linux', environment: <String, String>{}),
});
testUsingContext('Can successfully run without an index.html including status warning', () async {
fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList());
_setupMocks();
fileSystem.file(fileSystem.path.join('web', 'index.html'))
.deleteSync();
final ResidentWebRunner residentWebRunner = DwdsWebRunnerFactory().createWebRunner(
mockFlutterDevice,
flutterProject: FlutterProject.current(),
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
ipv6: true,
stayResident: false,
urlTunneller: null,
) as ResidentWebRunner;
mockFlutterDevice,
flutterProject: FlutterProject.current(),
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
ipv6: true,
stayResident: false,
urlTunneller: null,
) as ResidentWebRunner;
expect(await residentWebRunner.run(), 0);
expect(testLogger.statusText,
......
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