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 <natebiggs@google.com>
This commit is contained in:
Nate Biggs 2025-02-19 17:00:14 -05:00 committed by GitHub
parent 99ac47897e
commit 4b2a52fdfd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 97 additions and 8 deletions

View File

@ -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<vmservice.Event>? _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<void> exitApp() async {
await device!.exitApps();
if (stopAppDuringCleanup) {
await device!.exitApps();
}
appFinished();
}

View File

@ -970,9 +970,6 @@ abstract class ResidentHandlers {
Future<void> 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<void> detach();
/// Tear down the runner and exit the application.

View File

@ -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<DebugConnectionInfo> connectionInfoCompleter =
Completer<DebugConnectionInfo>();
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: <Type, Generator>{
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<DebugConnectionInfo> connectionInfoCompleter =
Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(connectionInfoCompleter: connectionInfoCompleter));
await connectionInfoCompleter.future;
expect(mockDevice.isRunning, true);
expect(residentWebRunner.stopAppDuringCleanup, true);
await residentWebRunner.cleanupAtFinish();
expect(mockDevice.isRunning, false);
},
overrides: <Type, Generator>{
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<String> 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;
}
}