Kill interactive script job xcdevice observe processes on tool/daemon shutdown (#157646)

To convince `xcdevice observe` to redirect to stdout it's being launched in an interactive shell `/usr/bin/script -t 0 /dev/null /usr/bin/arch -arm64e xcrun xcdevice observe --usb`
1cc8a07ace/packages/flutter_tools/lib/src/macos/xcdevice.dart (L261-L263)

When the `flutter` command exits, the interactive script process is terminated, but not its jobs `xcdevice observe --usb`.

Instead of `-9` sigterm killing the interactive script, send it a [`SIGHUP`](https://linux.die.net/Bash-Beginners-Guide/sect_12_01.html#sect_12_01_01_02) (signal hangup) during `XCDevice.dispose()`, which will exit its processes.  Add a shutdown hook to ensure `dispose` is run when the process exits.

Fixes https://github.com/flutter/flutter/issues/73859
This commit is contained in:
Jenn Magder 2024-11-05 11:01:01 -08:00 committed by GitHub
parent c1b7740ea4
commit b6fef5cfda
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 54 additions and 6 deletions

View File

@ -174,6 +174,7 @@ class ProcessSignal {
const ProcessSignal(this._delegate, {@visibleForTesting Platform platform = const LocalPlatform()})
: _platform = platform;
static const ProcessSignal sighup = PosixProcessSignal(io.ProcessSignal.sighup);
static const ProcessSignal sigwinch = PosixProcessSignal(io.ProcessSignal.sigwinch);
static const ProcessSignal sigterm = PosixProcessSignal(io.ProcessSignal.sigterm);
static const ProcessSignal sigusr1 = PosixProcessSignal(io.ProcessSignal.sigusr1);

View File

@ -376,6 +376,7 @@ Future<T> runInContext<T>(
),
fileSystem: globals.fs,
analytics: globals.analytics,
shutdownHooks: globals.shutdownHooks,
),
XcodeProjectInterpreter: () => XcodeProjectInterpreter(
logger: globals.logger,

View File

@ -71,6 +71,7 @@ class XCDevice {
required IProxy iproxy,
required FileSystem fileSystem,
required Analytics analytics,
required ShutdownHooks shutdownHooks,
@visibleForTesting
IOSCoreDeviceControl? coreDeviceControl,
XcodeDebug? xcodeDebug,
@ -105,12 +106,13 @@ class XCDevice {
_xcode = xcode,
_analytics = analytics {
shutdownHooks.addShutdownHook(dispose);
_setupDeviceIdentifierByEventStream();
}
void dispose() {
_usbDeviceObserveProcess?.kill();
_wifiDeviceObserveProcess?.kill();
_stopObservingTetheredIOSDevices();
_usbDeviceWaitProcess?.kill();
_wifiDeviceWaitProcess?.kill();
}
@ -227,13 +229,13 @@ class XCDevice {
final Future<void> usbProcessExited = _usbDeviceObserveProcess!.exitCode.then((int status) {
_logger.printTrace('xcdevice observe --usb exited with code $exitCode');
// Kill other process in case only one was killed.
_wifiDeviceObserveProcess?.kill();
_stopObservingTetheredIOSDevices();
});
final Future<void> wifiProcessExited = _wifiDeviceObserveProcess!.exitCode.then((int status) {
_logger.printTrace('xcdevice observe --wifi exited with code $exitCode');
// Kill other process in case only one was killed.
_usbDeviceObserveProcess?.kill();
_stopObservingTetheredIOSDevices();
});
unawaited(Future.wait(<Future<void>>[
@ -332,8 +334,15 @@ class XCDevice {
}
void _stopObservingTetheredIOSDevices() {
_usbDeviceObserveProcess?.kill();
_wifiDeviceObserveProcess?.kill();
// xcdevice observe is running in an interactive shell.
// Signal script child jobs to exit and exit the shell.
// See https://linux.die.net/Bash-Beginners-Guide/sect_12_01.html#sect_12_01_01_02.
if (_usbDeviceObserveProcess != null) {
ProcessSignal.sighup.kill(_usbDeviceObserveProcess!);
}
if (_wifiDeviceObserveProcess != null) {
ProcessSignal.sighup.kill(_wifiDeviceObserveProcess!);
}
}
XCDeviceEventNotification? _processXCDeviceStdOut(

View File

@ -9,6 +9,7 @@ import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/io.dart' show ProcessException;
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/base/user_messages.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/build_info.dart';
@ -539,6 +540,7 @@ void main() {
coreDeviceControl: FakeIOSCoreDeviceControl(),
xcodeDebug: FakeXcodeDebug(),
analytics: const NoOpAnalytics(),
shutdownHooks: FakeShutdownHooks(),
);
});
@ -553,6 +555,35 @@ void main() {
});
});
testWithoutContext('shutdown hooks disposes xcdevice observers', () async {
final ShutdownHooks shutdownHooks = ShutdownHooks();
final XCDevice xcdevice = XCDevice(
processManager: FakeProcessManager.any(),
logger: logger,
xcode: Xcode.test(processManager: FakeProcessManager.any()),
platform: FakePlatform(operatingSystem: 'macos'),
artifacts: Artifacts.test(),
cache: Cache.test(processManager: FakeProcessManager.any()),
iproxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
fileSystem: MemoryFileSystem.test(),
coreDeviceControl: FakeIOSCoreDeviceControl(),
xcodeDebug: FakeXcodeDebug(),
analytics: const NoOpAnalytics(),
shutdownHooks: shutdownHooks,
);
expect(shutdownHooks.registeredHooks, hasLength(1));
final Completer<void> doneCompleter = Completer<void>();
xcdevice.observedDeviceEvents()!.listen(null, onDone: () {
doneCompleter.complete();
});
await doneCompleter.future;
xcdevice.dispose();
expect(logger.traceText, contains('xcdevice observe --usb exited with code 0'));
expect(logger.traceText, contains('xcdevice observe --wifi exited with code 0'));
});
group('xcdevice', () {
late XCDevice xcdevice;
late Xcode xcode;
@ -580,6 +611,7 @@ void main() {
coreDeviceControl: coreDeviceControl,
xcodeDebug: FakeXcodeDebug(),
analytics: fakeAnalytics,
shutdownHooks: FakeShutdownHooks(),
);
});
@ -1688,6 +1720,11 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete
class FakeXcodeDebug extends Fake implements XcodeDebug {}
class FakeShutdownHooks extends Fake implements ShutdownHooks {
@override
void addShutdownHook(ShutdownHook shutdownHook) {}
}
class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {
List<FakeIOSCoreDevice> devices = <FakeIOSCoreDevice>[];