Skip to content

Commit 371aadd

Browse files
authored
[tool] fallback to sigkill when closing Chromium (#135521)
This implements flutter/flutter#132654 (comment), namely: Make `Chromium.close` more robust: * Send `SIGTERM` and wait up to 5 seconds, if the process exited, great! Return from the function. * If the process has not exited, then send a `SIGKILL`, which is a much firmer way to exit a process. Same as before, wait up to 5 seconds, if the process exited, great! Return from the function. * If it still hasn't exited then give up trying to exit Chromium, just print a warning to the console and return from the function. Bonus: a few nullability fixes and extra `-v` logging. Fixes flutter/flutter#132654
1 parent b5c8fd1 commit 371aadd

File tree

7 files changed

+212
-26
lines changed

7 files changed

+212
-26
lines changed

packages/flutter_tools/lib/src/base/io.dart

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,15 +191,27 @@ class ProcessSignal {
191191
///
192192
/// Returns true if the signal was delivered, false otherwise.
193193
///
194-
/// On Windows, this can only be used with [ProcessSignal.sigterm], which
195-
/// terminates the process.
194+
/// On Windows, this can only be used with [sigterm], which terminates the
195+
/// process.
196196
///
197-
/// This is implemented by sending the signal using [Process.killPid].
197+
/// This is implemented by sending the signal using [io.Process.killPid] and
198+
/// therefore cannot be faked in tests. To fake sending signals in tests, use
199+
/// [kill] instead.
198200
bool send(int pid) {
199201
assert(!_platform.isWindows || this == ProcessSignal.sigterm);
200202
return io.Process.killPid(pid, _delegate);
201203
}
202204

205+
/// A more testable variant of [send].
206+
///
207+
/// Sends this signal to the given `process` by invoking [io.Process.kill].
208+
///
209+
/// In tests this method can be faked by passing a fake implementation of the
210+
/// [io.Process] interface.
211+
bool kill(io.Process process) {
212+
return process.kill(_delegate);
213+
}
214+
203215
@override
204216
String toString() => _delegate.toString();
205217
}

packages/flutter_tools/lib/src/test/flutter_web_platform.dart

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,14 @@ class FlutterWebPlatform extends PlatformPlugin {
441441
if (_closed) {
442442
throw StateError('Load called on a closed FlutterWebPlatform');
443443
}
444+
445+
final String pathFromTest = _fileSystem.path.relative(path, from: _fileSystem.path.join(_root, 'test'));
446+
final Uri suiteUrl = url.resolveUri(_fileSystem.path.toUri('${_fileSystem.path.withoutExtension(pathFromTest)}.html'));
447+
final String relativePath = _fileSystem.path.relative(_fileSystem.path.normalize(path), from: _fileSystem.currentDirectory.path);
448+
if (_logger.isVerbose) {
449+
_logger.printTrace('Loading test suite $relativePath.');
450+
}
451+
444452
final PoolResource lockResource = await _suiteLock.request();
445453

446454
final Runtime browser = platform.runtime;
@@ -455,17 +463,23 @@ class FlutterWebPlatform extends PlatformPlugin {
455463
throw StateError('Load called on a closed FlutterWebPlatform');
456464
}
457465

458-
final String pathFromTest = _fileSystem.path.relative(path, from: _fileSystem.path.join(_root, 'test'));
459-
final Uri suiteUrl = url.resolveUri(_fileSystem.path.toUri('${_fileSystem.path.withoutExtension(pathFromTest)}.html'));
460-
final String relativePath = _fileSystem.path.relative(_fileSystem.path.normalize(path), from: _fileSystem.currentDirectory.path);
466+
if (_logger.isVerbose) {
467+
_logger.printTrace('Running test suite $relativePath.');
468+
}
469+
461470
final RunnerSuite suite = await _browserManager!.load(relativePath, suiteUrl, suiteConfig, message, onDone: () async {
462471
await _browserManager!.close();
463472
_browserManager = null;
464473
lockResource.release();
474+
if (_logger.isVerbose) {
475+
_logger.printTrace('Test suite $relativePath finished.');
476+
}
465477
});
478+
466479
if (_closed) {
467480
throw StateError('Load called on a closed FlutterWebPlatform');
468481
}
482+
469483
return suite;
470484
}
471485

packages/flutter_tools/lib/src/web/chrome.dart

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -225,10 +225,10 @@ class ChromiumLauncher {
225225
url,
226226
];
227227

228-
final Process? process = await _spawnChromiumProcess(args, chromeExecutable);
228+
final Process process = await _spawnChromiumProcess(args, chromeExecutable);
229229

230230
// When the process exits, copy the user settings back to the provided data-dir.
231-
if (process != null && cacheDir != null) {
231+
if (cacheDir != null) {
232232
unawaited(process.exitCode.whenComplete(() {
233233
_cacheUserSessionInformation(userDataDir, cacheDir);
234234
// cleanup temp dir
@@ -245,10 +245,11 @@ class ChromiumLauncher {
245245
url: url,
246246
process: process,
247247
chromiumLauncher: this,
248+
logger: _logger,
248249
), skipCheck);
249250
}
250251

251-
Future<Process?> _spawnChromiumProcess(List<String> args, String chromeExecutable) async {
252+
Future<Process> _spawnChromiumProcess(List<String> args, String chromeExecutable) async {
252253
if (_operatingSystemUtils.hostPlatform == HostPlatform.darwin_arm64) {
253254
final ProcessResult result = _processManager.runSync(<String>['file', chromeExecutable]);
254255
// Check if ARM Chrome is installed.
@@ -469,25 +470,58 @@ class Chromium {
469470
this.debugPort,
470471
this.chromeConnection, {
471472
this.url,
472-
Process? process,
473+
required Process process,
473474
required ChromiumLauncher chromiumLauncher,
475+
required Logger logger,
474476
}) : _process = process,
475-
_chromiumLauncher = chromiumLauncher;
477+
_chromiumLauncher = chromiumLauncher,
478+
_logger = logger;
476479

477480
final String? url;
478481
final int debugPort;
479-
final Process? _process;
482+
final Process _process;
480483
final ChromeConnection chromeConnection;
481484
final ChromiumLauncher _chromiumLauncher;
485+
final Logger _logger;
482486

483-
Future<int?> get onExit async => _process?.exitCode;
487+
/// Resolves to browser's main process' exit code, when the browser exits.
488+
Future<int> get onExit async => _process.exitCode;
489+
490+
/// The main Chromium process that represents this instance of Chromium.
491+
///
492+
/// Killing this process should result in the browser exiting.
493+
@visibleForTesting
494+
Process get process => _process;
484495

496+
/// Closes all connections to the browser and asks the browser to exit.
485497
Future<void> close() async {
498+
if (_logger.isVerbose) {
499+
_logger.printTrace('Shutting down Chromium.');
500+
}
486501
if (_chromiumLauncher.hasChromeInstance) {
487502
_chromiumLauncher.currentCompleter = Completer<Chromium>();
488503
}
489504
chromeConnection.close();
490-
_process?.kill();
491-
await _process?.exitCode;
505+
506+
// Try to exit Chromium nicely using SIGTERM, before exiting it rudely using
507+
// SIGKILL. Wait no longer than 5 seconds for Chromium to exit before
508+
// falling back to SIGKILL, and then to a warning message.
509+
ProcessSignal.sigterm.kill(_process);
510+
await _process.exitCode.timeout(const Duration(seconds: 5), onTimeout: () {
511+
_logger.printWarning(
512+
'Failed to exit Chromium (pid: ${_process.pid}) using SIGTERM. Will try '
513+
'sending SIGKILL instead.'
514+
);
515+
ProcessSignal.sigkill.kill(_process);
516+
return _process.exitCode.timeout(const Duration(seconds: 5), onTimeout: () async {
517+
_logger.printWarning(
518+
'Failed to exit Chromium (pid: ${_process.pid}) using SIGKILL. Giving '
519+
'up. Will continue, assuming Chromium has exited successfully, but '
520+
'it is possible that this left a dangling Chromium process running '
521+
'on the system.'
522+
);
523+
return 0;
524+
});
525+
});
492526
}
493527
}

packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ void main() {
213213
completer.complete();
214214
expect(secondLaunchResult.started, true);
215215
expect(secondLaunchResult.hasVmService, true);
216-
expect(await device.stopApp(iosApp), false);
216+
expect(await device.stopApp(iosApp), true);
217217
});
218218

219219
testWithoutContext('IOSDevice.startApp launches in debug mode via log reading on <iOS 13', () async {
@@ -291,7 +291,7 @@ void main() {
291291

292292
expect(launchResult.started, true);
293293
expect(launchResult.hasVmService, true);
294-
expect(await device.stopApp(iosApp), false);
294+
expect(await device.stopApp(iosApp), true);
295295
expect(logger.errorText, contains('The Dart VM Service was not discovered after 30 seconds. This is taking much longer than expected...'));
296296
expect(utf8.decoder.convert(stdin.writes.first), contains('process interrupt'));
297297
completer.complete();
@@ -336,7 +336,7 @@ void main() {
336336

337337
expect(launchResult.started, true);
338338
expect(launchResult.hasVmService, true);
339-
expect(await device.stopApp(iosApp), false);
339+
expect(await device.stopApp(iosApp), true);
340340
expect(logger.errorText, contains('The Dart VM Service was not discovered after 45 seconds. This is taking much longer than expected...'));
341341
expect(logger.errorText, contains('Click "Allow" to the prompt asking if you would like to find and connect devices on your local network.'));
342342
completer.complete();
@@ -388,7 +388,7 @@ void main() {
388388
expect(processManager, hasNoRemainingExpectations);
389389
expect(launchResult.started, true);
390390
expect(launchResult.hasVmService, true);
391-
expect(await device.stopApp(iosApp), false);
391+
expect(await device.stopApp(iosApp), true);
392392
});
393393

394394
testWithoutContext('IOSDevice.startApp does not retry when ios-deploy loses connection if not in CI', () async {

packages/flutter_tools/test/general.shard/resident_web_runner_test.dart

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
3939

4040
import '../src/common.dart';
4141
import '../src/context.dart';
42+
import '../src/fake_process_manager.dart';
4243
import '../src/fake_vm_services.dart';
4344

4445
const List<VmServiceExpectation> kAttachLogExpectations =
@@ -620,8 +621,9 @@ void main() {
620621
]);
621622
setupMocks();
622623
final TestChromiumLauncher chromiumLauncher = TestChromiumLauncher();
624+
final FakeProcess process = FakeProcess();
623625
final Chromium chrome =
624-
Chromium(1, chromeConnection, chromiumLauncher: chromiumLauncher);
626+
Chromium(1, chromeConnection, chromiumLauncher: chromiumLauncher, process: process, logger: logger);
625627
chromiumLauncher.setInstance(chrome);
626628

627629
flutterDevice.device = GoogleChromeDevice(
@@ -687,8 +689,9 @@ void main() {
687689
]);
688690
setupMocks();
689691
final TestChromiumLauncher chromiumLauncher = TestChromiumLauncher();
692+
final FakeProcess process = FakeProcess();
690693
final Chromium chrome =
691-
Chromium(1, chromeConnection, chromiumLauncher: chromiumLauncher);
694+
Chromium(1, chromeConnection, chromiumLauncher: chromiumLauncher, process: process, logger: logger);
692695
chromiumLauncher.setInstance(chrome);
693696

694697
flutterDevice.device = GoogleChromeDevice(
@@ -1025,8 +1028,9 @@ void main() {
10251028
setupMocks();
10261029
final FakeChromeConnection chromeConnection = FakeChromeConnection();
10271030
final TestChromiumLauncher chromiumLauncher = TestChromiumLauncher();
1031+
final FakeProcess process = FakeProcess();
10281032
final Chromium chrome =
1029-
Chromium(1, chromeConnection, chromiumLauncher: chromiumLauncher);
1033+
Chromium(1, chromeConnection, chromiumLauncher: chromiumLauncher, process: process, logger: logger);
10301034
chromiumLauncher.setInstance(chrome);
10311035

10321036
flutterDevice.device = GoogleChromeDevice(

packages/flutter_tools/test/src/fake_process_manager.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,10 +213,17 @@ class FakeProcess implements io.Process {
213213
/// The raw byte content of stdout.
214214
final List<int> _stdout;
215215

216+
/// The list of [kill] signals this process received so far.
217+
@visibleForTesting
218+
List<io.ProcessSignal> get signals => _signals;
219+
final List<io.ProcessSignal> _signals = <io.ProcessSignal>[];
220+
216221
@override
217222
bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
223+
_signals.add(signal);
224+
218225
// Killing a fake process has no effect.
219-
return false;
226+
return true;
220227
}
221228
}
222229

@@ -400,6 +407,7 @@ abstract class FakeProcessManager implements ProcessManager {
400407
if (fakeProcess == null) {
401408
return false;
402409
}
410+
fakeProcess.kill(signal);
403411
if (fakeProcess._completer != null) {
404412
fakeProcess._completer.complete();
405413
}

0 commit comments

Comments
 (0)