From 2fae58b08f4e0b1ef1e4e6299d4f1a496804f129 Mon Sep 17 00:00:00 2001 From: shindonghwi Date: Sun, 1 Feb 2026 15:37:48 +0900 Subject: [PATCH] fix: prevent duplicate Chrome launch in flutter drive flutter drive -d chrome was opening Chrome twice - once from WebDriver and once from web_device.dart. Added webNoLaunchChrome flag to DebuggingOptions so we can skip the second launch. --- .../flutter_tools/lib/src/commands/drive.dart | 3 +- .../flutter_tools/lib/src/commands/run.dart | 3 + packages/flutter_tools/lib/src/device.dart | 11 +++ .../lib/src/drive/web_driver_service.dart | 2 + .../flutter_tools/lib/src/web/web_device.dart | 5 +- .../test/general.shard/web/devices_test.dart | 87 +++++++++++++++++++ 6 files changed, 107 insertions(+), 4 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart index 18b060abd07..ce91d014045 100644 --- a/packages/flutter_tools/lib/src/commands/drive.dart +++ b/packages/flutter_tools/lib/src/commands/drive.dart @@ -332,9 +332,11 @@ class DriveCommand extends RunCommandBase { ); final DriverService driverService = _flutterDriverFactory!.createDriverService(web); final BuildInfo buildInfo = await getBuildInfo(); + final bool isWebDevice = device is ChromiumDevice || device is WebServerDevice; final DebuggingOptions debuggingOptions = await createDebuggingOptions( webDevServerConfig: webDevServerConfig, webChromeBinary: stringArg('chrome-binary'), + webNoLaunchChrome: isWebDevice, ); final File? applicationBinary = applicationBinaryPath == null ? null @@ -353,7 +355,6 @@ class DriveCommand extends RunCommandBase { mainPath: targetFile, platformArgs: { if (traceStartup) 'trace-startup': traceStartup, - if (web) '--no-launch-chrome': true, }, ); } else { diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 1b6fb938c14..cc828d1a206 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -285,6 +285,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment Future createDebuggingOptions({ WebDevServerConfig? webDevServerConfig, String? webChromeBinary, + bool webNoLaunchChrome = false, }) async { final BuildInfo buildInfo = await getBuildInfo(); final int? webBrowserDebugPort = @@ -315,6 +316,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment webBrowserFlags: webBrowserFlags, webCrossOriginIsolation: webCrossOriginIsolation, webChromeBinary: webChromeBinary, + webNoLaunchChrome: webNoLaunchChrome, webRenderer: webRenderer, webUseWasm: useWasm, enableImpeller: enableImpeller, @@ -369,6 +371,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment webBrowserDebugPort: webBrowserDebugPort, webBrowserFlags: webBrowserFlags, webChromeBinary: webChromeBinary, + webNoLaunchChrome: webNoLaunchChrome, webEnableExpressionEvaluation: featureFlags.isWebEnabled && boolArg('web-enable-expression-evaluation'), webLaunchUrl: featureFlags.isWebEnabled ? stringArg('web-launch-url') : null, diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 6f5f62263f7..8e190fa7824 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -986,6 +986,7 @@ class DebuggingOptions { this.webBrowserDebugPort, this.webBrowserFlags = const [], this.webChromeBinary, + this.webNoLaunchChrome = false, this.webEnableExpressionEvaluation = false, this.webLaunchUrl, bool? webCrossOriginIsolation, @@ -1022,6 +1023,7 @@ class DebuggingOptions { this.webBrowserDebugPort, this.webBrowserFlags = const [], this.webChromeBinary, + this.webNoLaunchChrome = false, this.webLaunchUrl, bool? webCrossOriginIsolation, WebRendererMode? webRenderer, @@ -1103,6 +1105,7 @@ class DebuggingOptions { required this.webBrowserDebugPort, required this.webBrowserFlags, required this.webChromeBinary, + required this.webNoLaunchChrome, required this.webEnableExpressionEvaluation, required this.webLaunchUrl, required this.webCrossOriginIsolation, @@ -1191,6 +1194,12 @@ class DebuggingOptions { /// Custom path to the Chrome executable. final String? webChromeBinary; + /// Whether to skip launching Chrome when running web apps. + /// + /// This is used by `flutter drive` to prevent launching a second Chrome + /// instance since the driver service launches Chrome separately. + final bool webNoLaunchChrome; + /// Enable expression evaluation for web target. final bool webEnableExpressionEvaluation; @@ -1301,6 +1310,7 @@ class DebuggingOptions { 'webBrowserDebugPort': webBrowserDebugPort, 'webBrowserFlags': webBrowserFlags, 'webChromeBinary': webChromeBinary, + 'webNoLaunchChrome': webNoLaunchChrome, 'webEnableExpressionEvaluation': webEnableExpressionEvaluation, 'webLaunchUrl': webLaunchUrl, 'webCrossOriginIsolation': webCrossOriginIsolation, @@ -1370,6 +1380,7 @@ class DebuggingOptions { webBrowserDebugPort: json['webBrowserDebugPort'] as int?, webBrowserFlags: (json['webBrowserFlags']! as List).cast(), webChromeBinary: json['webChromeBinary'] as String?, + webNoLaunchChrome: (json['webNoLaunchChrome'] as bool?) ?? false, webEnableExpressionEvaluation: json['webEnableExpressionEvaluation']! as bool, webLaunchUrl: json['webLaunchUrl'] as String?, webCrossOriginIsolation: json['webCrossOriginIsolation']! as bool, diff --git a/packages/flutter_tools/lib/src/drive/web_driver_service.dart b/packages/flutter_tools/lib/src/drive/web_driver_service.dart index 1f4091d9d7b..e5b1bc7bcaa 100644 --- a/packages/flutter_tools/lib/src/drive/web_driver_service.dart +++ b/packages/flutter_tools/lib/src/drive/web_driver_service.dart @@ -87,6 +87,7 @@ class WebDriverService extends DriverService { buildInfo, webDevServerConfig: debuggingOptions.webDevServerConfig, webChromeBinary: debuggingOptions.webChromeBinary, + webNoLaunchChrome: debuggingOptions.webNoLaunchChrome, webRenderer: debuggingOptions.webRenderer, webUseWasm: debuggingOptions.webUseWasm, ) @@ -94,6 +95,7 @@ class WebDriverService extends DriverService { buildInfo, webDevServerConfig: debuggingOptions.webDevServerConfig, webChromeBinary: debuggingOptions.webChromeBinary, + webNoLaunchChrome: debuggingOptions.webNoLaunchChrome, disablePortPublication: debuggingOptions.disablePortPublication, webRenderer: debuggingOptions.webRenderer, webUseWasm: debuggingOptions.webUseWasm, diff --git a/packages/flutter_tools/lib/src/web/web_device.dart b/packages/flutter_tools/lib/src/web/web_device.dart index f987f931b03..ef13cf85479 100644 --- a/packages/flutter_tools/lib/src/web/web_device.dart +++ b/packages/flutter_tools/lib/src/web/web_device.dart @@ -135,8 +135,7 @@ abstract class ChromiumDevice extends WebDevice { } else { url = platformArgs['uri']! as String; } - final launchChrome = platformArgs['no-launch-chrome'] != true; - if (launchChrome) { + if (!debuggingOptions.webNoLaunchChrome) { _chrome = await chromeLauncher.launch( url, cacheDir: _fileSystem.currentDirectory @@ -148,7 +147,7 @@ abstract class ChromiumDevice extends WebDevice { chromeBinary: debuggingOptions.webChromeBinary, ); } - _logger.sendEvent('app.webLaunchUrl', {'url': url, 'launched': launchChrome}); + _logger.sendEvent('app.webLaunchUrl', {'url': url, 'launched': !debuggingOptions.webNoLaunchChrome}); return LaunchResult.succeeded(vmServiceUri: Uri.parse(url)); } diff --git a/packages/flutter_tools/test/general.shard/web/devices_test.dart b/packages/flutter_tools/test/general.shard/web/devices_test.dart index a626461d7d7..6743b72e9a1 100644 --- a/packages/flutter_tools/test/general.shard/web/devices_test.dart +++ b/packages/flutter_tools/test/general.shard/web/devices_test.dart @@ -58,6 +58,52 @@ void main() { }, ); + testWithoutContext( + 'ChromiumDevice.startApp does not launch Chrome when webNoLaunchChrome is true', + () async { + final launcher = _TrackingChromiumLauncher(); + final chromiumDevice = _FakeChromiumDevice( + chromiumLauncher: launcher, + fileSystem: MemoryFileSystem.test(), + logger: BufferLogger.test(), + ); + + await chromiumDevice.startApp( + null, + debuggingOptions: DebuggingOptions.enabled( + BuildInfo.debug, + webNoLaunchChrome: true, + ), + platformArgs: {'uri': 'http://localhost:8080'}, + ); + + expect(launcher.launchCalled, isFalse); + }, + ); + + testWithoutContext( + 'ChromiumDevice.startApp launches Chrome when webNoLaunchChrome is false', + () async { + final launcher = _TrackingChromiumLauncher(); + final chromiumDevice = _FakeChromiumDevice( + chromiumLauncher: launcher, + fileSystem: MemoryFileSystem.test(), + logger: BufferLogger.test(), + ); + + await chromiumDevice.startApp( + null, + debuggingOptions: DebuggingOptions.enabled( + BuildInfo.debug, + webNoLaunchChrome: false, + ), + platformArgs: {'uri': 'http://localhost:8080'}, + ); + + expect(launcher.launchCalled, isTrue); + }, + ); + testWithoutContext('GoogleChromeDevice defaults', () async { final launcher = TestChromiumLauncher(); @@ -495,3 +541,44 @@ class _OnceClosableChromium extends Fake implements Chromium { } class _UnimplementedChromium extends Fake implements Chromium {} + +/// A test implementation of [ChromiumLauncher] that tracks whether launch() was called. +class _TrackingChromiumLauncher implements ChromiumLauncher { + bool launchCalled = false; + + @override + Completer currentCompleter = Completer(); + + @override + bool canFindExecutable() => true; + + @override + Future get connectedInstance => currentCompleter.future; + + @override + String findExecutable() => 'chrome'; + + @override + bool get hasChromeInstance => false; + + @override + Future launch( + String url, { + bool headless = false, + int? debugPort, + bool skipCheck = false, + Directory? cacheDir, + List webBrowserFlags = const [], + String? chromeBinary, + }) async { + launchCalled = true; + final chromium = _UnimplementedChromium(); + currentCompleter.complete(chromium); + return chromium; + } + + @override + Future connect(Chromium chrome, bool skipCheck) { + return currentCompleter.future; + } +}