Refactor signal and command line handler from resident runner (#35406)

diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart
index ff3661a..ef6eb69 100644
--- a/packages/flutter_tools/lib/src/android/android_device.dart
+++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -437,8 +437,8 @@
     DebuggingOptions debuggingOptions,
     Map<String, dynamic> platformArgs,
     bool prebuiltApplication = false,
-    bool usesTerminalUi = true,
     bool ipv6 = false,
+    bool usesTerminalUi = true,
   }) async {
     if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion())
       return LaunchResult.failed();
diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart
index e06d1c9..b7a477d 100644
--- a/packages/flutter_tools/lib/src/commands/attach.dart
+++ b/packages/flutter_tools/lib/src/commands/attach.dart
@@ -277,7 +277,7 @@
             target: targetFile,
             debuggingOptions: debuggingOptions,
             packagesFilePath: globalResults['packages'],
-            usesTerminalUI: daemon == null,
+            usesTerminalUi: daemon == null,
             projectRootPath: argResults['project-root'],
             dillOutputPath: argResults['output-dill'],
             ipv6: usesIpv6,
@@ -312,7 +312,15 @@
         result = await app.runner.waitForAppToFinish();
         assert(result != null);
       } else {
-        result = await runner.attach();
+        final Completer<void> onAppStart = Completer<void>.sync();
+        unawaited(onAppStart.future.whenComplete(() {
+          TerminalHandler(runner)
+            ..setupTerminal()
+            ..registerSignalHandlers();
+        }));
+        result = await runner.attach(
+          appStartedCompleter: onAppStart,
+        );
         assert(result != null);
       }
       if (result != 0) {
@@ -350,7 +358,7 @@
     List<FlutterDevice> devices, {
     String target,
     DebuggingOptions debuggingOptions,
-    bool usesTerminalUI = true,
+    bool usesTerminalUi = true,
     bool benchmarkMode = false,
     File applicationBinary,
     bool hostIsIde = false,
@@ -364,7 +372,7 @@
     devices,
     target: target,
     debuggingOptions: debuggingOptions,
-    usesTerminalUI: usesTerminalUI,
+    usesTerminalUi: usesTerminalUi,
     benchmarkMode: benchmarkMode,
     applicationBinary: applicationBinary,
     hostIsIde: hostIsIde,
diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart
index dff4b34..a441ecf 100644
--- a/packages/flutter_tools/lib/src/commands/daemon.dart
+++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -419,7 +419,7 @@
         <FlutterDevice>[flutterDevice],
         target: target,
         debuggingOptions: options,
-        usesTerminalUI: false,
+        usesTerminalUi: false,
         applicationBinary: applicationBinary,
         projectRootPath: projectRootPath,
         packagesFilePath: packagesFilePath,
@@ -432,8 +432,8 @@
         <FlutterDevice>[flutterDevice],
         target: target,
         debuggingOptions: options,
-        usesTerminalUI: false,
         applicationBinary: applicationBinary,
+        usesTerminalUi: false,
         ipv6: ipv6,
       );
     }
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index aeabe1f..35251f2 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -450,8 +450,8 @@
         applicationBinary: applicationBinaryPath == null
             ? null
             : fs.file(applicationBinaryPath),
-        stayResident: stayResident,
         ipv6: ipv6,
+        stayResident: stayResident,
       );
     }
 
@@ -463,7 +463,14 @@
     final Completer<void> appStartedTimeRecorder = Completer<void>.sync();
     // This callback can't throw.
     unawaited(appStartedTimeRecorder.future.then<void>(
-      (_) { appStartedTime = systemClock.now(); }
+      (_) {
+        appStartedTime = systemClock.now();
+        if (stayResident) {
+          TerminalHandler(runner)
+            ..setupTerminal()
+            ..registerSignalHandlers();
+        }
+      }
     ));
 
     final int result = await runner.run(
@@ -471,8 +478,9 @@
       route: route,
       shouldBuild: !runningWithPrebuiltApplication && argResults['build'],
     );
-    if (result != 0)
+    if (result != 0) {
       throwToolExit(null, exitCode: result);
+    }
     return FlutterCommandResult(
       ExitStatus.success,
       timingLabelParts: <String>[
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index 1bda148..5865744 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -406,8 +406,8 @@
     DebuggingOptions debuggingOptions,
     Map<String, dynamic> platformArgs,
     bool prebuiltApplication = false,
-    bool usesTerminalUi = true,
     bool ipv6 = false,
+    bool usesTerminalUi = true,
   });
 
   /// Whether this device implements support for hot reload.
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index 2cb9109..94ab882 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -115,7 +115,7 @@
   /// expressions requested during debugging of the application.
   /// This ensures that the reload process follows the normal orchestration of
   /// the Flutter Tools and not just the VM internal service.
