From 4585928a6f4f95efeb8df687c2e1c7403049d509 Mon Sep 17 00:00:00 2001 From: Jonas Finnemann Jensen Date: Tue, 18 Aug 2026 08:29:24 +0000 Subject: [PATCH] Support connecting to VM service via MessagePort / postMessage --- .../lib/src/framework/framework_core.dart | 52 ++++++----- .../service/_post_message_service_stub.dart | 13 +++ .../service/_post_message_service_web.dart | 88 +++++++++++++++++++ .../lib/src/service/post_message_service.dart | 14 +++ 4 files changed, 144 insertions(+), 23 deletions(-) create mode 100644 packages/devtools_app/lib/src/service/_post_message_service_stub.dart create mode 100644 packages/devtools_app/lib/src/service/_post_message_service_web.dart create mode 100644 packages/devtools_app/lib/src/service/post_message_service.dart diff --git a/packages/devtools_app/lib/src/framework/framework_core.dart b/packages/devtools_app/lib/src/framework/framework_core.dart index 7d48749b1a9..106fa56be15 100644 --- a/packages/devtools_app/lib/src/framework/framework_core.dart +++ b/packages/devtools_app/lib/src/framework/framework_core.dart @@ -18,6 +18,7 @@ import 'package:vm_service/vm_service.dart'; import '../extensions/extension_service.dart'; import '../screens/debugger/breakpoint_manager.dart'; +import '../service/post_message_service.dart'; import '../service/service_manager.dart'; import '../service/vm_service_wrapper.dart'; import '../shared/analytics/analytics.dart' as ga; @@ -140,29 +141,34 @@ extension FrameworkCore on Never { final finishedCompleter = Completer(); try { - final service = await connect( - uri: uri, - finishedCompleter: finishedCompleter, - serviceFactory: - ({ - // ignore: avoid-dynamic, mirrors types of [VmServiceFactory]. - required Stream /*String|List*/ inStream, - required void Function(String message) writeMessage, - Log? log, - DisposeHandler? disposeHandler, - Future? streamClosed, - String? wsUri, - bool trackFutures = false, - }) => VmServiceWrapper.defaultFactory( - inStream: inStream, - writeMessage: writeMessage, - log: log, - disposeHandler: disposeHandler, - streamClosed: streamClosed, - wsUri: wsUri, - trackFutures: integrationTestMode, - ), - ); + final service = isPostMessageUri(uri) + ? await connectWithPostMessage( + uri: uri, + finishedCompleter: finishedCompleter, + ) + : await connect( + uri: uri, + finishedCompleter: finishedCompleter, + serviceFactory: + ({ + // ignore: avoid-dynamic, mirrors types of [VmServiceFactory]. + required Stream /*String|List*/ inStream, + required void Function(String message) writeMessage, + Log? log, + DisposeHandler? disposeHandler, + Future? streamClosed, + String? wsUri, + bool trackFutures = false, + }) => VmServiceWrapper.defaultFactory( + inStream: inStream, + writeMessage: writeMessage, + log: log, + disposeHandler: disposeHandler, + streamClosed: streamClosed, + wsUri: wsUri, + trackFutures: integrationTestMode, + ), + ); await serviceConnection.serviceManager.vmServiceOpened( service, diff --git a/packages/devtools_app/lib/src/service/_post_message_service_stub.dart b/packages/devtools_app/lib/src/service/_post_message_service_stub.dart new file mode 100644 index 00000000000..0d351bc1026 --- /dev/null +++ b/packages/devtools_app/lib/src/service/_post_message_service_stub.dart @@ -0,0 +1,13 @@ +// Copyright 2026 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. + +import 'dart:async'; + +import 'vm_service_wrapper.dart'; + +/// Connects to a VM Service using a `MessagePort` transferred via postMessage. +Future connectWithPostMessage({ + required Uri uri, + required Completer finishedCompleter, +}) => throw UnsupportedError('unsupported platform'); diff --git a/packages/devtools_app/lib/src/service/_post_message_service_web.dart b/packages/devtools_app/lib/src/service/_post_message_service_web.dart new file mode 100644 index 00000000000..5a2691c81dc --- /dev/null +++ b/packages/devtools_app/lib/src/service/_post_message_service_web.dart @@ -0,0 +1,88 @@ +// Copyright 2026 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:js_interop'; + +import 'package:web/web.dart' as web; + +import 'vm_service_wrapper.dart'; + +/// Extracts the target origin for window.postMessage from [uri]. +/// +/// The URI is expected in the form `post-message:` (e.g. +/// `post-message:https://dartpad.dev` or `post-message:*`). +String _extractTargetOrigin(Uri uri) { + if (uri.path.isEmpty || uri.path == '*') { + return '*'; + } + final target = Uri.tryParse(uri.path); + if (target != null && target.hasScheme && target.hasAuthority) { + return target.origin; + } + return uri.path; +} + +/// Connects to a Dart VM service via a browser `MessagePort`. +/// +/// Creates a [web.MessageChannel], transfers `port1` to `window.parent` +/// via a postMessage with payload `{'vmServicePort': port1}`, and binds +/// `port2` to a [VmServiceWrapper]. +Future connectWithPostMessage({ + required Uri uri, + required Completer finishedCompleter, +}) async { + final targetOrigin = _extractTargetOrigin(uri); + final channel = web.MessageChannel(); + // ignore: avoid-dynamic, mirrors types of [Stream inStream] in [VmService]. + final inStreamController = StreamController(); + + final message = {'vmServicePort': channel.port1}.jsify(); + web.window.parent?.postMessage( + message, + web.WindowPostMessageOptions( + targetOrigin: targetOrigin, + transfer: [channel.port1].toJS, + ), + ); + + channel.port2.onmessage = ((web.MessageEvent event) { + final data = event.data; + if (data != null) { + if (data.isA()) { + inStreamController.add((data as JSString).toDart); + } else if (data.isA()) { + inStreamController.add((data as JSUint8Array).toDart); + } else if (data.isA()) { + inStreamController.add((data as JSArrayBuffer).toDart.asUint8List()); + } else { + inStreamController.add(jsonEncode(data.dartify())); + } + } + }).toJS; + channel.port2.start(); + + final service = VmServiceWrapper.defaultFactory( + inStream: inStreamController.stream, + writeMessage: (String msg) { + channel.port2.postMessage(msg.toJS); + }, + streamClosed: finishedCompleter.future, + wsUri: uri.toString(), + ); + + unawaited( + finishedCompleter.future.then((_) async { + channel.port2.close(); + await inStreamController.close(); + await service.dispose(); + }), + ); + + // Validate the connection with a getVersion call. + await service.getVersion(); + + return service; +} diff --git a/packages/devtools_app/lib/src/service/post_message_service.dart b/packages/devtools_app/lib/src/service/post_message_service.dart new file mode 100644 index 00000000000..cd8d869794c --- /dev/null +++ b/packages/devtools_app/lib/src/service/post_message_service.dart @@ -0,0 +1,14 @@ +// Copyright 2026 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. + +export '_post_message_service_stub.dart' + if (dart.library.js_interop) '_post_message_service_web.dart'; + +/// Returns whether [uri] uses the `post-message:` or `postmessage:` scheme. +/// +/// This scheme is used when DevTools is embedded in an `