Jonah Williams 377879825e
[flutter_tools] prevent StateError when log reader is disposed (#52173)
Previously the AdbLogReader did async setup in the StreamController.onListen callback, specifically it would query the api version and start the adb process. If the log subscription was cancelled before this setup completed, then the log output could (haven't confirmed) get added to a closed controller, causing the above state error.
2020-03-10 11:34:40 -07:00

217 lines
6.1 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';
// TODO(mklim): Test this, flutter/flutter#23031.
class ColdRunner extends ResidentRunner {
ColdRunner(
List<FlutterDevice> devices, {
String target,
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;
}
}
for (final FlutterDevice device in flutterDevices) {
final int result = await device.runCold(
coldRunner: this,
route: route,
);
if (result != 0) {
return result;
}
}
// Connect to observatory.
if (debuggingOptions.debuggingEnabled) {
try {
await connectToServiceProtocol();
} on String catch (message) {
globals.printError(message);
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();
await device.refreshViews();
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();
} on Exception catch (error) {
globals.printError('Error connecting to the service protocol: $error');
// https://github.com/flutter/flutter/issues/33050
// TODO(blasten): Remove this check once https://issuetracker.google.com/issues/132325318 has been fixed.
if (await hasDeviceRunningAndroidQ(flutterDevices) &&
error.toString().contains(kAndroidQHttpConnectionClosedExp)) {
globals.printStatus('🔨 If you are using an emulator running Android Q Beta, consider using an emulator running API level 29 or lower.');
globals.printStatus('Learn more about the status of this issue on https://issuetracker.google.com/issues/132325318');
}
return 2;
}
for (final FlutterDevice device in flutterDevices) {
await device.initLogReader();
}
await refreshViews();
for (final FlutterDevice device in flutterDevices) {
for (final FlutterView view in device.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);
}
}
await super.preExit();
}
}