From 4b2a52fdfd9c7131b67f2ed1b89085217ea0be65 Mon Sep 17 00:00:00 2001 From: Nate Biggs Date: Wed, 19 Feb 2025 17:00:14 -0500 Subject: [PATCH] Allow flutter tools to detach a running Chrome session (#163349) https://github.com/flutter/flutter/issues/163329 Tested locally to ensure pressing 'd' in a running `flutter run` session detaches and leaves Chrome open. Hitting 'q' or stopping with a signal both terminate Chrome as expected. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --------- Co-authored-by: Nate Biggs --- .../lib/src/isolated/resident_web_runner.dart | 14 +-- .../lib/src/resident_runner.dart | 3 - .../resident_web_runner_test.dart | 88 +++++++++++++++++++ 3 files changed, 97 insertions(+), 8 deletions(-) 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 9cca8f7021f..a3a32e79c83 100644 --- a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart @@ -174,10 +174,8 @@ class ResidentWebRunner extends ResidentRunner { debuggingOptions.buildInfo.ddcModuleFormat != DdcModuleFormat.ddc || debuggingOptions.buildInfo.canaryFeatures != true; - // TODO(srujzs): Return true when web supports detaching. - // https://github.com/flutter/flutter/issues/163329 @override - bool get supportsDetach => false; + bool get supportsDetach => stopAppDuringCleanup; ConnectionResult? _connectionResult; StreamSubscription? _stdOutSub; @@ -220,7 +218,11 @@ class ResidentWebRunner extends ResidentRunner { await _stdErrSub?.cancel(); await _serviceSub?.cancel(); await _extensionEventSub?.cancel(); - await device!.device!.stopApp(null); + + if (stopAppDuringCleanup) { + await device!.device!.stopApp(null); + } + _registeredMethodsForService.clear(); try { _generatedEntrypointDirectory?.deleteSync(recursive: true); @@ -808,7 +810,9 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). @override Future exitApp() async { - await device!.exitApps(); + if (stopAppDuringCleanup) { + await device!.exitApps(); + } appFinished(); } diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 1fb6909f60e..185201281fa 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -970,9 +970,6 @@ abstract class ResidentHandlers { Future cleanupAfterSignal(); /// Tear down the runner and leave the application running. - /// - /// This is not supported on web devices where the runner is running - /// the application server as well. Future detach(); /// Tear down the runner and exit the application. 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 5fef0e13540..f3c4ecde757 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 @@ -405,6 +405,90 @@ void main() { }, ); + testUsingContext( + 'Detach keeps device running', + () async { + final BufferLogger logger = BufferLogger.test(); + fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList()); + setupMocks(); + fileSystem.directory('web').deleteSync(recursive: true); + final ResidentWebRunner residentWebRunner = ResidentWebRunner( + flutterDevice, + flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + fileSystem: fileSystem, + logger: logger, + terminal: Terminal.test(), + platform: FakePlatform(), + outputPreferences: OutputPreferences.test(), + analytics: globals.analytics, + systemClock: globals.systemClock, + devtoolsHandler: createNoOpHandler, + ); + + mockDevice.dds = DartDevelopmentService(logger: logger); + + expect(mockDevice.isRunning, false); + final Completer connectionInfoCompleter = + Completer(); + unawaited(residentWebRunner.run(connectionInfoCompleter: connectionInfoCompleter)); + await connectionInfoCompleter.future; + expect(mockDevice.isRunning, true); + await residentWebRunner.detach(); + expect(residentWebRunner.stopAppDuringCleanup, false); + await residentWebRunner.exit(); + await residentWebRunner.cleanupAtFinish(); + expect(mockDevice.isRunning, true); + }, + overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + FeatureFlags: enableExplicitPackageDependencies, + Pub: FakePubWithPrimedDeps.new, + }, + ); + + testUsingContext( + 'Quit stops device', + () async { + final BufferLogger logger = BufferLogger.test(); + fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList()); + setupMocks(); + fileSystem.directory('web').deleteSync(recursive: true); + final ResidentWebRunner residentWebRunner = ResidentWebRunner( + flutterDevice, + flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + fileSystem: fileSystem, + logger: logger, + terminal: Terminal.test(), + platform: FakePlatform(), + outputPreferences: OutputPreferences.test(), + analytics: globals.analytics, + systemClock: globals.systemClock, + devtoolsHandler: createNoOpHandler, + ); + + mockDevice.dds = DartDevelopmentService(logger: logger); + + expect(mockDevice.isRunning, false); + final Completer connectionInfoCompleter = + Completer(); + unawaited(residentWebRunner.run(connectionInfoCompleter: connectionInfoCompleter)); + await connectionInfoCompleter.future; + expect(mockDevice.isRunning, true); + expect(residentWebRunner.stopAppDuringCleanup, true); + await residentWebRunner.cleanupAtFinish(); + expect(mockDevice.isRunning, false); + }, + overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + FeatureFlags: enableExplicitPackageDependencies, + Pub: FakePubWithPrimedDeps.new, + }, + ); + testUsingContext( 'Listens to stdout and stderr streams before running main', () async { @@ -1593,6 +1677,8 @@ class FakeDevice extends Fake implements Device { int count = 0; + bool isRunning = false; + @override Future get sdkNameAndVersion async => 'SDK Name and Version'; @@ -1613,6 +1699,7 @@ class FakeDevice extends Fake implements Device { bool ipv6 = false, String? userIdentifier, }) async { + isRunning = true; return LaunchResult.succeeded(); } @@ -1622,6 +1709,7 @@ class FakeDevice extends Fake implements Device { throw StateError('stopApp called more than once.'); } count += 1; + isRunning = false; return true; } }