// Copyright 2014 The Flutter 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:package_config/package_config.dart'; import 'package:vm_service/vm_service.dart' as vm_service; import 'package:meta/meta.dart'; import 'package:pool/pool.dart'; import 'base/async_guard.dart'; import 'base/context.dart'; import 'base/file_system.dart'; import 'base/logger.dart'; import 'base/platform.dart'; import 'base/utils.dart'; import 'build_info.dart'; import 'bundle.dart'; import 'compile.dart'; import 'convert.dart'; import 'dart/package_map.dart'; import 'devfs.dart'; import 'device.dart'; import 'globals.dart' as globals; import 'reporting/reporting.dart'; import 'resident_runner.dart'; import 'vmservice.dart'; ProjectFileInvalidator get projectFileInvalidator => context.get<ProjectFileInvalidator>() ?? _defaultInvalidator; final ProjectFileInvalidator _defaultInvalidator = ProjectFileInvalidator( fileSystem: globals.fs, platform: globals.platform, logger: globals.logger, ); HotRunnerConfig get hotRunnerConfig => context.get<HotRunnerConfig>(); class HotRunnerConfig { /// Should the hot runner assume that the minimal Dart dependencies do not change? bool stableDartDependencies = false; /// Whether the hot runner should scan for modified files asynchronously. bool asyncScanning = 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; } } const bool kHotReloadDefault = true; class DeviceReloadReport { DeviceReloadReport(this.device, this.reports); FlutterDevice device; List<vm_service.ReloadReport> 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, this.benchmarkMode = false, this.applicationBinary, this.hostIsIde = false, String projectRootPath, String packagesFilePath, String dillOutputPath, bool stayResident = true, bool ipv6 = false, }) : super(devices, target: target, debuggingOptions: debuggingOptions, projectRootPath: projectRootPath, packagesFilePath: packagesFilePath, stayResident: stayResident, hotMode: true, dillOutputPath: dillOutputPath, ipv6: ipv6); final bool benchmarkMode; final File applicationBinary; final bool hostIsIde; bool _didAttach = false; final Map<String, List<int>> benchmarkData = <String, List<int>>{}; DateTime firstBuildTime; bool _shouldResetAssetDirectory = true; 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(pause: pause); if (!result.isOk) { throw vm_service.RPCError( 'Unable to reload sources', RPCErrorCodes.kInternalError, '', ); } } Future<void> _restartService({ bool pause = false }) async { final OperationResult result = await restart(fullRestart: true, pause: pause); if (!result.isOk) { throw vm_service.RPCError( 'Unable to restart', RPCErrorCodes.kInternalError, '', ); } } Future<String> _compileExpressionService( String isolateId, String expression, List<String> definitions, List<String> typeDefinitions, String libraryUri, String klass, bool isStatic, ) async { for (final 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(globals.fs.file(compilerOutput.outputFilename).readAsBytesSync()); } } } throw 'Failed to compile $expression'; } @override Future<OperationResult> reloadMethod({ String libraryId, String classId }) async { final Stopwatch stopwatch = Stopwatch()..start(); final UpdateFSReport results = UpdateFSReport(success: true); final List<Uri> invalidated = <Uri>[Uri.parse(libraryId)]; final PackageConfig packageConfig = await loadPackageConfigWithLogging( globals.fs.file(globalPackagesPath), logger: globals.logger, ); for (final FlutterDevice device in flutterDevices) { results.incorporateResults(await device.updateDevFS( mainUri: globals.fs.file(mainPath).absolute.uri, target: target, bundle: assetBundle, firstBuildTime: firstBuildTime, bundleFirstUpload: false, bundleDirty: false, fullRestart: false, projectRootPath: projectRootPath, pathToReload: getReloadPath(fullRestart: false), invalidatedFiles: invalidated, packageConfig: packageConfig, dillOutputPath: dillOutputPath, )); } if (!results.success) { return OperationResult(1, 'Failed to compile'); } try { final String entryPath = globals.fs.path.relative( getReloadPath(fullRestart: false), from: projectRootPath, ); for (final FlutterDevice device in flutterDevices) { final List<Future<vm_service.ReloadReport>> reportFutures = await device.reloadSources( entryPath, pause: false, ); final List<vm_service.ReloadReport> reports = await Future.wait(reportFutures); final vm_service.ReloadReport firstReport = reports.first; await device.updateReloadStatus(validateReloadReport(firstReport.json, printErrors: false)); } } on Exception catch (error) { return OperationResult(1, error.toString()); } for (final FlutterDevice device in flutterDevices) { final List<FlutterView> views = await device.vmService.getFlutterViews(); for (final FlutterView view in views) { await device.vmService.flutterFastReassemble( classId, isolateId: view.uiIsolate.id, ); } } globals.printStatus('reloadMethod took ${stopwatch.elapsedMilliseconds}'); globals.flutterUsage.sendTiming('hot', 'ui', stopwatch.elapsed); return OperationResult.ok; } // 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, reloadMethod: reloadMethod, ); // Catches all exceptions, non-Exception objects are rethrown. } catch (error) { // ignore: avoid_catches_without_on_clauses if (error is! Exception && error is! String) { rethrow; } globals.printError('Error connecting to the service protocol: $error'); // https://github.com/flutter/flutter/issues/33050 // TODO(blasten): Remove this check once // https://issuetracker.google.com/issues/132325318 has been fixed. if (await hasDeviceRunningAndroidQ(flutterDevices) && error.toString().contains(kAndroidQHttpConnectionClosedExp)) { globals.printStatus( '🔨 If you are using an emulator running Android Q Beta, ' 'consider using an emulator running API level 29 or lower.', ); globals.printStatus( 'Learn more about the status of this issue on ' 'https://issuetracker.google.com/issues/132325318.', ); } return 2; } for (final FlutterDevice device in flutterDevices) { await device.initLogReader(); } try { final List<Uri> baseUris = await _initDevFS(); if (connectionInfoCompleter != null) { // Only handle one debugger connection. connectionInfoCompleter.complete( DebugConnectionInfo( httpUri: flutterDevices.first.vmService.httpAddress, wsUri: flutterDevices.first.vmService.wsAddress, baseUri: baseUris.first.toString(), ), ); } } on Exception catch (error) { globals.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; } for (final 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(); } final List<FlutterView> views = await device.vmService.getFlutterViews(); for (final FlutterView view in views) { globals.printTrace('Connected to $view.'); } } // In fast-start mode, apps are initialized from a placeholder splashscreen // app. We must do a restart here to load the program and assets for the // real app. if (debuggingOptions.fastStart) { await restart( fullRestart: true, benchmarkMode: !debuggingOptions.startPaused, reason: 'restart', silent: true, ); } appStartedCompleter?.complete(); if (benchmarkMode) { // We are running in benchmark mode. globals.printStatus('Running in benchmark mode.'); // Measure time to perform a hot restart. globals.printStatus('Benchmarking hot restart'); await restart(fullRestart: true, benchmarkMode: true); // Wait for notifications to finish. attempt to work around // timing issue caused by sentinel. await Future<void>.delayed(const Duration(seconds: 1)); globals.printStatus('Benchmarking hot reload'); // Measure time to perform a hot reload. await restart(fullRestart: false); if (stayResident) { await waitForAppToFinish(); } else { globals.printStatus('Benchmark completed. Exiting application.'); await _cleanupDevFS(); await stopEchoingDeviceLog(); await exitApp(); } final File benchmarkOutput = globals.fs.file('hot_benchmark.json'); benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData)); return 0; } writeVmserviceFile(); int result = 0; if (stayResident) { result = await waitForAppToFinish(); } await cleanupAtFinish(); return result; } @override Future<int> run({ Completer<DebugConnectionInfo> connectionInfoCompleter, Completer<void> appStartedCompleter, String route, }) async { if (!globals.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.'; } globals.printError(message); return 1; } firstBuildTime = DateTime.now(); final List<Future<bool>> startupTasks = <Future<bool>>[]; final PackageConfig packageConfig = await loadPackageConfigWithLogging( globals.fs.file(globalPackagesPath), logger: globals.logger, ); for (final FlutterDevice device in flutterDevices) { // Here we initialize the frontend_server concurrently with the platform // build, reducing overall initialization time. This is safe because the first // invocation of the frontend server produces a full dill file that the // subsequent invocation in devfs will not overwrite. if (device.generator != null) { startupTasks.add( device.generator.recompile( globals.fs.file(mainPath).uri, <Uri>[], outputPath: dillOutputPath ?? getDefaultApplicationKernelPath(trackWidgetCreation: debuggingOptions.buildInfo.trackWidgetCreation), packageConfig: packageConfig, ).then((CompilerOutput output) => output?.errorCount == 0) ); } startupTasks.add(device.runHot( hotRunner: this, route: route, ).then((int result) => result == 0)); } try { final List<bool> results = await Future.wait(startupTasks); if (!results.every((bool passed) => passed)) { return 1; } } on Exception catch (err) { globals.printError(err.toString()); return 1; } return attach( connectionInfoCompleter: connectionInfoCompleter, appStartedCompleter: appStartedCompleter, ); } Future<List<Uri>> _initDevFS() async { final String fsName = globals.fs.path.basename(projectRootPath); return <Uri>[ for (final FlutterDevice device in flutterDevices) await device.setupDevFS( fsName, globals.fs.directory(projectRootPath), packagesFilePath: packagesFilePath, ), ]; } Future<UpdateFSReport> _updateDevFS({ bool fullRestart = false }) async { final bool isFirstUpload = !assetBundle.wasBuiltOnce(); final bool rebuildBundle = assetBundle.needsBuild(); if (rebuildBundle) { globals.printTrace('Updating assets'); final int result = await assetBundle.build(); if (result != 0) { return UpdateFSReport(success: false); } } final InvalidationResult invalidationResult = await projectFileInvalidator.findInvalidated( lastCompiled: flutterDevices[0].devFS.lastCompiled, urisToMonitor: flutterDevices[0].devFS.sources, packagesPath: packagesFilePath, asyncScanning: hotRunnerConfig.asyncScanning, packageConfig: flutterDevices[0].devFS.lastPackageConfig, ); final UpdateFSReport results = UpdateFSReport(success: true); for (final FlutterDevice device in flutterDevices) { results.incorporateResults(await device.updateDevFS( mainUri: globals.fs.file(mainPath).absolute.uri, target: target, bundle: assetBundle, firstBuildTime: firstBuildTime, bundleFirstUpload: isFirstUpload, bundleDirty: !isFirstUpload && rebuildBundle, fullRestart: fullRestart, projectRootPath: projectRootPath, pathToReload: getReloadPath(fullRestart: fullRestart), invalidatedFiles: invalidationResult.uris, packageConfig: invalidationResult.packageConfig, dillOutputPath: dillOutputPath, )); } return results; } void _resetDirtyAssets() { for (final FlutterDevice device in flutterDevices) { device.devFS.assetPathsToEvict.clear(); } } Future<void> _cleanupDevFS() async { final List<Future<void>> futures = <Future<void>>[]; for (final 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) { globals.printTrace('Ignored error while cleaning up DevFS: $error'); })); } device.devFS = null; } await Future.wait(futures); } Future<void> _launchInView( FlutterDevice device, Uri main, Uri assetsDirectory, ) async { final List<FlutterView> views = await device.vmService.getFlutterViews(); await Future.wait(<Future<void>>[ for (final FlutterView view in views) device.vmService.runInView( viewId: view.id, main: main, assetsDirectory: assetsDirectory, ), ]); } Future<void> _launchFromDevFS(String mainScript) async { final String entryUri = globals.fs.path.relative(mainScript, from: projectRootPath); final List<Future<void>> futures = <Future<void>>[]; for (final FlutterDevice device in flutterDevices) { final Uri deviceEntryUri = device.devFS.baseUri.resolveUri( globals.fs.path.toUri(entryUri)); final Uri deviceAssetsDirectoryUri = device.devFS.baseUri.resolveUri( globals.fs.path.toUri(getAssetBuildDirectory())); futures.add(_launchInView(device, deviceEntryUri, deviceAssetsDirectoryUri)); } await Future.wait(futures); if (benchmarkMode) { futures.clear(); for (final FlutterDevice device in flutterDevices) { final List<FlutterView> views = await device.vmService.getFlutterViews(); for (final FlutterView view in views) { futures.add(device.vmService .flushUIThreadTasks(uiIsolateId: view.uiIsolate.id)); } } await Future.wait(futures); } } Future<OperationResult> _restartFromSources({ String reason, bool benchmarkMode = false, }) async { 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 (final FlutterDevice device in flutterDevices) { if (device.generator != null) { await device.generator.reject(); } } return OperationResult(1, 'DevFS synchronization failed'); } _resetDirtyAssets(); for (final 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>> operations = <Future<void>>[]; for (final FlutterDevice device in flutterDevices) { final Set<String> uiIsolatesIds = <String>{}; final List<FlutterView> views = await device.vmService.getFlutterViews(); for (final FlutterView view in views) { if (view.uiIsolate == null) { continue; } uiIsolatesIds.add(view.uiIsolate.id); // Reload the isolate. final Future<vm_service.Isolate> reloadIsolate = device.vmService .getIsolateOrNull(view.uiIsolate.id); operations.add(reloadIsolate.then((vm_service.Isolate isolate) async { if ((isolate != null) && isPauseEvent(isolate.pauseEvent.kind)) { // Resume the isolate so that it can be killed by the embedder. await device.vmService.resume(view.uiIsolate.id); } })); } // The engine handles killing and recreating isolates that it has spawned // ("uiIsolates"). The isolates that were spawned from these uiIsolates // will not be restared, and so they must be manually killed. final vm_service.VM vm = await device.vmService.getVM(); for (final vm_service.IsolateRef isolateRef in vm.isolates) { if (uiIsolatesIds.contains(isolateRef.id)) { continue; } operations.add(device.vmService.kill(isolateRef.id) .catchError((dynamic error, StackTrace stackTrace) { // Do nothing on a SentinelException since it means the isolate // has already been killed. }, test: (dynamic error) => error is vm_service.SentinelException)); } } await Future.wait(operations); await _launchFromDevFS(mainPath + '.dill'); restartTimer.stop(); globals.printTrace('Hot restart performed in ${getElapsedAsMilliseconds(restartTimer.elapsed)}.'); _addBenchmarkData('hotRestartMillisecondsToFrame', restartTimer.elapsed.inMilliseconds); // Send timing analytics. globals.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 (final FlutterDevice device in flutterDevices) { try { await device.vmService.streamListen('Isolate'); } on vm_service.RPCError { // Do nothing, we're already subcribed. } isolateNotifications.add( device.vmService.onIsolateEvent.firstWhere((vm_service.Event event) { return event.kind == vm_service.EventKind.kIsolateRunnable; }), ); } 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) { globals.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'] as List<dynamic>).isNotEmpty && (reloadReport['details']['notices'] as List<dynamic>).every( (dynamic item) => item is Map<String, dynamic> && item['message'] is String ) ) ) ) )) { if (printErrors) { globals.printError('Hot reload received invalid response: $reloadReport'); } return false; } if (!(reloadReport['success'] as bool)) { if (printErrors) { globals.printError('Hot reload was rejected:'); for (final Map<String, dynamic> notice in (reloadReport['details']['notices'] as List<dynamic>).cast<Map<String, dynamic>>()) { globals.printError('${notice['message']}'); } } return false; } return true; } @override bool get supportsRestart => true; @override Future<OperationResult> restart({ bool fullRestart = false, String reason, bool benchmarkMode = false, bool silent = false, bool pause = false, }) async { String targetPlatform; String sdkName; bool emulator; if (flutterDevices.length == 1) { final Device device = flutterDevices.first.device; targetPlatform = getNameForTargetPlatform(await device.targetPlatform); sdkName = await device.sdkNameAndVersion; emulator = await device.isLocalEmulator; } else if (flutterDevices.length > 1) { targetPlatform = 'multiple'; sdkName = 'multiple'; emulator = false; } else { targetPlatform = 'unknown'; sdkName = 'unknown'; emulator = false; } final Stopwatch timer = Stopwatch()..start(); if (fullRestart) { final OperationResult result = await _fullRestartHelper( targetPlatform: targetPlatform, sdkName: sdkName, emulator: emulator, reason: reason, benchmarkMode: benchmarkMode, silent: silent, ); if (!silent) { globals.printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.'); } return result; } final OperationResult result = await _hotReloadHelper( targetPlatform: targetPlatform, sdkName: sdkName, emulator: emulator, reason: reason, pause: pause, ); if (result.isOk) { final String elapsed = getElapsedAsMilliseconds(timer.elapsed); if (!silent) { globals.printStatus('${result.message} in $elapsed.'); } } return result; } Future<OperationResult> _fullRestartHelper({ String targetPlatform, String sdkName, bool emulator, String reason, bool benchmarkMode, bool silent, }) async { if (!canHotRestart) { return OperationResult(1, 'hotRestart not supported'); } Status status; if (!silent) { status = globals.logger.startProgress( 'Performing hot restart...', timeout: timeoutConfiguration.fastOperation, progressId: 'hot.restart', ); } OperationResult result; String restartEvent = 'restart'; try { if (!(await hotRunnerConfig.setupHotRestart())) { return OperationResult(1, 'setupHotRestart failed'); } // The current implementation of the vmservice and JSON rpc may throw // unhandled exceptions into the zone that cannot be caught with a regular // try catch. The usage is [asyncGuard] is required to normalize the error // handling, at least until we can refactor the underlying code. result = await asyncGuard(() => _restartFromSources( reason: reason, benchmarkMode: benchmarkMode, )); if (!result.isOk) { restartEvent = 'restart-failed'; } } on vm_service.SentinelException catch (err, st) { restartEvent = 'exception'; return OperationResult(1, 'hot restart failed to complete: $err\n$st', fatal: true); } on vm_service.RPCError catch (err, st) { restartEvent = 'exception'; return OperationResult(1, 'hot restart failed to complete: $err\n$st', fatal: true); } finally { HotEvent(restartEvent, targetPlatform: targetPlatform, sdkName: sdkName, emulator: emulator, fullRestart: true, reason: reason).send(); status?.cancel(); } return result; } Future<OperationResult> _hotReloadHelper({ String targetPlatform, String sdkName, bool emulator, String reason, bool pause, }) async { Status status = globals.logger.startProgress( 'Performing hot reload...', timeout: timeoutConfiguration.fastOperation, progressId: 'hot.reload', ); OperationResult result; try { result = await _reloadSources( targetPlatform: targetPlatform, sdkName: sdkName, emulator: emulator, reason: reason, pause: pause, onSlow: (String message) { status?.cancel(); status = globals.logger.startProgress( message, timeout: timeoutConfiguration.slowOperation, progressId: 'hot.reload', ); }, ); } on vm_service.RPCError { HotEvent('exception', targetPlatform: targetPlatform, sdkName: sdkName, emulator: emulator, fullRestart: false, reason: reason).send(); return OperationResult(1, 'hot reload failed to complete', fatal: true); } finally { status.cancel(); } return result; } Future<OperationResult> _reloadSources({ String targetPlatform, String sdkName, bool emulator, bool pause = false, String reason, void Function(String message) onSlow, }) async { for (final FlutterDevice device in flutterDevices) { final List<FlutterView> views = await device.vmService.getFlutterViews(); for (final FlutterView view in views) { if (view.uiIsolate == null) { return OperationResult(2, 'Application isolate not found', fatal: true); } } } final Stopwatch reloadTimer = Stopwatch()..start(); final Stopwatch devFSTimer = Stopwatch()..start(); final UpdateFSReport updatedDevFS = await _updateDevFS(); // Record time it took to synchronize to DevFS. bool shouldReportReloadTime = true; _addBenchmarkData('hotReloadDevFSSyncMilliseconds', devFSTimer.elapsed.inMilliseconds); if (!updatedDevFS.success) { return OperationResult(1, 'DevFS synchronization failed'); } String reloadMessage; final Stopwatch vmReloadTimer = Stopwatch()..start(); Map<String, dynamic> firstReloadDetails; try { final String entryPath = globals.fs.path.relative( getReloadPath(fullRestart: false), from: projectRootPath, ); final List<Future<DeviceReloadReport>> allReportsFutures = <Future<DeviceReloadReport>>[]; for (final FlutterDevice device in flutterDevices) { if (_shouldResetAssetDirectory) { // Asset directory has to be set only once when we switch from // running from bundle to uploaded files. await device.resetAssetDirectory(); _shouldResetAssetDirectory = false; } final List<Future<vm_service.ReloadReport>> reportFutures = await device.reloadSources( entryPath, pause: pause, ); allReportsFutures.add(Future.wait(reportFutures).then( (List<vm_service.ReloadReport> reports) async { // TODO(aam): Investigate why we are validating only first reload report, // which seems to be current behavior final vm_service.ReloadReport firstReport = reports.first; // Don't print errors because they will be printed further down when // `validateReloadReport` is called again. await device.updateReloadStatus( validateReloadReport(firstReport.json, printErrors: false), ); return DeviceReloadReport(device, reports); }, )); } final List<DeviceReloadReport> reports = await Future.wait(allReportsFutures); for (final DeviceReloadReport report in reports) { final vm_service.ReloadReport reloadReport = report.reports[0]; if (!validateReloadReport(reloadReport.json)) { // Reload failed. HotEvent('reload-reject', targetPlatform: targetPlatform, sdkName: sdkName, emulator: emulator, fullRestart: false, reason: reason, ).send(); return OperationResult(1, 'Reload rejected'); } // Collect stats only from the first device. If/when run -d all is // refactored, we'll probably need to send one hot reload/restart event // per device to analytics. firstReloadDetails ??= castStringKeyedMap(reloadReport.json['details']); final int loadedLibraryCount = reloadReport.json['details']['loadedLibraryCount'] as int; final int finalLibraryCount = reloadReport.json['details']['finalLibraryCount'] as int; globals.printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries'); reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries'; } } on Map<String, dynamic> catch (error, stackTrace) { globals.printTrace('Hot reload failed: $error\n$stackTrace'); final int errorCode = error['code'] as int; String errorMessage = error['message'] as String; if (errorCode == 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)'; HotEvent('reload-barred', targetPlatform: targetPlatform, sdkName: sdkName, emulator: emulator, fullRestart: false, reason: reason, ).send(); return OperationResult(errorCode, errorMessage); } return OperationResult(errorCode, '$errorMessage (error code: $errorCode)'); } on Exception catch (error, stackTrace) { globals.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(); await _evictDirtyAssets(); // Check if any isolates are paused and reassemble those // that aren't. final Map<FlutterView, vm_service.VmService> reassembleViews = <FlutterView, vm_service.VmService>{}; final List<Future<void>> reassembleFutures = <Future<void>>[]; String serviceEventKind; int pausedIsolatesFound = 0; bool failedReassemble = false; for (final FlutterDevice device in flutterDevices) { final List<FlutterView> views = await device.vmService.getFlutterViews(); for (final FlutterView view in 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 vm_service.Isolate isolate = await device.vmService .getIsolateOrNull(view.uiIsolate.id); final vm_service.Event pauseEvent = isolate?.pauseEvent; if (pauseEvent != null && isPauseEvent(pauseEvent.kind) && pauseEvent.kind != vm_service.EventKind.kPausePostRequest) { pausedIsolatesFound += 1; if (serviceEventKind == null) { serviceEventKind = pauseEvent.kind; } else if (serviceEventKind != pauseEvent.kind) { serviceEventKind = ''; // many kinds } } else { reassembleViews[view] = device.vmService; reassembleFutures.add(device.vmService.flutterReassemble( isolateId: view.uiIsolate.id, ).catchError((dynamic error) { failedReassemble = true; globals.printError('Reassembling ${view.uiIsolate.name} failed: $error'); }, test: (dynamic error) => error is Exception)); } } } if (pausedIsolatesFound > 0) { if (onSlow != null) { onSlow('${_describePausedIsolates(pausedIsolatesFound, serviceEventKind)}; interface might not update.'); } if (reassembleViews.isEmpty) { globals.printTrace('Skipping reassemble because all isolates are paused.'); return OperationResult(OperationResult.ok.code, reloadMessage); } } assert(reassembleViews.isNotEmpty); globals.printTrace('Reassembling application'); final Future<void> reassembleFuture = Future.wait<void>(reassembleFutures); 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. globals.printTrace('This is taking a long time; will now check for paused isolates.'); int postReloadPausedIsolatesFound = 0; String serviceEventKind; for (final FlutterView view in reassembleViews.keys) { final vm_service.Isolate isolate = await reassembleViews[view] .getIsolateOrNull(view.uiIsolate.id); if (isolate == null) { continue; } if (isolate.pauseEvent != null && isPauseEvent(isolate.pauseEvent.kind)) { postReloadPausedIsolatesFound += 1; if (serviceEventKind == null) { serviceEventKind = isolate.pauseEvent.kind; } else if (serviceEventKind != isolate.pauseEvent.kind) { serviceEventKind = ''; // many kinds } } } globals.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; // 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. HotEvent('reload', targetPlatform: targetPlatform, sdkName: sdkName, emulator: emulator, fullRestart: false, reason: reason, overallTimeInMs: reloadInMs, finalLibraryCount: firstReloadDetails['finalLibraryCount'] as int, syncedLibraryCount: firstReloadDetails['receivedLibraryCount'] as int, syncedClassesCount: firstReloadDetails['receivedClassesCount'] as int, syncedProceduresCount: firstReloadDetails['receivedProceduresCount'] as int, syncedBytes: updatedDevFS.syncedBytes, invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount, transferTimeInMs: devFSTimer.elapsed.inMilliseconds, ).send(); if (shouldReportReloadTime) { globals.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) { globals.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) { message.write('The application is '); plural = false; } else { message.write('$pausedIsolatesFound isolates are '); plural = true; } assert(serviceEventKind != null); switch (serviceEventKind) { case vm_service.EventKind.kPauseStart: message.write('paused (probably due to --start-paused)'); break; case vm_service.EventKind.kPauseExit: message.write('paused because ${ plural ? 'they have' : 'it has' } terminated'); break; case vm_service.EventKind.kPauseBreakpoint: message.write('paused in the debugger on a breakpoint'); break; case vm_service.EventKind.kPauseInterrupted: message.write('paused due in the debugger'); break; case vm_service.EventKind.kPauseException: message.write('paused in the debugger after an exception was thrown'); break; case vm_service.EventKind.kPausePostRequest: message.write('paused'); break; case '': message.write('paused for various reasons'); break; default: message.write('paused'); } return message.toString(); } @override void printHelp({ @required bool details }) { globals.printStatus('Flutter run key commands.'); commandHelp.r.print(); if (canHotRestart) { commandHelp.R.print(); } commandHelp.h.print(); if (_didAttach) { commandHelp.d.print(); } commandHelp.c.print(); commandHelp.q.print(); if (details) { printHelpDetails(); } for (final FlutterDevice device in flutterDevices) { final String dname = device.device.name; // Caution: This log line is parsed by device lab tests. globals.printStatus( 'An Observatory debugger and profiler on $dname is available at: ' '${device.vmService.httpAddress}', ); } } Future<void> _evictDirtyAssets() async { final List<Future<Map<String, dynamic>>> futures = <Future<Map<String, dynamic>>>[]; for (final FlutterDevice device in flutterDevices) { if (device.devFS.assetPathsToEvict.isEmpty) { continue; } final List<FlutterView> views = await device.vmService.getFlutterViews(); if (views.first.uiIsolate == null) { globals.printError('Application isolate not found for $device'); continue; } for (final String assetPath in device.devFS.assetPathsToEvict) { futures.add( device.vmService .flutterEvictAsset( assetPath, isolateId: views.first.uiIsolate.id, ) ); } 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 exitApp(); } } @override Future<void> preExit() async { await _cleanupDevFS(); await hotRunnerConfig.runPreShutdownOperations(); await super.preExit(); } @override Future<void> cleanupAtFinish() async { for (final FlutterDevice flutterDevice in flutterDevices) { await flutterDevice.device.dispose(); } await _cleanupDevFS(); await stopEchoingDeviceLog(); } } /// The result of an invalidation check from [ProjectFileInvalidator]. class InvalidationResult { const InvalidationResult({ this.uris, this.packageConfig, }); final List<Uri> uris; final PackageConfig packageConfig; } /// The [ProjectFileInvalidator] track the dependencies for a running /// application to determine when they are dirty. class ProjectFileInvalidator { ProjectFileInvalidator({ @required FileSystem fileSystem, @required Platform platform, @required Logger logger, }): _fileSystem = fileSystem, _platform = platform, _logger = logger; final FileSystem _fileSystem; final Platform _platform; final Logger _logger; static const String _pubCachePathLinuxAndMac = '.pub-cache'; static const String _pubCachePathWindows = 'Pub/Cache'; // As of writing, Dart supports up to 32 asynchronous I/O threads per // isolate. We also want to avoid hitting platform limits on open file // handles/descriptors. // // This value was chosen based on empirical tests scanning a set of // ~2000 files. static const int _kMaxPendingStats = 8; Future<InvalidationResult> findInvalidated({ @required DateTime lastCompiled, @required List<Uri> urisToMonitor, @required String packagesPath, @required PackageConfig packageConfig, bool asyncScanning = false, }) async { assert(urisToMonitor != null); assert(packagesPath != null); if (lastCompiled == null) { // Initial load. assert(urisToMonitor.isEmpty); return InvalidationResult( packageConfig: await _createPackageConfig(packagesPath), uris: <Uri>[] ); } final Stopwatch stopwatch = Stopwatch()..start(); final List<Uri> urisToScan = <Uri>[ // Don't watch pub cache directories to speed things up a little. for (final Uri uri in urisToMonitor) if (_isNotInPubCache(uri)) uri, ]; final List<Uri> invalidatedFiles = <Uri>[]; if (asyncScanning) { final Pool pool = Pool(_kMaxPendingStats); final List<Future<void>> waitList = <Future<void>>[]; for (final Uri uri in urisToScan) { waitList.add(pool.withResource<void>( () => _fileSystem .stat(uri.toFilePath(windows: _platform.isWindows)) .then((FileStat stat) { final DateTime updatedAt = stat.modified; if (updatedAt != null && updatedAt.isAfter(lastCompiled)) { invalidatedFiles.add(uri); } }) )); } await Future.wait<void>(waitList); } else { for (final Uri uri in urisToScan) { final DateTime updatedAt = _fileSystem.statSync( uri.toFilePath(windows: _platform.isWindows)).modified; if (updatedAt != null && updatedAt.isAfter(lastCompiled)) { invalidatedFiles.add(uri); } } } // We need to check the .packages file too since it is not used in compilation. final Uri packageUri = _fileSystem.file(packagesPath).uri; final DateTime updatedAt = _fileSystem.statSync( packageUri.toFilePath(windows: _platform.isWindows)).modified; if (updatedAt != null && updatedAt.isAfter(lastCompiled)) { invalidatedFiles.add(packageUri); packageConfig = await _createPackageConfig(packagesPath); // The frontend_server might be monitoring the package_config.json file, // Pub should always produce both files. // TODO(jonahwilliams): remove after https://github.com/flutter/flutter/issues/55249 if (_fileSystem.path.basename(packagesPath) == '.packages') { final File packageConfigFile = _fileSystem.file(packagesPath) .parent.childDirectory('.dart_tool') .childFile('package_config.json'); if (packageConfigFile.existsSync()) { invalidatedFiles.add(packageConfigFile.uri); } } } _logger.printTrace( 'Scanned through ${urisToScan.length} files in ' '${stopwatch.elapsedMilliseconds}ms' '${asyncScanning ? " (async)" : ""}', ); return InvalidationResult( packageConfig: packageConfig, uris: invalidatedFiles, ); } bool _isNotInPubCache(Uri uri) { return !(_platform.isWindows && uri.path.contains(_pubCachePathWindows)) && !uri.path.contains(_pubCachePathLinuxAndMac); } Future<PackageConfig> _createPackageConfig(String packagesPath) { return loadPackageConfigWithLogging( _fileSystem.file(globalPackagesPath), logger: _logger, ); } }