[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);
+}
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index a065fa6..1e93925 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/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 @@
       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 bec7601..5191a1f 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/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 @@
 
   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 0000000..444ebfd
--- /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<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 {}