[ Tool ] Output app.dtd and app.devTools in machine mode (#176655)

Fixes https://github.com/flutter/flutter/issues/176310
This commit is contained in:
Ben Konyi 2025-10-07 18:55:27 -04:00 committed by GitHub
parent 00da9435e3
commit c8c09f6d05
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 189 additions and 49 deletions

View File

@ -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

View File

@ -796,15 +796,18 @@ class AppDomain extends Domain {
// As it just writes to stdout.
unawaited(
connectionInfoCompleter.future.then<void>((DebugConnectionInfo info) {
final params = <String, Object?>{
_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);
}),
);
}

View File

@ -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<void> 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: <WebCompilerConfig>[_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<ConnectionResult?>? 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<void> exitApp() async {
if (stopAppDuringCleanup) {
await device!.exitApps();
await flutterDevice!.exitApps();
}
appFinished();
}

View File

@ -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.

View File

@ -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,
),
);
}

View File

@ -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<Uri?> 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,
),
);
}

View File

@ -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<TargetPlatform> 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<void> done;
@override
Uri? uri;
@override
Uri? devToolsUri;
@override
Uri? dtdUri;
@override
Future<void> startDartDevelopmentService(
Uri vmServiceUri, {
int? ddsPort,
FlutterDevice? device,
bool? ipv6,
bool? disableServiceAuthCodes,
bool enableDevTools = false,
bool cacheStartupProfile = false,
String? google3WorkspaceRoot,
Uri? devToolsServerAddress,
}) async {}
@override
Future<void> shutdown() async {}
}
class TestFlutterDevice extends FlutterDevice {
TestFlutterDevice({
required Device device,

View File

@ -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,

View File

@ -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<String> 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<void> devTools =
_waitFor(
event: 'app.devTools',
timeout: appStartTimeout,
ignoreAppStopEvent: true,
).then((event) async {
_devToolsUri = Uri.parse(
(event['params']! as Map<String, Object?>)['uri']! as String,
);
});
final Future<void> dtd =
_waitFor(event: 'app.dtd', timeout: appStartTimeout, ignoreAppStopEvent: true).then((
event,
) {
_dtdUri = Uri.parse((event['params']! as Map<String, Object?>)['uri']! as String);
});
late final Map<String, Object?> 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<String, Object?>)['wsUri']! as String;
_vmServiceWsUri = Uri.parse(wsUriString);