[ Widget Preview ] Add UUID to registered DTD streams and services (#180140)

DTD only supports a single instance of a registered service with a given
name. For widget preview development, we sometimes want to use a DTD
instance that's attached to an IDE to test IDE integration. However,
IDEs frequently spawn their own widget preview instances which register
services with DTD. In the case where `flutter widget-preview start
--dtd-url=<dtd-url>` is run and `dtd-url` points to a DTD instance with
another widget preview service running, the process simply crashes.

This change adds a unique identifier to the widget preview DTD service
and stream names that allows for each `flutter widget-preview start`
instance to register its own unique widget preview DTD services, even if
other widget preview instances are using the same DTD instance.

Fixes https://github.com/flutter/flutter/issues/179883
This commit is contained in:
Ben Konyi 2026-01-05 14:52:27 -05:00 committed by GitHub
parent 5832fcef02
commit afb13d8bb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 96 additions and 28 deletions

View File

@ -166,6 +166,11 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
'Generated the widget preview environment scaffolding at a given location '
'for testing purposes.',
hide: !verbose,
)
..addFlag(
kDisableDtdServiceUuid,
help: 'Disables the addition of a UUID to the widget preview DTD service and stream.',
hide: !verbose,
);
}
@ -175,9 +180,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
static const kHeadless = 'headless';
static const kWebServer = 'web-server';
static const kWidgetPreviewScaffoldOutputDir = 'scaffold-output-dir';
/// Environment variable used to pass the DTD URI to the widget preview scaffold.
static const kWidgetPreviewDtdUriEnvVar = 'WIDGET_PREVIEW_DTD_URI';
static const kDisableDtdServiceUuid = 'disable-dtd-service-uuid';
@visibleForTesting
static const kBrowserNotFoundErrorMessage =
@ -258,6 +261,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
onHotRestartPreviewerRequest: onHotRestartRequest,
dtdLauncher: DtdLauncher(logger: logger, artifacts: artifacts, processManager: processManager),
project: rootProject.widgetPreviewScaffoldProject,
addUuidToServiceName: !boolArg(kDisableDtdServiceUuid),
);
/// The currently running instance of the widget preview scaffold.
@ -401,7 +405,11 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
logger.printTrace('Connecting to existing DTD instance at: $existingDtdUri...');
await _dtdService.connect(dtdWsUri: existingDtdUri);
}
_previewCodeGenerator.populateDtdConnectionInfo(_dtdService.dtdUri!);
_previewCodeGenerator.populateDtdConnectionInfo(
dtdUri: _dtdService.dtdUri!,
widgetPreviewServiceName: _dtdService.widgetPreviewService,
widgetPreviewScaffoldStreamName: _dtdService.widgetPreviewScaffoldStream,
);
}
Future<int> runPreviewEnvironment({required FlutterProject widgetPreviewScaffoldProject}) async {

View File

@ -10,6 +10,7 @@ import 'package:json_rpc_2/json_rpc_2.dart';
import 'package:meta/meta.dart';
import 'package:package_config/package_config_types.dart';
import 'package:process/process.dart';
import 'package:uuid/uuid.dart';
import '../artifacts.dart';
import '../base/common.dart';
@ -37,6 +38,7 @@ class WidgetPreviewDtdServices {
required this.dtdLauncher,
required this.onHotRestartPreviewerRequest,
required this.project,
required this.addUuidToServiceName,
}) {
shutdownHooks.addShutdownHook(() async {
await _dtd?.close();
@ -44,12 +46,35 @@ class WidgetPreviewDtdServices {
});
}
/// The name of the widget preview service, without a UUID.
@visibleForTesting
static const kWidgetPreviewServiceRoot = 'widget-preview';
/// The actual name of the registered widget preview service.
late final String widgetPreviewService = _withUuid(kWidgetPreviewServiceRoot);
/// The name of the widget preview stream, without a UUID.
@visibleForTesting
static const kWidgetPreviewScaffoldStreamRoot = 'WidgetPreviewScaffold';
/// The actual name of the widget preview stream.
late final String widgetPreviewScaffoldStream = _withUuid(kWidgetPreviewScaffoldStreamRoot);
/// The unique identifier added to registered service and stream names if [addUuidToServiceName]
/// is true.
late final String serviceUuid = const Uuid().v4();
/// Adds a unique identifier to the service and stream registered by the widget previewer to
/// avoid conflicts with other widget previewer instances connected to DTD.
///
/// If false, no UUID is added to the registered service and stream names.
final bool addUuidToServiceName;
// WARNING: Keep these constants and services in sync with those defined in the widget preview
// scaffold's dtd_services.dart.
//
// START KEEP SYNCED
static const kWidgetPreviewService = 'widget-preview';
static const kIsWindows = 'isWindows';
static const kHotRestartPreviewer = 'hotRestartPreviewer';
static const kResolveUri = 'resolveUri';
@ -57,7 +82,6 @@ class WidgetPreviewDtdServices {
static const kGetPreference = 'getPreference';
static const kGetDevToolsUri = 'getDevToolsUri';
static const kWidgetPreviewScaffoldStream = 'WidgetPreviewScaffold';
static const kWidgetPreviewConnectedEvent = 'Connected';
/// Error code for RpcException thrown when attempting to load a key from
@ -144,21 +168,20 @@ class WidgetPreviewDtdServices {
);
}
String _withUuid(String name) => addUuidToServiceName ? '$name-$serviceUuid' : name;
Future<void> _registerServices() async {
final DartToolingDaemon dtd = _dtd!;
dtd.onEvent(kWidgetPreviewScaffoldStream).listen((DTDEvent event) {
if (event case DTDEvent(
stream: kWidgetPreviewScaffoldStream,
kind: kWidgetPreviewConnectedEvent,
)) {
dtd.onEvent(widgetPreviewScaffoldStream).listen((DTDEvent event) {
if (event.kind == kWidgetPreviewConnectedEvent) {
previewAnalytics.reportPreviewerConnected();
}
});
await Future.wait(<Future<void>>[
dtd.streamListen(kWidgetPreviewScaffoldStream),
dtd.streamListen(widgetPreviewScaffoldStream),
for (final (String method, DTDServiceCallback callback) in services)
dtd
.registerService(kWidgetPreviewService, method, callback)
.registerService(widgetPreviewService, method, callback)
.then((_) => logger.printTrace('Registered DTD method: $method')),
]);
}

View File

@ -55,7 +55,11 @@ class PreviewCodeGenerator {
static String getGeneratedDtdConnectionInfoFilePath(FileSystem fs) =>
fs.path.join('lib', 'src', 'dtd', 'dtd_connection_info.dart');
void populateDtdConnectionInfo(Uri dtdUri) {
void populateDtdConnectionInfo({
required Uri dtdUri,
required String widgetPreviewServiceName,
required String widgetPreviewScaffoldStreamName,
}) {
final emitter = cb.DartEmitter.scoped(useNullSafetySyntax: true);
final lib = cb.Library(
(cb.LibraryBuilder b) => b
@ -68,6 +72,20 @@ class PreviewCodeGenerator {
..type = cb.refer('String')
..assignment = cb.literalString(dtdUri.toString()).code;
}),
cb.Field((b) {
b
..name = 'kWidgetPreviewService'
..modifier = cb.FieldModifier.constant
..type = cb.refer('String')
..assignment = cb.literalString(widgetPreviewServiceName).code;
}),
cb.Field((b) {
b
..name = 'kWidgetPreviewScaffoldStream'
..modifier = cb.FieldModifier.constant
..type = cb.refer('String')
..assignment = cb.literalString(widgetPreviewScaffoldStreamName).code;
}),
]),
);
final File generatedDtdConnectionInfoFile = fs.file(

View File

@ -3,3 +3,5 @@
// found in the LICENSE file.
const String kWidgetPreviewDtdUri = '';
const String kWidgetPreviewService = 'widget-preview';
const String kWidgetPreviewScaffoldStream = 'WidgetPreviewScaffold';

View File

@ -17,7 +17,6 @@ class WidgetPreviewScaffoldDtdServices with DtdEditorService {
//
// START KEEP SYNCED
static const kWidgetPreviewService = 'widget-preview';
static const kIsWindows = 'isWindows';
static const kHotRestartPreviewer = 'hotRestartPreviewer';
static const kResolveUri = 'resolveUri';
@ -40,7 +39,7 @@ class WidgetPreviewScaffoldDtdServices with DtdEditorService {
dtd = await DartToolingDaemon.connect(dtdWsUri);
unawaited(
dtd.postEvent(
'WidgetPreviewScaffold',
kWidgetPreviewScaffoldStream,
'Connected',
const <String, Object?>{},
),

View File

@ -418,13 +418,19 @@ List<_i1.WidgetPreview> previews() => [];
// Populate the DTD connection info.
final Uri dtdUri = Uri.parse('ws://localhost:1234');
codeGenerator.populateDtdConnectionInfo(dtdUri);
codeGenerator.populateDtdConnectionInfo(
dtdUri: dtdUri,
widgetPreviewServiceName: 'widget-preview-service',
widgetPreviewScaffoldStreamName: 'widget-preview-stream',
);
final expectedDtdConnectionInfo =
'''
// ignore_for_file: implementation_imports
const String kWidgetPreviewDtdUri = '$dtdUri';
const String kWidgetPreviewService = 'widget-preview-service';
const String kWidgetPreviewScaffoldStream = 'widget-preview-stream';
''';
expect(generatedDtdConnectionInfoFile.readAsStringSync(), expectedDtdConnectionInfo);
},

View File

@ -45,6 +45,13 @@ class FakeWidgetPreviewScaffoldDtdServices extends Fake implements WidgetPreview
@override
Uri? get dtdUri => Uri();
@override
final String widgetPreviewService = WidgetPreviewDtdServices.kWidgetPreviewServiceRoot;
@override
final String widgetPreviewScaffoldStream =
WidgetPreviewDtdServices.kWidgetPreviewScaffoldStreamRoot;
@override
Future<void> launchAndConnect() async {}
}

View File

@ -79,6 +79,7 @@ void main() {
'start',
'--verbose',
'--${WidgetPreviewStartCommand.kHeadless}',
'--${WidgetPreviewStartCommand.kDisableDtdServiceUuid}',
if (useWebServer) '--${WidgetPreviewStartCommand.kWebServer}',
if (dtdUri != null) '--${WidgetPreviewStartCommand.kDtdUrl}=$dtdUri',
if (devToolsServerAddress != null)
@ -192,14 +193,15 @@ void main() {
// The preview scaffold will send a 'Connected' event on this stream once it has initialized
// and is ready.
final DartToolingDaemon dtdConnection = await DartToolingDaemon.connect(dtdUri);
const kWidgetPreviewScaffoldStream = 'WidgetPreviewScaffold';
final completer = Completer<void>();
dtdConnection.onEvent(kWidgetPreviewScaffoldStream).listen((DTDEvent event) {
expect(event.stream, kWidgetPreviewScaffoldStream);
dtdConnection.onEvent(WidgetPreviewDtdServices.kWidgetPreviewScaffoldStreamRoot).listen((
DTDEvent event,
) {
expect(event.stream, WidgetPreviewDtdServices.kWidgetPreviewScaffoldStreamRoot);
expect(event.kind, 'Connected');
completer.complete();
});
await dtdConnection.streamListen(kWidgetPreviewScaffoldStream);
await dtdConnection.streamListen(WidgetPreviewDtdServices.kWidgetPreviewScaffoldStreamRoot);
// Start the widget preview and wait for the 'Connected' event.
await runWidgetPreview(expectedMessages: firstLaunchMessagesWeb, dtdUri: dtdUri);
@ -242,17 +244,18 @@ void main() {
// The preview scaffold will send a 'Connected' event on this stream once it has initialized
// and is ready.
final DartToolingDaemon dtdConnection = await DartToolingDaemon.connect(dtdUri);
const kWidgetPreviewScaffoldStream = 'WidgetPreviewScaffold';
final completer = Completer<void>();
var firstConnection = true;
dtdConnection.onEvent(kWidgetPreviewScaffoldStream).listen((DTDEvent event) {
expect(event.stream, kWidgetPreviewScaffoldStream);
dtdConnection.onEvent(WidgetPreviewDtdServices.kWidgetPreviewScaffoldStreamRoot).listen((
DTDEvent event,
) {
expect(event.stream, WidgetPreviewDtdServices.kWidgetPreviewScaffoldStreamRoot);
expect(event.kind, 'Connected');
if (firstConnection) {
firstConnection = false;
runFlutterClean();
dtdConnection.call(
WidgetPreviewDtdServices.kWidgetPreviewService,
WidgetPreviewDtdServices.kWidgetPreviewServiceRoot,
WidgetPreviewDtdServices.kHotRestartPreviewer,
);
return;
@ -262,7 +265,7 @@ void main() {
// crashed.
completer.complete();
});
await dtdConnection.streamListen(kWidgetPreviewScaffoldStream);
await dtdConnection.streamListen(WidgetPreviewDtdServices.kWidgetPreviewScaffoldStreamRoot);
// Start the widget preview and wait for the 'Connected' event.
await runWidgetPreview(expectedMessages: firstLaunchMessagesWeb, dtdUri: dtdUri);

View File

@ -3,3 +3,5 @@
// found in the LICENSE file.
const String kWidgetPreviewDtdUri = '';
const String kWidgetPreviewService = 'widget-preview';
const String kWidgetPreviewScaffoldStream = 'WidgetPreviewScaffold';

View File

@ -17,7 +17,6 @@ class WidgetPreviewScaffoldDtdServices with DtdEditorService {
//
// START KEEP SYNCED
static const kWidgetPreviewService = 'widget-preview';
static const kIsWindows = 'isWindows';
static const kHotRestartPreviewer = 'hotRestartPreviewer';
static const kResolveUri = 'resolveUri';
@ -40,7 +39,7 @@ class WidgetPreviewScaffoldDtdServices with DtdEditorService {
dtd = await DartToolingDaemon.connect(dtdWsUri);
unawaited(
dtd.postEvent(
'WidgetPreviewScaffold',
kWidgetPreviewScaffoldStream,
'Connected',
const <String, Object?>{},
),

View File

@ -62,6 +62,7 @@ void main() {
),
onHotRestartPreviewerRequest: onHotRestartPreviewerRequest ?? () {},
project: FakeFlutterProject(),
addUuidToServiceName: false,
);
await server.launchAndConnect();
return server;