From c8c09f6d052b60fbd2803f6acd0bc127c7786877 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Tue, 7 Oct 2025 18:55:27 -0400 Subject: [PATCH] [ Tool ] Output `app.dtd` and `app.devTools` in machine mode (#176655) Fixes https://github.com/flutter/flutter/issues/176310 --- packages/flutter_tools/doc/daemon.md | 52 +++++++++++--- .../lib/src/commands/daemon.dart | 13 ++-- .../lib/src/isolated/resident_web_runner.dart | 70 +++++++++++-------- .../lib/src/resident_runner.dart | 5 +- packages/flutter_tools/lib/src/run_cold.dart | 12 +++- packages/flutter_tools/lib/src/run_hot.dart | 9 ++- .../test/general.shard/cold_test.dart | 34 +++++++++ .../integration.shard/flutter_run_test.dart | 12 ++++ .../test/integration.shard/test_driver.dart | 31 ++++++++ 9 files changed, 189 insertions(+), 49 deletions(-) diff --git a/packages/flutter_tools/doc/daemon.md b/packages/flutter_tools/doc/daemon.md index 5fc9d22273e..1d4a4a381ab 100644 --- a/packages/flutter_tools/doc/daemon.md +++ b/packages/flutter_tools/doc/daemon.md @@ -17,13 +17,13 @@ A set of `flutter daemon` commands/events are also exposed via `flutter run --ma The daemon speaks [JSON-RPC](http://json-rpc.org/) to clients. It uses stdin and stdout as the transport protocol. To send a command to the server, create your command as a JSON-RPC message, encode it to JSON, surround the encoded text with square brackets, and write it as one line of text to the stdin of the process: ```json -[{"method":"daemon.version","id":0}] +[{ "method": "daemon.version", "id": 0 }] ``` The response will come back as a single line from stdout: ```json -[{"id":0,"result":"0.1.0"}] +[{ "id": 0, "result": "0.1.0" }] ``` All requests and responses should be wrapped in square brackets. This ensures that the communications are resilient to stray output in the stdout/stdin stream. @@ -35,17 +35,42 @@ Each command should have a `method` field. This is in the form '`domain.command` Any params for that command should be passed in through a `params` field. Here's an example request/response for the `device.getDevices` method: ```json -[{"method":"device.getDevices","id":2}] +[{ "method": "device.getDevices", "id": 2 }] ``` ```json -[{"id":2,"result":[{"id":"702ABC1F-5EA5-4F83-84AB-6380CA91D39A","name":"iPhone 6","platform":"ios_x64","available":true}]}] +[ + { + "id": 2, + "result": [ + { + "id": "702ABC1F-5EA5-4F83-84AB-6380CA91D39A", + "name": "iPhone 6", + "platform": "ios_x64", + "available": true + } + ] + } +] ``` Events that come from the server will have an `event` field containing the type of event, along with a `params` field. ```json -[{"event":"device.added","params":{"id":"1DD6786B-37D4-4355-AA15-B818A87A18B4","name":"iPhone XS Max","platform":"ios","emulator":true,"ephemeral":false,"platformType":"ios","category":"mobile"}}] +[ + { + "event": "device.added", + "params": { + "id": "1DD6786B-37D4-4355-AA15-B818A87A18B4", + "name": "iPhone XS Max", + "platform": "ios", + "emulator": true, + "ephemeral": false, + "platformType": "ios", + "category": "mobile" + } + } +] ``` ## Domains and Commands @@ -75,7 +100,7 @@ The schema for each element in `reasons` is: - reasonText (String) - a description of why the platform is not supported - fixText (String) - human readable instructions of how to fix this reason - fixCode (String) - stringified version of the `_ReasonCode` enum. To be used -by daemon clients who intend to auto-fix. + by daemon clients who intend to auto-fix. The possible platform types are the `PlatformType` enumeration in the lib/src/device.dart library. @@ -170,6 +195,17 @@ This is sent when an app is stopped or detached from. The `params` field will be This is sent once a web application is being served and available for the user to access. The `params` field will be a map with a string `url` field and a boolean `launched` indicating whether the application has already been launched in a browser (this will generally be true for a browser device unless `--no-web-browser-launch` was used, and false for the headless `web-server` device). +#### app.devTools + +This is sent after the [`app.debugPort`](#appdebugPort) event if DevTools is being served for this application instance. The +`params` field will be a map with the string `uri` field containing the DevTools URI with query parameters already set to connect +to the running application. + +#### app.dtd + +This is sent after the [`app.debugPort`](#appdebugPort) event if the Dart Tooling Daemon (DTD) is being served for this application +instance. The `params` field will be a map with the string `uri` field containing the DTD URI. + ### Daemon-to-Editor Requests These requests come _from_ the Flutter daemon and should be responded to by the client/editor. @@ -224,11 +260,11 @@ Removed a forwarded port. It takes `deviceId`, `devicePort`, and `hostPort` as r #### device.added -This is sent when a device is connected (and polling has been enabled via `enable()`). The `params` field will be a map with the fields `id`, `name`, `platform`, `category`, `platformType`, `ephemeral`, and `emulator`. For more information on `platform`, `category`, `platformType`, and `ephemeral` see `device.getDevices`. +This is sent when a device is connected (and polling has been enabled via `enable()`). The `params` field will be a map with the fields `id`, `name`, `platform`, `category`, `platformType`, `ephemeral`, and `emulator`. For more information on `platform`, `category`, `platformType`, and `ephemeral` see `device.getDevices`. #### device.removed -This is sent when a device is disconnected (and polling has been enabled via `enable()`). The `params` field will be a map with the fields `id`, `name`, `platform`, `category`, `platformType`, `ephemeral`, and `emulator`. For more information on `platform`, `category`, `platformType`, and `ephemeral` see `device.getDevices`. +This is sent when a device is disconnected (and polling has been enabled via `enable()`). The `params` field will be a map with the fields `id`, `name`, `platform`, `category`, `platformType`, `ephemeral`, and `emulator`. For more information on `platform`, `category`, `platformType`, and `ephemeral` see `device.getDevices`. ### emulator domain diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index 678f7a793ec..ae3e5a2d9a6 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -796,15 +796,18 @@ class AppDomain extends Domain { // As it just writes to stdout. unawaited( connectionInfoCompleter.future.then((DebugConnectionInfo info) { - final params = { + _sendAppEvent(app, 'debugPort', { // The web vmservice proxy does not have an http address. 'port': info.httpUri?.port ?? info.wsUri!.port, 'wsUri': info.wsUri.toString(), - }; - if (info.baseUri != null) { - params['baseUri'] = info.baseUri; + 'baseUri': ?info.baseUri, + }); + if (info.devToolsUri != null) { + _sendAppEvent(app, 'devTools', {'uri': info.devToolsUri!.toString()}); + } + if (info.dtdUri != null) { + _sendAppEvent(app, 'dtd', {'uri': info.dtdUri!.toString()}); } - _sendAppEvent(app, 'debugPort', params); }), ); } diff --git a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart index 12bf47fbb17..2284eba0cb8 100644 --- a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart @@ -138,7 +138,7 @@ class ResidentWebRunner extends ResidentRunner { @override FileSystem get fileSystem => _fileSystem; - FlutterDevice? get device => flutterDevices.first; + FlutterDevice? get flutterDevice => flutterDevices.first; final FlutterProject flutterProject; // Mapping from service name to service method. @@ -156,11 +156,11 @@ class ResidentWebRunner extends ResidentRunner { /// Device is debuggable if not a WebServer device, or if running with /// --start-paused or using DWDS WebSocket connection (WebServer device). late final bool _deviceIsDebuggable = - device!.device is! WebServerDevice || + flutterDevice!.device is! WebServerDevice || debuggingOptions.startPaused || useDwdsWebSocketConnection; - late final useDwdsWebSocketConnection = device!.device is! ChromiumDevice; + late final useDwdsWebSocketConnection = flutterDevice!.device is! ChromiumDevice; @override // Web uses a different plugin registry. @@ -220,7 +220,7 @@ class ResidentWebRunner extends ResidentRunner { await _extensionEventSub?.cancel(); if (stopAppDuringCleanup) { - await device!.device!.stopApp(null); + await flutterDevice!.device!.stopApp(null); } _registeredMethodsForService.clear(); @@ -243,7 +243,7 @@ class ResidentWebRunner extends ResidentRunner { @override Future stopEchoingDeviceLog() async { // Do nothing for ResidentWebRunner - await device!.stopEchoingDeviceLog(); + await flutterDevice!.stopEchoingDeviceLog(); } @override @@ -264,10 +264,10 @@ class ResidentWebRunner extends ResidentRunner { final String modeName = debuggingOptions.buildInfo.mode.friendlyName; _logger.printStatus( 'Launching ${getDisplayPath(target, _fileSystem)} ' - 'on ${device!.device!.displayName} in $modeName mode...', + 'on ${flutterDevice!.device!.displayName} in $modeName mode...', ); - if (device!.device is ChromiumDevice) { - _chromiumLauncher = (device!.device! as ChromiumDevice).chromeLauncher; + if (flutterDevice!.device is ChromiumDevice) { + _chromiumLauncher = (flutterDevice!.device! as ChromiumDevice).chromeLauncher; } try { @@ -280,10 +280,10 @@ class ResidentWebRunner extends ResidentRunner { final WebDevServerConfig updatedConfig = originalConfig.copyWith(port: resolvedPort); final ExpressionCompiler? expressionCompiler = debuggingOptions.webEnableExpressionEvaluation - ? WebExpressionCompiler(device!.generator!, fileSystem: _fileSystem) + ? WebExpressionCompiler(flutterDevice!.generator!, fileSystem: _fileSystem) : null; - device!.devFS = WebDevFS( + flutterDevice!.devFS = WebDevFS( webDevServerConfig: updatedConfig, packagesFilePath: packagesFilePath, urlTunneller: _urlTunneller, @@ -313,7 +313,7 @@ class ResidentWebRunner extends ResidentRunner { logger: logger, platform: _platform, ); - Uri url = await device!.devFS!.create(); + Uri url = await flutterDevice!.devFS!.create(); if (updatedConfig.https?.certKeyPath != null && updatedConfig.https?.certPath != null) { url = url.replace(scheme: 'https'); } @@ -325,7 +325,7 @@ class ResidentWebRunner extends ResidentRunner { appFailedToStart(); return 1; } - device!.generator!.accept(); + flutterDevice!.generator!.accept(); cacheInitialDillCompilation(); } else { final webBuilder = WebBuilder( @@ -344,15 +344,15 @@ class ResidentWebRunner extends ResidentRunner { compilerConfigs: [_compilerConfig], ); } - final webDevFS = device!.devFS! as WebDevFS; + final webDevFS = flutterDevice!.devFS! as WebDevFS; final bool useDebugExtension = - device!.device is WebServerDevice && debuggingOptions.startPaused; + flutterDevice!.device is WebServerDevice && debuggingOptions.startPaused; // Listen for connected apps early and then await this `Future` later // when we attach. final Future? connectDebug = supportsServiceProtocol ? webDevFS.connect(useDebugExtension) : null; - await device!.device!.startApp( + await flutterDevice!.device!.startApp( package, mainPath: target, debuggingOptions: debuggingOptions, @@ -427,7 +427,7 @@ class ResidentWebRunner extends ResidentRunner { } final String targetPlatform = getNameForTargetPlatform(TargetPlatform.web_javascript); - final String sdkName = await device!.device!.sdkNameAndVersion; + final String sdkName = await flutterDevice!.device!.sdkNameAndVersion; // Will be null if there is no report. final UpdateFSReport? report; @@ -437,10 +437,10 @@ class ResidentWebRunner extends ResidentRunner { // wasteful. report = await _updateDevFS(fullRestart: fullRestart, resetCompiler: false); if (report.success) { - device!.generator!.accept(); + flutterDevice!.generator!.accept(); } else { status.stop(); - await device!.generator!.reject(); + await flutterDevice!.generator!.reject(); if (report.hotReloadRejected) { // We cannot capture the reason why the reload was rejected as it may // contain user information. @@ -725,16 +725,17 @@ class ResidentWebRunner extends ResidentRunner { } } final InvalidationResult invalidationResult = await projectFileInvalidator.findInvalidated( - lastCompiled: device!.devFS!.lastCompiled, - urisToMonitor: device!.devFS!.sources, + lastCompiled: flutterDevice!.devFS!.lastCompiled, + urisToMonitor: flutterDevice!.devFS!.sources, packagesPath: packagesFilePath, - packageConfig: device!.devFS!.lastPackageConfig ?? debuggingOptions.buildInfo.packageConfig, + packageConfig: + flutterDevice!.devFS!.lastPackageConfig ?? debuggingOptions.buildInfo.packageConfig, ); final Status devFSStatus = _logger.startProgress( 'Waiting for connection from debug service on ' - '${device!.device!.displayName}...', + '${flutterDevice!.device!.displayName}...', ); - final UpdateFSReport report = await device!.devFS!.update( + final UpdateFSReport report = await flutterDevice!.devFS!.update( mainUri: await _generateEntrypoint( _fileSystem.file(mainPath).absolute.uri, invalidationResult.packageConfig, @@ -742,7 +743,7 @@ class ResidentWebRunner extends ResidentRunner { target: target, bundle: assetBundle, bundleFirstUpload: isFirstUpload, - generator: device!.generator!, + generator: flutterDevice!.generator!, fullRestart: fullRestart, resetCompiler: resetCompiler, dillOutputPath: dillOutputPath, @@ -750,7 +751,7 @@ class ResidentWebRunner extends ResidentRunner { invalidatedFiles: invalidationResult.uris!, packageConfig: invalidationResult.packageConfig!, trackWidgetCreation: debuggingOptions.buildInfo.trackWidgetCreation, - shaderCompiler: device!.developmentShaderCompiler, + shaderCompiler: flutterDevice!.developmentShaderCompiler, ); devFSStatus.stop(); _logger.printTrace('Synced ${getSizeAsPlatformMB(report.syncedBytes)}.'); @@ -841,20 +842,23 @@ class ResidentWebRunner extends ResidentRunner { // It is safe to ignore this error because we expect an error to be // thrown if we're not already subscribed. } + final Device device = flutterDevice!.device!; await setUpVmService( reloadSources: (String isolateId, {bool? force, bool? pause}) async { await restart(pause: pause); }, - device: device!.device, + device: device, flutterProject: flutterProject, printStructuredErrorLogMethod: printStructuredErrorLog, vmService: _vmService.service, ); final Uri websocketUri = Uri.parse(debugConnection.uri); - device!.vmService = _vmService; + flutterDevice!.vmService = _vmService; if (debugConnection.devToolsUri != null) { - (device!.device! as WebDevice).devToolsUri = Uri.parse(debugConnection.devToolsUri!); + (flutterDevice!.device! as WebDevice).devToolsUri = Uri.parse( + debugConnection.devToolsUri!, + ); } // Run main immediately if the app is not started paused or if there @@ -881,7 +885,13 @@ class ResidentWebRunner extends ResidentRunner { // service message instead. _logger.printStatus('Debug service listening on $websocketUri'); printDebuggerList(); - connectionInfoCompleter?.complete(DebugConnectionInfo(wsUri: websocketUri)); + connectionInfoCompleter?.complete( + DebugConnectionInfo( + wsUri: websocketUri, + devToolsUri: Uri.tryParse(debugConnection.devToolsUri ?? ''), + // TODO(bkonyi): surface DTD URI once it's visible from DWDS + ), + ); }), ); } else { @@ -902,7 +912,7 @@ class ResidentWebRunner extends ResidentRunner { @override Future exitApp() async { if (stopAppDuringCleanup) { - await device!.exitApps(); + await flutterDevice!.exitApps(); } appFinished(); } diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 534cfc22a89..d4837c2d5aa 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -1867,11 +1867,14 @@ class TerminalHandler { } class DebugConnectionInfo { - DebugConnectionInfo({this.httpUri, this.wsUri, this.baseUri}); + DebugConnectionInfo({this.httpUri, this.wsUri, this.baseUri, this.dtdUri, this.devToolsUri}); final Uri? httpUri; final Uri? wsUri; final String? baseUri; + + final Uri? dtdUri; + final Uri? devToolsUri; } /// Returns the next platform value for the switcher. diff --git a/packages/flutter_tools/lib/src/run_cold.dart b/packages/flutter_tools/lib/src/run_cold.dart index 1d37ae0976e..dba7bd1b962 100644 --- a/packages/flutter_tools/lib/src/run_cold.dart +++ b/packages/flutter_tools/lib/src/run_cold.dart @@ -4,6 +4,7 @@ import 'dart:async'; +import 'base/dds.dart'; import 'base/file_system.dart'; import 'base/logger.dart'; import 'build_info.dart'; @@ -75,12 +76,17 @@ class ColdRunner extends ResidentRunner { } } - if (flutterDevices.first.vmServiceUris != null) { + final FlutterDevice flutterDevice = flutterDevices.first; + if (flutterDevice.vmServiceUris != null) { + final FlutterVmService? vmService = flutterDevice.vmService; + final DartDevelopmentService dds = flutterDevice.device!.dds; // For now, only support one debugger connection. connectionInfoCompleter?.complete( DebugConnectionInfo( - httpUri: flutterDevices.first.vmService!.httpAddress, - wsUri: flutterDevices.first.vmService!.wsAddress, + httpUri: vmService!.httpAddress, + wsUri: vmService.wsAddress, + devToolsUri: dds.devToolsUri, + dtdUri: dds.dtdUri, ), ); } diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index a6aeaaa6a37..b22c97e42d0 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -11,6 +11,7 @@ import 'package:unified_analytics/unified_analytics.dart'; import 'package:vm_service/vm_service.dart' as vm_service; import 'base/context.dart'; +import 'base/dds.dart'; import 'base/file_system.dart'; import 'base/logger.dart'; import 'base/platform.dart'; @@ -278,12 +279,16 @@ class HotRunner extends ResidentRunner { try { final List baseUris = await _initDevFS(); if (connectionInfoCompleter != null) { + final FlutterVmService vmService = flutterDevices.first.vmService!; + final DartDevelopmentService dds = flutterDevices.first.device!.dds; // Only handle one debugger connection. connectionInfoCompleter.complete( DebugConnectionInfo( - httpUri: flutterDevices.first.vmService!.httpAddress, - wsUri: flutterDevices.first.vmService!.wsAddress, + httpUri: vmService.httpAddress, + wsUri: vmService.wsAddress, baseUri: baseUris.first.toString(), + devToolsUri: dds.devToolsUri, + dtdUri: dds.dtdUri, ), ); } diff --git a/packages/flutter_tools/test/general.shard/cold_test.dart b/packages/flutter_tools/test/general.shard/cold_test.dart index b383fac2ecb..e2b89c41d83 100644 --- a/packages/flutter_tools/test/general.shard/cold_test.dart +++ b/packages/flutter_tools/test/general.shard/cold_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:file/memory.dart'; +import 'package:flutter_tools/src/base/dds.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/platform.dart'; @@ -210,6 +211,9 @@ class FakeDevice extends Fake implements Device { @override Future get targetPlatform async => TargetPlatform.tester; + @override + DartDevelopmentService get dds => FakeDartDevelopmentService(); + var wasDisposed = false; @override @@ -218,6 +222,36 @@ class FakeDevice extends Fake implements Device { } } +class FakeDartDevelopmentService extends Fake implements DartDevelopmentService { + @override + late Future done; + + @override + Uri? uri; + + @override + Uri? devToolsUri; + + @override + Uri? dtdUri; + + @override + Future startDartDevelopmentService( + Uri vmServiceUri, { + int? ddsPort, + FlutterDevice? device, + bool? ipv6, + bool? disableServiceAuthCodes, + bool enableDevTools = false, + bool cacheStartupProfile = false, + String? google3WorkspaceRoot, + Uri? devToolsServerAddress, + }) async {} + + @override + Future shutdown() async {} +} + class TestFlutterDevice extends FlutterDevice { TestFlutterDevice({ required Device device, diff --git a/packages/flutter_tools/test/integration.shard/flutter_run_test.dart b/packages/flutter_tools/test/integration.shard/flutter_run_test.dart index d40653389d2..f592c93875c 100644 --- a/packages/flutter_tools/test/integration.shard/flutter_run_test.dart +++ b/packages/flutter_tools/test/integration.shard/flutter_run_test.dart @@ -52,6 +52,18 @@ void main() { } }); + testWithoutContext('flutter run outputs DTD and DevTools events', () async { + await flutter.run(startPaused: true, withDebugger: true); + expect(flutter.devToolsUri, isNotNull); + expect(flutter.dtdUri, isNotNull); + }); + + testWithoutContext('flutter run does not output DTD and DevTools events', () async { + await flutter.run(startPaused: true, withDebugger: true, noDevtools: true); + expect(flutter.devToolsUri, isNull); + expect(flutter.dtdUri, isNull); + }); + testWithoutContext('sets activeDevToolsServerAddress extension', () async { await flutter.run( startPaused: true, diff --git a/packages/flutter_tools/test/integration.shard/test_driver.dart b/packages/flutter_tools/test/integration.shard/test_driver.dart index 58eed5e0e70..6b33bcdbc0d 100644 --- a/packages/flutter_tools/test/integration.shard/test_driver.dart +++ b/packages/flutter_tools/test/integration.shard/test_driver.dart @@ -52,6 +52,8 @@ abstract final class FlutterTestDriver { final _errorBuffer = StringBuffer(); String? _lastResponse; Uri? _vmServiceWsUri; + Uri? _devToolsUri; + Uri? _dtdUri; int? _attachPort; var _hasExited = false; @@ -62,6 +64,8 @@ abstract final class FlutterTestDriver { int? get vmServicePort => _vmServiceWsUri?.port; bool get hasExited => _hasExited; Uri? get vmServiceWsUri => _vmServiceWsUri; + Uri? get devToolsUri => _devToolsUri; + Uri? get dtdUri => _dtdUri; /// Completes with the full method name for the 'reloadSources' service once /// it's registered (e.g., `s0.reloadSources`). @@ -602,8 +606,14 @@ final class FlutterRunTestDriver extends FlutterTestDriver { ...?additionalCommandArgs, ], withDebugger: withDebugger, + withDevtools: !noDevtools, startPaused: startPaused, waitForDebugPort: device != WebServerDevice.kWebServerDeviceId && !wasm, + waitForDtdAndDevTools: + device != WebServerDevice.kWebServerDeviceId && + device != GoogleChromeDevice.kChromeDeviceId && + !noDevtools && + spawnDdsInstance, pauseOnExceptions: pauseOnExceptions, script: script, verbose: verbose, @@ -642,9 +652,11 @@ final class FlutterRunTestDriver extends FlutterTestDriver { List args, { String? script, bool withDebugger = false, + bool withDevtools = false, bool startPaused = false, bool pauseOnExceptions = false, bool waitForDebugPort = false, + bool waitForDtdAndDevTools = true, bool verbose = false, int? attachPort, }) async { @@ -685,11 +697,30 @@ final class FlutterRunTestDriver extends FlutterTestDriver { event: 'app.started', timeout: appStartTimeout, ); + final Future devTools = + _waitFor( + event: 'app.devTools', + timeout: appStartTimeout, + ignoreAppStopEvent: true, + ).then((event) async { + _devToolsUri = Uri.parse( + (event['params']! as Map)['uri']! as String, + ); + }); + final Future dtd = + _waitFor(event: 'app.dtd', timeout: appStartTimeout, ignoreAppStopEvent: true).then(( + event, + ) { + _dtdUri = Uri.parse((event['params']! as Map)['uri']! as String); + }); late final Map debugPort; if (waitForDebugPort || withDebugger || attachPort != null) { debugPort = await _waitFor(event: 'app.debugPort', timeout: appStartTimeout); } + if (withDebugger && waitForDtdAndDevTools) { + await Future.wait([devTools, dtd]); + } if (withDebugger || attachPort != null) { final wsUriString = (debugPort['params']! as Map)['wsUri']! as String; _vmServiceWsUri = Uri.parse(wsUriString);