-  Future<void> _connect({
+  Future<void> connect({
     ReloadSources reloadSources,
     Restart restart,
     CompileExpression compileExpression,
@@ -375,7 +375,7 @@
       platformArgs: platformArgs,
       route: route,
       prebuiltApplication: prebuiltMode,
-      usesTerminalUi: hotRunner.usesTerminalUI,
+      usesTerminalUi: hotRunner.usesTerminalUi,
       ipv6: hotRunner.ipv6,
     );
 
@@ -437,7 +437,7 @@
       platformArgs: platformArgs,
       route: route,
       prebuiltApplication: prebuiltMode,
-      usesTerminalUi: coldRunner.usesTerminalUI,
+      usesTerminalUi: coldRunner.usesTerminalUi,
       ipv6: coldRunner.ipv6,
     );
 
@@ -509,11 +509,12 @@
     this.flutterDevices, {
     this.target,
     this.debuggingOptions,
-    this.usesTerminalUI = true,
     String projectRootPath,
     String packagesFilePath,
-    this.stayResident,
     this.ipv6,
+    this.usesTerminalUi = true,
+    this.stayResident = true,
+    this.hotMode = true,
   }) {
     _mainPath = findMainDartFile(target);
     _projectRootPath = projectRootPath ?? fs.currentDirectory.path;
@@ -525,11 +526,12 @@
   final List<FlutterDevice> flutterDevices;
   final String target;
   final DebuggingOptions debuggingOptions;
-  final bool usesTerminalUI;
+  final bool usesTerminalUi;
   final bool stayResident;
   final bool ipv6;
   final Completer<int> _finished = Completer<int>();
   bool _exited = false;
+  bool hotMode ;
   String _packagesFilePath;
   String get packagesFilePath => _packagesFilePath;
   String _projectRootPath;
@@ -601,68 +603,68 @@
     await Future.wait(futures);
   }
 
-  Future<void> _debugDumpApp() async {
+  Future<void> debugDumpApp() async {
     await refreshViews();
     for (FlutterDevice device in flutterDevices)
       await device.debugDumpApp();
   }
 
-  Future<void> _debugDumpRenderTree() async {
+  Future<void> debugDumpRenderTree() async {
     await refreshViews();
     for (FlutterDevice device in flutterDevices)
       await device.debugDumpRenderTree();
   }
 
-  Future<void> _debugDumpLayerTree() async {
+  Future<void> debugDumpLayerTree() async {
     await refreshViews();
     for (FlutterDevice device in flutterDevices)
       await device.debugDumpLayerTree();
   }
 
-  Future<void> _debugDumpSemanticsTreeInTraversalOrder() async {
+  Future<void> debugDumpSemanticsTreeInTraversalOrder() async {
     await refreshViews();
     for (FlutterDevice device in flutterDevices)
       await device.debugDumpSemanticsTreeInTraversalOrder();
   }
 
-  Future<void> _debugDumpSemanticsTreeInInverseHitTestOrder() async {
+  Future<void> debugDumpSemanticsTreeInInverseHitTestOrder() async {
     await refreshViews();
     for (FlutterDevice device in flutterDevices)
       await device.debugDumpSemanticsTreeInInverseHitTestOrder();
   }
 
-  Future<void> _debugToggleDebugPaintSizeEnabled() async {
+  Future<void> debugToggleDebugPaintSizeEnabled() async {
     await refreshViews();
     for (FlutterDevice device in flutterDevices)
       await device.toggleDebugPaintSizeEnabled();
   }
 
-  Future<void> _debugToggleDebugCheckElevationsEnabled() async {
+  Future<void> debugToggleDebugCheckElevationsEnabled() async {
     await refreshViews();
     for (FlutterDevice device in flutterDevices)
       await device.toggleDebugCheckElevationsEnabled();
   }
 
-  Future<void> _debugTogglePerformanceOverlayOverride() async {
+  Future<void> debugTogglePerformanceOverlayOverride() async {
     await refreshViews();
     for (FlutterDevice device in flutterDevices)
       await device.debugTogglePerformanceOverlayOverride();
   }
 
-  Future<void> _debugToggleWidgetInspector() async {
+  Future<void> debugToggleWidgetInspector() async {
     await refreshViews();
     for (FlutterDevice device in flutterDevices)
       await device.toggleWidgetInspector();
   }
 
-  Future<void> _debugToggleProfileWidgetBuilds() async {
+  Future<void> debugToggleProfileWidgetBuilds() async {
     await refreshViews();
     for (FlutterDevice device in flutterDevices) {
       await device.toggleProfileWidgetBuilds();
     }
   }
 
-  Future<void> _screenshot(FlutterDevice device) async {
+  Future<void> screenshot(FlutterDevice device) async {
     final Status status = logger.startProgress('Taking screenshot for ${device.device.name}...', timeout: timeoutConfiguration.fastOperation);
     final File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png');
     try {
@@ -700,7 +702,7 @@
     }
   }
 
-  Future<void> _debugTogglePlatform() async {
+  Future<void> debugTogglePlatform() async {
     await refreshViews();
     final String from = await flutterDevices[0].views[0].uiIsolate.flutterPlatformOverride();
     String to;
@@ -709,39 +711,6 @@
     printStatus('Switched operating system to $to');
   }
 
-  void registerSignalHandlers() {
-    assert(stayResident);
-    io.ProcessSignal.SIGINT.watch().listen(_cleanUpAndExit);
-    io.ProcessSignal.SIGTERM.watch().listen(_cleanUpAndExit);
-    if (!supportsServiceProtocol || !supportsRestart)
-      return;
-    io.ProcessSignal.SIGUSR1.watch().listen(_handleSignal);
-    io.ProcessSignal.SIGUSR2.watch().listen(_handleSignal);
-  }
-
-  Future<void> _cleanUpAndExit(io.ProcessSignal signal) async {
-    _resetTerminal();
-    await cleanupAfterSignal();
-    io.exit(0);
-  }
-
-  bool _processingUserRequest = false;
-  Future<void> _handleSignal(io.ProcessSignal signal) async {
-    if (_processingUserRequest) {
-      printTrace('Ignoring signal: "$signal" because we are busy.');
-      return;
-    }
-    _processingUserRequest = true;
-
-    final bool fullRestart = signal == io.ProcessSignal.SIGUSR2;
-
-    try {
-      await restart(fullRestart: fullRestart);
-    } finally {
-      _processingUserRequest = false;
-    }
-  }
-
   Future<void> stopEchoingDeviceLog() async {
     await Future.wait<void>(
       flutterDevices.map<Future<void>>((FlutterDevice device) => device.stopEchoingDeviceLog())
@@ -764,7 +733,7 @@
 
     bool viewFound = false;
     for (FlutterDevice device in flutterDevices) {
-      await device._connect(
+      await device.connect(
         reloadSources: reloadSources,
         restart: restart,
         compileExpression: compileExpression,
@@ -805,125 +774,6 @@
     return Future<void>.error(error, stack);
   }
 
-  /// Returns [true] if the input has been handled by this function.
-  Future<bool> _commonTerminalInputHandler(String character) async {
-
-    printStatus(''); // the key the user tapped might be on this line
-    switch(character) {
-      case 'a':
-        if (supportsServiceProtocol) {
-          await _debugToggleProfileWidgetBuilds();
-          return true;
-        }
-        return false;
-      case 'd':
-      case 'D':
-        await detach();
-        return true;
-      case 'h':
-      case 'H':
-      case '?':
-        // help
-        printHelp(details: true);
-        return true;
-      case 'i':
-      case 'I':
-        if (supportsServiceProtocol) {
-          await _debugToggleWidgetInspector();
-          return true;
-        }
-        return false;
-      case 'L':
-        if (supportsServiceProtocol) {
-          await _debugDumpLayerTree();
-          return true;
-        }
-        return false;
-      case 'o':
-      case 'O':
-        if (supportsServiceProtocol && isRunningDebug) {
-          await _debugTogglePlatform();
-          return true;
-        }
-        return false;
-      case 'p':
-        if (supportsServiceProtocol && isRunningDebug) {
-          await _debugToggleDebugPaintSizeEnabled();
-          return true;
-        }
-        return false;
-      case 'P':
-        if (supportsServiceProtocol) {
-          await _debugTogglePerformanceOverlayOverride();
-          return true;
-        }
-        return false;
-      case 'q':
-      case 'Q':
-        // exit
-        await exit();
-        return true;
-      case 's':
-        for (FlutterDevice device in flutterDevices) {
-          if (device.device.supportsScreenshot)
-            await _screenshot(device);
-        }
-        return true;
-      case 'S':
-        if (supportsServiceProtocol) {
-          await _debugDumpSemanticsTreeInTraversalOrder();
-          return true;
-        }
-        return false;
-      case 't':
-      case 'T':
-        if (supportsServiceProtocol) {
-          await _debugDumpRenderTree();
-          return true;
-        }
-        return false;
-      case 'U':
-        if (supportsServiceProtocol) {
-          await _debugDumpSemanticsTreeInInverseHitTestOrder();
-          return true;
-        }
-        return false;
-      case 'w':
-      case 'W':
-        if (supportsServiceProtocol) {
-          await _debugDumpApp();
-          return true;
-        }
-        return false;
-      case 'z':
-      case 'Z':
-        await _debugToggleDebugCheckElevationsEnabled();
-        return true;
-    }
-
-    return false;
-  }
-
-  Future<void> processTerminalInput(String command) async {
-    // When terminal doesn't support line mode, '\n' can sneak into the input.
-    command = command.trim();
-    if (_processingUserRequest) {
-      printTrace('Ignoring terminal input: "$command" because we are busy.');
-      return;
-    }
-    _processingUserRequest = true;
-    try {
-      final bool handled = await _commonTerminalInputHandler(command);
-      if (!handled)
-        await handleTerminalCommand(command);
-    } catch (error, st) {
-      printError('$error\n$st');
-      await _cleanUpAndExit(null);
-    } finally {
-      _processingUserRequest = false;
-    }
-  }
-
   void _serviceDisconnected() {
     if (_exited) {
       // User requested the application exit.
@@ -932,7 +782,6 @@
     if (_finished.isCompleted)
       return;
     printStatus('Lost connection to device.');
-    _resetTerminal();
     _finished.complete(0);
   }
 
@@ -940,27 +789,9 @@
     if (_finished.isCompleted)
       return;
     printStatus('Application finished.');
-    _resetTerminal();
     _finished.complete(0);
   }
 
-  void _resetTerminal() {
-    if (usesTerminalUI)
-      terminal.singleCharMode = false;
-  }
-
-  void setupTerminal() {
-    assert(stayResident);
-    if (usesTerminalUI) {
-      if (!logger.quiet) {
-        printStatus('');
-        printHelp(details: false);
-      }
-      terminal.singleCharMode = true;
-      terminal.keystrokes.listen(processTerminalInput);
-    }
-  }
-
   Future<int> waitForAppToFinish() async {
     final int exitCode = await _finished.future;
     assert(exitCode != null);
@@ -1004,10 +835,38 @@
 
   /// Called when a signal has requested we exit.
   Future<void> cleanupAfterSignal();
+
   /// Called right before we exit.
   Future<void> cleanupAtFinish();
+
   /// Called when the runner should handle a terminal command.
-  Future<void> handleTerminalCommand(String code);
+  Future<void> handleTerminalCommand(String code) async {
+    switch (code) {
+      case 'r':
+        final OperationResult result = await restart(fullRestart: false);
+        if (!result.isOk) {
+          printStatus('Try again after fixing the above error(s).', emphasis: true);
+        }
+        return;
+      case 'R':
+        // If hot restart is not supported for all devices, ignore the command.
+        if (!canHotRestart) {
+          return;
+        }
+        final OperationResult result = await restart(fullRestart: true);
+        if (!result.isOk) {
+          printStatus('Try again after fixing the above error(s).', emphasis: true);
+        }
+        return;
+      case 'l':
+      case 'L':
+        final List<FlutterView> views = flutterDevices.expand((FlutterDevice d) => d.views).toList();
+        printStatus('Connected ${pluralize('view', views.length)}:');
+        for (FlutterView v in views) {
+          printStatus('${v.uiIsolate.name} (${v.uiIsolate.id})', indent: 2);
+        }
+    }
+  }
 }
 
 class OperationResult {
@@ -1051,6 +910,176 @@
   }
 }
 
+/// Redirects terminal commands to the correct resident runner methods.
+class TerminalHandler {
+  TerminalHandler(this.residentRunner);
+
+  final ResidentRunner residentRunner;
+  bool _processingUserRequest = false;
+  StreamSubscription<void> subscription;
+
+  void setupTerminal() {
+    if (!logger.quiet) {
+      printStatus('');
+      residentRunner.printHelp(details: false);
+    }
+    terminal.singleCharMode = true;
+    subscription = terminal.keystrokes.listen(processTerminalInput);
+  }
+
+  void registerSignalHandlers() {
+    assert(residentRunner.stayResident);
+    io.ProcessSignal.SIGINT.watch().listen(_cleanUpAndExit);
+    io.ProcessSignal.SIGTERM.watch().listen(_cleanUpAndExit);
+    if (!residentRunner.supportsServiceProtocol || !residentRunner.supportsRestart)
+      return;
+    io.ProcessSignal.SIGUSR1.watch().listen(_handleSignal);
+    io.ProcessSignal.SIGUSR2.watch().listen(_handleSignal);
+  }
+
+  /// Returns [true] if the input has been handled by this function.
+  Future<bool> _commonTerminalInputHandler(String character) async {
+    printStatus(''); // the key the user tapped might be on this line
+    switch(character) {
+      case 'a':
+        if (residentRunner.supportsServiceProtocol) {
+          await residentRunner.debugToggleProfileWidgetBuilds();
+          return true;
+        }
+        return false;
+      case 'd':
+      case 'D':
+        await residentRunner.detach();
+        return true;
+      case 'h':
+      case 'H':
+      case '?':
+        // help
+        residentRunner.printHelp(details: true);
+        return true;
+      case 'i':
+      case 'I':
+        if (residentRunner.supportsServiceProtocol) {
+          await residentRunner.debugToggleWidgetInspector();
+          return true;
+        }
+        return false;
+      case 'L':
+        if (residentRunner.supportsServiceProtocol) {
+          await residentRunner.debugDumpLayerTree();
+          return true;
+        }
+        return false;
+      case 'o':
+      case 'O':
+        if (residentRunner.supportsServiceProtocol && residentRunner.isRunningDebug) {
+          await residentRunner.debugTogglePlatform();
+          return true;
+        }
+        return false;
+      case 'p':
+        if (residentRunner.supportsServiceProtocol && residentRunner.isRunningDebug) {
+          await residentRunner.debugToggleDebugPaintSizeEnabled();
+          return true;
+        }
+        return false;
+      case 'P':
+        if (residentRunner.supportsServiceProtocol) {
+          await residentRunner.debugTogglePerformanceOverlayOverride();
+          return true;
+        }
+        return false;
+      case 'q':
+      case 'Q':
+        // exit
+        await residentRunner.exit();
+        return true;
+      case 's':
+        for (FlutterDevice device in residentRunner.flutterDevices) {
+          if (device.device.supportsScreenshot)
+            await residentRunner.screenshot(device);
+        }
+        return true;
+      case 'S':
+        if (residentRunner.supportsServiceProtocol) {
+          await residentRunner.debugDumpSemanticsTreeInTraversalOrder();
+          return true;
+        }
+        return false;
+      case 't':
+      case 'T':
+        if (residentRunner.supportsServiceProtocol) {
+          await residentRunner.debugDumpRenderTree();
+          return true;
+        }
+        return false;
+      case 'U':
+        if (residentRunner.supportsServiceProtocol) {
+          await residentRunner.debugDumpSemanticsTreeInInverseHitTestOrder();
+          return true;
+        }
+        return false;
+      case 'w':
+      case 'W':
+        if (residentRunner.supportsServiceProtocol) {
+          await residentRunner.debugDumpApp();
+          return true;
+        }
+        return false;
+      case 'z':
+      case 'Z':
+        await residentRunner.debugToggleDebugCheckElevationsEnabled();
+        return true;
+    }
+
+    return false;
+  }
+
+
+  Future<void> processTerminalInput(String command) async {
+    // When terminal doesn't support line mode, '\n' can sneak into the input.
+    command = command.trim();
+    if (_processingUserRequest) {
+      printTrace('Ignoring terminal input: "$command" because we are busy.');
+      return;
+    }
+    _processingUserRequest = true;
+    try {
+      final bool handled = await _commonTerminalInputHandler(command);
+      if (!handled)
+        await residentRunner.handleTerminalCommand(command);
+    } catch (error, st) {
+      printError('$error\n$st');
+      await _cleanUpAndExit(null);
+    } finally {
+      _processingUserRequest = false;
+    }
+  }
+
+  Future<void> _handleSignal(io.ProcessSignal signal) async {
+    if (_processingUserRequest) {
+      printTrace('Ignoring signal: "$signal" because we are busy.');
+      return;
+    }
+    _processingUserRequest = true;
+
+    final bool fullRestart = signal == io.ProcessSignal.SIGUSR2;
+
+    try {
+      await residentRunner.restart(fullRestart: fullRestart);
+    } finally {
+      _processingUserRequest = false;
+    }
+  }
+
+  Future<void> _cleanUpAndExit(io.ProcessSignal signal) async {
+    terminal.singleCharMode = false;
+    await subscription.cancel();
+    await residentRunner.cleanupAfterSignal();
+    io.exit(0);
+  }
+}
+
 class DebugConnectionInfo {
   DebugConnectionInfo({ this.httpUri, this.wsUri, this.baseUri });
 
diff --git a/packages/flutter_tools/lib/src/resident_web_runner.dart b/packages/flutter_tools/lib/src/resident_web_runner.dart
index 36c3d12..55dad48 100644
--- a/packages/flutter_tools/lib/src/resident_web_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_web_runner.dart
@@ -37,10 +37,10 @@
   }) : super(
           flutterDevices,
           target: target,
-          usesTerminalUI: true,
-          stayResident: true,
           debuggingOptions: debuggingOptions,
           ipv6: ipv6,
+          usesTerminalUi: true,
+          stayResident: true,
         );
 
   WebAssetServer _server;
@@ -54,7 +54,6 @@
       {Completer<DebugConnectionInfo> connectionInfoCompleter,
       Completer<void> appStartedCompleter}) async {
     connectionInfoCompleter?.complete(DebugConnectionInfo());
-    setupTerminal();
     final int result = await waitForAppToFinish();
     await cleanupAtFinish();
     return result;
diff --git a/packages/flutter_tools/lib/src/run_cold.dart b/packages/flutter_tools/lib/src/run_cold.dart
index b2f7214..ad91f56 100644
--- a/packages/flutter_tools/lib/src/run_cold.dart
+++ b/packages/flutter_tools/lib/src/run_cold.dart
@@ -19,16 +19,17 @@
     List<FlutterDevice> devices, {
     String target,
     DebuggingOptions debuggingOptions,
-    bool usesTerminalUI = true,
     this.traceStartup = false,
     this.awaitFirstFrameWhenTracing = true,
     this.applicationBinary,
-    bool stayResident = true,
     bool ipv6 = false,
+    bool usesTerminalUi = false,
+    bool stayResident = true,
   }) : super(devices,
              target: target,
              debuggingOptions: debuggingOptions,
-             usesTerminalUI: usesTerminalUI,
+             hotMode: false,
+             usesTerminalUi: usesTerminalUi,
              stayResident: stayResident,
              ipv6: ipv6);
 
@@ -104,9 +105,6 @@
         );
       }
       appFinished();
-    } else if (stayResident) {
-      setupTerminal();
-      registerSignalHandlers();
     }
 
     appStartedCompleter?.complete();
@@ -138,10 +136,6 @@
         printTrace('Connected to $view.');
       }
     }
-    if (stayResident) {
-      setupTerminal();
-      registerSignalHandlers();
-    }
     appStartedCompleter?.complete();
     if (stayResident) {
       return waitForAppToFinish();
@@ -151,9 +145,6 @@
   }
 
   @override
-  Future<void> handleTerminalCommand(String code) async { }
-
-  @override
   Future<void> cleanupAfterSignal() async {
     await stopEchoingDeviceLog();
     if (_didAttach) {
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index 767b4b5..c19057e 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -57,7 +57,7 @@
     List<FlutterDevice> devices, {
     String target,
     DebuggingOptions debuggingOptions,
-    bool usesTerminalUI = true,
+    bool usesTerminalUi = true,
     this.benchmarkMode = false,
     this.applicationBinary,
     this.hostIsIde = false,
@@ -69,10 +69,11 @@
   }) : super(devices,
              target: target,
              debuggingOptions: debuggingOptions,
-             usesTerminalUI: usesTerminalUI,
+             usesTerminalUi: usesTerminalUi,
              projectRootPath: projectRootPath,
              packagesFilePath: packagesFilePath,
              stayResident: stayResident,
+             hotMode: true,
              ipv6: ipv6);
 
   final bool benchmarkMode;
@@ -194,11 +195,6 @@
         printTrace('Connected to $view.');
     }
 
-    if (stayResident) {
-      setupTerminal();
-      registerSignalHandlers();
-    }
-
     appStartedCompleter?.complete();
 
     if (benchmarkMode) {
@@ -264,32 +260,6 @@
     );
   }
 
-  @override
-  Future<void> handleTerminalCommand(String code) async {
-    final String lower = code.toLowerCase();
-    if (lower == 'r') {
-      OperationResult result;
-      if (code == 'R') {
-        // If hot restart is not supported for all devices, ignore the command.
-        if (!canHotRestart) {
-          return;
-        }
-        result = await restart(fullRestart: true);
-      } else {
-        result = await restart(fullRestart: false);
-      }
-      if (!result.isOk) {
-        printStatus('Try again after fixing the above error(s).', emphasis: true);
-      }
-    } else if (lower == 'l') {
-      final List<FlutterView> views = flutterDevices.expand((FlutterDevice d) => d.views).toList();
-      printStatus('Connected ${pluralize('view', views.length)}:');
-      for (FlutterView v in views) {
-        printStatus('${v.uiIsolate.name} (${v.uiIsolate.id})', indent: 2);
-      }
-    }
-  }
-
   Future<List<Uri>> _initDevFS() async {
     final String fsName = fs.path.basename(projectRootPath);
     final List<Uri> devFSUris = <Uri>[];
diff --git a/packages/flutter_tools/test/commands/attach_test.dart b/packages/flutter_tools/test/commands/attach_test.dart
index 769f315..ecd9df9 100644
--- a/packages/flutter_tools/test/commands/attach_test.dart
+++ b/packages/flutter_tools/test/commands/attach_test.dart
@@ -24,15 +24,18 @@
 import '../src/mocks.dart';
 
 void main() {
-  final StreamLogger logger = StreamLogger();
   group('attach', () {
-    final FileSystem testFileSystem = MemoryFileSystem(
-      style: platform.isWindows ? FileSystemStyle.windows : FileSystemStyle
-          .posix,
-    );
+    StreamLogger logger;
+    FileSystem testFileSystem;
 
     setUp(() {
       Cache.disableLocking();
+      logger = StreamLogger();
+      testFileSystem = MemoryFileSystem(
+      style: platform.isWindows
+          ? FileSystemStyle.windows
+          : FileSystemStyle.posix,
+      );
       testFileSystem.directory('lib').createSync();
       testFileSystem.file(testFileSystem.path.join('lib', 'main.dart')).createSync();
     });
@@ -108,7 +111,8 @@
         const String outputDill = '/tmp/output.dill';
 
         final MockHotRunner mockHotRunner = MockHotRunner();
-        when(mockHotRunner.attach()).thenAnswer((_) async => 0);
+        when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter')))
+            .thenAnswer((_) async => 0);
 
         final MockHotRunnerFactory mockHotRunnerFactory = MockHotRunnerFactory();
         when(
@@ -119,7 +123,7 @@
             dillOutputPath: anyNamed('dillOutputPath'),
             debuggingOptions: anyNamed('debuggingOptions'),
             packagesFilePath: anyNamed('packagesFilePath'),
-            usesTerminalUI: anyNamed('usesTerminalUI'),
+            usesTerminalUi: anyNamed('usesTerminalUi'),
             flutterProject: anyNamed('flutterProject'),
             ipv6: false,
           ),
@@ -151,7 +155,7 @@
             dillOutputPath: outputDill,
             debuggingOptions: anyNamed('debuggingOptions'),
             packagesFilePath: anyNamed('packagesFilePath'),
-            usesTerminalUI: anyNamed('usesTerminalUI'),
+            usesTerminalUi: anyNamed('usesTerminalUi'),
             flutterProject: anyNamed('flutterProject'),
             ipv6: false,
           ),
@@ -219,14 +223,14 @@
         .thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
       when(portForwarder.unforward(any))
         .thenAnswer((_) async => null);
-      when(mockHotRunner.attach())
-        .thenAnswer((_) async => 0);
+      when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter')))
+          .thenAnswer((_) async => 0);
       when(mockHotRunnerFactory.build(
         any,
         target: anyNamed('target'),
         debuggingOptions: anyNamed('debuggingOptions'),
         packagesFilePath: anyNamed('packagesFilePath'),
-        usesTerminalUI: anyNamed('usesTerminalUI'),
+        usesTerminalUi: anyNamed('usesTerminalUi'),
         flutterProject: anyNamed('flutterProject'),
         ipv6: false,
       )).thenReturn(mockHotRunner);
@@ -256,7 +260,7 @@
         target: foo.path,
         debuggingOptions: anyNamed('debuggingOptions'),
         packagesFilePath: anyNamed('packagesFilePath'),
-        usesTerminalUI: anyNamed('usesTerminalUI'),
+        usesTerminalUi: anyNamed('usesTerminalUi'),
         flutterProject: anyNamed('flutterProject'),
         ipv6: false,
       )).called(1);
diff --git a/packages/flutter_tools/test/resident_runner_test.dart b/packages/flutter_tools/test/resident_runner_test.dart
index a4c96b3..024d08a 100644
--- a/packages/flutter_tools/test/resident_runner_test.dart
+++ b/packages/flutter_tools/test/resident_runner_test.dart
@@ -3,91 +3,114 @@
 // found in the LICENSE file.
 
 import 'dart:async';
+
 import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/devfs.dart';
 import 'package:flutter_tools/src/device.dart';
 import 'package:flutter_tools/src/resident_runner.dart';
+import 'package:flutter_tools/src/run_hot.dart';
+import 'package:flutter_tools/src/vmservice.dart';
 import 'package:mockito/mockito.dart';
 
 import 'src/common.dart';
-import 'src/context.dart';
-
-class TestRunner extends ResidentRunner {
-  TestRunner(List<FlutterDevice> devices)
-    : super(devices);
-
-  bool hasHelpBeenPrinted = false;
-  String receivedCommand;
-
-  @override
-  Future<void> cleanupAfterSignal() async { }
-
-  @override
-  Future<void> cleanupAtFinish() async { }
-
-  @override
-  Future<void> handleTerminalCommand(String code) async {
-    receivedCommand = code;
-  }
-
-  @override
-  void printHelp({ bool details }) {
-    hasHelpBeenPrinted = true;
-  }
-
-  @override
-  Future<int> run({
-    Completer<DebugConnectionInfo> connectionInfoCompleter,
-    Completer<void> appStartedCompleter,
-    String route,
-    bool shouldBuild = true,
-  }) async => null;
-
-  @override
-  Future<int> attach({
-    Completer<DebugConnectionInfo> connectionInfoCompleter,
-    Completer<void> appStartedCompleter,
-  }) async => null;
-}
+import 'src/testbed.dart';
 
 void main() {
-  TestRunner createTestRunner() {
-    // TODO(jacobr): make these tests run with `trackWidgetCreation: true` as
-    // well as the default flags.
-    return TestRunner(
-      <FlutterDevice>[FlutterDevice(MockDevice(), trackWidgetCreation: false, buildMode: BuildMode.debug)],
-    );
-  }
+  group('ResidentRunner', () {
+    final Uri testUri = Uri.parse('foo://bar');
+    Testbed testbed;
+    MockDevice mockDevice;
+    MockVMService mockVMService;
+    MockDevFS mockDevFS;
+    ResidentRunner residentRunner;
 
-  group('keyboard input handling', () {
-    testUsingContext('single help character', () async {
-      final TestRunner testRunner = createTestRunner();
-      expect(testRunner.hasHelpBeenPrinted, isFalse);
-      await testRunner.processTerminalInput('h');
-      expect(testRunner.hasHelpBeenPrinted, isTrue);
+    setUp(() {
+      testbed = Testbed(setup: () {
+        residentRunner = HotRunner(
+          <FlutterDevice>[
+            mockDevice,
+          ],
+          stayResident: false,
+          debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
+        );
+      });
+      mockDevice = MockDevice();
+      mockVMService = MockVMService();
+      mockDevFS = MockDevFS();
+      // DevFS Mocks
+      when(mockDevFS.lastCompiled).thenReturn(DateTime(2000));
+      when(mockDevFS.sources).thenReturn(<Uri>[]);
+      when(mockDevFS.destroy()).thenAnswer((Invocation invocation) async { });
+      // FlutterDevice Mocks.
+      when(mockDevice.updateDevFS(
+        // Intentionally provide empty list to match above mock.
+        invalidatedFiles: <Uri>[],
+        mainPath: anyNamed('mainPath'),
+        target: anyNamed('target'),
+        bundle: anyNamed('bundle'),
+        firstBuildTime: anyNamed('firstBuildTime'),
+        bundleFirstUpload: anyNamed('bundleFirstUpload'),
+        bundleDirty: anyNamed('bundleDirty'),
+        fullRestart: anyNamed('fullRestart'),
+        projectRootPath: anyNamed('projectRootPath'),
+        pathToReload: anyNamed('pathToReload'),
+      )).thenAnswer((Invocation invocation) async {
+        return UpdateFSReport(
+          success: true,
+          syncedBytes: 0,
+          invalidatedSourcesCount: 0,
+        );
+      });
+      when(mockDevice.devFS).thenReturn(mockDevFS);
+      when(mockDevice.views).thenReturn(<FlutterView>[
+        MockFlutterView(),
+      ]);
+      when(mockDevice.stopEchoingDeviceLog()).thenAnswer((Invocation invocation) async { });
+      when(mockDevice.observatoryUris).thenReturn(<Uri>[
+        testUri,
+      ]);
+      when(mockDevice.connect(
+        reloadSources: anyNamed('reloadSources'),
+        restart: anyNamed('restart'),
+        compileExpression: anyNamed('compileExpression')
+      )).thenAnswer((Invocation invocation) async { });
+      when(mockDevice.setupDevFS(any, any, packagesFilePath: anyNamed('packagesFilePath')))
+        .thenAnswer((Invocation invocation) async {
+          return testUri;
+        });
+      when(mockDevice.vmServices).thenReturn(<VMService>[
+        mockVMService,
+      ]);
+      when(mockDevice.refreshViews()).thenAnswer((Invocation invocation) async { });
+      // VMService mocks.
+      when(mockVMService.wsAddress).thenReturn(testUri);
+      when(mockVMService.done).thenAnswer((Invocation invocation) {
+        final Completer<void> result = Completer<void>.sync();
+        return result.future;
+      });
     });
-    testUsingContext('help character surrounded with newlines', () async {
-      final TestRunner testRunner = createTestRunner();
-      expect(testRunner.hasHelpBeenPrinted, isFalse);
-      await testRunner.processTerminalInput('\nh\n');
-      expect(testRunner.hasHelpBeenPrinted, isTrue);
-    });
-    testUsingContext('reload character with trailing newline', () async {
-      final TestRunner testRunner = createTestRunner();
-      expect(testRunner.receivedCommand, isNull);
-      await testRunner.processTerminalInput('r\n');
-      expect(testRunner.receivedCommand, equals('r'));
-    });
-    testUsingContext('newlines', () async {
-      final TestRunner testRunner = createTestRunner();
-      expect(testRunner.receivedCommand, isNull);
-      await testRunner.processTerminalInput('\n\n');
-      expect(testRunner.receivedCommand, equals(''));
-    });
+
+    test('Can attach to device successfully', () => testbed.run(() async {
+      final Completer<DebugConnectionInfo> onConnectionInfo = Completer<DebugConnectionInfo>.sync();
+      final Completer<void> onAppStart = Completer<void>.sync();
+      final Future<int> result = residentRunner.attach(
+        appStartedCompleter: onAppStart,
+        connectionInfoCompleter: onConnectionInfo,
+      );
+      final Future<DebugConnectionInfo> connectionInfo = onConnectionInfo.future;
+
+      expect(await result, 0);
+
+      verify(mockDevice.initLogReader()).called(1);
+
+      expect(onConnectionInfo.isCompleted, true);
+      expect((await connectionInfo).baseUri, 'foo://bar');
+      expect(onAppStart.isCompleted, true);
+    }));
   });
 }
 
-class MockDevice extends Mock implements Device {
-  MockDevice() {
-    when(isSupported()).thenReturn(true);
-  }
-}
+class MockDevice extends Mock implements FlutterDevice {}
+class MockFlutterView extends Mock implements FlutterView {}
+class MockVMService extends Mock implements VMService {}
+class MockDevFS extends Mock implements DevFS {}
diff --git a/packages/flutter_tools/test/terminal_handler_test.dart b/packages/flutter_tools/test/terminal_handler_test.dart
new file mode 100644
index 0000000..088c712
--- /dev/null
+++ b/packages/flutter_tools/test/terminal_handler_test.dart
@@ -0,0 +1,332 @@
+// Copyright 2017 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 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/resident_runner.dart';
+import 'package:mockito/mockito.dart';
+
+import 'src/common.dart';
+import 'src/context.dart';
+
+void main() {
+  TestRunner createTestRunner() {
+    // TODO(jacobr): make these tests run with `trackWidgetCreation: true` as
+    // well as the default flags.
+    return TestRunner(
+      <FlutterDevice>[FlutterDevice(MockDevice(), trackWidgetCreation: false, buildMode: BuildMode.debug)],
+    );
+  }
+
+  group('keyboard input handling', () {
+    testUsingContext('single help character', () async {
+      final TestRunner testRunner = createTestRunner();
+      final TerminalHandler terminalHandler = TerminalHandler(testRunner);
+      expect(testRunner.hasHelpBeenPrinted, isFalse);
+      await terminalHandler.processTerminalInput('h');
+      expect(testRunner.hasHelpBeenPrinted, isTrue);
+    });
+
+    testUsingContext('help character surrounded with newlines', () async {
+      final TestRunner testRunner = createTestRunner();
+      final TerminalHandler terminalHandler = TerminalHandler(testRunner);
+      expect(testRunner.hasHelpBeenPrinted, isFalse);
+      await terminalHandler.processTerminalInput('\nh\n');
+      expect(testRunner.hasHelpBeenPrinted, isTrue);
+    });
+
+    testUsingContext('reload character with trailing newline', () async {
+      final TestRunner testRunner = createTestRunner();
+      final TerminalHandler terminalHandler = TerminalHandler(testRunner);
+      expect(testRunner.receivedCommand, isNull);
+      await terminalHandler.processTerminalInput('r\n');
+      expect(testRunner.receivedCommand, equals('r'));
+    });
+
+    testUsingContext('newlines', () async {
+      final TestRunner testRunner = createTestRunner();
+      final TerminalHandler terminalHandler = TerminalHandler(testRunner);
+      expect(testRunner.receivedCommand, isNull);
+      await terminalHandler.processTerminalInput('\n\n');
+      expect(testRunner.receivedCommand, equals(''));
+    });
+  });
+
+  group('keycode verification, brought to you by the letter r', () {
+    MockResidentRunner mockResidentRunner;
+    TerminalHandler terminalHandler;
+
+    setUp(() {
+      mockResidentRunner = MockResidentRunner();
+      terminalHandler = TerminalHandler(mockResidentRunner);
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(true);
+      when(mockResidentRunner.handleTerminalCommand(any)).thenReturn(null);
+    });
+
+    testUsingContext('a - debugToggleProfileWidgetBuilds with service protocol', () async {
+      await terminalHandler.processTerminalInput('a');
+
+      verify(mockResidentRunner.debugToggleProfileWidgetBuilds()).called(1);
+    });
+
+    testUsingContext('a - debugToggleProfileWidgetBuilds without service protocol', () async {
+       when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      await terminalHandler.processTerminalInput('a');
+
+      verifyNever(mockResidentRunner.debugToggleProfileWidgetBuilds());
+    });
+
+
+    testUsingContext('a - debugToggleProfileWidgetBuilds', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(true);
+      await terminalHandler.processTerminalInput('a');
+
+      verify(mockResidentRunner.debugToggleProfileWidgetBuilds()).called(1);
+    });
+
+    testUsingContext('d,D - detach', () async {
+      await terminalHandler.processTerminalInput('d');
+      await terminalHandler.processTerminalInput('D');
+
+      verify(mockResidentRunner.detach()).called(2);
+    });
+
+    testUsingContext('h,H,? - printHelp', () async {
+      await terminalHandler.processTerminalInput('h');
+      await terminalHandler.processTerminalInput('H');
+      await terminalHandler.processTerminalInput('?');
+
+      verify(mockResidentRunner.printHelp(details: true)).called(3);
+    });
+
+    testUsingContext('i, I - debugToggleWidgetInspector with service protocol', () async {
+      await terminalHandler.processTerminalInput('i');
+      await terminalHandler.processTerminalInput('I');
+
+      verify(mockResidentRunner.debugToggleWidgetInspector()).called(2);
+    });
+
+    testUsingContext('i, I - debugToggleWidgetInspector without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      await terminalHandler.processTerminalInput('i');
+      await terminalHandler.processTerminalInput('I');
+
+      verifyNever(mockResidentRunner.debugToggleWidgetInspector());
+    });
+
+    testUsingContext('L - debugDumpLayerTree with service protocol', () async {
+      await terminalHandler.processTerminalInput('L');
+
+      verify(mockResidentRunner.debugDumpLayerTree()).called(1);
+    });
+
+    testUsingContext('L - debugDumpLayerTree without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      await terminalHandler.processTerminalInput('L');
+
+      verifyNever(mockResidentRunner.debugDumpLayerTree());
+    });
+
+    testUsingContext('o,O - debugTogglePlatform with service protocol and debug mode', () async {
+      when(mockResidentRunner.isRunningDebug).thenReturn(true);
+      await terminalHandler.processTerminalInput('o');
+      await terminalHandler.processTerminalInput('O');
+
+      verify(mockResidentRunner.debugTogglePlatform()).called(2);
+    });
+
+    testUsingContext('o,O - debugTogglePlatform without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      when(mockResidentRunner.isRunningDebug).thenReturn(true);
+      await terminalHandler.processTerminalInput('o');
+      await terminalHandler.processTerminalInput('O');
+
+      verifyNever(mockResidentRunner.debugTogglePlatform());
+    });
+
+    testUsingContext('p - debugToggleDebugPaintSizeEnabled with service protocol and debug mode', () async {
+      when(mockResidentRunner.isRunningDebug).thenReturn(true);
+      await terminalHandler.processTerminalInput('p');
+
+      verify(mockResidentRunner.debugToggleDebugPaintSizeEnabled()).called(1);
+    });
+
+    testUsingContext('p - debugTogglePlatform without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      when(mockResidentRunner.isRunningDebug).thenReturn(true);
+      await terminalHandler.processTerminalInput('p');
+
+      verifyNever(mockResidentRunner.debugToggleDebugPaintSizeEnabled());
+    });
+
+    testUsingContext('p - debugToggleDebugPaintSizeEnabled with service protocol and debug mode', () async {
+      when(mockResidentRunner.isRunningDebug).thenReturn(true);
+      await terminalHandler.processTerminalInput('p');
+
+      verify(mockResidentRunner.debugToggleDebugPaintSizeEnabled()).called(1);
+    });
+
+    testUsingContext('p - debugTogglePlatform without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      when(mockResidentRunner.isRunningDebug).thenReturn(true);
+      await terminalHandler.processTerminalInput('p');
+
+      verifyNever(mockResidentRunner.debugToggleDebugPaintSizeEnabled());
+    });
+
+    testUsingContext('P - debugTogglePerformanceOverlayOverride with service protocol', () async {
+      await terminalHandler.processTerminalInput('P');
+
+      verify(mockResidentRunner.debugTogglePerformanceOverlayOverride()).called(1);
+    });
+
+    testUsingContext('P - debugTogglePerformanceOverlayOverride without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      await terminalHandler.processTerminalInput('P');
+
+      verifyNever(mockResidentRunner.debugTogglePerformanceOverlayOverride());
+    });
+
+    testUsingContext('q,Q - exit', () async {
+      await terminalHandler.processTerminalInput('q');
+      await terminalHandler.processTerminalInput('Q');
+
+      verify(mockResidentRunner.exit()).called(2);
+    });
+
+    testUsingContext('s - screenshot', () async {
+      final MockDevice mockDevice = MockDevice();
+      final MockFlutterDevice mockFlutterDevice = MockFlutterDevice();
+      when(mockResidentRunner.isRunningDebug).thenReturn(true);
+      when(mockResidentRunner.flutterDevices).thenReturn(<FlutterDevice>[mockFlutterDevice]);
+      when(mockFlutterDevice.device).thenReturn(mockDevice);
+      when(mockDevice.supportsScreenshot).thenReturn(true);
+
+      await terminalHandler.processTerminalInput('s');
+
+      verify(mockResidentRunner.screenshot(mockFlutterDevice)).called(1);
+    });
+
+    testUsingContext('S - debugDumpSemanticsTreeInTraversalOrder with service protocol', () async {
+      await terminalHandler.processTerminalInput('S');
+
+      verify(mockResidentRunner.debugDumpSemanticsTreeInTraversalOrder()).called(1);
+    });
+
+    testUsingContext('S - debugDumpSemanticsTreeInTraversalOrder without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      await terminalHandler.processTerminalInput('S');
+
+      verifyNever(mockResidentRunner.debugDumpSemanticsTreeInTraversalOrder());
+    });
+
+    testUsingContext('t,T - debugDumpRenderTree with service protocol', () async {
+      await terminalHandler.processTerminalInput('t');
+      await terminalHandler.processTerminalInput('T');
+
+      verify(mockResidentRunner.debugDumpRenderTree()).called(2);
+    });
+
+    testUsingContext('t,T - debugDumpSemanticsTreeInTraversalOrder without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      await terminalHandler.processTerminalInput('t');
+      await terminalHandler.processTerminalInput('T');
+
+      verifyNever(mockResidentRunner.debugDumpRenderTree());
+    });
+
+    testUsingContext('U - debugDumpRenderTree with service protocol', () async {
+      await terminalHandler.processTerminalInput('U');
+
+      verify(mockResidentRunner.debugDumpSemanticsTreeInInverseHitTestOrder()).called(1);
+    });
+
+    testUsingContext('U - debugDumpSemanticsTreeInTraversalOrder without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      await terminalHandler.processTerminalInput('U');
+
+      verifyNever(mockResidentRunner.debugDumpSemanticsTreeInInverseHitTestOrder());
+    });
+
+    testUsingContext('w,W - debugDumpApp with service protocol', () async {
+      await terminalHandler.processTerminalInput('w');
+      await terminalHandler.processTerminalInput('W');
+
+      verify(mockResidentRunner.debugDumpApp()).called(2);
+    });
+
+    testUsingContext('w,W - debugDumpApp without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      await terminalHandler.processTerminalInput('w');
+      await terminalHandler.processTerminalInput('W');
+
+      verifyNever(mockResidentRunner.debugDumpApp());
+    });
+
+    testUsingContext('z,Z - debugToggleDebugCheckElevationsEnabled with service protocol', () async {
+      await terminalHandler.processTerminalInput('z');
+      await terminalHandler.processTerminalInput('Z');
+
+      verify(mockResidentRunner.debugToggleDebugCheckElevationsEnabled()).called(2);
+    });
+
+    testUsingContext('z,Z - debugToggleDebugCheckElevationsEnabled without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      await terminalHandler.processTerminalInput('z');
+      await terminalHandler.processTerminalInput('Z');
+
+      // This should probably be disable when the service protocol is not enabled.
+      verify(mockResidentRunner.debugToggleDebugCheckElevationsEnabled()).called(2);
+    });
+  });
+}
+
+class MockDevice extends Mock implements Device {
+  MockDevice() {
+    when(isSupported()).thenReturn(true);
+  }
+}
+
+class MockResidentRunner extends Mock implements ResidentRunner {}
+
+class MockFlutterDevice extends Mock implements FlutterDevice {}
+
+class TestRunner extends ResidentRunner {
+  TestRunner(List<FlutterDevice> devices)
+    : super(devices);
+
+  bool hasHelpBeenPrinted = false;
+  String receivedCommand;
+
+  @override
+  Future<void> cleanupAfterSignal() async { }
+
+  @override
+  Future<void> cleanupAtFinish() async { }
+
+  @override
+  Future<void> handleTerminalCommand(String code) async {
+    receivedCommand = code;
+  }
+
+  @override
+  void printHelp({ bool details }) {
+    hasHelpBeenPrinted = true;
+  }
+
+  @override
+  Future<int> run({
+    Completer<DebugConnectionInfo> connectionInfoCompleter,
+    Completer<void> appStartedCompleter,
+    String route,
+    bool shouldBuild = true,
+  }) async => null;
+
+  @override
+  Future<int> attach({
+    Completer<DebugConnectionInfo> connectionInfoCompleter,
+    Completer<void> appStartedCompleter,
+  }) async => null;
+}