Unverified Commit 7fa5dd74 authored by Zachary Anderson's avatar Zachary Anderson Committed by GitHub

[flutter_tools] Allows adding multiple signal handlers (#41304)

parent 3fce2608
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'async_guard.dart';
import 'context.dart';
import 'io.dart';
typedef SignalHandler = FutureOr<void> Function(ProcessSignal signal);
Signals get signals => Signals.instance;
/// A class that manages signal handlers
///
/// Signal handlers are run in the order that they were added.
abstract class Signals {
factory Signals() => _DefaultSignals._();
static Signals get instance => context.get<Signals>();
/// Adds a signal handler to run on receipt of signal.
///
/// The handler will run after all handlers that were previously added for the
/// signal. The function returns an abstract token that should be provided to
/// removeHandler to remove the handler.
Object addHandler(ProcessSignal signal, SignalHandler handler);
/// Removes a signal handler.
///
/// Removes the signal handler for the signal identified by the abstract
/// token parameter. Returns true if the handler was removed and false
/// otherwise.
Future<bool> removeHandler(ProcessSignal signal, Object token);
/// If a [SignalHandler] throws an error, either synchronously or
/// asynchronously, it will be added to this stream instead of propagated.
Stream<Object> get errors;
}
class _DefaultSignals implements Signals {
_DefaultSignals._();
// A table mapping (signal, token) -> signal handler.
final Map<ProcessSignal, Map<Object, SignalHandler>> _handlersTable =
<ProcessSignal, Map<Object, SignalHandler>>{};
// A table mapping (signal) -> signal handler list. The list is in the order
// that the signal handlers should be run.
final Map<ProcessSignal, List<SignalHandler>> _handlersList =
<ProcessSignal, List<SignalHandler>>{};
// A table mapping (signal) -> low-level signal event stream.
final Map<ProcessSignal, StreamSubscription<ProcessSignal>> _streamSubscriptions =
<ProcessSignal, StreamSubscription<ProcessSignal>>{};
// The stream controller for errors coming from signal handlers.
final StreamController<Object> _errorStreamController = StreamController<Object>.broadcast();
@override
Stream<Object> get errors => _errorStreamController.stream;
@override
Object addHandler(ProcessSignal signal, SignalHandler handler) {
final Object token = Object();
_handlersTable.putIfAbsent(signal, () => <Object, SignalHandler>{});
_handlersTable[signal][token] = handler;
_handlersList.putIfAbsent(signal, () => <SignalHandler>[]);
_handlersList[signal].add(handler);
// If we added the first one, then call signal.watch(), listen, and cache
// the stream controller.
if (_handlersList[signal].length == 1) {
_streamSubscriptions[signal] = signal.watch().listen(_handleSignal);
}
return token;
}
@override
Future<bool> removeHandler(ProcessSignal signal, Object token) async {
// We don't know about this signal.
if (!_handlersTable.containsKey(signal)) {
return false;
}
// We don't know about this token.
if (!_handlersTable[signal].containsKey(token)) {
return false;
}
final SignalHandler handler = _handlersTable[signal][token];
final bool removed = _handlersList[signal].remove(handler);
if (!removed) {
return false;
}
// If _handlersList[signal] is empty, then lookup the cached stream
// controller and unsubscribe from the stream.
if (_handlersList.isEmpty) {
await _streamSubscriptions[signal].cancel();
}
return true;
}
Future<void> _handleSignal(ProcessSignal s) async {
for (SignalHandler handler in _handlersList[s]) {
try {
await asyncGuard<void>(() => handler(s));
} catch (e) {
if (_errorStreamController.hasListener) {
_errorStreamController.add(e);
}
}
}
// If this was a signal that should cause the process to go down, then
// call exit();
if (_shouldExitFor(s)) {
exit(0);
}
}
// The list of signals that should cause the process to exit.
static const List<ProcessSignal> _exitingSignals = <ProcessSignal>[
ProcessSignal.SIGTERM,
ProcessSignal.SIGINT,
ProcessSignal.SIGKILL,
];
bool _shouldExitFor(ProcessSignal signal) => _exitingSignals.contains(signal);
}
......@@ -20,6 +20,7 @@ import 'base/logger.dart';
import 'base/os.dart';
import 'base/platform.dart';
import 'base/process.dart';
import 'base/signals.dart';
import 'base/time.dart';
import 'base/user_messages.dart';
import 'base/utils.dart';
......@@ -106,6 +107,7 @@ Future<T> runInContext<T>(
OperatingSystemUtils: () => OperatingSystemUtils(),
ProcessInfo: () => ProcessInfo(),
ProcessUtils: () => ProcessUtils(),
Signals: () => Signals(),
SimControl: () => SimControl(),
Stdio: () => const Stdio(),
SystemClock: () => const SystemClock(),
......
......@@ -13,6 +13,7 @@ import 'base/common.dart';
import 'base/file_system.dart';
import 'base/io.dart' as io;
import 'base/logger.dart';
import 'base/signals.dart';
import 'base/terminal.dart';
import 'base/utils.dart';
import 'build_info.dart';
......@@ -993,19 +994,13 @@ class TerminalHandler {
void registerSignalHandlers() {
assert(residentRunner.stayResident);
io.ProcessSignal.SIGINT.watch().listen((io.ProcessSignal signal) {
_cleanUp(signal);
io.exit(0);
});
io.ProcessSignal.SIGTERM.watch().listen((io.ProcessSignal signal) {
_cleanUp(signal);
io.exit(0);
});
signals.addHandler(io.ProcessSignal.SIGINT, _cleanUp);
signals.addHandler(io.ProcessSignal.SIGTERM, _cleanUp);
if (!residentRunner.supportsServiceProtocol || !residentRunner.supportsRestart) {
return;
}
io.ProcessSignal.SIGUSR1.watch().listen(_handleSignal);
io.ProcessSignal.SIGUSR2.watch().listen(_handleSignal);
signals.addHandler(io.ProcessSignal.SIGUSR1, _handleSignal);
signals.addHandler(io.ProcessSignal.SIGUSR2, _handleSignal);
}
/// Returns [true] if the input has been handled by this function.
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:io' as io;
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/signals.dart';
import 'package:mockito/mockito.dart';
import '../../src/common.dart';
import '../../src/context.dart';
void main() {
group('Signals', () {
MockIoProcessSignal mockSignal;
ProcessSignal signalUnderTest;
StreamController<io.ProcessSignal> controller;
setUp(() {
mockSignal = MockIoProcessSignal();
signalUnderTest = ProcessSignal(mockSignal);
controller = StreamController<io.ProcessSignal>();
when(mockSignal.watch()).thenAnswer((Invocation invocation) => controller.stream);
});
testUsingContext('signal handler runs', () async {
final Completer<void> completer = Completer<void>();
signals.addHandler(signalUnderTest, (ProcessSignal s) {
expect(s, signalUnderTest);
completer.complete();
});
controller.add(mockSignal);
await completer.future;
});
testUsingContext('signal handlers run in order', () async {
final Completer<void> completer = Completer<void>();
bool first = false;
signals.addHandler(signalUnderTest, (ProcessSignal s) {
expect(s, signalUnderTest);
first = true;
});
signals.addHandler(signalUnderTest, (ProcessSignal s) {
expect(s, signalUnderTest);
expect(first, isTrue);
completer.complete();
});
controller.add(mockSignal);
await completer.future;
});
testUsingContext('signal handler error goes on error stream', () async {
signals.addHandler(signalUnderTest, (ProcessSignal s) {
throw 'Error';
});
final Completer<void> completer = Completer<void>();
final List<Object> errList = <Object>[];
final StreamSubscription<Object> errSub = signals.errors.listen((Object err) {
errList.add(err);
completer.complete();
});
controller.add(mockSignal);
await completer.future;
await errSub.cancel();
expect(errList, <Object>['Error']);
});
testUsingContext('removed signal handler does not run', () async {
final Object token = signals.addHandler(signalUnderTest, (ProcessSignal s) {
fail('Signal handler should have been removed.');
});
await signals.removeHandler(signalUnderTest, token);
final List<Object> errList = <Object>[];
final StreamSubscription<Object> errSub = signals.errors.listen((Object err) {
errList.add(err);
});
controller.add(mockSignal);
await errSub.cancel();
expect(errList, isEmpty);
});
testUsingContext('non-removed signal handler still runs', () async {
final Completer<void> completer = Completer<void>();
signals.addHandler(signalUnderTest, (ProcessSignal s) {
expect(s, signalUnderTest);
completer.complete();
});
final Object token = signals.addHandler(signalUnderTest, (ProcessSignal s) {
fail('Signal handler should have been removed.');
});
await signals.removeHandler(signalUnderTest, token);
final List<Object> errList = <Object>[];
final StreamSubscription<Object> errSub = signals.errors.listen((Object err) {
errList.add(err);
});
controller.add(mockSignal);
await completer.future;
await errSub.cancel();
expect(errList, isEmpty);
});
testUsingContext('only handlers for the correct signal run', () async {
final MockIoProcessSignal mockSignal2 = MockIoProcessSignal();
final StreamController<io.ProcessSignal> controller2 = StreamController<io.ProcessSignal>();
final ProcessSignal otherSignal = ProcessSignal(mockSignal2);
when(mockSignal2.watch()).thenAnswer((Invocation invocation) => controller2.stream);
final Completer<void> completer = Completer<void>();
signals.addHandler(signalUnderTest, (ProcessSignal s) {
expect(s, signalUnderTest);
completer.complete();
});
signals.addHandler(otherSignal, (ProcessSignal s) {
fail('Wrong signal!.');
});
final List<Object> errList = <Object>[];
final StreamSubscription<Object> errSub = signals.errors.listen((Object err) {
errList.add(err);
});
controller.add(mockSignal);
await completer.future;
await errSub.cancel();
expect(errList, isEmpty);
});
});
}
class MockIoProcessSignal extends Mock implements io.ProcessSignal {}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment