[flutter_tools] Allows adding multiple signal handlers (#41304)
diff --git a/packages/flutter_tools/lib/src/base/signals.dart b/packages/flutter_tools/lib/src/base/signals.dart new file mode 100644 index 0000000..b5b403b --- /dev/null +++ b/packages/flutter_tools/lib/src/base/signals.dart
@@ -0,0 +1,130 @@ +// 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); +}