blob: 9185762cdfe5499c1d55314c43bb2b2e9653fb0b [file] [log] [blame]
// Copyright 2014 The Flutter 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 'package:meta/meta.dart';
import 'async_guard.dart';
import 'io.dart';
typedef SignalHandler = FutureOr<void> Function(ProcessSignal signal);
/// A class that manages signal handlers.
///
/// Signal handlers are run in the order that they were added.
abstract class Signals {
@visibleForTesting
factory Signals.test({
List<ProcessSignal> exitSignals = defaultExitSignals,
}) => LocalSignals._(exitSignals);
// The default list of signals that should cause the process to exit.
static const List<ProcessSignal> defaultExitSignals = <ProcessSignal>[
ProcessSignal.sigterm,
ProcessSignal.sigint,
];
/// 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;
}
/// A class that manages the real dart:io signal handlers.
///
/// We use a singleton instance of this class to ensure that all handlers for
/// fatal signals run before this class calls exit().
class LocalSignals implements Signals {
LocalSignals._(this.exitSignals);
static LocalSignals instance = LocalSignals._(
Signals.defaultExitSignals,
);
final List<ProcessSignal> exitSignals;
// 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,
onError: (Object e) {
_handlersTable[signal]?.remove(token);
_handlersList[signal]?.remove(handler);
},
);
}
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]!.remove(token);
if (handler == null) {
return false;
}
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 {
final List<SignalHandler>? handlers = _handlersList[s];
if (handlers != null) {
final List<SignalHandler> handlersCopy = handlers.toList();
for (final SignalHandler handler in handlersCopy) {
try {
await asyncGuard<void>(() async => handler(s));
} on Exception 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);
}
}
bool _shouldExitFor(ProcessSignal signal) => exitSignals.contains(signal);
}