Ben Konyi 5ea2be69ca
Reland "Fix issue where DevTools would not be immediately available when using --start-paused (#126698)" (#129368)
**Original Description:**

> Service extensions are unable to handle requests when the isolate they
were registered on is paused. The DevTools launcher logic was waiting
for some service extension invocations to complete before advertising
the already active DevTools instance, but when --start-paused was
provided these requests would never complete, preventing users from
using DevTools to resume the paused isolate.
> 
> Fixes https://github.com/flutter/flutter/issues/126691

**Additional changes in this PR:**

The failures listed in https://github.com/flutter/flutter/pull/128117
appear to be related to a shutdown race. It's possible for the test to
complete while the tool is in the process of starting and advertising
DevTools, so we need to perform a check of `_shutdown` in
`FlutterResidentDevtoolsHandler` before advertising DevTools.

Before the original fix, this check was being performed immediately
after invoking the service extensions, which creates an asynchronous gap
in execution. With #126698, the callsite of the service extensions was
moved and the `_shutdown` check wasn't, allowing for the tool to attempt
to advertise DevTools after the DevTools server had been cleaned up.

---------

Co-authored-by: Zachary Anderson <zanderso@users.noreply.github.com>
2023-06-28 00:16:13 +05:30

240 lines
6.6 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 'base/file_system.dart';
import 'base/logger.dart';
import 'build_info.dart';
import 'globals.dart' as globals;
import 'resident_runner.dart';
import 'tracing.dart';
import 'vmservice.dart';
const String kFlutterTestOutputsDirEnvName = 'FLUTTER_TEST_OUTPUTS_DIR';
class ColdRunner extends ResidentRunner {
ColdRunner(
super.flutterDevices, {
required super.target,
required super.debuggingOptions,
this.traceStartup = false,
this.awaitFirstFrameWhenTracing = true,
this.applicationBinary,
this.multidexEnabled = false,
bool super.ipv6 = false,
super.stayResident,
super.machine,
super.devtoolsHandler,
}) : super(
hotMode: false,
);
final bool traceStartup;
final bool awaitFirstFrameWhenTracing;
final File? applicationBinary;
final bool multidexEnabled;
bool _didAttach = false;
@override
bool get canHotReload => false;
@override
Logger get logger => globals.logger;
@override
FileSystem get fileSystem => globals.fs;
@override
Future<int> run({
Completer<DebugConnectionInfo>? connectionInfoCompleter,
Completer<void>? appStartedCompleter,
bool enableDevTools = false,
String? route,
}) async {
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, stack) {
globals.printError('$err\n$stack');
appFailedToStart();
return 1;
}
// Connect to the VM Service.
if (debuggingEnabled) {
try {
await connectToServiceProtocol(allowExistingDdsInstance: false);
} on Exception catch (exception) {
globals.printError(exception.toString());
appFailedToStart();
return 2;
}
}
if (debuggingEnabled) {
if (enableDevTools) {
// The method below is guaranteed never to return a failing future.
unawaited(residentDevtoolsHandler!.serveAndAnnounceDevTools(
devToolsServerAddress: debuggingOptions.devToolsServerAddress,
flutterDevices: flutterDevices,
isStartPaused: debuggingOptions.startPaused,
));
}
if (debuggingOptions.serveObservatory) {
await enableObservatory();
}
}
if (flutterDevices.first.vmServiceUris != 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}.');
final String outputPath = globals.platform.environment[kFlutterTestOutputsDirEnvName] ?? getBuildDirectory();
await downloadStartupTrace(
device.vmService!,
awaitFirstFrame: awaitFirstFrameWhenTracing,
logger: globals.logger,
output: globals.fs.directory(outputPath),
);
}
appFinished();
}
appStartedCompleter?.complete();
writeVmServiceFile();
if (stayResident && !traceStartup) {
return waitForAppToFinish();
}
await cleanupAtFinish();
return 0;
}
@override
Future<int> attach({
Completer<DebugConnectionInfo>? connectionInfoCompleter,
Completer<void>? appStartedCompleter,
bool allowExistingDdsInstance = false,
bool enableDevTools = false,
bool needsFullRestart = true,
}) async {
_didAttach = true;
try {
await connectToServiceProtocol(
getSkSLMethod: writeSkSL,
allowExistingDdsInstance: allowExistingDdsInstance,
);
} 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.');
}
}
if (debuggingEnabled) {
if (enableDevTools) {
// The method below is guaranteed never to return a failing future.
unawaited(residentDevtoolsHandler!.serveAndAnnounceDevTools(
devToolsServerAddress: debuggingOptions.devToolsServerAddress,
flutterDevices: flutterDevices,
isStartPaused: debuggingOptions.startPaused,
));
}
if (debuggingOptions.serveObservatory) {
await enableObservatory();
}
}
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 residentDevtoolsHandler!.shutdown();
await stopEchoingDeviceLog();
}
@override
void printHelp({ required bool details }) {
globals.printStatus('Flutter run key commands.');
if (details) {
printHelpDetails();
commandHelp.hWithDetails.print();
} else {
commandHelp.hWithoutDetails.print();
}
if (_didAttach) {
commandHelp.d.print();
}
commandHelp.c.print();
commandHelp.q.print();
printDebuggerList();
}
@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();
}
}