Unverified Commit c11633e8 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

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

parent 9ecb4ce9
...@@ -469,7 +469,10 @@ class AndroidDevice extends Device { ...@@ -469,7 +469,10 @@ class AndroidDevice extends Device {
} }
@override @override
bool get supportsHotMode => true; bool get supportsHotReload => true;
@override
bool get supportsHotRestart => true;
@override @override
Future<bool> stopApp(ApplicationPackage app) { Future<bool> stopApp(ApplicationPackage app) {
......
...@@ -209,7 +209,9 @@ class AttachCommand extends FlutterCommand { ...@@ -209,7 +209,9 @@ class AttachCommand extends FlutterCommand {
} }
} finally { } finally {
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList(); 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; return null;
} }
......
...@@ -458,7 +458,7 @@ class AppDomain extends Domain { ...@@ -458,7 +458,7 @@ class AppDomain extends Domain {
} }
bool isRestartSupported(bool enableHotReload, Device device) => bool isRestartSupported(bool enableHotReload, Device device) =>
enableHotReload && device.supportsHotMode; enableHotReload && device.supportsHotRestart;
Future<OperationResult> _inProgressHotReload; Future<OperationResult> _inProgressHotReload;
......
...@@ -336,8 +336,8 @@ class RunCommand extends RunCommandBase { ...@@ -336,8 +336,8 @@ class RunCommand extends RunCommandBase {
if (hotMode) { if (hotMode) {
for (Device device in devices) { for (Device device in devices) {
if (!device.supportsHotMode) if (!device.supportsHotReload)
throwToolExit('Hot mode is not supported by ${device.name}. Run with --no-hot.'); throwToolExit('Hot reload is not supported by ${device.name}. Run with --no-hot.');
} }
} }
......
...@@ -270,8 +270,11 @@ abstract class Device { ...@@ -270,8 +270,11 @@ abstract class Device {
bool ipv6 = false, bool ipv6 = false,
}); });
/// Does this device implement support for hot reloading / restarting? /// Whether this device implements support for hot reload.
bool get supportsHotMode => true; bool get supportsHotReload => true;
/// Whether this device implements support for hot restart.
bool get supportsHotRestart => true;
/// Stop an app package on the current device. /// Stop an app package on the current device.
Future<bool> stopApp(ApplicationPackage app); Future<bool> stopApp(ApplicationPackage app);
......
...@@ -97,7 +97,10 @@ class FuchsiaDevice extends Device { ...@@ -97,7 +97,10 @@ class FuchsiaDevice extends Device {
FuchsiaDevice(String id, { this.name }) : super(id); FuchsiaDevice(String id, { this.name }) : super(id);
@override @override
bool get supportsHotMode => true; bool get supportsHotReload => true;
@override
bool get supportsHotRestart => false;
@override @override
final String name; final String name;
......
...@@ -124,7 +124,10 @@ class IOSDevice extends Device { ...@@ -124,7 +124,10 @@ class IOSDevice extends Device {
final String _sdkVersion; final String _sdkVersion;
@override @override
bool get supportsHotMode => true; bool get supportsHotReload => true;
@override
bool get supportsHotRestart => true;
@override @override
final String name; final String name;
......
...@@ -223,7 +223,10 @@ class IOSSimulator extends Device { ...@@ -223,7 +223,10 @@ class IOSSimulator extends Device {
Future<bool> get isLocalEmulator async => true; Future<bool> get isLocalEmulator async => true;
@override @override
bool get supportsHotMode => true; bool get supportsHotReload => true;
@override
bool get supportsHotRestart => true;
Map<ApplicationPackage, _IOSSimulatorLogReader> _logReaders; Map<ApplicationPackage, _IOSSimulatorLogReader> _logReaders;
_IOSSimulatorDevicePortForwarder _portForwarder; _IOSSimulatorDevicePortForwarder _portForwarder;
......
...@@ -460,6 +460,17 @@ abstract class ResidentRunner { ...@@ -460,6 +460,17 @@ abstract class ResidentRunner {
bool get isRunningRelease => debuggingOptions.buildInfo.isRelease; bool get isRunningRelease => debuggingOptions.buildInfo.isRelease;
bool get supportsServiceProtocol => isRunningDebug || isRunningProfile; 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. /// Start the app and keep the process running during its lifetime.
Future<int> run({ Future<int> run({
Completer<DebugConnectionInfo> connectionInfoCompleter, Completer<DebugConnectionInfo> connectionInfoCompleter,
......
...@@ -289,7 +289,16 @@ class HotRunner extends ResidentRunner { ...@@ -289,7 +289,16 @@ class HotRunner extends ResidentRunner {
Future<void> handleTerminalCommand(String code) async { Future<void> handleTerminalCommand(String code) async {
final String lower = code.toLowerCase(); final String lower = code.toLowerCase();
if (lower == 'r') { 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) { if (!result.isOk) {
// TODO(johnmccutchan): Attempt to determine the number of errors that // TODO(johnmccutchan): Attempt to determine the number of errors that
// occurred and tighten this message. // occurred and tighten this message.
...@@ -541,6 +550,9 @@ class HotRunner extends ResidentRunner { ...@@ -541,6 +550,9 @@ class HotRunner extends ResidentRunner {
Future<OperationResult> restart({ bool fullRestart = false, bool pauseAfterRestart = false, String reason }) async { Future<OperationResult> restart({ bool fullRestart = false, bool pauseAfterRestart = false, String reason }) async {
final Stopwatch timer = Stopwatch()..start(); final Stopwatch timer = Stopwatch()..start();
if (fullRestart) { if (fullRestart) {
if (!canHotRestart) {
return OperationResult(1, 'hotRestart not supported');
}
final Status status = logger.startProgress( final Status status = logger.startProgress(
'Performing hot restart...', 'Performing hot restart...',
progressId: 'hot.restart', progressId: 'hot.restart',
...@@ -780,9 +792,12 @@ class HotRunner extends ResidentRunner { ...@@ -780,9 +792,12 @@ class HotRunner extends ResidentRunner {
@override @override
void printHelp({ @required bool details }) { void printHelp({ @required bool details }) {
const String fire = '🔥'; 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( final String message = terminal.color(
fire + terminal.bolden(' To hot reload changes while running, press "r". ' fire + terminal.bolden(rawMessage),
'To hot restart (and rebuild state), press "R".'),
TerminalColor.red, TerminalColor.red,
); );
printStatus(message); printStatus(message);
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter_tools/src/artifacts.dart'; 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/device.dart';
import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/run_hot.dart'; import 'package:flutter_tools/src/run_hot.dart';
...@@ -94,32 +95,113 @@ void main() { ...@@ -94,32 +95,113 @@ void main() {
group('hotRestart', () { group('hotRestart', () {
final MockResidentCompiler residentCompiler = MockResidentCompiler(); final MockResidentCompiler residentCompiler = MockResidentCompiler();
final MockDevFs mockDevFs = MockDevFs();
MockLocalEngineArtifacts mockArtifacts; 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(() { setUp(() {
mockArtifacts = MockLocalEngineArtifacts(); mockArtifacts = MockLocalEngineArtifacts();
when(mockArtifacts.getArtifactPath(Artifact.flutterPatchedSdkPath)).thenReturn('some/path'); when(mockArtifacts.getArtifactPath(Artifact.flutterPatchedSdkPath)).thenReturn('some/path');
}); });
testUsingContext('no setup', () async { 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); expect((await HotRunner(devices).restart(fullRestart: true)).isOk, false);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Artifacts: () => mockArtifacts, Artifacts: () => mockArtifacts,
}); });
testUsingContext('setup function succeeds', () async { testUsingContext('Does not hot restart when device does not support it', () async {
final List<FlutterDevice> devices = <FlutterDevice>[FlutterDevice(MockDevice(), generator: residentCompiler, trackWidgetCreation: false)]; // 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); final OperationResult result = await HotRunner(devices).restart(fullRestart: true);
// Expect hot restart failed.
expect(result.isOk, false); expect(result.isOk, false);
expect(result.message, isNot('setupHotRestart failed')); expect(result.message, 'hotRestart not supported');
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Artifacts: () => mockArtifacts, 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 { 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); final OperationResult result = await HotRunner(devices).restart(fullRestart: true);
expect(result.isOk, false); expect(result.isOk, false);
expect(result.message, 'setupHotRestart failed'); expect(result.message, 'setupHotRestart failed');
...@@ -127,9 +209,29 @@ void main() { ...@@ -127,9 +209,29 @@ void main() {
Artifacts: () => mockArtifacts, Artifacts: () => mockArtifacts,
HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: false), 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 MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {}
class MockDevice extends Mock implements Device { class MockDevice extends Mock implements Device {
...@@ -139,7 +241,9 @@ class MockDevice extends Mock implements Device { ...@@ -139,7 +241,9 @@ class MockDevice extends Mock implements Device {
} }
class TestHotRunnerConfig extends HotRunnerConfig { class TestHotRunnerConfig extends HotRunnerConfig {
TestHotRunnerConfig({@required this.successfulSetup}); TestHotRunnerConfig({@required this.successfulSetup, bool computeDartDependencies = true}) {
this.computeDartDependencies = computeDartDependencies;
}
bool successfulSetup; bool successfulSetup;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment