|  | // 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/common.dart'; | 
|  | import 'base/context.dart'; | 
|  | import 'base/file_system.dart'; | 
|  | import 'base/logger.dart'; | 
|  | import 'base/platform.dart'; | 
|  | import 'base/terminal.dart'; | 
|  | import 'base/utils.dart'; | 
|  | import 'build_info.dart'; | 
|  | import 'compile.dart'; | 
|  | import 'convert.dart'; | 
|  | import 'devfs.dart'; | 
|  | import 'device.dart'; | 
|  | import 'globals.dart'; | 
|  | import 'resident_runner.dart'; | 
|  | import 'usage.dart'; | 
|  | import 'vmservice.dart'; | 
|  |  | 
|  | class HotRunnerConfig { | 
|  | /// Should the hot runner assume that the minimal Dart dependencies do not change? | 
|  | bool stableDartDependencies = false; | 
|  | /// A hook for implementations to perform any necessary initialization prior | 
|  | /// to a hot restart. Should return true if the hot restart should continue. | 
|  | Future<bool> setupHotRestart() async { | 
|  | return true; | 
|  | } | 
|  | /// A hook for implementations to perform any necessary operations right | 
|  | /// before the runner is about to be shut down. | 
|  | Future<void> runPreShutdownOperations() async { | 
|  | return; | 
|  | } | 
|  | } | 
|  |  | 
|  | HotRunnerConfig get hotRunnerConfig => context[HotRunnerConfig]; | 
|  |  | 
|  | const bool kHotReloadDefault = true; | 
|  |  | 
|  | class DeviceReloadReport { | 
|  | DeviceReloadReport(this.device, this.reports); | 
|  |  | 
|  | FlutterDevice device; | 
|  | List<Map<String, dynamic>> reports; // List has one report per Flutter view. | 
|  | } | 
|  |  | 
|  | // TODO(mklim): Test this, flutter/flutter#23031. | 
|  | 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 saveCompilationTrace = false, | 
|  | bool stayResident = true, | 
|  | bool ipv6 = false, | 
|  | }) : super(devices, | 
|  | target: target, | 
|  | debuggingOptions: debuggingOptions, | 
|  | usesTerminalUI: usesTerminalUI, | 
|  | projectRootPath: projectRootPath, | 
|  | packagesFilePath: packagesFilePath, | 
|  | saveCompilationTrace: saveCompilationTrace, | 
|  | stayResident: stayResident, | 
|  | ipv6: ipv6); | 
|  |  | 
|  | final bool benchmarkMode; | 
|  | final File applicationBinary; | 
|  | final bool hostIsIde; | 
|  | bool _didAttach = false; | 
|  | 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); | 
|  | } | 
|  |  | 
|  | Future<void> _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 rpc.RpcException( | 
|  | rpc_error_code.INTERNAL_ERROR, | 
|  | 'Unable to reload sources', | 
|  | ); | 
|  | } | 
|  | } | 
|  |  | 
|  | Future<void> _restartService({ bool pause = false }) async { | 
|  | final OperationResult result = | 
|  | await restart(fullRestart: true, pauseAfterRestart: pause); | 
|  | if (!result.isOk) { | 
|  | throw rpc.RpcException( | 
|  | rpc_error_code.INTERNAL_ERROR, | 
|  | 'Unable to restart', | 
|  | ); | 
|  | } | 
|  | } | 
|  |  | 
|  | Future<String> _compileExpressionService( | 
|  | String isolateId, | 
|  | String expression, | 
|  | List<String> definitions, | 
|  | List<String> typeDefinitions, | 
|  | String libraryUri, | 
|  | String klass, | 
|  | bool isStatic, | 
|  | ) async { | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | if (device.generator != null) { | 
|  | final CompilerOutput compilerOutput = | 
|  | await device.generator.compileExpression(expression, definitions, | 
|  | typeDefinitions, libraryUri, klass, isStatic); | 
|  | if (compilerOutput != null && compilerOutput.outputFilename != null) { | 
|  | return base64.encode(fs.file(compilerOutput.outputFilename).readAsBytesSync()); | 
|  | } | 
|  | } | 
|  | } | 
|  | throw 'Failed to compile $expression'; | 
|  | } | 
|  |  | 
|  | // Returns the exit code of the flutter tool process, like [run]. | 
|  | @override | 
|  | Future<int> attach({ | 
|  | Completer<DebugConnectionInfo> connectionInfoCompleter, | 
|  | Completer<void> appStartedCompleter, | 
|  | }) async { | 
|  | _didAttach = true; | 
|  | try { | 
|  | await connectToServiceProtocol( | 
|  | reloadSources: _reloadSourcesService, | 
|  | restart: _restartService, | 
|  | compileExpression: _compileExpressionService, | 
|  | ); | 
|  | } 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( | 
|  | 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 = Stopwatch()..start(); | 
|  | final UpdateFSReport devfsResult = await _updateDevFS(fullRestart: true); | 
|  | _addBenchmarkData( | 
|  | 'hotReloadInitialDevFSSyncMilliseconds', | 
|  | initialUpdateDevFSsTimer.elapsed.inMilliseconds, | 
|  | ); | 
|  | if (!devfsResult.success) | 
|  | 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, benchmarkMode: true); | 
|  | 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; | 
|  | } | 
|  |  | 
|  | int result = 0; | 
|  | if (stayResident) | 
|  | result = await waitForAppToFinish(); | 
|  | await cleanupAtFinish(); | 
|  | return result; | 
|  | } | 
|  |  | 
|  | @override | 
|  | Future<int> run({ | 
|  | Completer<DebugConnectionInfo> connectionInfoCompleter, | 
|  | Completer<void> 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; | 
|  | } | 
|  |  | 
|  | firstBuildTime = 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<void> handleTerminalCommand(String code) async { | 
|  | final String lower = code.toLowerCase(); | 
|  | if (lower == '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) { | 
|  | printStatus('Try again after fixing the above error(s).', emphasis: true); | 
|  | } | 
|  | } else if (lower == 'l') { | 
|  | final List<FlutterView> views = flutterDevices.expand((FlutterDevice d) => d.views).toList(); | 
|  | printStatus('Connected ${pluralize('view', views.length)}:'); | 
|  | for (FlutterView v in views) { | 
|  | printStatus('${v.uiIsolate.name} (${v.uiIsolate.id})', indent: 2); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | 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<UpdateFSReport> _updateDevFS({ bool fullRestart = false }) async { | 
|  | 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 UpdateFSReport(success: false); | 
|  | } | 
|  |  | 
|  | // Picking up first device's compiler as a source of truth - compilers | 
|  | // for all devices should be in sync. | 
|  | final List<Uri> invalidatedFiles = ProjectFileInvalidator.findInvalidated( | 
|  | lastCompiled: flutterDevices[0].devFS.lastCompiled, | 
|  | urisToMonitor: flutterDevices[0].devFS.sources, | 
|  | packagesPath: packagesFilePath, | 
|  | ); | 
|  | final UpdateFSReport results = UpdateFSReport(success: true); | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | results.incorporateResults(await device.updateDevFS( | 
|  | mainPath: mainPath, | 
|  | target: target, | 
|  | bundle: assetBundle, | 
|  | firstBuildTime: firstBuildTime, | 
|  | bundleFirstUpload: isFirstUpload, | 
|  | bundleDirty: isFirstUpload == false && rebuildBundle, | 
|  | fullRestart: fullRestart, | 
|  | projectRootPath: projectRootPath, | 
|  | pathToReload: getReloadPath(fullRestart: fullRestart), | 
|  | invalidatedFiles: invalidatedFiles, | 
|  | )); | 
|  | } | 
|  | return results; | 
|  | } | 
|  |  | 
|  | void _resetDirtyAssets() { | 
|  | for (FlutterDevice device in flutterDevices) | 
|  | device.devFS.assetPathsToEvict.clear(); | 
|  | } | 
|  |  | 
|  | Future<void> _cleanupDevFS() async { | 
|  | final List<Future<void>> futures = <Future<void>>[]; | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | if (device.devFS != null) { | 
|  | // Cleanup the devFS, but don't wait indefinitely. | 
|  | // We ignore any errors, because it's not clear what we would do anyway. | 
|  | futures.add(device.devFS.destroy() | 
|  | .timeout(const Duration(milliseconds: 250)) | 
|  | .catchError((dynamic error) { | 
|  | printTrace('Ignored error while cleaning up DevFS: $error'); | 
|  | })); | 
|  | } | 
|  | device.devFS = null; | 
|  | } | 
|  | await Future.wait(futures); | 
|  | } | 
|  |  | 
|  | Future<void> _launchInView( | 
|  | FlutterDevice device, | 
|  | Uri entryUri, | 
|  | Uri packagesUri, | 
|  | Uri assetsDirectoryUri, | 
|  | ) { | 
|  | final List<Future<void>> futures = <Future<void>>[]; | 
|  | for (FlutterView view in device.views) | 
|  | futures.add(view.runFromSource(entryUri, packagesUri, assetsDirectoryUri)); | 
|  | final Completer<void> completer = Completer<void>(); | 
|  | Future.wait(futures).whenComplete(() { completer.complete(null); }); | 
|  | return completer.future; | 
|  | } | 
|  |  | 
|  | Future<void> _launchFromDevFS(String mainScript) async { | 
|  | final String entryUri = fs.path.relative(mainScript, from: projectRootPath); | 
|  | final List<Future<void>> futures = <Future<void>>[]; | 
|  | 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())); | 
|  | futures.add(_launchInView(device, | 
|  | deviceEntryUri, | 
|  | devicePackagesUri, | 
|  | deviceAssetsDirectoryUri)); | 
|  | } | 
|  | await Future.wait(futures); | 
|  | if (benchmarkMode) { | 
|  | futures.clear(); | 
|  | for (FlutterDevice device in flutterDevices) | 
|  | for (FlutterView view in device.views) | 
|  | futures.add(view.flushUIThreadTasks()); | 
|  | await Future.wait(futures); | 
|  | } | 
|  |  | 
|  | } | 
|  |  | 
|  | Future<OperationResult> _restartFromSources({ String reason, bool benchmarkMode = false }) async { | 
|  | final Map<String, String> analyticsParameters = | 
|  | reason == null | 
|  | ? null | 
|  | : <String, String>{kEventReloadReasonParameterName: reason}; | 
|  |  | 
|  | if (!_isPaused()) { | 
|  | printTrace('Refreshing active FlutterViews before restarting.'); | 
|  | await refreshViews(); | 
|  | } | 
|  |  | 
|  | final Stopwatch restartTimer = Stopwatch()..start(); | 
|  | // TODO(aam): Add generator reset logic once we switch to using incremental | 
|  | // compiler for full application recompilation on restart. | 
|  | final UpdateFSReport updatedDevFS = await _updateDevFS(fullRestart: true); | 
|  | if (!updatedDevFS.success) { | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | if (device.generator != null) | 
|  | await device.generator.reject(); | 
|  | } | 
|  | return 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. | 
|  | final List<Future<void>> futures = <Future<void>>[]; | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | for (FlutterView view in device.views) { | 
|  | if (view.uiIsolate != null) { | 
|  | // Reload the isolate. | 
|  | final Completer<void> completer = Completer<void>(); | 
|  | futures.add(completer.future); | 
|  | unawaited(view.uiIsolate.reload().then( | 
|  | (ServiceObject _) { | 
|  | final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent; | 
|  | if ((pauseEvent != null) && pauseEvent.isPauseEvent) { | 
|  | // Resume the isolate so that it can be killed by the embedder. | 
|  | return view.uiIsolate.resume(); | 
|  | } | 
|  | }, | 
|  | ).whenComplete( | 
|  | () { completer.complete(null); }, | 
|  | )); | 
|  | } | 
|  | } | 
|  | } | 
|  | await Future.wait(futures); | 
|  | // We are now running from source. | 
|  | _runningFromSnapshot = false; | 
|  | await _launchFromDevFS(mainPath + '.dill'); | 
|  | restartTimer.stop(); | 
|  | printTrace('Hot restart performed in ${getElapsedAsMilliseconds(restartTimer.elapsed)}.'); | 
|  | // We are now running from sources. | 
|  | _runningFromSnapshot = false; | 
|  | _addBenchmarkData('hotRestartMillisecondsToFrame', | 
|  | restartTimer.elapsed.inMilliseconds); | 
|  | flutterUsage.sendEvent('hot', 'restart', parameters: analyticsParameters); | 
|  | flutterUsage.sendTiming('hot', 'restart', restartTimer.elapsed); | 
|  |  | 
|  | // In benchmark mode, make sure all stream notifications have finished. | 
|  | if (benchmarkMode) { | 
|  | final List<Future<void>> isolateNotifications = <Future<void>>[]; | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | for (FlutterView view in device.views) { | 
|  | isolateNotifications.add( | 
|  | view.owner.vm.vmService.onIsolateEvent.then((Stream<ServiceEvent> serviceEvents) async { | 
|  | await for (ServiceEvent serviceEvent in serviceEvents) { | 
|  | if (serviceEvent.owner.name.contains('_spawn') && serviceEvent.kind == ServiceEvent.kIsolateExit) { | 
|  | return; | 
|  | } | 
|  | } | 
|  | }), | 
|  | ); | 
|  | } | 
|  | } | 
|  | await Future.wait(isolateNotifications); | 
|  | } | 
|  | 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 == null) { | 
|  | if (printErrors) | 
|  | printError('Hot reload did not receive reload report.'); | 
|  | return false; | 
|  | } | 
|  | if (!(reloadReport['type'] == 'ReloadReport' && | 
|  | (reloadReport['success'] == true || | 
|  | (reloadReport['success'] == false && | 
|  | (reloadReport['details'] is Map<String, dynamic> && | 
|  | reloadReport['details']['notices'] is List<dynamic> && | 
|  | reloadReport['details']['notices'].isNotEmpty && | 
|  | reloadReport['details']['notices'].every( | 
|  | (dynamic item) => item is Map<String, dynamic> && item['message'] is String | 
|  | ) | 
|  | ) | 
|  | ) | 
|  | ) | 
|  | )) { | 
|  | 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, String reason, bool benchmarkMode = false }) async { | 
|  | final Stopwatch timer = Stopwatch()..start(); | 
|  | if (fullRestart) { | 
|  | if (!canHotRestart) { | 
|  | return OperationResult(1, 'hotRestart not supported'); | 
|  | } | 
|  | final Status status = logger.startProgress( | 
|  | 'Performing hot restart...', | 
|  | timeout: timeoutConfiguration.fastOperation, | 
|  | progressId: 'hot.restart', | 
|  | ); | 
|  | try { | 
|  | if (!(await hotRunnerConfig.setupHotRestart())) | 
|  | return OperationResult(1, 'setupHotRestart failed'); | 
|  | final OperationResult result = await _restartFromSources(reason: reason, benchmarkMode: benchmarkMode,); | 
|  | if (!result.isOk) | 
|  | return result; | 
|  | } finally { | 
|  | status.cancel(); | 
|  | } | 
|  | printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.'); | 
|  | return OperationResult.ok; | 
|  | } else { | 
|  | final bool reloadOnTopOfSnapshot = _runningFromSnapshot; | 
|  | final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing'; | 
|  | Status status = logger.startProgress( | 
|  | '$progressPrefix hot reload...', | 
|  | timeout: timeoutConfiguration.fastOperation, | 
|  | progressId: 'hot.reload', | 
|  | ); | 
|  | OperationResult result; | 
|  | bool showTime = true; | 
|  | try { | 
|  | result = await _reloadSources( | 
|  | pause: pauseAfterRestart, | 
|  | reason: reason, | 
|  | onSlow: (String message) { | 
|  | status?.cancel(); | 
|  | status = logger.startProgress( | 
|  | message, | 
|  | timeout: timeoutConfiguration.slowOperation, | 
|  | progressId: 'hot.reload', | 
|  | ); | 
|  | showTime = false; | 
|  | }, | 
|  | ); | 
|  | } finally { | 
|  | status.cancel(); | 
|  | } | 
|  | if (result.isOk) { | 
|  | if (showTime) { | 
|  | printStatus('${result.message} in ${getElapsedAsMilliseconds(timer.elapsed)}.'); | 
|  | } else { | 
|  | printStatus('${result.message}.'); | 
|  | } | 
|  | } | 
|  | if (result.hintMessage != null) | 
|  | printStatus('\n${result.hintMessage}'); | 
|  | return result; | 
|  | } | 
|  | } | 
|  |  | 
|  | Future<OperationResult> _reloadSources({ bool pause = false, String reason, void Function(String message) onSlow }) async { | 
|  | final Map<String, String> analyticsParameters = <String, String>{}; | 
|  | if (reason != null) { | 
|  | analyticsParameters[kEventReloadReasonParameterName] = reason; | 
|  | } | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | for (FlutterView view in device.views) { | 
|  | if (view.uiIsolate == null) | 
|  | throw 'Application isolate not found'; | 
|  | } | 
|  | } | 
|  |  | 
|  | // 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. | 
|  | bool shouldReportReloadTime = !_runningFromSnapshot; | 
|  | final Stopwatch reloadTimer = Stopwatch()..start(); | 
|  |  | 
|  | if (!_isPaused()) { | 
|  | printTrace('Refreshing active FlutterViews before reloading.'); | 
|  | await refreshViews(); | 
|  | } | 
|  |  | 
|  | final Stopwatch devFSTimer = Stopwatch()..start(); | 
|  | final UpdateFSReport updatedDevFS = await _updateDevFS(); | 
|  | // Record time it took to synchronize to DevFS. | 
|  | _addBenchmarkData('hotReloadDevFSSyncMilliseconds', devFSTimer.elapsed.inMilliseconds); | 
|  | if (!updatedDevFS.success) | 
|  | return OperationResult(1, 'DevFS synchronization failed'); | 
|  | String reloadMessage; | 
|  | final Stopwatch vmReloadTimer = Stopwatch()..start(); | 
|  | try { | 
|  | final String entryPath = fs.path.relative( | 
|  | getReloadPath(fullRestart: false), | 
|  | from: projectRootPath, | 
|  | ); | 
|  | final List<Future<DeviceReloadReport>> allReportsFutures = <Future<DeviceReloadReport>>[]; | 
|  | 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(); | 
|  | } | 
|  | final Completer<DeviceReloadReport> completer = Completer<DeviceReloadReport>(); | 
|  | allReportsFutures.add(completer.future); | 
|  | final List<Future<Map<String, dynamic>>> reportFutures = device.reloadSources( | 
|  | entryPath, pause: pause, | 
|  | ); | 
|  | unawaited(Future.wait(reportFutures).then( | 
|  | (List<Map<String, dynamic>> reports) async { | 
|  | // TODO(aam): Investigate why we are validating only first reload report, | 
|  | // which seems to be current behavior | 
|  | final Map<String, dynamic> firstReport = reports.first; | 
|  | // Don't print errors because they will be printed further down when | 
|  | // `validateReloadReport` is called again. | 
|  | await device.updateReloadStatus( | 
|  | validateReloadReport(firstReport, printErrors: false), | 
|  | ); | 
|  | completer.complete(DeviceReloadReport(device, reports)); | 
|  | }, | 
|  | )); | 
|  | } | 
|  | final List<DeviceReloadReport> reports = await Future.wait(allReportsFutures); | 
|  | for (DeviceReloadReport report in reports) { | 
|  | final Map<String, dynamic> reloadReport = report.reports[0]; | 
|  | if (!validateReloadReport(reloadReport)) { | 
|  | // Reload failed. | 
|  | flutterUsage.sendEvent('hot', 'reload-reject'); | 
|  | return OperationResult(1, 'Reload rejected'); | 
|  | } else { | 
|  | // Collect stats that help understand scale of update for this hot reload request. | 
|  | // For example, [syncedLibraryCount]/[finalLibraryCount] indicates how | 
|  | // many libraries were affected by the hot reload request. | 
|  | // Relation of [invalidatedSourcesCount] to [syncedLibraryCount] should help | 
|  | // understand sync/transfer "overhead" of updating this number of source files. | 
|  | final Map<String, dynamic> details = reloadReport['details']; | 
|  | analyticsParameters[kEventReloadFinalLibraryCount] = "${details['finalLibraryCount']}"; | 
|  | analyticsParameters[kEventReloadSyncedLibraryCount] = "${details['receivedLibraryCount']}"; | 
|  | analyticsParameters[kEventReloadSyncedClassesCount] = "${details['receivedClassesCount']}"; | 
|  | analyticsParameters[kEventReloadSyncedProceduresCount] = "${details['receivedProceduresCount']}"; | 
|  | analyticsParameters[kEventReloadSyncedBytes] = '${updatedDevFS.syncedBytes}'; | 
|  | analyticsParameters[kEventReloadInvalidatedSourcesCount] = '${updatedDevFS.invalidatedSourcesCount}'; | 
|  | analyticsParameters[kEventReloadTransferTimeInMs] = '${devFSTimer.elapsed.inMilliseconds}'; | 
|  | 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, stackTrace) { | 
|  | printTrace('Hot reload failed: $error\n$stackTrace'); | 
|  | final int errorCode = error['code']; | 
|  | String errorMessage = error['message']; | 
|  | if (errorCode == Isolate.kIsolateReloadBarred) { | 
|  | errorMessage = 'Unable to hot reload application due to an unrecoverable error in ' | 
|  | 'the source code. Please address the error and then use "R" to ' | 
|  | 'restart the app.\n' | 
|  | '$errorMessage (error code: $errorCode)'; | 
|  | flutterUsage.sendEvent('hot', 'reload-barred'); | 
|  | return OperationResult(errorCode, errorMessage); | 
|  | } | 
|  | return OperationResult(errorCode, '$errorMessage (error code: $errorCode)'); | 
|  | } catch (error, stackTrace) { | 
|  | printTrace('Hot reload failed: $error\n$stackTrace'); | 
|  | return OperationResult(1, '$error'); | 
|  | } | 
|  | // Record time it took for the VM to reload the sources. | 
|  | _addBenchmarkData('hotReloadVMReloadMilliseconds', vmReloadTimer.elapsed.inMilliseconds); | 
|  |  | 
|  | final Stopwatch reassembleTimer = Stopwatch()..start(); | 
|  | // Reload the isolate. | 
|  | final List<Future<void>> allDevices = <Future<void>>[]; | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | printTrace('Sending reload events to ${device.device.name}'); | 
|  | final List<Future<ServiceObject>> futuresViews = <Future<ServiceObject>>[]; | 
|  | for (FlutterView view in device.views) { | 
|  | printTrace('Sending reload event to "${view.uiIsolate.name}"'); | 
|  | futuresViews.add(view.uiIsolate.reload()); | 
|  | } | 
|  | final Completer<void> deviceCompleter = Completer<void>(); | 
|  | unawaited(Future.wait(futuresViews).whenComplete(() { | 
|  | deviceCompleter.complete(device.refreshViews()); | 
|  | })); | 
|  | allDevices.add(deviceCompleter.future); | 
|  | } | 
|  | await Future.wait(allDevices); | 
|  | // We are now running from source. | 
|  | _runningFromSnapshot = false; | 
|  | // Check if any isolates are paused. | 
|  | final List<FlutterView> reassembleViews = <FlutterView>[]; | 
|  | String serviceEventKind; | 
|  | int pausedIsolatesFound = 0; | 
|  | 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) { | 
|  | pausedIsolatesFound += 1; | 
|  | if (serviceEventKind == null) { | 
|  | serviceEventKind = pauseEvent.kind; | 
|  | } else if (serviceEventKind != pauseEvent.kind) { | 
|  | serviceEventKind = ''; // many kinds | 
|  | } | 
|  | } else { | 
|  | reassembleViews.add(view); | 
|  | } | 
|  | } | 
|  | } | 
|  | if (pausedIsolatesFound > 0) { | 
|  | if (onSlow != null) | 
|  | onSlow('${_describePausedIsolates(pausedIsolatesFound, serviceEventKind)}; interface might not update.'); | 
|  | if (reassembleViews.isEmpty) { | 
|  | printTrace('Skipping reassemble because all isolates are paused.'); | 
|  | return OperationResult(OperationResult.ok.code, reloadMessage); | 
|  | } | 
|  | } | 
|  | printTrace('Evicting dirty assets'); | 
|  | await _evictDirtyAssets(); | 
|  | assert(reassembleViews.isNotEmpty); | 
|  | printTrace('Reassembling application'); | 
|  | bool failedReassemble = false; | 
|  | final List<Future<void>> futures = <Future<void>>[]; | 
|  | for (FlutterView view in reassembleViews) { | 
|  | futures.add(() async { | 
|  | try { | 
|  | await view.uiIsolate.flutterReassemble(); | 
|  | } catch (error) { | 
|  | failedReassemble = true; | 
|  | printError('Reassembling ${view.uiIsolate.name} failed: $error'); | 
|  | return; | 
|  | } | 
|  | }()); | 
|  | } | 
|  | final Future<void> reassembleFuture = Future.wait<void>(futures).then<void>((List<void> values) { }); | 
|  | await reassembleFuture.timeout( | 
|  | const Duration(seconds: 2), | 
|  | onTimeout: () async { | 
|  | if (pausedIsolatesFound > 0) { | 
|  | shouldReportReloadTime = false; | 
|  | return; // probably no point waiting, they're probably deadlocked and we've already warned. | 
|  | } | 
|  | // Check if any isolate is newly paused. | 
|  | printTrace('This is taking a long time; will now check for paused isolates.'); | 
|  | int postReloadPausedIsolatesFound = 0; | 
|  | String serviceEventKind; | 
|  | for (FlutterView view in reassembleViews) { | 
|  | await view.uiIsolate.reload(); | 
|  | final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent; | 
|  | if (pauseEvent != null && pauseEvent.isPauseEvent) { | 
|  | postReloadPausedIsolatesFound += 1; | 
|  | if (serviceEventKind == null) { | 
|  | serviceEventKind = pauseEvent.kind; | 
|  | } else if (serviceEventKind != pauseEvent.kind) { | 
|  | serviceEventKind = ''; // many kinds | 
|  | } | 
|  | } | 
|  | } | 
|  | printTrace('Found $postReloadPausedIsolatesFound newly paused isolate(s).'); | 
|  | if (postReloadPausedIsolatesFound == 0) { | 
|  | await reassembleFuture; // must just be taking a long time... keep waiting! | 
|  | return; | 
|  | } | 
|  | shouldReportReloadTime = false; | 
|  | if (onSlow != null) | 
|  | onSlow('${_describePausedIsolates(postReloadPausedIsolatesFound, serviceEventKind)}.'); | 
|  | }, | 
|  | ); | 
|  | // Record time it took for Flutter to reassemble the application. | 
|  | _addBenchmarkData('hotReloadFlutterReassembleMilliseconds', reassembleTimer.elapsed.inMilliseconds); | 
|  |  | 
|  | reloadTimer.stop(); | 
|  | final Duration reloadDuration = reloadTimer.elapsed; | 
|  | final int reloadInMs = reloadDuration.inMilliseconds; | 
|  |  | 
|  | analyticsParameters[kEventReloadOverallTimeInMs] = '$reloadInMs'; | 
|  | flutterUsage.sendEvent('hot', 'reload', parameters: analyticsParameters); | 
|  |  | 
|  | if (shouldReportReloadTime) { | 
|  | printTrace('Hot reload performed in ${getElapsedAsMilliseconds(reloadDuration)}.'); | 
|  | // Record complete time it took for the reload. | 
|  | _addBenchmarkData('hotReloadMillisecondsToFrame', reloadInMs); | 
|  | } | 
|  | // Only report timings if we reloaded a single view without any errors. | 
|  | if ((reassembleViews.length == 1) && !failedReassemble && shouldReportReloadTime) | 
|  | flutterUsage.sendTiming('hot', 'reload', reloadDuration); | 
|  | return OperationResult( | 
|  | failedReassemble ? 1 : OperationResult.ok.code, | 
|  | reloadMessage, | 
|  | ); | 
|  | } | 
|  |  | 
|  | String _describePausedIsolates(int pausedIsolatesFound, String serviceEventKind) { | 
|  | assert(pausedIsolatesFound > 0); | 
|  | final StringBuffer message = StringBuffer(); | 
|  | bool plural; | 
|  | if (pausedIsolatesFound == 1) { | 
|  | if (flutterDevices.length == 1 && flutterDevices.single.views.length == 1) { | 
|  | message.write('The application is '); | 
|  | } else { | 
|  | message.write('An isolate is '); | 
|  | } | 
|  | plural = false; | 
|  | } else { | 
|  | message.write('$pausedIsolatesFound isolates are '); | 
|  | plural = true; | 
|  | } | 
|  | assert(serviceEventKind != null); | 
|  | switch (serviceEventKind) { | 
|  | case ServiceEvent.kPauseStart: message.write('paused (probably due to --start-paused)'); break; | 
|  | case ServiceEvent.kPauseExit: message.write('paused because ${ plural ? 'they have' : 'it has' } terminated'); break; | 
|  | case ServiceEvent.kPauseBreakpoint: message.write('paused in the debugger on a breakpoint'); break; | 
|  | case ServiceEvent.kPauseInterrupted: message.write('paused due in the debugger'); break; | 
|  | case ServiceEvent.kPauseException: message.write('paused in the debugger after an exception was thrown'); break; | 
|  | case ServiceEvent.kPausePostRequest: message.write('paused'); break; | 
|  | case '': message.write('paused for various reasons'); break; | 
|  | default: | 
|  | message.write('paused'); | 
|  | } | 
|  | return message.toString(); | 
|  | } | 
|  |  | 
|  | 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 = '🔥'; | 
|  | 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(rawMessage), | 
|  | TerminalColor.red, | 
|  | ); | 
|  | printStatus(message); | 
|  | 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'); | 
|  | } | 
|  | final String quitMessage = _didAttach | 
|  | ? 'To detach, press "d"; to quit, press "q".' | 
|  | : 'To quit, press "q".'; | 
|  | if (details) { | 
|  | printHelpDetails(); | 
|  | printStatus('To repeat this help message, press "h". $quitMessage'); | 
|  | } else { | 
|  | printStatus('For a more detailed help message, press "h". $quitMessage'); | 
|  | } | 
|  | } | 
|  |  | 
|  | Future<void> _evictDirtyAssets() { | 
|  | final List<Future<Map<String, dynamic>>> futures = <Future<Map<String, dynamic>>>[]; | 
|  | for (FlutterDevice device in flutterDevices) { | 
|  | if (device.devFS.assetPathsToEvict.isEmpty) | 
|  | continue; | 
|  | if (device.views.first.uiIsolate == null) { | 
|  | printError('Application isolate not found for $device'); | 
|  | continue; | 
|  | } | 
|  | for (String assetPath in device.devFS.assetPathsToEvict) { | 
|  | futures.add(device.views.first.uiIsolate.flutterEvictAsset(assetPath)); | 
|  | } | 
|  | device.devFS.assetPathsToEvict.clear(); | 
|  | } | 
|  | return Future.wait<Map<String, dynamic>>(futures); | 
|  | } | 
|  |  | 
|  | @override | 
|  | Future<void> cleanupAfterSignal() async { | 
|  | await stopEchoingDeviceLog(); | 
|  | await hotRunnerConfig.runPreShutdownOperations(); | 
|  | if (_didAttach) { | 
|  | appFinished(); | 
|  | } else { | 
|  | await stopApp(); | 
|  | } | 
|  | } | 
|  |  | 
|  | @override | 
|  | Future<void> preStop() async { | 
|  | await _cleanupDevFS(); | 
|  | await hotRunnerConfig.runPreShutdownOperations(); | 
|  | } | 
|  |  | 
|  | @override | 
|  | Future<void> cleanupAtFinish() async { | 
|  | await _cleanupDevFS(); | 
|  | await stopEchoingDeviceLog(); | 
|  | } | 
|  | } | 
|  |  | 
|  | class ProjectFileInvalidator { | 
|  | static const String _pubCachePathLinuxAndMac = '.pub-cache'; | 
|  | static const String _pubCachePathWindows = 'Pub/Cache'; | 
|  |  | 
|  | static List<Uri> findInvalidated({ | 
|  | @required DateTime lastCompiled, | 
|  | @required List<Uri> urisToMonitor, | 
|  | @required String packagesPath, | 
|  | }) { | 
|  | final List<Uri> invalidatedFiles = <Uri>[]; | 
|  | int scanned = 0; | 
|  | final Stopwatch stopwatch = Stopwatch()..start(); | 
|  | for (Uri uri in urisToMonitor) { | 
|  | if ((platform.isWindows && uri.path.contains(_pubCachePathWindows)) | 
|  | || uri.path.contains(_pubCachePathLinuxAndMac)) { | 
|  | // Don't watch pub cache directories to speed things up a little. | 
|  | continue; | 
|  | } | 
|  | final DateTime updatedAt = fs.statSync( | 
|  | uri.toFilePath(windows: platform.isWindows)).modified; | 
|  | scanned++; | 
|  | if (updatedAt == null) { | 
|  | continue; | 
|  | } | 
|  | if (updatedAt.millisecondsSinceEpoch > lastCompiled.millisecondsSinceEpoch) { | 
|  | invalidatedFiles.add(uri); | 
|  | } | 
|  | } | 
|  | // we need to check the .packages file too since it is not used in compilation. | 
|  | final DateTime packagesUpdatedAt = fs.statSync(packagesPath).modified; | 
|  | if (lastCompiled != null && packagesUpdatedAt != null | 
|  | && packagesUpdatedAt.millisecondsSinceEpoch > lastCompiled.millisecondsSinceEpoch) { | 
|  | invalidatedFiles.add(fs.file(packagesPath).uri); | 
|  | scanned++; | 
|  | } | 
|  | printTrace('Scanned through $scanned files in ${stopwatch.elapsedMilliseconds}ms'); | 
|  | return invalidatedFiles; | 
|  | } | 
|  | } |