mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

New feature in upcoming Dart 3.2. See https://github.com/dart-lang/language/issues/2020. Feature is enabled by bumping the min SDK version to 3.2. Part of https://github.com/flutter/flutter/issues/134476.
351 lines
11 KiB
Dart
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();
|
|
}
|