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 0000000000..b5b403bf22 --- /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 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(); + + /// 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 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 get errors; +} + +class _DefaultSignals implements Signals { + _DefaultSignals._(); + + // A table mapping (signal, token) -> signal handler. + final Map> _handlersTable = + >{}; + + // A table mapping (signal) -> signal handler list. The list is in the order + // that the signal handlers should be run. + final Map> _handlersList = + >{}; + + // A table mapping (signal) -> low-level signal event stream. + final Map> _streamSubscriptions = + >{}; + + // The stream controller for errors coming from signal handlers. + final StreamController _errorStreamController = StreamController.broadcast(); + + @override + Stream get errors => _errorStreamController.stream; + + @override + Object addHandler(ProcessSignal signal, SignalHandler handler) { + final Object token = Object(); + _handlersTable.putIfAbsent(signal, () => {}); + _handlersTable[signal][token] = handler; + + _handlersList.putIfAbsent(signal, () => []); + _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 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 _handleSignal(ProcessSignal s) async { + for (SignalHandler handler in _handlersList[s]) { + try { + await asyncGuard(() => 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 _exitingSignals = [ + ProcessSignal.SIGTERM, + ProcessSignal.SIGINT, + ProcessSignal.SIGKILL, + ]; + + bool _shouldExitFor(ProcessSignal signal) => _exitingSignals.contains(signal); +} diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index a065fa6a16..1e939257c1 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -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 runInContext( OperatingSystemUtils: () => OperatingSystemUtils(), ProcessInfo: () => ProcessInfo(), ProcessUtils: () => ProcessUtils(), + Signals: () => Signals(), SimControl: () => SimControl(), Stdio: () => const Stdio(), SystemClock: () => const SystemClock(), diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index bec7601e1f..5191a1f326 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -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. diff --git a/packages/flutter_tools/test/general.shard/base/signals_test.dart b/packages/flutter_tools/test/general.shard/base/signals_test.dart new file mode 100644 index 0000000000..444ebfdbed --- /dev/null +++ b/packages/flutter_tools/test/general.shard/base/signals_test.dart @@ -0,0 +1,148 @@ +// 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 controller; + + setUp(() { + mockSignal = MockIoProcessSignal(); + signalUnderTest = ProcessSignal(mockSignal); + controller = StreamController(); + when(mockSignal.watch()).thenAnswer((Invocation invocation) => controller.stream); + }); + + testUsingContext('signal handler runs', () async { + final Completer completer = Completer(); + 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 completer = Completer(); + + 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 completer = Completer(); + final List errList = []; + final StreamSubscription errSub = signals.errors.listen((Object err) { + errList.add(err); + completer.complete(); + }); + + controller.add(mockSignal); + await completer.future; + await errSub.cancel(); + expect(errList, ['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 errList = []; + final StreamSubscription 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 completer = Completer(); + 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 errList = []; + final StreamSubscription 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 controller2 = StreamController(); + final ProcessSignal otherSignal = ProcessSignal(mockSignal2); + + when(mockSignal2.watch()).thenAnswer((Invocation invocation) => controller2.stream); + + final Completer completer = Completer(); + signals.addHandler(signalUnderTest, (ProcessSignal s) { + expect(s, signalUnderTest); + completer.complete(); + }); + + signals.addHandler(otherSignal, (ProcessSignal s) { + fail('Wrong signal!.'); + }); + + final List errList = []; + final StreamSubscription 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 {}