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;