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

https://github.com/dart-lang/webdev/issues/2584 Reassemble was being called in DWDS in the injected client until v24.3.7. Flutter tools should now instead be the one to call the service extension. Now that it's in Flutter tools, we can also report how long it took. Similarly, we should update analytics on various things like, whether there was a reload rejection, how long the compile took, and more. Adds test to check that these analytics are being reported correctly. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing.
388 lines
12 KiB
Dart
388 lines
12 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.
|
|
|
|
// TODO(bkonyi): remove this file when ready to serve DevTools from DDS.
|
|
//
|
|
// See https://github.com/flutter/flutter/issues/150044
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package:meta/meta.dart';
|
|
|
|
import 'base/io.dart';
|
|
import 'base/logger.dart';
|
|
import 'build_info.dart';
|
|
import 'resident_runner.dart';
|
|
import 'vmservice.dart';
|
|
import 'web/chrome.dart';
|
|
|
|
typedef ResidentDevtoolsHandlerFactory =
|
|
ResidentDevtoolsHandler Function(DevtoolsLauncher?, ResidentRunner, Logger, ChromiumLauncher);
|
|
|
|
ResidentDevtoolsHandler createDefaultHandler(
|
|
DevtoolsLauncher? launcher,
|
|
ResidentRunner runner,
|
|
Logger logger,
|
|
ChromiumLauncher chromiumLauncher,
|
|
) {
|
|
return FlutterResidentDevtoolsHandler(launcher, runner, logger, chromiumLauncher);
|
|
}
|
|
|
|
/// 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;
|
|
|
|
/// The Dart Tooling Daemon (DTD) URI for the DTD instance being hosted by
|
|
/// DevTools server.
|
|
///
|
|
/// This will be null if the DevTools server is not served through Flutter
|
|
/// tools (e.g. if it is served from an IDE).
|
|
Uri? get dtdUri;
|
|
|
|
/// Whether to print the Dart Tooling Daemon URI.
|
|
///
|
|
/// This will always return false when there is not a DTD instance being
|
|
/// served from the DevTools server.
|
|
bool get printDtdUri;
|
|
|
|
/// 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,
|
|
this._chromiumLauncher,
|
|
);
|
|
|
|
static const Duration launchInBrowserTimeout = Duration(seconds: 15);
|
|
|
|
final DevtoolsLauncher? _devToolsLauncher;
|
|
final ResidentRunner _residentRunner;
|
|
final ChromiumLauncher _chromiumLauncher;
|
|
final Logger _logger;
|
|
bool _shutdown = false;
|
|
|
|
@visibleForTesting
|
|
bool launchedInBrowser = false;
|
|
|
|
@override
|
|
DevToolsServerAddress? get activeDevToolsServer {
|
|
assert(!_readyToAnnounce || _devToolsLauncher?.activeDevToolsServer != null);
|
|
return _devToolsLauncher?.activeDevToolsServer;
|
|
}
|
|
|
|
@override
|
|
Uri? get dtdUri => _devToolsLauncher?.dtdUri;
|
|
|
|
@override
|
|
bool get printDtdUri => _devToolsLauncher?.printDtdUri ?? false;
|
|
|
|
@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();
|
|
}
|
|
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;
|
|
}
|
|
|
|
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');
|
|
|
|
_chromiumLauncher.launch(devToolsUrl).catchError((Object e) {
|
|
_logger.printError('Failed to launch web browser: $e');
|
|
throw ProcessException('Chrome', <String>[
|
|
devToolsUrl,
|
|
], 'Failed to launch browser for dev tools');
|
|
}).ignore();
|
|
}
|
|
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) {
|
|
if (!_shutdown) {
|
|
_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) {
|
|
if (!_shutdown) {
|
|
_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 {
|
|
_shutdown = true;
|
|
if (_devToolsLauncher == null) {
|
|
return;
|
|
}
|
|
_readyToAnnounce = false;
|
|
await _devToolsLauncher.close();
|
|
}
|
|
}
|
|
|
|
@visibleForTesting
|
|
NoOpDevtoolsHandler createNoOpHandler(
|
|
DevtoolsLauncher? launcher,
|
|
ResidentRunner runner,
|
|
Logger logger,
|
|
ChromiumLauncher? chromiumLauncher,
|
|
) {
|
|
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;
|
|
}
|
|
|
|
@override
|
|
Uri? get dtdUri => null;
|
|
|
|
@override
|
|
bool get printDtdUri => false;
|
|
}
|
|
|
|
/// 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();
|
|
}
|