Make device debuggable if useDwdsWebSocketConnection is true and added simple test case (#171648)
- Make device debuggable if useDwdsWebSocketConnection is true - This
change is needed to support hot reload on web-server devices over
websocket-based DWDS.
- Added simple test case to test hot reload over websocket
related to https://github.com/dart-lang/webdev/issues/2605,
https://github.com/dart-lang/webdev/issues/2645 and
https://github.com/dart-lang/webdev/issues/2646
---------
Co-authored-by: Ben Konyi <bkonyi@google.com>
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 25173ae..9be9111 100644
--- a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart
+++ b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart
@@ -158,8 +158,17 @@
@override
bool get debuggingEnabled => isRunningDebug && deviceIsDebuggable;
- /// WebServer device is debuggable when running with --start-paused.
- bool get deviceIsDebuggable => device!.device is! WebServerDevice || debuggingOptions.startPaused;
+ /// Device is debuggable if not a WebServer device, or if running with
+ /// --start-paused or using DWDS WebSocket connection (WebServer device).
+ bool get deviceIsDebuggable =>
+ device!.device is! WebServerDevice ||
+ debuggingOptions.startPaused ||
+ _useDwdsWebSocketConnection;
+
+ bool get _useDwdsWebSocketConnection {
+ final DevFS? devFS = device?.devFS;
+ return devFS is WebDevFS && devFS.useDwdsWebSocketConnection;
+ }
@override
// Web uses a different plugin registry.
@@ -790,86 +799,102 @@
Uri? websocketUri;
if (supportsServiceProtocol) {
assert(connectDebug != null);
- _connectionResult = await connectDebug;
- unawaited(_connectionResult!.debugConnection!.onDone.whenComplete(_cleanupAndExit));
+ unawaited(
+ connectDebug!.then((connectionResult) async {
+ _connectionResult = connectionResult;
+ unawaited(_connectionResult!.debugConnection!.onDone.whenComplete(_cleanupAndExit));
- void onLogEvent(vmservice.Event event) {
- final String message = processVmServiceMessage(event);
- _logger.printStatus(message);
- }
-
- // This flag is needed to manage breakpoints properly.
- if (debuggingOptions.startPaused && debuggingOptions.debuggingEnabled) {
- try {
- final vmservice.Response result = await _vmService.service.setFlag(
- 'pause_isolates_on_start',
- 'true',
- );
- if (result is! vmservice.Success) {
- _logger.printError('setFlag failure: $result');
+ void onLogEvent(vmservice.Event event) {
+ final String message = processVmServiceMessage(event);
+ _logger.printStatus(message);
}
- } on Exception catch (e) {
- _logger.printError(
- 'Failed to set pause_isolates_on_start=true, proceeding. '
- 'Error: $e',
+
+ // This flag is needed to manage breakpoints properly.
+ if (debuggingOptions.startPaused && debuggingOptions.debuggingEnabled) {
+ try {
+ final vmservice.Response result = await _vmService.service.setFlag(
+ 'pause_isolates_on_start',
+ 'true',
+ );
+ if (result is! vmservice.Success) {
+ _logger.printError('setFlag failure: $result');
+ }
+ } on Exception catch (e) {
+ _logger.printError(
+ 'Failed to set pause_isolates_on_start=true, proceeding. '
+ 'Error: $e',
+ );
+ }
+ }
+
+ _stdOutSub = _vmService.service.onStdoutEvent.listen(onLogEvent);
+ _stdErrSub = _vmService.service.onStderrEvent.listen(onLogEvent);
+ _serviceSub = _vmService.service.onServiceEvent.listen(_onServiceEvent);
+ try {
+ await _vmService.service.streamListen(vmservice.EventStreams.kStdout);
+ } on vmservice.RPCError {
+ // It is safe to ignore this error because we expect an error to be
+ // thrown if we're already subscribed.
+ }
+ try {
+ await _vmService.service.streamListen(vmservice.EventStreams.kStderr);
+ } on vmservice.RPCError {
+ // It is safe to ignore this error because we expect an error to be
+ // thrown if we're already subscribed.
+ }
+ try {
+ await _vmService.service.streamListen(vmservice.EventStreams.kService);
+ } on vmservice.RPCError {
+ // It is safe to ignore this error because we expect an error to be
+ // thrown if we're already subscribed.
+ }
+ try {
+ await _vmService.service.streamListen(vmservice.EventStreams.kIsolate);
+ } on vmservice.RPCError {
+ // It is safe to ignore this error because we expect an error to be
+ // thrown if we're not already subscribed.
+ }
+ await setUpVmService(
+ reloadSources: (String isolateId, {bool? force, bool? pause}) async {
+ await restart(pause: pause);
+ },
+ device: device!.device,
+ flutterProject: flutterProject,
+ printStructuredErrorLogMethod: printStructuredErrorLog,
+ vmService: _vmService.service,
);
- }
- }
- _stdOutSub = _vmService.service.onStdoutEvent.listen(onLogEvent);
- _stdErrSub = _vmService.service.onStderrEvent.listen(onLogEvent);
- _serviceSub = _vmService.service.onServiceEvent.listen(_onServiceEvent);
- try {
- await _vmService.service.streamListen(vmservice.EventStreams.kStdout);
- } on vmservice.RPCError {
- // It is safe to ignore this error because we expect an error to be
- // thrown if we're already subscribed.
- }
- try {
- await _vmService.service.streamListen(vmservice.EventStreams.kStderr);
- } on vmservice.RPCError {
- // It is safe to ignore this error because we expect an error to be
- // thrown if we're already subscribed.
- }
- try {
- await _vmService.service.streamListen(vmservice.EventStreams.kService);
- } on vmservice.RPCError {
- // It is safe to ignore this error because we expect an error to be
- // thrown if we're already subscribed.
- }
- try {
- await _vmService.service.streamListen(vmservice.EventStreams.kIsolate);
- } on vmservice.RPCError {
- // It is safe to ignore this error because we expect an error to be
- // thrown if we're not already subscribed.
- }
- await setUpVmService(
- reloadSources: (String isolateId, {bool? force, bool? pause}) async {
- await restart(pause: pause);
- },
- device: device!.device,
- flutterProject: flutterProject,
- printStructuredErrorLogMethod: printStructuredErrorLog,
- vmService: _vmService.service,
- );
+ websocketUri = Uri.parse(_connectionResult!.debugConnection!.uri);
+ device!.vmService = _vmService;
- websocketUri = Uri.parse(_connectionResult!.debugConnection!.uri);
- device!.vmService = _vmService;
-
- // Run main immediately if the app is not started paused or if there
- // is no debugger attached. Otherwise, runMain when a resume event
- // is received.
- if (!debuggingOptions.startPaused || !supportsServiceProtocol) {
- _connectionResult!.appConnection!.runMain();
- } else {
- late StreamSubscription<void> resumeSub;
- resumeSub = _vmService.service.onDebugEvent.listen((vmservice.Event event) {
- if (event.type == vmservice.EventKind.kResume) {
+ // Run main immediately if the app is not started paused or if there
+ // is no debugger attached. Otherwise, runMain when a resume event
+ // is received.
+ if (!debuggingOptions.startPaused || !supportsServiceProtocol) {
_connectionResult!.appConnection!.runMain();
- resumeSub.cancel();
+ } else {
+ late StreamSubscription<void> resumeSub;
+ resumeSub = _vmService.service.onDebugEvent.listen((vmservice.Event event) {
+ if (event.type == vmservice.EventKind.kResume) {
+ _connectionResult!.appConnection!.runMain();
+ resumeSub.cancel();
+ }
+ });
}
- });
- }
+
+ if (websocketUri != null) {
+ if (debuggingOptions.vmserviceOutFile != null) {
+ _fileSystem.file(debuggingOptions.vmserviceOutFile)
+ ..createSync(recursive: true)
+ ..writeAsStringSync(websocketUri.toString());
+ }
+ _logger.printStatus('Debug service listening on $websocketUri');
+ }
+ connectionInfoCompleter?.complete(DebugConnectionInfo(wsUri: websocketUri));
+ }),
+ );
+ } else {
+ connectionInfoCompleter?.complete(DebugConnectionInfo());
}
// TODO(bkonyi): remove when ready to serve DevTools from DDS.
if (debuggingOptions.enableDevTools) {
@@ -881,16 +906,8 @@
),
);
}
- if (websocketUri != null) {
- if (debuggingOptions.vmserviceOutFile != null) {
- _fileSystem.file(debuggingOptions.vmserviceOutFile)
- ..createSync(recursive: true)
- ..writeAsStringSync(websocketUri.toString());
- }
- _logger.printStatus('Debug service listening on $websocketUri');
- }
+
appStartedCompleter?.complete();
- connectionInfoCompleter?.complete(DebugConnectionInfo(wsUri: websocketUri));
if (stayResident) {
await waitForAppToFinish();
} else {
diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml
index fa69ee5..14485a3 100644
--- a/packages/flutter_tools/pubspec.yaml
+++ b/packages/flutter_tools/pubspec.yaml
@@ -13,7 +13,7 @@
archive: 3.6.1
args: 2.7.0
dds: 5.0.3
- dwds: 24.4.0
+ dwds: 24.4.1
code_builder: 4.10.1
collection: 1.19.1
completion: 1.0.2
@@ -126,4 +126,4 @@
dartdoc:
# Exclude this package from the hosted API docs.
nodoc: true
-# PUBSPEC CHECKSUM: kav93d
+# PUBSPEC CHECKSUM: 932agm
diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart
index 9895d19..00ae682 100644
--- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart
+++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart
@@ -1665,7 +1665,9 @@
mainLibName: 'my_app',
packages: <String, String>{'path_provider_linux': '../../path_provider_linux'},
);
- expect(await residentWebRunner.run(), 0);
+ final connectionInfoCompleter = Completer<DebugConnectionInfo>();
+ expect(await residentWebRunner.run(connectionInfoCompleter: connectionInfoCompleter), 0);
+ await connectionInfoCompleter.future;
final File generatedLocalizationsFile = globals.fs
.directory('lib')
.childDirectory('l10n')
@@ -2056,6 +2058,9 @@
PackageConfig? lastPackageConfig = PackageConfig.empty;
@override
+ var useDwdsWebSocketConnection = false;
+
+ @override
Future<Uri> create() async {
return baseUri;
}
diff --git a/packages/flutter_tools/test/integration.shard/hot_reload_websocket_test.dart b/packages/flutter_tools/test/integration.shard/hot_reload_websocket_test.dart
new file mode 100644
index 0000000..c28ca83
--- /dev/null
+++ b/packages/flutter_tools/test/integration.shard/hot_reload_websocket_test.dart
@@ -0,0 +1,193 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+@Tags(<String>['flutter-test-driver'])
+library;
+
+import 'dart:async';
+import 'dart:io' as io;
+
+import 'package:file/file.dart';
+import 'package:flutter_tools/src/web/chrome.dart';
+import 'package:flutter_tools/src/web/web_device.dart' show WebServerDevice;
+
+import '../src/common.dart';
+import 'test_data/hot_reload_project.dart';
+import 'test_driver.dart';
+import 'test_utils.dart';
+import 'transition_test_utils.dart';
+
+void main() {
+ testAll();
+}
+
+void testAll({List<String> additionalCommandArgs = const <String>[]}) {
+ group('WebSocket DWDS connection'
+ '${additionalCommandArgs.isEmpty ? '' : ' with args: $additionalCommandArgs'}', () {
+ // Test configuration constants
+ const debugUrlTimeout = Duration(seconds: 20);
+ const appStartTimeout = Duration(seconds: 15);
+ const hotReloadTimeout = Duration(seconds: 10);
+
+ late Directory tempDir;
+ final project = HotReloadProject();
+ late FlutterRunTestDriver flutter;
+
+ setUp(() async {
+ tempDir = createResolvedTempDirectorySync('hot_reload_websocket_test.');
+ await project.setUpIn(tempDir);
+ flutter = FlutterRunTestDriver(tempDir);
+ });
+
+ tearDown(() async {
+ await flutter.stop();
+ tryToDelete(tempDir);
+ });
+
+ testWithoutContext(
+ 'hot reload with headless Chrome WebSocket connection',
+ () async {
+ debugPrint('Starting WebSocket DWDS test with headless Chrome...');
+
+ // Set up listening for app output before starting
+ final stdout = StringBuffer();
+ final sawDebugUrl = Completer<String>();
+ final StreamSubscription<String> subscription = flutter.stdout.listen((String e) {
+ stdout.writeln(e);
+ // Extract the debug connection URL
+ if (e.contains('Waiting for connection from Dart debug extension at http://')) {
+ final debugUrlPattern = RegExp(
+ r'Waiting for connection from Dart debug extension at (http://[^\s]+)',
+ );
+ final Match? match = debugUrlPattern.firstMatch(e);
+ if (match != null && !sawDebugUrl.isCompleted) {
+ sawDebugUrl.complete(match.group(1)!);
+ }
+ }
+ });
+
+ io.Process? chromeProcess;
+ try {
+ // Step 1: Start Flutter app with web-server device (will wait for debug connection)
+ debugPrint('Step 1: Starting Flutter app with web-server device...');
+ // Start the app but don't wait for it to complete - it won't complete until Chrome connects
+ final Future<void> appStartFuture = runFlutterWithWebServerDevice(
+ flutter,
+ additionalCommandArgs: [...additionalCommandArgs, '--no-web-resources-cdn'],
+ );
+
+ // Step 2: Wait for DWDS debug URL to be available
+ debugPrint('Step 2: Waiting for DWDS debug service URL...');
+ final String debugUrl = await sawDebugUrl.future.timeout(
+ debugUrlTimeout,
+ onTimeout: () {
+ throw Exception('DWDS debug URL not found - app may not have started correctly');
+ },
+ );
+ debugPrint('✓ DWDS debug service available at: $debugUrl');
+
+ // Step 3: Launch headless Chrome to connect to DWDS
+ debugPrint('Step 3: Launching headless Chrome to connect to DWDS...');
+ chromeProcess = await _launchHeadlessChrome(debugUrl);
+ debugPrint('✓ Headless Chrome launched and connecting to DWDS');
+
+ // Step 4: Wait for app to start (Chrome connection established)
+ debugPrint('Step 4: Waiting for Flutter app to start after Chrome connection...');
+ await appStartFuture.timeout(
+ appStartTimeout,
+ onTimeout: () {
+ throw Exception('App startup did not complete after Chrome connection');
+ },
+ );
+ debugPrint('✓ Flutter app started successfully with WebSocket connection');
+
+ // Step 5: Test hot reload functionality
+ debugPrint('Step 5: Testing hot reload with WebSocket connection...');
+ await flutter.hotReload().timeout(
+ hotReloadTimeout,
+ onTimeout: () {
+ throw Exception('Hot reload timed out');
+ },
+ );
+
+ // Give some time for logs to capture
+ await Future<void>.delayed(const Duration(seconds: 2));
+
+ final output = stdout.toString();
+ expect(output, contains('Reloaded'), reason: 'Hot reload should complete successfully');
+ debugPrint('✓ Hot reload completed successfully with WebSocket connection');
+
+ // Verify the correct infrastructure was used
+ expect(
+ output,
+ contains('Waiting for connection from Dart debug extension'),
+ reason: 'Should wait for debug connection (WebSocket infrastructure)',
+ );
+ expect(output, contains('web-server'), reason: 'Should use web-server device');
+
+ debugPrint('✓ WebSocket DWDS test completed successfully');
+ debugPrint('✓ Verified: web-server device + DWDS + WebSocket connection + hot reload');
+ } finally {
+ await _cleanupResources(chromeProcess, subscription);
+ }
+ },
+ skip: !platform.isMacOS, // Skip on non-macOS platforms where Chrome paths may differ
+ );
+ });
+}
+
+/// Launches headless Chrome with the given debug URL.
+/// Uses findChromeExecutable to locate Chrome on the current platform.
+Future<io.Process> _launchHeadlessChrome(String debugUrl) async {
+ const chromeArgs = [
+ '--headless',
+ '--disable-gpu',
+ '--no-sandbox',
+ '--disable-extensions',
+ '--disable-dev-shm-usage',
+ '--remote-debugging-port=0',
+ ];
+
+ final String chromePath = findChromeExecutable(platform, fileSystem);
+
+ try {
+ return await io.Process.start(chromePath, [...chromeArgs, debugUrl]);
+ } on Exception catch (e) {
+ throw Exception(
+ 'Could not launch Chrome at $chromePath: $e. Please ensure Chrome is installed.',
+ );
+ }
+}
+
+/// Cleans up test resources (Chrome process and stdout subscription).
+Future<void> _cleanupResources(
+ io.Process? chromeProcess,
+ StreamSubscription<String> subscription,
+) async {
+ if (chromeProcess != null) {
+ try {
+ chromeProcess.kill();
+ await chromeProcess.exitCode;
+ debugPrint('Chrome process cleaned up');
+ } on Exception catch (e) {
+ debugPrint('Warning: Failed to clean up Chrome process: $e');
+ }
+ }
+ await subscription.cancel();
+}
+
+// Helper to run flutter with web-server device using WebSocket connection.
+Future<void> runFlutterWithWebServerDevice(
+ FlutterRunTestDriver flutter, {
+ bool verbose = false,
+ bool withDebugger = true, // Enable debugger by default for WebSocket connection
+ bool startPaused = false, // Don't start paused for this test
+ List<String> additionalCommandArgs = const <String>[],
+}) => flutter.run(
+ verbose: verbose,
+ withDebugger: withDebugger, // Enable debugger to establish WebSocket connection
+ startPaused: startPaused, // Let the app start normally after debugger connects
+ device: WebServerDevice.kWebServerDeviceId,
+ additionalCommandArgs: additionalCommandArgs,
+);
diff --git a/packages/flutter_tools/test/integration.shard/test_driver.dart b/packages/flutter_tools/test/integration.shard/test_driver.dart
index e173616..a6c5887 100644
--- a/packages/flutter_tools/test/integration.shard/test_driver.dart
+++ b/packages/flutter_tools/test/integration.shard/test_driver.dart
@@ -11,7 +11,7 @@
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/utils.dart';
import 'package:flutter_tools/src/tester/flutter_tester.dart';
-import 'package:flutter_tools/src/web/web_device.dart' show GoogleChromeDevice;
+import 'package:flutter_tools/src/web/web_device.dart' show GoogleChromeDevice, WebServerDevice;
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import 'package:vm_service/vm_service.dart';
@@ -568,6 +568,7 @@
],
withDebugger: withDebugger,
startPaused: startPaused,
+ waitForDebugPort: device != WebServerDevice.kWebServerDeviceId,
pauseOnExceptions: pauseOnExceptions,
script: script,
verbose: verbose,
@@ -608,6 +609,7 @@
bool withDebugger = false,
bool startPaused = false,
bool pauseOnExceptions = false,
+ bool waitForDebugPort = false,
bool verbose = false,
int? attachPort,
}) async {
@@ -648,12 +650,11 @@
event: 'app.started',
timeout: appStartTimeout,
);
-
+ late final Map<String, Object?> debugPort;
+ if (waitForDebugPort || withDebugger) {
+ debugPort = await _waitFor(event: 'app.debugPort', timeout: appStartTimeout);
+ }
if (withDebugger) {
- final Map<String, Object?> debugPort = await _waitFor(
- event: 'app.debugPort',
- timeout: appStartTimeout,
- );
final wsUriString = (debugPort['params']! as Map<String, Object?>)['wsUri']! as String;
_vmServiceWsUri = Uri.parse(wsUriString);
await connectToVmService(pauseOnExceptions: pauseOnExceptions);