flutter/packages/flutter_tools/lib/src/run_cold.dart
Jonah Williams 2188196125
[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
2020-06-30 12:26:18 -07:00

218 lines
5.8 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:meta/meta.dart';
import 'base/file_system.dart';
import 'device.dart';
import 'globals.dart' as globals;
import 'resident_runner.dart';
import 'tracing.dart';
import 'vmservice.dart';
class ColdRunner extends ResidentRunner {
ColdRunner(
List<FlutterDevice> devices, {
String target,
@required DebuggingOptions debuggingOptions,
this.traceStartup = false,
this.awaitFirstFrameWhenTracing = true,
this.applicationBinary,
bool ipv6 = false,
bool stayResident = true,
}) : super(devices,
target: target,
debuggingOptions: debuggingOptions,
hotMode: false,
stayResident: stayResident,
ipv6: ipv6);
final bool traceStartup;
final bool awaitFirstFrameWhenTracing;
final File applicationBinary;
bool _didAttach = false;
@override
bool get canHotReload => false;
@override
bool get canHotRestart => false;
@override
Future<int> run({
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
String route,
}) async {
final bool prebuiltMode = applicationBinary != null;
if (!prebuiltMode) {
if (!globals.fs.isFileSync(mainPath)) {
String message = 'Tried to run $mainPath, but that file does not exist.';
if (target == null) {
message += '\nConsider using the -t option to specify the Dart file to start.';
}
globals.printError(message);
return 1;
}
}
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.
if (debuggingOptions.debuggingEnabled) {
try {
await connectToServiceProtocol();
} on String catch (message) {
globals.printError(message);
appFailedToStart();
return 2;
}
}
if (flutterDevices.first.observatoryUris != null) {
// For now, only support one debugger connection.
connectionInfoCompleter?.complete(DebugConnectionInfo(
httpUri: flutterDevices.first.vmService.httpAddress,
wsUri: flutterDevices.first.vmService.wsAddress,
));
}
globals.printTrace('Application running.');
for (final FlutterDevice device in flutterDevices) {
if (device.vmService == null) {
continue;
}
await device.initLogReader();
globals.printTrace('Connected to ${device.device.name}');
}
if (traceStartup) {
// Only trace startup for the first device.
final FlutterDevice device = flutterDevices.first;
if (device.vmService != null) {
globals.printStatus('Tracing startup on ${device.device.name}.');
await downloadStartupTrace(
device.vmService,
awaitFirstFrame: awaitFirstFrameWhenTracing,
);
}
appFinished();
}
appStartedCompleter?.complete();
writeVmserviceFile();
if (stayResident && !traceStartup) {
return waitForAppToFinish();
}
await cleanupAtFinish();
return 0;
}
@override
Future<int> attach({
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
}) async {
_didAttach = true;
try {
await connectToServiceProtocol(
getSkSLMethod: writeSkSL,
);
} on Exception catch (error) {
globals.printError('Error connecting to the service protocol: $error');
return 2;
}
for (final FlutterDevice device in flutterDevices) {
await device.initLogReader();
}
for (final FlutterDevice device in flutterDevices) {
final List<FlutterView> views = await device.vmService.getFlutterViews();
for (final FlutterView view in views) {
globals.printTrace('Connected to $view.');
}
}
appStartedCompleter?.complete();
if (stayResident) {
return waitForAppToFinish();
}
await cleanupAtFinish();
return 0;
}
@override
Future<void> cleanupAfterSignal() async {
await stopEchoingDeviceLog();
if (_didAttach) {
appFinished();
}
await exitApp();
}
@override
Future<void> cleanupAtFinish() async {
for (final FlutterDevice flutterDevice in flutterDevices) {
await flutterDevice.device.dispose();
}
await stopEchoingDeviceLog();
}
@override
void printHelp({ @required bool details }) {
globals.printStatus('Flutter run key commands.');
if (supportsServiceProtocol) {
if (details) {
printHelpDetails();
}
}
commandHelp.h.print();
if (_didAttach) {
commandHelp.d.print();
}
commandHelp.c.print();
commandHelp.q.print();
for (final FlutterDevice device in flutterDevices) {
final String dname = device.device.name;
if (device.vmService != null) {
// Caution: This log line is parsed by device lab tests.
globals.printStatus(
'An Observatory debugger and profiler on $dname is available at: '
'${device.vmService.httpAddress}',
);
}
}
}
@override
Future<void> preExit() async {
for (final FlutterDevice device in flutterDevices) {
// If we're running in release mode, stop the app using the device logic.
if (device.vmService == null) {
await device.device.stopApp(device.package, userIdentifier: device.userIdentifier);
}
}
await super.preExit();
}
}