flutter/packages/flutter_tools/lib/src/resident_devtools_handler.dart
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

351 lines
11 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:browser_launcher/browser_launcher.dart';
import 'package:meta/meta.dart';
import 'base/logger.dart';
import 'build_info.dart';
import 'resident_runner.dart';
import 'vmservice.dart';
typedef ResidentDevtoolsHandlerFactory = ResidentDevtoolsHandler Function(DevtoolsLauncher?, ResidentRunner, Logger);
ResidentDevtoolsHandler createDefaultHandler(DevtoolsLauncher? launcher, ResidentRunner runner, Logger logger) {
return FlutterResidentDevtoolsHandler(launcher, runner, logger);
}
/// Helper class to manage the life-cycle of devtools and its interaction with
/// the resident runner.
abstract class ResidentDevtoolsHandler {
/// The current devtools server, or null if one is not running.
DevToolsServerAddress? get activeDevToolsServer;
/// Whether it's ok to announce the [activeDevToolsServer].
///
/// This should only return true once all the devices have been notified
/// of the DevTools.
bool get readyToAnnounce;
Future<void> hotRestart(List<FlutterDevice?> flutterDevices);
Future<void> serveAndAnnounceDevTools({
Uri? devToolsServerAddress,
required List<FlutterDevice?> flutterDevices,
bool isStartPaused = false,
});
bool launchDevToolsInBrowser({required List<FlutterDevice?> flutterDevices});
Future<void> shutdown();
}
class FlutterResidentDevtoolsHandler implements ResidentDevtoolsHandler {
FlutterResidentDevtoolsHandler(this._devToolsLauncher, this._residentRunner, this._logger);
static const Duration launchInBrowserTimeout = Duration(seconds: 15);
final DevtoolsLauncher? _devToolsLauncher;
final ResidentRunner _residentRunner;
final Logger _logger;
bool _shutdown = false;
bool _served = false;
@visibleForTesting
bool launchedInBrowser = false;
@override
DevToolsServerAddress? get activeDevToolsServer {
assert(!_readyToAnnounce || _devToolsLauncher?.activeDevToolsServer != null);
return _devToolsLauncher?.activeDevToolsServer;
}
@override
bool get readyToAnnounce => _readyToAnnounce;
bool _readyToAnnounce = false;
// This must be guaranteed not to return a Future that fails.
@override
Future<void> serveAndAnnounceDevTools({
Uri? devToolsServerAddress,
required List<FlutterDevice?> flutterDevices,
bool isStartPaused = false,
}) async {
assert(!_readyToAnnounce);
if (!_residentRunner.supportsServiceProtocol || _devToolsLauncher == null) {
return;
}
if (devToolsServerAddress != null) {
_devToolsLauncher!.devToolsUrl = devToolsServerAddress;
} else {
await _devToolsLauncher!.serve();
_served = true;
}
await _devToolsLauncher!.ready;
// Do not attempt to print debugger list if the connection has failed or if we're shutting down.
if (_devToolsLauncher!.activeDevToolsServer == null || _shutdown) {
assert(!_readyToAnnounce);
return;
}
final Uri? devToolsUrl = _devToolsLauncher!.devToolsUrl;
if (devToolsUrl != null) {
for (final FlutterDevice? device in flutterDevices) {
if (device == null) {
continue;
}
// Notify the DDS instances that there's a DevTools instance available so they can correctly
// redirect DevTools related requests.
device.device?.dds.setExternalDevToolsUri(devToolsUrl);
}
}
Future<void> callServiceExtensions() async {
final List<FlutterDevice?> devicesWithExtension = await _devicesWithExtensions(flutterDevices);
await Future.wait(
<Future<void>>[
_maybeCallDevToolsUriServiceExtension(devicesWithExtension),
_callConnectedVmServiceUriExtension(devicesWithExtension)
]
);
}
// If the application is starting paused, we can't invoke service extensions
// as they're handled on the target app's paused isolate. Since invoking
// service extensions will block in this situation, we should wait to invoke
// them until after we've output the DevTools connection details.
if (!isStartPaused) {
await callServiceExtensions();
}
// This check needs to happen after the possible asynchronous call above,
// otherwise a shutdown event might be missed and the DevTools launcher may
// no longer be initialized.
if (_shutdown) {
// If we're shutting down, no point reporting the debugger list.
return;
}
_readyToAnnounce = true;
assert(_devToolsLauncher!.activeDevToolsServer != null);
if (_residentRunner.reportedDebuggers) {
// Since the DevTools only just became available, we haven't had a chance to
// report their URLs yet. Do so now.
_residentRunner.printDebuggerList(includeVmService: false);
}
if (isStartPaused) {
await callServiceExtensions();
}
}
// This must be guaranteed not to return a Future that fails.
@override
bool launchDevToolsInBrowser({required List<FlutterDevice?> flutterDevices}) {
if (!_residentRunner.supportsServiceProtocol || _devToolsLauncher == null) {
return false;
}
if (_devToolsLauncher!.devToolsUrl == null) {
_logger.startProgress('Waiting for Flutter DevTools to be served...');
unawaited(_devToolsLauncher!.ready.then((_) {
_launchDevToolsForDevices(flutterDevices);
}));
} else {
_launchDevToolsForDevices(flutterDevices);
}
return true;
}
void _launchDevToolsForDevices(List<FlutterDevice?> flutterDevices) {
assert(activeDevToolsServer != null);
for (final FlutterDevice? device in flutterDevices) {
final String devToolsUrl = activeDevToolsServer!.uri!.replace(
queryParameters: <String, dynamic>{'uri': '${device!.vmService!.httpAddress}'},
).toString();
_logger.printStatus('Launching Flutter DevTools for ${device.device!.name} at $devToolsUrl');
unawaited(Chrome.start(<String>[devToolsUrl]));
}
launchedInBrowser = true;
}
Future<void> _maybeCallDevToolsUriServiceExtension(
List<FlutterDevice?> flutterDevices,
) async {
if (_devToolsLauncher?.activeDevToolsServer == null) {
return;
}
await Future.wait(<Future<void>>[
for (final FlutterDevice? device in flutterDevices)
if (device?.vmService != null) _callDevToolsUriExtension(device!),
]);
}
Future<void> _callDevToolsUriExtension(
FlutterDevice device,
) async {
try {
await _invokeRpcOnFirstView(
'ext.flutter.activeDevToolsServerAddress',
device: device,
params: <String, dynamic>{
'value': _devToolsLauncher!.activeDevToolsServer!.uri.toString(),
},
);
} on Exception catch (e) {
_logger.printError(
'Failed to set DevTools server address: $e. Deep links to'
' DevTools will not show in Flutter errors.',
);
}
}
Future<List<FlutterDevice?>> _devicesWithExtensions(List<FlutterDevice?> flutterDevices) async {
return Future.wait(<Future<FlutterDevice?>>[
for (final FlutterDevice? device in flutterDevices) _waitForExtensionsForDevice(device!),
]);
}
/// Returns null if the service extension cannot be found on the device.
Future<FlutterDevice?> _waitForExtensionsForDevice(FlutterDevice flutterDevice) async {
const String extension = 'ext.flutter.connectedVmServiceUri';
try {
await flutterDevice.vmService?.findExtensionIsolate(
extension,
);
return flutterDevice;
} on VmServiceDisappearedException {
_logger.printTrace(
'The VM Service for ${flutterDevice.device} disappeared while trying to'
' find the $extension service extension. Skipping subsequent DevTools '
'setup for this device.',
);
return null;
}
}
Future<void> _callConnectedVmServiceUriExtension(List<FlutterDevice?> flutterDevices) async {
await Future.wait(<Future<void>>[
for (final FlutterDevice? device in flutterDevices)
if (device?.vmService != null) _callConnectedVmServiceExtension(device!),
]);
}
Future<void> _callConnectedVmServiceExtension(FlutterDevice device) async {
final Uri? uri = device.vmService!.httpAddress ?? device.vmService!.wsAddress;
if (uri == null) {
return;
}
try {
await _invokeRpcOnFirstView(
'ext.flutter.connectedVmServiceUri',
device: device,
params: <String, dynamic>{
'value': uri.toString(),
},
);
} on Exception catch (e) {
_logger.printError(e.toString());
_logger.printError(
'Failed to set vm service URI: $e. Deep links to DevTools'
' will not show in Flutter errors.',
);
}
}
Future<void> _invokeRpcOnFirstView(
String method, {
required FlutterDevice device,
required Map<String, dynamic> params,
}) async {
if (device.targetPlatform == TargetPlatform.web_javascript) {
await device.vmService!.callMethodWrapper(
method,
args: params,
);
return;
}
final List<FlutterView> views = await device.vmService!.getFlutterViews();
if (views.isEmpty) {
return;
}
await device.vmService!.invokeFlutterExtensionRpcRaw(
method,
args: params,
isolateId: views.first.uiIsolate!.id!,
);
}
@override
Future<void> hotRestart(List<FlutterDevice?> flutterDevices) async {
final List<FlutterDevice?> devicesWithExtension = await _devicesWithExtensions(flutterDevices);
await Future.wait(<Future<void>>[
_maybeCallDevToolsUriServiceExtension(devicesWithExtension),
_callConnectedVmServiceUriExtension(devicesWithExtension),
]);
}
@override
Future<void> shutdown() async {
if (_devToolsLauncher == null || _shutdown || !_served) {
return;
}
_shutdown = true;
_readyToAnnounce = false;
await _devToolsLauncher!.close();
}
}
@visibleForTesting
NoOpDevtoolsHandler createNoOpHandler(DevtoolsLauncher? launcher, ResidentRunner runner, Logger logger) {
return NoOpDevtoolsHandler();
}
@visibleForTesting
class NoOpDevtoolsHandler implements ResidentDevtoolsHandler {
bool wasShutdown = false;
@override
DevToolsServerAddress? get activeDevToolsServer => null;
@override
bool get readyToAnnounce => false;
@override
Future<void> hotRestart(List<FlutterDevice?> flutterDevices) async {
return;
}
@override
Future<void> serveAndAnnounceDevTools({
Uri? devToolsServerAddress,
List<FlutterDevice?>? flutterDevices,
bool isStartPaused = false,
}) async {
return;
}
@override
bool launchDevToolsInBrowser({List<FlutterDevice?>? flutterDevices}) {
return false;
}
@override
Future<void> shutdown() async {
wasShutdown = true;
return;
}
}
/// Convert a [URI] with query parameters into a display format instead
/// of the default URI encoding.
String urlToDisplayString(Uri uri) {
final StringBuffer base = StringBuffer(uri.replace(
queryParameters: <String, String>{},
).toString());
base.write(uri.queryParameters.keys.map((String key) => '$key=${uri.queryParameters[key]}').join('&'));
return base.toString();
}