Separate hot reload and hot restart capabilities. (#24122)

diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart
index 94b165e..601b04a 100644
--- a/packages/flutter_tools/lib/src/android/android_device.dart
+++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -469,7 +469,10 @@
   }
 
   @override
-  bool get supportsHotMode => true;
+  bool get supportsHotReload => true;
+
+  @override
+  bool get supportsHotRestart => true;
 
   @override
   Future<bool> stopApp(ApplicationPackage app) {
diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart
index b2282c1..a4d9d6c 100644
--- a/packages/flutter_tools/lib/src/commands/attach.dart
+++ b/packages/flutter_tools/lib/src/commands/attach.dart
@@ -209,7 +209,9 @@
       }
     } finally {
       final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
-      ports.forEach(device.portForwarder.unforward);
+      for (ForwardedPort port in ports) {
+        await device.portForwarder.unforward(port);
+      }
     }
     return null;
   }
diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart
index f8799ae..5bf05f4 100644
--- a/packages/flutter_tools/lib/src/commands/daemon.dart
+++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -458,7 +458,7 @@
   }
 
   bool isRestartSupported(bool enableHotReload, Device device) =>
-      enableHotReload && device.supportsHotMode;
+      enableHotReload && device.supportsHotRestart;
 
   Future<OperationResult> _inProgressHotReload;
 
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 461c4fc..cb0e66e 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -336,8 +336,8 @@
 
     if (hotMode) {
       for (Device device in devices) {
-        if (!device.supportsHotMode)
-          throwToolExit('Hot mode is not supported by ${device.name}. Run with --no-hot.');
+        if (!device.supportsHotReload)
+          throwToolExit('Hot reload is not supported by ${device.name}. Run with --no-hot.');
       }
     }
 
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index 8225f4f..3c6d378 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -270,8 +270,11 @@
     bool ipv6 = false,
   });
 
-  /// Does this device implement support for hot reloading / restarting?
-  bool get supportsHotMode => true;
+  /// Whether this device implements support for hot reload.
+  bool get supportsHotReload => true;
+
+  /// Whether this device implements support for hot restart.
+  bool get supportsHotRestart => true;
 
   /// Stop an app package on the current device.
   Future<bool> stopApp(ApplicationPackage app);
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
index 42ea084..dba2a3a 100644
--- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
@@ -97,7 +97,10 @@
   FuchsiaDevice(String id, { this.name }) : super(id);
 
   @override
-  bool get supportsHotMode => true;
+  bool get supportsHotReload => true;
+
+  @override
+  bool get supportsHotRestart => false;
 
   @override
   final String name;
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index d65689f..c55cb85 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -124,7 +124,10 @@
   final String _sdkVersion;
 
   @override
-  bool get supportsHotMode => true;
+  bool get supportsHotReload => true;
+
+  @override
+  bool get supportsHotRestart => true;
 
   @override
   final String name;
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart
index 14b8dcb..69c8a1c 100644
--- a/packages/flutter_tools/lib/src/ios/simulators.dart
+++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -223,7 +223,10 @@
   Future<bool> get isLocalEmulator async => true;
 
   @override
-  bool get supportsHotMode => true;
+  bool get supportsHotReload => true;
+
+  @override
+  bool get supportsHotRestart => true;
 
   Map<ApplicationPackage, _IOSSimulatorLogReader> _logReaders;
   _IOSSimulatorDevicePortForwarder _portForwarder;
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index 05c9562..5be4191 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -460,6 +460,17 @@
   bool get isRunningRelease => debuggingOptions.buildInfo.isRelease;
   bool get supportsServiceProtocol => isRunningDebug || isRunningProfile;
 
