Skip to content

Commit 30e53b0

Browse files
authored
[ Widget Preview ] Add initial support for communications over the Dart Tooling Daemon (DTD) (flutter#166698)
This will eventually be used as the main communication channel between the widget preview scaffold, the Flutter tool, and other developer tooling (e.g., IDEs). Fixes flutter#166417
1 parent 9bf18f0 commit 30e53b0

File tree

14 files changed

+305
-3
lines changed

14 files changed

+305
-3
lines changed

packages/flutter_tools/lib/executable.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,8 @@ List<FlutterCommand> generateCommands({required bool verboseHelp, required bool
258258
platform: globals.platform,
259259
shutdownHooks: globals.shutdownHooks,
260260
os: globals.os,
261+
processManager: globals.processManager,
262+
artifacts: globals.artifacts!,
261263
),
262264
UpgradeCommand(verboseHelp: verboseHelp),
263265
SymbolizeCommand(stdio: globals.stdio, fileSystem: globals.fs),

packages/flutter_tools/lib/src/commands/widget_preview.dart

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import 'package:args/args.dart';
66
import 'package:meta/meta.dart';
77
import 'package:package_config/package_config.dart';
8+
import 'package:process/process.dart';
89

10+
import '../artifacts.dart';
911
import '../base/common.dart';
1012
import '../base/deferred_component.dart';
1113
import '../base/file_system.dart';
@@ -24,6 +26,8 @@ import '../linux/build_linux.dart';
2426
import '../macos/build_macos.dart';
2527
import '../project.dart';
2628
import '../runner/flutter_command.dart';
29+
import '../runner/flutter_command_runner.dart';
30+
import '../widget_preview/dtd_services.dart';
2731
import '../widget_preview/preview_code_generator.dart';
2832
import '../widget_preview/preview_detector.dart';
2933
import '../widget_preview/preview_manifest.dart';
@@ -41,6 +45,8 @@ class WidgetPreviewCommand extends FlutterCommand {
4145
required Platform platform,
4246
required ShutdownHooks shutdownHooks,
4347
required OperatingSystemUtils os,
48+
required ProcessManager processManager,
49+
required Artifacts artifacts,
4450
}) {
4551
addSubcommand(
4652
WidgetPreviewStartCommand(
@@ -52,6 +58,8 @@ class WidgetPreviewCommand extends FlutterCommand {
5258
platform: platform,
5359
shutdownHooks: shutdownHooks,
5460
os: os,
61+
processManager: processManager,
62+
artifacts: artifacts,
5563
),
5664
);
5765
addSubcommand(
@@ -118,6 +126,8 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
118126
required this.platform,
119127
required this.shutdownHooks,
120128
required this.os,
129+
required this.processManager,
130+
required this.artifacts,
121131
}) {
122132
addPubOptions();
123133
argParser
@@ -152,6 +162,9 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
152162
static const String kHeadlessWeb = 'headless-web';
153163
static const String kWidgetPreviewScaffoldOutputDir = 'scaffold-output-dir';
154164

165+
/// Environment variable used to pass the DTD URI to the widget preview scaffold.
166+
static const String kWidgetPreviewDtdUriEnvVar = 'WIDGET_PREVIEW_DTD_URI';
167+
155168
@override
156169
Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
157170
// Ensure the Flutter Web SDK is installed.
@@ -185,6 +198,10 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
185198

186199
final OperatingSystemUtils os;
187200

201+
final ProcessManager processManager;
202+
203+
final Artifacts artifacts;
204+
188205
late final FlutterProject rootProject = getRootProject();
189206

190207
late final PreviewDetector _previewDetector = PreviewDetector(
@@ -203,6 +220,12 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
203220
cache: cache,
204221
);
205222

223+
late final WidgetPreviewDtdServices _dtdService = WidgetPreviewDtdServices(
224+
logger: logger,
225+
shutdownHooks: shutdownHooks,
226+
dtdLauncher: DtdLauncher(logger: logger, artifacts: artifacts, processManager: processManager),
227+
);
228+
206229
/// The currently running instance of the widget preview scaffold.
207230
AppInstance? _widgetPreviewApp;
208231

@@ -284,6 +307,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
284307
shutdownHooks.addShutdownHook(() async {
285308
await _widgetPreviewApp?.stop();
286309
});
310+
await configureDtd();
287311
_widgetPreviewApp = await runPreviewEnvironment(
288312
widgetPreviewScaffoldProject: rootProject.widgetPreviewScaffoldProject,
289313
);
@@ -309,6 +333,31 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
309333
_populatePreviewPubspec(rootProject: rootProject);
310334
}
311335

336+
/// Configures the Dart Tooling Daemon connection.
337+
///
338+
/// If --dtd-uri is provided, the existing DTD instance will be used. If the tool fails to
339+
/// connect to this URI, it will start its own DTD instance.
340+
///
341+
/// If --dtd-uri is not provided, a DTD instance managed by the tool will be started.
342+
Future<void> configureDtd() async {
343+
final String? existingDtdUriStr = stringArg(FlutterGlobalOptions.kDtdUrl, global: true);
344+
Uri? existingDtdUri;
345+
try {
346+
if (existingDtdUriStr != null) {
347+
existingDtdUri = Uri.parse(existingDtdUriStr);
348+
}
349+
} on FormatException {
350+
logger.printWarning('Failed to parse value of --dtd-uri: $existingDtdUriStr.');
351+
}
352+
if (existingDtdUri == null) {
353+
logger.printTrace('Launching a fresh DTD instance...');
354+
await _dtdService.launchAndConnect();
355+
} else {
356+
logger.printTrace('Connecting to existing DTD instance at: $existingDtdUri...');
357+
await _dtdService.connect(dtdWsUri: existingDtdUri);
358+
}
359+
}
360+
312361
/// Builds the application binary for the widget preview scaffold the first
313362
/// time the widget preview command is run.
314363
///
@@ -457,6 +506,12 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
457506
BuildMode.debug,
458507
null,
459508
treeShakeIcons: false,
509+
// Provide the DTD connection information directly to the preview scaffold.
510+
// This could, in theory, be provided via a follow up call to a service extension
511+
// registered by the preview scaffold, but there's some uncertainty around how service
512+
// extensions will work with Flutter web embedded in VSCode without a Chrome debugger
513+
// connection.
514+
dartDefines: <String>['$kWidgetPreviewDtdUriEnvVar=${_dtdService.dtdUri}'],
460515
extraFrontEndOptions:
461516
isWeb ? <String>['--dartdevc-canary', '--dartdevc-module-format=ddc'] : null,
462517
packageConfigPath: widgetPreviewScaffoldProject.packageConfig.path,
@@ -599,6 +654,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
599654
if (offline) '--offline',
600655
'--directory',
601656
widgetPreviewScaffoldProject.directory.path,
657+
'dtd',
602658
'flutter_lints',
603659
'stack_trace',
604660
],

packages/flutter_tools/lib/src/runner/flutter_command_runner.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ abstract final class FlutterGlobalOptions {
3636
static const String kMachineFlag = 'machine';
3737
static const String kPackagesOption = 'packages';
3838
static const String kPrefixedErrorsFlag = 'prefixed-errors';
39+
static const String kDtdUrl = 'dtd-url';
3940
static const String kPrintDtd = 'print-dtd';
4041
static const String kQuietFlag = 'quiet';
4142
static const String kShowTestDeviceFlag = 'show-test-device';
@@ -151,6 +152,12 @@ class FlutterCommandRunner extends CommandRunner<void> {
151152
hide: !verboseHelp,
152153
help: 'Path to your "package_config.json" file.',
153154
);
155+
argParser.addOption(
156+
FlutterGlobalOptions.kDtdUrl,
157+
help:
158+
'The address of an existing Dart Tooling Daemon instance to be used by the Flutter CLI.',
159+
hide: !verboseHelp,
160+
);
154161
argParser.addFlag(
155162
FlutterGlobalOptions.kPrintDtd,
156163
negatable: false,
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
7+
import 'package:dtd/dtd.dart';
8+
import 'package:process/process.dart';
9+
10+
import '../artifacts.dart';
11+
import '../base/common.dart';
12+
import '../base/io.dart';
13+
import '../base/logger.dart';
14+
import '../base/process.dart';
15+
import '../convert.dart';
16+
17+
/// Provides services, streams, and RPC invocations to interact with the Widget Preview Scaffold.
18+
class WidgetPreviewDtdServices {
19+
WidgetPreviewDtdServices({
20+
required this.logger,
21+
required this.shutdownHooks,
22+
required this.dtdLauncher,
23+
}) {
24+
shutdownHooks.addShutdownHook(() async {
25+
await _dtd?.close();
26+
await dtdLauncher.dispose();
27+
});
28+
}
29+
30+
final Logger logger;
31+
final ShutdownHooks shutdownHooks;
32+
final DtdLauncher dtdLauncher;
33+
34+
DartToolingDaemon? _dtd;
35+
36+
/// The [Uri] pointing to the currently connected DTD instance.
37+
///
38+
/// Returns `null` if there is no DTD connection.
39+
Uri? get dtdUri => _dtdUri;
40+
Uri? _dtdUri;
41+
42+
/// Starts DTD in a child process before invoking [connect] with a [Uri] pointing to the new
43+
/// DTD instance.
44+
Future<void> launchAndConnect() async {
45+
// Connect to the new DTD instance.
46+
await connect(dtdWsUri: await dtdLauncher.launch());
47+
}
48+
49+
/// Connects to an existing DTD instance and registers any relevant services.
50+
Future<void> connect({required Uri dtdWsUri}) async {
51+
_dtdUri = dtdWsUri;
52+
_dtd = await DartToolingDaemon.connect(dtdWsUri);
53+
// TODO(bkonyi): register services.
54+
logger.printTrace('Connected to DTD and registered services.');
55+
}
56+
}
57+
58+
/// Manages the lifecycle of a Dart Tooling Daemon (DTD) instance.
59+
class DtdLauncher {
60+
DtdLauncher({required this.logger, required this.artifacts, required this.processManager});
61+
62+
/// Starts a new DTD instance and returns the web socket URI it's available on.
63+
Future<Uri> launch() async {
64+
if (_dtdProcess != null) {
65+
throw StateError('Attempted to launch DTD twice.');
66+
}
67+
68+
// Start DTD.
69+
_dtdProcess = await processManager.start(<Object>[
70+
artifacts.getArtifactPath(Artifact.engineDartBinary),
71+
'tooling-daemon',
72+
'--machine',
73+
]);
74+
75+
// Wait for the DTD connection information.
76+
final Completer<Uri> dtdUri = Completer<Uri>();
77+
late final StreamSubscription<String> sub;
78+
sub = _dtdProcess!.stdout.transform(const Utf8Decoder()).listen((String data) async {
79+
await sub.cancel();
80+
final Map<String, Object?> jsonData = json.decode(data) as Map<String, Object?>;
81+
if (jsonData case {'tooling_daemon_details': {'uri': final String dtdUriString}}) {
82+
dtdUri.complete(Uri.parse(dtdUriString));
83+
} else {
84+
throwToolExit('Unable to start the Dart Tooling Daemon.');
85+
}
86+
});
87+
return dtdUri.future;
88+
}
89+
90+
/// Kills the spawned DTD instance.
91+
Future<void> dispose() async {
92+
_dtdProcess?.kill();
93+
_dtdProcess = null;
94+
}
95+
96+
final Logger logger;
97+
final Artifacts artifacts;
98+
final ProcessManager processManager;
99+
100+
Process? _dtdProcess;
101+
}

packages/flutter_tools/templates/template_manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@
356356
"templates/widget_preview_scaffold/lib/src/widget_preview.dart.tmpl",
357357
"templates/widget_preview_scaffold/lib/src/widget_preview_rendering.dart.tmpl",
358358
"templates/widget_preview_scaffold/lib/src/controls.dart.tmpl",
359+
"templates/widget_preview_scaffold/lib/src/dtd_services.dart.tmpl",
359360
"templates/widget_preview_scaffold/lib/src/generated_preview.dart.tmpl",
360361
"templates/widget_preview_scaffold/lib/src/utils.dart.tmpl",
361362
"templates/widget_preview_scaffold/pubspec.yaml.tmpl",
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
7+
import 'package:dtd/dtd.dart';
8+
9+
/// Provides services, streams, and RPC invocations to interact with Flutter developer tooling.
10+
class WidgetPreviewScaffoldDtdServices {
11+
/// Environment variable for the DTD URI.
12+
static const String kWidgetPreviewDtdUriEnvVar = 'WIDGET_PREVIEW_DTD_URI';
13+
14+
/// Connects to the Dart Tooling Daemon (DTD) specified by the Flutter tool.
15+
///
16+
/// If the connection is successful, the Widget Preview Scaffold will register services and
17+
/// subscribe to various streams to interact directly with other tooling (e.g., IDEs).
18+
Future<void> connect() async {
19+
final Uri dtdWsUri = Uri.parse(
20+
const String.fromEnvironment(kWidgetPreviewDtdUriEnvVar),
21+
);
22+
_dtd = await DartToolingDaemon.connect(dtdWsUri);
23+
unawaited(
24+
_dtd.postEvent(
25+
'WidgetPreviewScaffold',
26+
'Connected',
27+
const <String, Object?>{},
28+
),
29+
);
30+
}
31+
32+
late final DartToolingDaemon _dtd;
33+
}

packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview_rendering.dart.tmpl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'package:flutter/services.dart';
1313
import 'package:stack_trace/stack_trace.dart';
1414

1515
import 'controls.dart';
16+
import 'dtd_services.dart';
1617
import 'generated_preview.dart';
1718
import 'utils.dart';
1819
import 'widget_preview.dart';
@@ -410,6 +411,8 @@ class PreviewAssetBundle extends PlatformAssetBundle {
410411
/// the preview scaffold project which prevents us from being able to use hot
411412
/// restart to iterate on this file.
412413
Future<void> mainImpl() async {
414+
// TODO(bkonyi): store somewhere.
415+
await WidgetPreviewScaffoldDtdServices().connect();
413416
runApp(_WidgetPreviewScaffold());
414417
}
415418

packages/flutter_tools/templates/widget_preview_scaffold/pubspec.yaml.tmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ dependencies:
1212
flutter_test:
1313
sdk: flutter
1414
# These will be replaced with proper constraints after the template is hydrated.
15+
dtd: any
1516
flutter_lints: any
1617
stack_trace: any
1718

packages/flutter_tools/test/commands.shard/hermetic/widget_preview/widget_preview_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
import 'package:file/memory.dart';
6+
import 'package:flutter_tools/src/artifacts.dart';
67
import 'package:flutter_tools/src/base/file_system.dart';
78
import 'package:flutter_tools/src/base/logger.dart';
89
import 'package:flutter_tools/src/base/os.dart';
@@ -47,6 +48,8 @@ void main() {
4748
platform: platform,
4849
processManager: processManager,
4950
),
51+
processManager: FakeProcessManager.any(),
52+
artifacts: Artifacts.test(fileSystem: fileSystem),
5053
);
5154
rootProject = FakeFlutterProject(
5255
projectRoot: 'some_project',

packages/flutter_tools/test/commands.shard/permeable/widget_preview_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:io' as io show IOOverrides;
66

77
import 'package:args/command_runner.dart';
88
import 'package:file_testing/file_testing.dart';
9+
import 'package:flutter_tools/src/artifacts.dart';
910
import 'package:flutter_tools/src/base/bot_detector.dart';
1011
import 'package:flutter_tools/src/base/common.dart';
1112
import 'package:flutter_tools/src/base/file_system.dart';
@@ -80,6 +81,8 @@ void main() {
8081
logger: logger,
8182
platform: platform,
8283
),
84+
artifacts: Artifacts.test(),
85+
processManager: FakeProcessManager.any(),
8386
),
8487
);
8588
await runner.run(<String>['widget-preview', ...arguments]);

0 commit comments

Comments
 (0)