|  | // 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 'package:json_rpc_2/error_code.dart' as rpc_error_code; | 
|  | import 'package:json_rpc_2/json_rpc_2.dart' as rpc; | 
|  | import 'package:meta/meta.dart'; | 
|  |  | 
|  | import 'base/context.dart'; | 
|  | import 'base/file_system.dart'; | 
|  | import 'base/logger.dart'; | 
|  | import 'base/utils.dart'; | 
|  | import 'build_info.dart'; | 
|  | import 'dart/dependencies.dart'; | 
|  | import 'device.dart'; | 
|  | import 'globals.dart'; | 
|  | import 'resident_runner.dart'; | 
|  | import 'usage.dart'; | 
|  | import 'vmservice.dart'; | 
|  |  | 
|  | class HotRunnerConfig { | 
|  | /// Should the hot runner compute the minimal Dart dependencies? | 
|  | bool computeDartDependencies = true; | 
|  | /// Should the hot runner assume that the minimal Dart dependencies do not change? | 
|  | bool stableDartDependencies = false; | 
|  | } | 
|  |  | 
|  | HotRunnerConfig get hotRunnerConfig => context[HotRunnerConfig]; | 
|  |  | 
|  | const bool kHotReloadDefault = true; | 
|  |  | 
|  | class HotRunner extends ResidentRunner { | 
|  | HotRunner( | 
|  | List<FlutterDevice> devices, { | 
|  | String target, | 
|  | DebuggingOptions debuggingOptions, | 
|  | bool usesTerminalUI: true, | 
|  | this.benchmarkMode: false, | 
|  | this.applicationBinary, | 
|  | this.hostIsIde: false, | 
|  | String projectRootPath, | 
|  | String packagesFilePath, | 
|  | this.dillOutputPath, | 
|  | bool stayResident: true, | 
|  | bool ipv6: false, | 
|  | }) : super(devices, | 
|  | target: target, | 
|  | debuggingOptions: debuggingOptions, | 
|  | usesTerminalUI: usesTerminalUI, | 
|  | projectRootPath: projectRootPath, | 
|  | packagesFilePath: packagesFilePath, | 
|  | stayResident: stayResident, | 
|  | ipv6: ipv6); | 
|  |  | 
|  | final bool benchmarkMode; | 
|  | final String applicationBinary; | 
|  | final bool hostIsIde; | 
|  | Set<String> _dartDependencies; | 
|  | final String dillOutputPath; | 
|  |  | 
|  | final Map<String, List<int>> benchmarkData = <String, List<int>>{}; | 
|  | // The initial launch is from a snapshot. | 
|  | bool _runningFromSnapshot = true; | 
|  | DateTime firstBuildTime; | 
|  |  | 
|  | void _addBenchmarkData(String name, int value) { | 
|  | benchmarkData[name] ??= <int>[]; | 
|  | benchmarkData[name].add(value); | 
|  | } | 
|  |  | 
|  | bool _refreshDartDependencies() { | 
|  | if (!hotRunnerConfig.computeDartDependencies) { | 
|  | // Disabled. | 
|  | return true; | 
|  | } | 
|  | if (_dartDependencies != null) { | 
|  | // Already computed. | 
|  | return true; | 
|  | } | 
|  | final DartDependencySetBuilder dartDependencySetBuilder = | 
|  | new DartDependencySetBuilder(mainPath, packagesFilePath); | 
|  | try { | 
|  | _dartDependencies = new Set<String>.from(dartDependencySetBuilder.build()); | 
|  | } on DartDependencyException catch (error) { | 
|  | printError( | 
|  | 'Your application could not be compiled, because its dependencies could not be established.\n' | 
|  | '$error' | 
|  | ); | 
|  | return false; | 
|  | } | 
|  | return true; | 
|  | } | 
|  |  | 
|  | Future<Null> _reloadSourcesService(String isolateId, | 
|  | { bool force: false, bool pause: false }) async { | 
|  | // TODO(cbernaschina): check that isolateId is the id of the UI isolate. | 
|  | final OperationResult result = await restart(pauseAfterRestart: pause); | 
|  | if (!result.isOk) { | 
|  | throw new rpc.RpcException( | 
|  | rpc_error_code.INTERNAL_ERROR, | 
|  | 'Unable to reload sources', | 
|  | ); | 
|  | } | 
|  | } | 
|  |  | 
|  | Future<int> attach({ | 
|  | Completer<DebugConnectionInfo> connectionInfoCompleter, | 
|  | Completer<Null> appStartedCompleter, | 
|  | String viewFilter, | 
|  | }) async { | 
|  | try { | 
|  | await connectToServiceProtocol(viewFilter: viewFilter, | 
|  | reloadSources: _reloadSourcesService); | 
|  | } catch (error) { | 
|  | printError('Error connecting to the service protocol: $error'); | 
|  | return 2; | 
|  | } | 
|  |  | 
|  | for (FlutterDevice device in flutterDevices) | 
|  | device.initLogReader(); | 
|  | try { | 
|  | final List<Uri> baseUris = await _initDevFS(); | 
|  | if (connectionInfoCompleter != null) { | 
|  | // Only handle one debugger connection. | 
|  | connectionInfoCompleter.complete( | 
|  | new DebugConnectionInfo( | 
|  | httpUri: flutterDevices.first.observatoryUris.first, | 
|  | wsUri: flutterDevices.first.vmServices.first.wsAddress, | 
|  | baseUri: baseUris.first.toString() | 
|  | ) | 
|  | ); | 
|  | } | 
|  | } catch (error) { | 
|  | printError('Error initializing DevFS: $error'); | 
|  | return 3; | 
|  | } | 
|  | final Stopwatch initialUpdateDevFSsTimer = new Stopwatch()..start(); | 
|  | final bool devfsResult = await _updateDevFS(); | 
|  | _addBenchmarkData('hotReloadInitialDevFSSyncMilliseconds', | 
|  | initialUpdateDevFSsTimer.elapsed.inMilliseconds); | 
|  | if (!devfsResult) | 
|  | return 3; | 
|  |  | 
|  | await refreshViews(); | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | // VM must have accepted the kernel binary, there will be no reload | 
|  | // report, so we let incremental compiler know that source code was accepted. | 
|  | if (device.generator != null) | 
|  | device.generator.accept(); | 
|  | for (FlutterView view in device.views) | 
|  | printTrace('Connected to $view.'); | 
|  | } | 
|  |  | 
|  | if (stayResident) { | 
|  | setupTerminal(); | 
|  | registerSignalHandlers(); | 
|  | } | 
|  |  | 
|  | appStartedCompleter?.complete(); | 
|  |  | 
|  | 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); | 
|  | // TODO(johnmccutchan): Modify script entry point. | 
|  | printStatus('Benchmarking hot reload'); | 
|  | // Measure time to perform a hot reload. | 
|  | await restart(fullRestart: false); | 
|  | if (stayResident) { | 
|  | await waitForAppToFinish(); | 
|  | } else { | 
|  | printStatus('Benchmark completed. Exiting application.'); | 
|  | await _cleanupDevFS(); | 
|  | await stopEchoingDeviceLog(); | 
|  | await stopApp(); | 
|  | } | 
|  | final File benchmarkOutput = fs.file('hot_benchmark.json'); | 
|  | benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData)); | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | if (stayResident) | 
|  | return waitForAppToFinish(); | 
|  | await cleanupAtFinish(); | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | @override | 
|  | Future<int> run({ | 
|  | Completer<DebugConnectionInfo> connectionInfoCompleter, | 
|  | Completer<Null> appStartedCompleter, | 
|  | String route, | 
|  | bool shouldBuild: true | 
|  | }) async { | 
|  | if (!fs.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; | 
|  | } | 
|  |  | 
|  | // Determine the Dart dependencies eagerly. | 
|  | if (!_refreshDartDependencies()) { | 
|  | // Some kind of source level error or missing file in the Dart code. | 
|  | return 1; | 
|  | } | 
|  |  | 
|  | firstBuildTime = new DateTime.now(); | 
|  |  | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | final int result = await device.runHot( | 
|  | hotRunner: this, | 
|  | route: route, | 
|  | shouldBuild: shouldBuild, | 
|  | ); | 
|  | if (result != 0) { | 
|  | return result; | 
|  | } | 
|  | } | 
|  |  | 
|  | return attach( | 
|  | connectionInfoCompleter: connectionInfoCompleter, | 
|  | appStartedCompleter: appStartedCompleter | 
|  | ); | 
|  | } | 
|  |  | 
|  | @override | 
|  | Future<Null> handleTerminalCommand(String code) async { | 
|  | final String lower = code.toLowerCase(); | 
|  | if (lower == 'r') { | 
|  | final OperationResult result = await restart(fullRestart: code == 'R'); | 
|  | 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); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | Future<List<Uri>> _initDevFS() async { | 
|  | final String fsName = fs.path.basename(projectRootPath); | 
|  | final List<Uri> devFSUris = <Uri>[]; | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | final Uri uri = await device.setupDevFS( | 
|  | fsName, | 
|  | fs.directory(projectRootPath), | 
|  | packagesFilePath: packagesFilePath | 
|  | ); | 
|  | devFSUris.add(uri); | 
|  | } | 
|  | return devFSUris; | 
|  | } | 
|  |  | 
|  | Future<bool> _updateDevFS({ bool fullRestart: false }) async { | 
|  | if (!_refreshDartDependencies()) { | 
|  | // Did not update DevFS because of a Dart source error. | 
|  | return false; | 
|  | } | 
|  | final bool isFirstUpload = assetBundle.wasBuiltOnce() == false; | 
|  | final bool rebuildBundle = assetBundle.needsBuild(); | 
|  | if (rebuildBundle) { | 
|  | printTrace('Updating assets'); | 
|  | final int result = await assetBundle.build(); | 
|  | if (result != 0) | 
|  | return false; | 
|  | } | 
|  |  | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | final bool result = await device.updateDevFS( | 
|  | mainPath: mainPath, | 
|  | target: target, | 
|  | bundle: assetBundle, | 
|  | firstBuildTime: firstBuildTime, | 
|  | bundleFirstUpload: isFirstUpload, | 
|  | bundleDirty: isFirstUpload == false && rebuildBundle, | 
|  | fileFilter: _dartDependencies, | 
|  | fullRestart: fullRestart, | 
|  | projectRootPath: projectRootPath, | 
|  | ); | 
|  | if (!result) | 
|  | return false; | 
|  | } | 
|  |  | 
|  | if (!hotRunnerConfig.stableDartDependencies) { | 
|  | // Clear the set after the sync so they are recomputed next time. | 
|  | _dartDependencies = null; | 
|  | } | 
|  | return true; | 
|  | } | 
|  |  | 
|  | Future<Null> _evictDirtyAssets() async { | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | if (device.devFS.assetPathsToEvict.isEmpty) | 
|  | return; | 
|  | if (device.views.first.uiIsolate == null) | 
|  | throw 'Application isolate not found'; | 
|  | for (String assetPath in device.devFS.assetPathsToEvict) | 
|  | await device.views.first.uiIsolate.flutterEvictAsset(assetPath); | 
|  | device.devFS.assetPathsToEvict.clear(); | 
|  | } | 
|  | } | 
|  |  | 
|  | void _resetDirtyAssets() { | 
|  | for (FlutterDevice device in flutterDevices) | 
|  | device.devFS.assetPathsToEvict.clear(); | 
|  | } | 
|  |  | 
|  | Future<Null> _cleanupDevFS() async { | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | if (device.devFS != null) { | 
|  | // Cleanup the devFS; don't wait indefinitely, and ignore any errors. | 
|  | await device.devFS.destroy() | 
|  | .timeout(const Duration(milliseconds: 250)) | 
|  | .catchError((dynamic error) { | 
|  | printTrace('$error'); | 
|  | }); | 
|  | } | 
|  | device.devFS = null; | 
|  | } | 
|  | } | 
|  |  | 
|  | Future<Null> _launchInView(FlutterDevice device, | 
|  | Uri entryUri, | 
|  | Uri packagesUri, | 
|  | Uri assetsDirectoryUri) async { | 
|  | for (FlutterView view in device.views) | 
|  | await view.runFromSource(entryUri, packagesUri, assetsDirectoryUri); | 
|  | } | 
|  |  | 
|  | Future<Null> _launchFromDevFS(String mainScript) async { | 
|  | final String entryUri = fs.path.relative(mainScript, from: projectRootPath); | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | final Uri deviceEntryUri = device.devFS.baseUri.resolveUri( | 
|  | fs.path.toUri(entryUri)); | 
|  | final Uri devicePackagesUri = device.devFS.baseUri.resolve('.packages'); | 
|  | final Uri deviceAssetsDirectoryUri = device.devFS.baseUri.resolveUri( | 
|  | fs.path.toUri(getAssetBuildDirectory())); | 
|  | await _launchInView(device, | 
|  | deviceEntryUri, | 
|  | devicePackagesUri, | 
|  | deviceAssetsDirectoryUri); | 
|  | if (benchmarkMode) { | 
|  | for (FlutterDevice device in flutterDevices) | 
|  | for (FlutterView view in device.views) | 
|  | await view.flushUIThreadTasks(); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | Future<OperationResult> _restartFromSources() async { | 
|  | if (!_isPaused()) { | 
|  | printTrace('Refreshing active FlutterViews before restarting.'); | 
|  | await refreshViews(); | 
|  | } | 
|  |  | 
|  | final Stopwatch restartTimer = new Stopwatch()..start(); | 
|  | // TODO(aam): Add generator reset logic once we switch to using incremental | 
|  | // compiler for full application recompilation on restart. | 
|  | final bool updatedDevFS = await _updateDevFS(fullRestart: true); | 
|  | if (!updatedDevFS) { | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | if (device.generator != null) | 
|  | device.generator.reject(); | 
|  | } | 
|  | return new OperationResult(1, 'DevFS synchronization failed'); | 
|  | } | 
|  | _resetDirtyAssets(); | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | // VM must have accepted the kernel binary, there will be no reload | 
|  | // report, so we let incremental compiler know that source code was accepted. | 
|  | if (device.generator != null) | 
|  | device.generator.accept(); | 
|  | } | 
|  | // Check if the isolate is paused and resume it. | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | for (FlutterView view in device.views) { | 
|  | if (view.uiIsolate != null) { | 
|  | // Reload the isolate. | 
|  | await view.uiIsolate.reload(); | 
|  | final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent; | 
|  | if ((pauseEvent != null) && pauseEvent.isPauseEvent) { | 
|  | // Resume the isolate so that it can be killed by the embedder. | 
|  | await view.uiIsolate.resume(); | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  | // We are now running from source. | 
|  | _runningFromSnapshot = false; | 
|  | final String launchPath = debuggingOptions.buildInfo.previewDart2 | 
|  | ? mainPath + '.dill' | 
|  | : mainPath; | 
|  | await _launchFromDevFS(launchPath); | 
|  | restartTimer.stop(); | 
|  | printTrace('Restart performed in ' | 
|  | '${getElapsedAsMilliseconds(restartTimer.elapsed)}.'); | 
|  | // We are now running from sources. | 
|  | _runningFromSnapshot = false; | 
|  | _addBenchmarkData('hotRestartMillisecondsToFrame', | 
|  | restartTimer.elapsed.inMilliseconds); | 
|  | flutterUsage.sendEvent('hot', 'restart'); | 
|  | flutterUsage.sendTiming('hot', 'restart', restartTimer.elapsed); | 
|  | return OperationResult.ok; | 
|  | } | 
|  |  | 
|  | /// Returns [true] if the reload was successful. | 
|  | /// Prints errors if [printErrors] is [true]. | 
|  | static bool validateReloadReport(Map<String, dynamic> reloadReport, | 
|  | { bool printErrors: true }) { | 
|  | if (reloadReport['type'] != 'ReloadReport') { | 
|  | if (printErrors) | 
|  | printError('Hot reload received invalid response: $reloadReport'); | 
|  | return false; | 
|  | } | 
|  | if (!reloadReport['success']) { | 
|  | if (printErrors) { | 
|  | printError('Hot reload was rejected:'); | 
|  | for (Map<String, dynamic> notice in reloadReport['details']['notices']) | 
|  | printError('${notice['message']}'); | 
|  | } | 
|  | return false; | 
|  | } | 
|  | return true; | 
|  | } | 
|  |  | 
|  | @override | 
|  | bool get supportsRestart => true; | 
|  |  | 
|  | @override | 
|  | Future<OperationResult> restart({ bool fullRestart: false, bool pauseAfterRestart: false }) async { | 
|  | if (fullRestart) { | 
|  | final Status status = logger.startProgress( | 
|  | 'Performing full restart...', | 
|  | progressId: 'hot.restart' | 
|  | ); | 
|  | try { | 
|  | final Stopwatch timer = new Stopwatch()..start(); | 
|  | await _restartFromSources(); | 
|  | timer.stop(); | 
|  | status.cancel(); | 
|  | printStatus('Restarted app in ${getElapsedAsMilliseconds(timer.elapsed)}.'); | 
|  | return OperationResult.ok; | 
|  | } catch (error) { | 
|  | status.cancel(); | 
|  | rethrow; | 
|  | } | 
|  | } else { | 
|  | final bool reloadOnTopOfSnapshot = _runningFromSnapshot; | 
|  | final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing'; | 
|  | final Status status = logger.startProgress( | 
|  | '$progressPrefix hot reload...', | 
|  | progressId: 'hot.reload' | 
|  | ); | 
|  | try { | 
|  | final Stopwatch timer = new Stopwatch()..start(); | 
|  | final OperationResult result = await _reloadSources(pause: pauseAfterRestart); | 
|  | timer.stop(); | 
|  | status.cancel(); | 
|  | if (result.isOk) | 
|  | printStatus('${result.message} in ${getElapsedAsMilliseconds(timer.elapsed)}.'); | 
|  | if (result.hintMessage != null) | 
|  | printStatus('\n${result.hintMessage}'); | 
|  | return result; | 
|  | } catch (error) { | 
|  | status.cancel(); | 
|  | rethrow; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | String _uriToRelativePath(Uri uri) { | 
|  | final String path = uri.toString(); | 
|  | final String base = new Uri.file(projectRootPath).toString(); | 
|  | if (path.startsWith(base)) | 
|  | return path.substring(base.length + 1); | 
|  | return path; | 
|  | } | 
|  |  | 
|  | Future<OperationResult> _reloadSources({ bool pause: false }) async { | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | for (FlutterView view in device.views) { | 
|  | if (view.uiIsolate == null) | 
|  | throw 'Application isolate not found'; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (!_isPaused()) { | 
|  | printTrace('Refreshing active FlutterViews before reloading.'); | 
|  | await refreshViews(); | 
|  | } | 
|  |  | 
|  | // The initial launch is from a script snapshot. When we reload from source | 
|  | // on top of a script snapshot, the first reload will be a worst case reload | 
|  | // because all of the sources will end up being dirty (library paths will | 
|  | // change from host path to a device path). Subsequent reloads will | 
|  | // not be affected, so we resume reporting reload times on the second | 
|  | // reload. | 
|  | final bool reportUnused = !debuggingOptions.buildInfo.previewDart2; | 
|  | final bool shouldReportReloadTime = !_runningFromSnapshot; | 
|  | final Stopwatch reloadTimer = new Stopwatch()..start(); | 
|  |  | 
|  | final Stopwatch devFSTimer = new Stopwatch()..start(); | 
|  | final bool updatedDevFS = await _updateDevFS(); | 
|  | // Record time it took to synchronize to DevFS. | 
|  | _addBenchmarkData('hotReloadDevFSSyncMilliseconds', | 
|  | devFSTimer.elapsed.inMilliseconds); | 
|  | if (!updatedDevFS) | 
|  | return new OperationResult(1, 'DevFS synchronization failed'); | 
|  | String reloadMessage; | 
|  | final Stopwatch vmReloadTimer = new Stopwatch()..start(); | 
|  | try { | 
|  | final String entryPath = fs.path.relative( | 
|  | debuggingOptions.buildInfo.previewDart2 ? mainPath + '.dill' : mainPath, | 
|  | from: projectRootPath, | 
|  | ); | 
|  | final Completer<Map<String, dynamic>> retrieveFirstReloadReport = new Completer<Map<String, dynamic>>(); | 
|  |  | 
|  | int countExpectedReports = 0; | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | if (_runningFromSnapshot) { | 
|  | // Asset directory has to be set only once when we switch from | 
|  | // running from snapshot to running from uploaded files. | 
|  | await device.resetAssetDirectory(); | 
|  | } | 
|  |  | 
|  | // List has one report per Flutter view. | 
|  | final List<Future<Map<String, dynamic>>> reports = device.reloadSources( | 
|  | entryPath, | 
|  | pause: pause | 
|  | ); | 
|  | countExpectedReports += reports.length; | 
|  | Future.wait(reports).catchError((dynamic error) { | 
|  | return <Map<String, dynamic>>[error]; | 
|  | }).then((List<Map<String, dynamic>> list) { | 
|  | // TODO(aam): Investigate why we are validating only first reload report, | 
|  | // which seems to be current behavior | 
|  | final Map<String, dynamic> firstReport = list.first; | 
|  | // Don't print errors because they will be printed further down when | 
|  | // `validateReloadReport` is called again. | 
|  | device.updateReloadStatus(validateReloadReport(firstReport, | 
|  | printErrors: false)); | 
|  | if (!retrieveFirstReloadReport.isCompleted) | 
|  | retrieveFirstReloadReport.complete(firstReport); | 
|  | }, onError: (dynamic error, StackTrace stack) { | 
|  | retrieveFirstReloadReport.completeError(error, stack); | 
|  | }); | 
|  | } | 
|  |  | 
|  | if (countExpectedReports == 0) { | 
|  | printError('Unable to hot reload. No instance of Flutter is currently running.'); | 
|  | return new OperationResult(1, 'No instances running'); | 
|  | } | 
|  | final Map<String, dynamic> reloadReport = await retrieveFirstReloadReport.future; | 
|  | if (!validateReloadReport(reloadReport)) { | 
|  | // Reload failed. | 
|  | flutterUsage.sendEvent('hot', 'reload-reject'); | 
|  | return new OperationResult(1, 'Reload rejected'); | 
|  | } else { | 
|  | flutterUsage.sendEvent('hot', 'reload'); | 
|  | final int loadedLibraryCount = reloadReport['details']['loadedLibraryCount']; | 
|  | final int finalLibraryCount = reloadReport['details']['finalLibraryCount']; | 
|  | printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries'); | 
|  | reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries'; | 
|  | } | 
|  | } on Map<String, dynamic> catch (error, st) { | 
|  | printError('Hot reload failed: $error\n$st'); | 
|  | final int errorCode = error['code']; | 
|  | final String errorMessage = error['message']; | 
|  | 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); | 
|  | } catch (error, st) { | 
|  | printError('Hot reload failed: $error\n$st'); | 
|  | return new OperationResult(1, '$error'); | 
|  | } | 
|  | // Record time it took for the VM to reload the sources. | 
|  | _addBenchmarkData('hotReloadVMReloadMilliseconds', | 
|  | vmReloadTimer.elapsed.inMilliseconds); | 
|  | final Stopwatch reassembleTimer = new Stopwatch()..start(); | 
|  | // Reload the isolate. | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | printTrace('Sending reload events to ${device.device.name}'); | 
|  | for (FlutterView view in device.views) { | 
|  | printTrace('Sending reload event to "${view.uiIsolate.name}"'); | 
|  | await view.uiIsolate.reload(); | 
|  | } | 
|  | await device.refreshViews(); | 
|  | } | 
|  | // We are now running from source. | 
|  | _runningFromSnapshot = false; | 
|  | // Check if the isolate is paused. | 
|  |  | 
|  | final List<FlutterView> reassembleViews = <FlutterView>[]; | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | for (FlutterView view in device.views) { | 
|  | // Check if the isolate is paused, and if so, don't reassemble. Ignore the | 
|  | // PostPauseEvent event - the client requesting the pause will resume the app. | 
|  | final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent; | 
|  | if (pauseEvent != null && pauseEvent.isPauseEvent && pauseEvent.kind != ServiceEvent.kPausePostRequest) { | 
|  | continue; | 
|  | } | 
|  | reassembleViews.add(view); | 
|  | } | 
|  | } | 
|  | if (reassembleViews.isEmpty) { | 
|  | printTrace('Skipping reassemble because all isolates are paused.'); | 
|  | return new OperationResult(OperationResult.ok.code, reloadMessage); | 
|  | } | 
|  | printTrace('Evicting dirty assets'); | 
|  | await _evictDirtyAssets(); | 
|  | printTrace('Reassembling application'); | 
|  | bool reassembleAndScheduleErrors = false; | 
|  | bool reassembleTimedOut = false; | 
|  | for (FlutterView view in reassembleViews) { | 
|  | try { | 
|  | await view.uiIsolate.flutterReassemble(); | 
|  | } on TimeoutException { | 
|  | reassembleTimedOut = true; | 
|  | printTrace('Reassembling ${view.uiIsolate.name} took too long.'); | 
|  | printStatus('Hot reloading ${view.uiIsolate.name} took too long; the reload may have failed.'); | 
|  | continue; | 
|  | } catch (error) { | 
|  | reassembleAndScheduleErrors = true; | 
|  | printError('Reassembling ${view.uiIsolate.name} failed: $error'); | 
|  | continue; | 
|  | } | 
|  | try { | 
|  | /* ensure that a frame is scheduled */ | 
|  | await view.uiIsolate.uiWindowScheduleFrame(); | 
|  | } catch (error) { | 
|  | reassembleAndScheduleErrors = true; | 
|  | printError('Scheduling a frame for ${view.uiIsolate.name} failed: $error'); | 
|  | } | 
|  | } | 
|  | // Record time it took for Flutter to reassemble the application. | 
|  | _addBenchmarkData('hotReloadFlutterReassembleMilliseconds', | 
|  | reassembleTimer.elapsed.inMilliseconds); | 
|  |  | 
|  | reloadTimer.stop(); | 
|  | printTrace('Hot reload performed in ' | 
|  | '${getElapsedAsMilliseconds(reloadTimer.elapsed)}.'); | 
|  | // Record complete time it took for the reload. | 
|  | _addBenchmarkData('hotReloadMillisecondsToFrame', | 
|  | reloadTimer.elapsed.inMilliseconds); | 
|  | // Only report timings if we reloaded a single view without any | 
|  | // errors or timeouts. | 
|  | if ((reassembleViews.length == 1) && | 
|  | !reassembleAndScheduleErrors && | 
|  | !reassembleTimedOut && | 
|  | shouldReportReloadTime) | 
|  | flutterUsage.sendTiming('hot', 'reload', reloadTimer.elapsed); | 
|  |  | 
|  | String unusedElementMessage; | 
|  | if (reportUnused && !reassembleAndScheduleErrors && !reassembleTimedOut) { | 
|  | final List<Future<List<ProgramElement>>> unusedReports = | 
|  | <Future<List<ProgramElement>>>[]; | 
|  | for (FlutterDevice device in flutterDevices) | 
|  | unusedReports.add(device.unusedChangesInLastReload()); | 
|  | final List<ProgramElement> unusedElements = <ProgramElement>[]; | 
|  | for (Future<List<ProgramElement>> unusedReport in unusedReports) | 
|  | unusedElements.addAll(await unusedReport); | 
|  |  | 
|  | if (unusedElements.isNotEmpty) { | 
|  | final String restartCommand = hostIsIde ? '' : ' (by pressing "R")'; | 
|  | unusedElementMessage = | 
|  | 'Some program elements were changed during reload but did not run when the view was reassembled;\n' | 
|  | 'you may need to restart the app$restartCommand for the changes to have an effect.'; | 
|  | for (ProgramElement unusedElement in unusedElements) { | 
|  | final String name = unusedElement.qualifiedName; | 
|  | final String path = _uriToRelativePath(unusedElement.uri); | 
|  | final int line = unusedElement.line; | 
|  | final String description = line == null ? '$name ($path)' : '$name ($path:$line)'; | 
|  | unusedElementMessage += '\n  • $description'; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | return new OperationResult( | 
|  | reassembleAndScheduleErrors ? 1 : OperationResult.ok.code, | 
|  | reloadMessage, | 
|  | hintMessage: unusedElementMessage, | 
|  | hintId: unusedElementMessage != null ? 'restartRecommended' : null, | 
|  | ); | 
|  | } | 
|  |  | 
|  | bool _isPaused() { | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | for (FlutterView view in device.views) { | 
|  | if (view.uiIsolate != null) { | 
|  | final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent; | 
|  | if (pauseEvent != null && pauseEvent.isPauseEvent) { | 
|  | return true; | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | return false; | 
|  | } | 
|  |  | 
|  | @override | 
|  | void printHelp({ @required bool details }) { | 
|  | const String fire = '🔥'; | 
|  | const String red = '\u001B[31m'; | 
|  | const String bold = '\u001B[0;1m'; | 
|  | const String reset = '\u001B[0m'; | 
|  | printStatus( | 
|  | '$fire  To hot reload your app on the fly, press "r". To restart the app entirely, press "R".', | 
|  | ansiAlternative: '$red$fire$bold  To hot reload your app on the fly, ' | 
|  | 'press "r". To restart the app entirely, press "R".$reset' | 
|  | ); | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | final String dname = device.device.name; | 
|  | for (Uri uri in device.observatoryUris) | 
|  | printStatus('An Observatory debugger and profiler on $dname is available at: $uri'); | 
|  | } | 
|  | if (details) { | 
|  | printHelpDetails(); | 
|  | printStatus('To repeat this help message, press "h". To quit, press "q".'); | 
|  | } else { | 
|  | printStatus('For a more detailed help message, press "h". To quit, press "q".'); | 
|  | } | 
|  | } | 
|  |  | 
|  | @override | 
|  | Future<Null> cleanupAfterSignal() async { | 
|  | await stopEchoingDeviceLog(); | 
|  | await stopApp(); | 
|  | } | 
|  |  | 
|  | @override | 
|  | Future<Null> preStop() => _cleanupDevFS(); | 
|  |  | 
|  | @override | 
|  | Future<Null> cleanupAtFinish() async { | 
|  | await _cleanupDevFS(); | 
|  | await stopEchoingDeviceLog(); | 
|  | } | 
|  | } |