blob: bfbf17782669c78f40ba2955af65f79d3fd99772 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:stack_trace/stack_trace.dart';
import 'application_package.dart';
import 'asset.dart';
import 'base/logger.dart';
import 'base/process.dart';
import 'base/utils.dart';
import 'build_info.dart';
import 'cache.dart';
import 'commands/build_apk.dart';
import 'commands/install.dart';
import 'dart/package_map.dart';
import 'devfs.dart';
import 'device.dart';
import 'globals.dart';
import 'resident_runner.dart';
import 'toolchain.dart';
import 'vmservice.dart';
const bool kHotReloadDefault = true;
String getDevFSLoaderScript() {
return path.absolute(path.join(Cache.flutterRoot,
'packages',
'flutter',
'bin',
'loader',
'loader_app.dart'));
}
class DartDependencySetBuilder {
DartDependencySetBuilder(this.mainScriptPath,
this.projectRootPath);
final String mainScriptPath;
final String projectRootPath;
Set<String> build() {
final String skySnapshotPath =
ToolConfiguration.instance.getHostToolPath(HostTool.SkySnapshot);
final List<String> args = <String>[
skySnapshotPath,
'--packages=${path.absolute(PackageMap.globalPackagesPath)}',
'--print-deps',
mainScriptPath
];
String output = runSyncAndThrowStdErrOnError(args);
final List<String> lines = LineSplitter.split(output).toList();
final Set<String> minimalDependencies = new Set<String>();
for (String line in lines) {
// We need to convert the uris so that they are relative to the project
// root and tweak package: uris so that they reflect their devFS location.
if (line.startsWith('package:')) {
// Swap out package: for packages/ because we place all package sources
// under packages/.
line = line.replaceFirst('package:', 'packages/');
} else {
// Ensure paths are relative to the project root.
line = path.relative(line, from: projectRootPath);
}
minimalDependencies.add(line);
}
return minimalDependencies;
}
}
class FirstFrameTimer {
FirstFrameTimer(this.vmService);
void start() {
stopwatch.reset();
stopwatch.start();
_subscription = vmService.onExtensionEvent.listen(_onExtensionEvent);
}
/// Returns a Future which completes after the first frame event is received.
Future<Null> firstFrame() => _completer.future;
void _onExtensionEvent(ServiceEvent event) {
if (event.extensionKind == 'Flutter.FirstFrame')
_stop();
}
void _stop() {
_subscription?.cancel();
_subscription = null;
stopwatch.stop();
_completer.complete(null);
}
Duration get elapsed {
assert(!stopwatch.isRunning);
return stopwatch.elapsed;
}
final VMService vmService;
final Stopwatch stopwatch = new Stopwatch();
final Completer<Null> _completer = new Completer<Null>();
StreamSubscription<ServiceEvent> _subscription;
}
class HotRunner extends ResidentRunner {
HotRunner(
Device device, {
String target,
DebuggingOptions debuggingOptions,
bool usesTerminalUI: true,
this.benchmarkMode: false,
}) : super(device,
target: target,
debuggingOptions: debuggingOptions,
usesTerminalUI: usesTerminalUI) {
_projectRootPath = Directory.current.path;
}
ApplicationPackage _package;
String _mainPath;
String _projectRootPath;
Set<String> _dartDependencies;
int _observatoryPort;
final AssetBundle bundle = new AssetBundle();
final bool benchmarkMode;
final Map<String, int> benchmarkData = new Map<String, int>();
@override
Future<int> run({
Completer<DebugConnectionInfo> connectionInfoCompleter,
String route,
bool shouldBuild: true
}) {
// Don't let uncaught errors kill the process.
return Chain.capture(() {
return _run(
connectionInfoCompleter: connectionInfoCompleter,
route: route,
shouldBuild: shouldBuild
);
}, onError: (dynamic error, StackTrace stackTrace) {
printError('Exception from flutter run: $error', stackTrace);
});
}
bool _refreshDartDependencies() {
if (_dartDependencies != null) {
// Already computed.
return true;
}
DartDependencySetBuilder dartDependencySetBuilder =
new DartDependencySetBuilder(_mainPath, _projectRootPath);
try {
_dartDependencies = dartDependencySetBuilder.build();
} catch (error) {
printStatus('Error detected in application source code:', emphasis: true);
printError(error);
return false;
}
return true;
}
Future<int> _run({
Completer<DebugConnectionInfo> connectionInfoCompleter,
String route,
bool shouldBuild: true
}) async {
_mainPath = findMainDartFile(target);
if (!FileSystemEntity.isFileSync(_mainPath)) {
String message = 'Tried to run $_mainPath, but that file does not exist.';
if (target == null)
message += '\nConsider using the -t option to specify the Dart file to start.';
printError(message);
return 1;
}
_package = getApplicationPackageForPlatform(device.platform);
if (_package == null) {
String message = 'No application found for ${device.platform}.';
String hint = getMissingPackageHintForPlatform(device.platform);
if (hint != null)
message += '\n$hint';
printError(message);
return 1;
}
// Determine the Dart dependencies eagerly.
if (!_refreshDartDependencies()) {
// Some kind of source level error or missing file in the Dart code.
return 1;
}
// TODO(devoncarew): We shouldn't have to do type checks here.
if (shouldBuild && device is AndroidDevice) {
printTrace('Running build command.');
int result = await buildApk(
device.platform,
target: target,
buildMode: debuggingOptions.buildMode
);
if (result != 0)
return result;
}
// TODO(devoncarew): Move this into the device.startApp() impls.
if (_package != null) {
printTrace("Stopping app '${_package.name}' on ${device.name}.");
// We don't wait for the stop command to complete.
device.stopApp(_package);
}
// Allow any stop commands from above to start work.
await new Future<Duration>.delayed(Duration.ZERO);
// TODO(devoncarew): This fails for ios devices - we haven't built yet.
if (device is AndroidDevice) {
printTrace('Running install command.');
if (!(installApp(device, _package, uninstall: false)))
return 1;
}
Map<String, dynamic> platformArgs = new Map<String, dynamic>();
await startEchoingDeviceLog();
printTrace('Launching loader on ${device.name}...');
// Start the loader.
Future<LaunchResult> futureResult = device.startApp(
_package,
debuggingOptions.buildMode,
mainPath: getDevFSLoaderScript(),
debuggingOptions: debuggingOptions,
platformArgs: platformArgs,
route: route
);
LaunchResult result = await futureResult;
if (!result.started) {
printError('Error launching DevFS loader on ${device.name}.');
await stopEchoingDeviceLog();
return 2;
}
_observatoryPort = result.observatoryPort;
try {
await connectToServiceProtocol(_observatoryPort);
} catch (error) {
printError('Error connecting to the service protocol: $error');
return 2;
}
try {
Uri baseUri = await _initDevFS();
if (connectionInfoCompleter != null) {
connectionInfoCompleter.complete(
new DebugConnectionInfo(_observatoryPort, baseUri: baseUri.toString())
);
}
} catch (error) {
printError('Error initializing DevFS: $error');
return 3;
}
_loaderShowMessage('Connecting...', progress: 0);
_loaderShowExplanation('You can use hot reload to update your app on the fly, without restarting it.');
bool devfsResult = await _updateDevFS(
progressReporter: (int progress, int max) {
if (progress % 10 == 0)
_loaderShowMessage('Syncing files to device...', progress: progress, max: max);
}
);
if (!devfsResult) {
_loaderShowMessage('Failed.');
printError('Could not perform initial file synchronization.');
return 3;
}
await vmService.vm.refreshViews();
printTrace('Connected to ${vmService.vm.mainView}.');
printStatus('Running ${getDisplayPath(_mainPath)} on ${device.name}...');
_loaderShowMessage('Launching...');
await _launchFromDevFS(_package, _mainPath);
printTrace('Application running.');
setupTerminal();
registerSignalHandlers();
if (benchmarkMode) {
// We are running in benchmark mode.
printStatus('Running in benchmark mode.');
// Measure time to perform a hot restart.
printStatus('Benchmarking hot restart');
await restart(fullRestart: true);
await vmService.vm.refreshViews();
// TODO(johnmccutchan): Modify script entry point.
printStatus('Benchmarking hot reload');
// Measure time to perform a hot reload.
await restart(fullRestart: false);
printStatus('Benchmark completed. Exiting application.');
await _cleanupDevFS();
await stopEchoingDeviceLog();
await stopApp();
File benchmarkOutput = new File('hot_benchmark.json');
benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData));
}
return waitForAppToFinish();
}
@override
Future<Null> handleTerminalCommand(String code) async {
final String lower = code.toLowerCase();
if ((lower == 'r') || (code == AnsiTerminal.KEY_F5)) {
OperationResult result = OperationResult.ok;
// F5, restart
if ((code == 'r') || (code == AnsiTerminal.KEY_F5)) {
// lower-case 'r'
result = await _reloadSources();
} else {
// upper-case 'R'.
result = await _restartFromSources();
}
if (!result.isOk) {
// TODO(johnmccutchan): Attempt to determine the number of errors that
// occurred and tighten this message.
printStatus('Try again after fixing the above error(s).', emphasis: true);
}
}
}
void _loaderShowMessage(String message, { int progress, int max }) {
currentView.uiIsolate.flutterLoaderShowMessage(message);
if (progress != null) {
currentView.uiIsolate.flutterLoaderSetProgress(progress.toDouble());
currentView.uiIsolate.flutterLoaderSetProgressMax(max?.toDouble() ?? 0.0);
} else {
currentView.uiIsolate.flutterLoaderSetProgress(0.0);
currentView.uiIsolate.flutterLoaderSetProgressMax(-1.0);
}
}
void _loaderShowExplanation(String explanation) {
currentView.uiIsolate.flutterLoaderShowExplanation(explanation);
}
DevFS _devFS;
Future<Uri> _initDevFS() {
String fsName = path.basename(_projectRootPath);
_devFS = new DevFS(vmService,
fsName,
new Directory(_projectRootPath));
return _devFS.create();
}
Future<bool> _updateDevFS({ DevFSProgressReporter progressReporter }) async {
if (!_refreshDartDependencies()) {
// Did not update DevFS because of a Dart source error.
return false;
}
Status devFSStatus = logger.startProgress('Syncing files to device...');
final bool rebuildBundle = bundle.needsBuild();
if (rebuildBundle) {
printTrace('Updating assets');
await bundle.build();
}
await _devFS.update(progressReporter: progressReporter,
bundle: bundle,
bundleDirty: rebuildBundle,
fileFilter: _dartDependencies);
devFSStatus.stop(showElapsedTime: true);
// Clear the set after the sync.
_dartDependencies = null;
printTrace('Synced ${getSizeAsMB(_devFS.bytes)}.');
return true;
}
Future<Null> _evictDirtyAssets() async {
if (_devFS.dirtyAssetEntries.length == 0)
return;
if (currentView.uiIsolate == null)
throw 'Application isolate not found';
for (DevFSEntry entry in _devFS.dirtyAssetEntries) {
await currentView.uiIsolate.flutterEvictAsset(entry.assetPath);
}
}
Future<Null> _cleanupDevFS() async {
if (_devFS != null) {
// Cleanup the devFS; don't wait indefinitely, and ignore any errors.
await _devFS.destroy()
.timeout(new Duration(milliseconds: 250))
.catchError((dynamic error) {
printTrace('$error');
});
}
_devFS = null;
}
Future<Null> _launchInView(String entryPath,
String packagesPath,
String assetsDirectoryPath) async {
FlutterView view = vmService.vm.mainView;
return view.runFromSource(entryPath, packagesPath, assetsDirectoryPath);
}
Future<Null> _launchFromDevFS(ApplicationPackage package,
String mainScript) async {
String entryPath = path.relative(mainScript, from: _projectRootPath);
String deviceEntryPath =
_devFS.baseUri.resolve(entryPath).toFilePath();
String devicePackagesPath =
_devFS.baseUri.resolve('.packages').toFilePath();
String deviceAssetsDirectoryPath =
_devFS.baseUri.resolve(getAssetBuildDirectory()).toFilePath();
await _launchInView(deviceEntryPath,
devicePackagesPath,
deviceAssetsDirectoryPath);
}
Future<OperationResult> _restartFromSources() async {
FirstFrameTimer firstFrameTimer = new FirstFrameTimer(vmService);
firstFrameTimer.start();
bool updatedDevFS = await _updateDevFS();
if (!updatedDevFS)
return new OperationResult(1, 'Dart Source Error');
await _launchFromDevFS(_package, _mainPath);
bool waitForFrame =
await currentView.uiIsolate.flutterFrameworkPresent();
Status restartStatus =
logger.startProgress('Waiting for application to start...');
if (waitForFrame) {
// Wait for the first frame to be rendered.
await firstFrameTimer.firstFrame();
}
restartStatus.stop(showElapsedTime: true);
if (waitForFrame) {
printStatus('Restart performed in '
'${getElapsedAsMilliseconds(firstFrameTimer.elapsed)}.');
if (benchmarkMode) {
benchmarkData['hotRestartMillisecondsToFrame'] =
firstFrameTimer.elapsed.inMilliseconds;
}
flutterUsage.sendTiming('hot', 'restart', firstFrameTimer.elapsed);
}
flutterUsage.sendEvent('hot', 'restart');
return OperationResult.ok;
}
/// Returns [true] if the reload was successful.
bool _printReloadReport(Map<String, dynamic> reloadReport) {
if (!reloadReport['success']) {
printError('Hot reload was rejected:');
for (Map<String, dynamic> notice in reloadReport['details']['notices'])
printError('${notice['message']}');
return false;
}
int loadedLibraryCount = reloadReport['details']['loadedLibraryCount'];
int finalLibraryCount = reloadReport['details']['finalLibraryCount'];
printStatus('Reloaded $loadedLibraryCount of $finalLibraryCount libraries.');
return true;
}
@override
Future<OperationResult> restart({ bool fullRestart: false, bool pauseAfterRestart: false }) async {
if (fullRestart) {
return _restartFromSources();
} else {
return _reloadSources(pause: pauseAfterRestart);
}
}
Future<OperationResult> _reloadSources({ bool pause: false }) async {
if (currentView.uiIsolate == null)
throw 'Application isolate not found';
FirstFrameTimer firstFrameTimer = new FirstFrameTimer(vmService);
firstFrameTimer.start();
if (_devFS != null) {
bool updatedDevFS = await _updateDevFS();
if (!updatedDevFS)
return new OperationResult(1, 'Dart Source Error');
}
Status reloadStatus = logger.startProgress('Performing hot reload...');
try {
Map<String, dynamic> reloadReport =
await currentView.uiIsolate.reloadSources(pause: pause);
reloadStatus.stop(showElapsedTime: true);
if (!_printReloadReport(reloadReport)) {
// Reload failed.
flutterUsage.sendEvent('hot', 'reload-reject');
return new OperationResult(1, 'reload rejected');
} else {
flutterUsage.sendEvent('hot', 'reload');
}
} catch (error, st) {
int errorCode = error['code'];
String errorMessage = error['message'];
reloadStatus.stop(showElapsedTime: true);
if (errorCode == Isolate.kIsolateReloadBarred) {
printError('Unable to hot reload app due to an unrecoverable error in '
'the source code. Please address the error and then use '
'"R" to restart the app.');
flutterUsage.sendEvent('hot', 'reload-barred');
return new OperationResult(errorCode, errorMessage);
}
printError('Hot reload failed:\ncode = $errorCode\nmessage = $errorMessage\n$st');
return new OperationResult(errorCode, errorMessage);
}
// Reload the isolate.
await currentView.uiIsolate.reload();
// Check if the isolate is paused.
final ServiceEvent pauseEvent = currentView.uiIsolate.pauseEvent;
if ((pauseEvent != null) && (pauseEvent.isPauseEvent)) {
// Isolate is paused. Stop here.
printTrace('Skipping reassemble because isolate is paused.');
return OperationResult.ok;
}
await _evictDirtyAssets();
printTrace('Reassembling application');
bool waitForFrame = true;
try {
waitForFrame = (await currentView.uiIsolate.flutterReassemble() != null);
} catch (_) {
printError('Reassembling application failed.');
return new OperationResult(1, 'error reassembling application');
}
try {
/* ensure that a frame is scheduled */
await currentView.uiIsolate.uiWindowScheduleFrame();
} catch (_) {
/* ignore any errors */
}
if (waitForFrame) {
// When the framework is present, we can wait for the first frame
// event and measure reload time.
await firstFrameTimer.firstFrame();
printStatus('Hot reload performed in '
'${getElapsedAsMilliseconds(firstFrameTimer.elapsed)}.');
if (benchmarkMode) {
benchmarkData['hotReloadMillisecondsToFrame'] =
firstFrameTimer.elapsed.inMilliseconds;
}
flutterUsage.sendTiming('hot', 'reload', firstFrameTimer.elapsed);
}
return OperationResult.ok;
}
@override
void printHelp({ @required bool details }) {
printStatus('🔥 To hot reload your app on the fly, press "r" or F5. To restart the app entirely, press "R".', emphasis: true);
printStatus('The Observatory debugger and profiler is available at: http://127.0.0.1:$_observatoryPort/');
if (details) {
printStatus('To dump the widget hierarchy of the app (debugDumpApp), press "w".');
printStatus('To dump the rendering tree of the app (debugDumpRenderTree), press "r".');
printStatus('To repeat this help message, press "h" or F1. To quit, press "q", F10, or Ctrl-C.');
} else {
printStatus('For a more detailed help message, press "h" or F1. To quit, press "q", F10, or Ctrl-C.');
}
}
@override
Future<Null> cleanupAfterSignal() async {
await stopEchoingDeviceLog();
await stopApp();
}
@override
Future<Null> preStop() => _cleanupDevFS();
@override
Future<Null> cleanupAtFinish() async {
await _cleanupDevFS();
await stopEchoingDeviceLog();
}
}