| // 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 '../globals.dart' as globals; | |
| import 'async_guard.dart'; | |
| import 'io.dart'; | |
| import 'process.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, | |
| ShutdownHooks? shutdownHooks, | |
| }) => LocalSignals._(exitSignals, shutdownHooks: shutdownHooks); | |
| // The default list of signals that should cause the process to exit. | |
| static const 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, {ShutdownHooks? shutdownHooks}) | |
| : _shutdownHooks = shutdownHooks ?? globals.shutdownHooks; | |
| static LocalSignals instance = LocalSignals._(Signals.defaultExitSignals); | |
| final List<ProcessSignal> exitSignals; | |
| final ShutdownHooks _shutdownHooks; | |
| // A table mapping (signal, token) -> signal handler. | |
| final _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 _handlersList = <ProcessSignal, List<SignalHandler>>{}; | |
| // A table mapping (signal) -> low-level signal event stream. | |
| final _streamSubscriptions = <ProcessSignal, StreamSubscription<ProcessSignal>>{}; | |
| // The stream controller for errors coming from signal handlers. | |
| final _errorStreamController = StreamController<Object>.broadcast(); | |
| @override | |
| Stream<Object> get errors => _errorStreamController.stream; | |
| @override | |
| Object addHandler(ProcessSignal signal, SignalHandler handler) { | |
| final 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[signal]!.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 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)) { | |
| await exitWithHooks(0, shutdownHooks: _shutdownHooks); | |
| } | |
| } | |
| bool _shouldExitFor(ProcessSignal signal) => exitSignals.contains(signal); | |
| } |