+  /// Whether this runner can hot restart.
+  ///
+  /// To prevent scenarios where only a subset of devices are hot restarted,
+  /// the runner requires that all attached devices can support hot restart
+  /// before enabling it.
+  bool get canHotRestart {
+    return flutterDevices.every((FlutterDevice device) {
+      return device.device.supportsHotRestart;
+    });
+  }
+
   /// Start the app and keep the process running during its lifetime.
   Future<int> run({
     Completer<DebugConnectionInfo> connectionInfoCompleter,
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index b0de03a..ded7a25 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -289,7 +289,16 @@
   Future<void> handleTerminalCommand(String code) async {
     final String lower = code.toLowerCase();
     if (lower == 'r') {
-      final OperationResult result = await restart(fullRestart: code == '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) {
         // TODO(johnmccutchan): Attempt to determine the number of errors that
         // occurred and tighten this message.
@@ -541,6 +550,9 @@
   Future<OperationResult> restart({ bool fullRestart = false, bool pauseAfterRestart = false, String reason }) async {
     final Stopwatch timer = Stopwatch()..start();
     if (fullRestart) {
+      if (!canHotRestart) {
+        return OperationResult(1, 'hotRestart not supported');
+      }
       final Status status = logger.startProgress(
         'Performing hot restart...',
         progressId: 'hot.restart',
@@ -780,9 +792,12 @@
   @override
   void printHelp({ @required bool details }) {
     const String fire = '🔥';
+    String rawMessage = '  To hot reload changes while running, press "r". ';
+    if (canHotRestart) {
+      rawMessage += 'To hot restart (and rebuild state), press "R".';
+    }
     final String message = terminal.color(
-      fire + terminal.bolden('  To hot reload changes while running, press "r". '
-          'To hot restart (and rebuild state), press "R".'),
+      fire + terminal.bolden(rawMessage),
       TerminalColor.red,
     );
     printStatus(message);
diff --git a/packages/flutter_tools/test/hot_test.dart b/packages/flutter_tools/test/hot_test.dart
index 4ee6ec4..a6f70b9 100644
--- a/packages/flutter_tools/test/hot_test.dart
+++ b/packages/flutter_tools/test/hot_test.dart
@@ -5,6 +5,7 @@
 import 'dart:async';
 
 import 'package:flutter_tools/src/artifacts.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';
@@ -94,32 +95,113 @@
 
   group('hotRestart', () {
     final MockResidentCompiler residentCompiler = MockResidentCompiler();
+    final MockDevFs mockDevFs = MockDevFs();
     MockLocalEngineArtifacts mockArtifacts;
 
+    when(mockDevFs.update(
+      mainPath: anyNamed('mainPath'),
+      target: anyNamed('target'),
+      bundle: anyNamed('bundle'),
+      firstBuildTime: anyNamed('firstBuildTime'),
+      bundleFirstUpload: anyNamed('bundleFirstUpload'),
+      bundleDirty: anyNamed('bundleDirty'),
+      fileFilter: anyNamed('fileFilter'),
+      generator: anyNamed('generator'),
+      fullRestart: anyNamed('fullRestart'),
+      dillOutputPath: anyNamed('dillOutputPath'),
+      trackWidgetCreation: anyNamed('trackWidgetCreation'),
+      projectRootPath: anyNamed('projectRootPath'),
+      pathToReload: anyNamed('pathToReload'),
+    )).thenAnswer((Invocation _) => Future<int>.value(1000));
+    when(mockDevFs.assetPathsToEvict).thenReturn(Set<String>());
+    when(mockDevFs.baseUri).thenReturn(Uri.file('test'));
+
     setUp(() {
       mockArtifacts = MockLocalEngineArtifacts();
       when(mockArtifacts.getArtifactPath(Artifact.flutterPatchedSdkPath)).thenReturn('some/path');
     });
 
     testUsingContext('no setup', () async {
-      final List<FlutterDevice> devices = <FlutterDevice>[FlutterDevice(MockDevice(), generator: residentCompiler, trackWidgetCreation: false)];
+      final MockDevice mockDevice = MockDevice();
+      when(mockDevice.supportsHotReload).thenReturn(true);
+      when(mockDevice.supportsHotRestart).thenReturn(true);
+      final List<FlutterDevice> devices = <FlutterDevice>[
+        FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false),
+      ];
       expect((await HotRunner(devices).restart(fullRestart: true)).isOk, false);
     }, overrides: <Type, Generator>{
       Artifacts: () => mockArtifacts,
     });
 
-    testUsingContext('setup function succeeds', () async {
-      final List<FlutterDevice> devices = <FlutterDevice>[FlutterDevice(MockDevice(), generator: residentCompiler, trackWidgetCreation: false)];
+    testUsingContext('Does not hot restart when device does not support it', () async {
+      // Setup mocks
+      final MockDevice mockDevice = MockDevice();
+      when(mockDevice.supportsHotReload).thenReturn(true);
+      when(mockDevice.supportsHotRestart).thenReturn(false);
+      // Trigger hot restart.
+      final List<FlutterDevice> devices = <FlutterDevice>[
+        FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false)..devFS = mockDevFs
+      ];
       final OperationResult result = await HotRunner(devices).restart(fullRestart: true);
+      // Expect hot restart failed.
       expect(result.isOk, false);
-      expect(result.message, isNot('setupHotRestart failed'));
+      expect(result.message, 'hotRestart not supported');
     }, overrides: <Type, Generator>{
       Artifacts: () => mockArtifacts,
-      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
+      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true, computeDartDependencies: false),
+    });
+
+    testUsingContext('Does not hot restart when one of many devices does not support it', () async {
+      // Setup mocks
+      final MockDevice mockDevice = MockDevice();
+      final MockDevice mockHotDevice = MockDevice();
+      when(mockDevice.supportsHotReload).thenReturn(true);
+      when(mockDevice.supportsHotRestart).thenReturn(false);
+      when(mockHotDevice.supportsHotReload).thenReturn(true);
+      when(mockHotDevice.supportsHotRestart).thenReturn(true);
+      // Trigger hot restart.
+      final List<FlutterDevice> devices = <FlutterDevice>[
+        FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false)..devFS = mockDevFs,
+        FlutterDevice(mockHotDevice, generator: residentCompiler, trackWidgetCreation: false)..devFS = mockDevFs,
+      ];
+      final OperationResult result = await HotRunner(devices).restart(fullRestart: true);
+      // Expect hot restart failed.
+      expect(result.isOk, false);
+      expect(result.message, 'hotRestart not supported');
+    }, overrides: <Type, Generator>{
+      Artifacts: () => mockArtifacts,
+      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true, computeDartDependencies: false),
+    });
+
+    testUsingContext('Does hot restarts when all devices support it', () async {
+      // Setup mocks
+      final MockDevice mockDevice = MockDevice();
+      final MockDevice mockHotDevice = MockDevice();
+      when(mockDevice.supportsHotReload).thenReturn(true);
+      when(mockDevice.supportsHotRestart).thenReturn(true);
+      when(mockHotDevice.supportsHotReload).thenReturn(true);
+      when(mockHotDevice.supportsHotRestart).thenReturn(true);
+      // Trigger a restart.
+      final List<FlutterDevice> devices = <FlutterDevice>[
+        FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false)..devFS = mockDevFs,
+        FlutterDevice(mockHotDevice, generator: residentCompiler, trackWidgetCreation: false)..devFS = mockDevFs,
+      ];
+      final OperationResult result = await HotRunner(devices).restart(fullRestart: true);
+      // Expect hot restart was successful.
+      expect(result.isOk, true);
+      expect(result.message, isNot('hotRestart not supported'));
+    }, overrides: <Type, Generator>{
+      Artifacts: () => mockArtifacts,
+      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true, computeDartDependencies: false),
     });
 
     testUsingContext('setup function fails', () async {
-      final List<FlutterDevice> devices = <FlutterDevice>[FlutterDevice(MockDevice(), generator: residentCompiler, trackWidgetCreation: false)];
+      final MockDevice mockDevice = MockDevice();
+      when(mockDevice.supportsHotReload).thenReturn(true);
+      when(mockDevice.supportsHotRestart).thenReturn(true);
+      final List<FlutterDevice> devices = <FlutterDevice>[
+        FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false)
+      ];
       final OperationResult result = await HotRunner(devices).restart(fullRestart: true);
       expect(result.isOk, false);
       expect(result.message, 'setupHotRestart failed');
@@ -127,9 +209,29 @@
       Artifacts: () => mockArtifacts,
       HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: false),
     });
+
+    testUsingContext('hot restart supported', () async {
+      // Setup mocks
+      final MockDevice mockDevice = MockDevice();
+      when(mockDevice.supportsHotReload).thenReturn(true);
+      when(mockDevice.supportsHotRestart).thenReturn(true);
+      // Trigger hot restart.
+      final List<FlutterDevice> devices = <FlutterDevice>[
+        FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false)..devFS = mockDevFs
+      ];
+      final OperationResult result = await HotRunner(devices).restart(fullRestart: true);
+      // Expect hot restart successful.
+      expect(result.isOk, true);
+      expect(result.message, isNot('setupHotRestart failed'));
+    }, overrides: <Type, Generator>{
+      Artifacts: () => mockArtifacts,
+      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true, computeDartDependencies: false),
+    });
   });
 }
 
+class MockDevFs extends Mock implements DevFS {}
+
 class MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {}
 
 class MockDevice extends Mock implements Device {
@@ -139,7 +241,9 @@
 }
 
 class TestHotRunnerConfig extends HotRunnerConfig {
-  TestHotRunnerConfig({@required this.successfulSetup});
+  TestHotRunnerConfig({@required this.successfulSetup, bool computeDartDependencies = true}) {
+    this.computeDartDependencies = computeDartDependencies;
+  }
 
   bool successfulSetup;