| // 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:meta/meta.dart'; |
| import 'package:package_config/package_config.dart'; |
| import 'package:pool/pool.dart'; |
| import 'package:vm_service/vm_service.dart' as vm_service; |
| |
| 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 'compile.dart'; |
| import 'convert.dart'; |
| import 'dart/package_map.dart'; |
| import 'devfs.dart'; |
| import 'device.dart'; |
| import 'globals.dart' as globals; |
| import 'native_assets.dart'; |
| import 'project.dart'; |
| import 'reporting/reporting.dart'; |
| import 'resident_runner.dart'; |
| import 'vmservice.dart'; |
| |
| ProjectFileInvalidator get projectFileInvalidator => context.get<ProjectFileInvalidator>() ?? 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 initialization prior |
| /// to a hot reload. Should return true if the hot restart should continue. |
| Future<bool?> setupHotReload() async { |
| return true; |
| } |
| |
| /// A hook for implementations to perform any necessary cleanup after the |
| /// devfs sync is complete. At this point the flutter_tools no longer needs to |
| /// access the source files and assets. |
| void updateDevFSComplete() {} |
| |
| /// 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. |
| } |
| |
| class HotRunner extends ResidentRunner { |
| HotRunner( |
| super.flutterDevices, { |
| required super.target, |
| required super.debuggingOptions, |
| this.benchmarkMode = false, |
| this.applicationBinary, |
| this.hostIsIde = false, |
| super.projectRootPath, |
| super.dillOutputPath, |
| super.stayResident, |
| bool super.ipv6 = false, |
| super.machine, |
| this.multidexEnabled = false, |
| super.devtoolsHandler, |
| StopwatchFactory stopwatchFactory = const StopwatchFactory(), |
| ReloadSourcesHelper reloadSourcesHelper = defaultReloadSourcesHelper, |
| ReassembleHelper reassembleHelper = _defaultReassembleHelper, |
| NativeAssetsBuildRunner? buildRunner, |
| }) : _stopwatchFactory = stopwatchFactory, |
| _reloadSourcesHelper = reloadSourcesHelper, |
| _reassembleHelper = reassembleHelper, |
| _buildRunner = buildRunner, |
| super( |
| hotMode: true, |
| ); |
| |
| final StopwatchFactory _stopwatchFactory; |
| final ReloadSourcesHelper _reloadSourcesHelper; |
| final ReassembleHelper _reassembleHelper; |
| |
| final bool benchmarkMode; |
| final File? applicationBinary; |
| final bool hostIsIde; |
| final bool multidexEnabled; |
| |
| /// When performing a hot restart, the tool needs to upload a new main.dart.dill to |
| /// each attached device's devfs. Replacing the existing file is not safe and does |
| /// not work at all on the windows embedder, because the old dill file will still be |
| /// memory-mapped by the embedder. To work around this issue, the tool will alternate |
| /// names for the uploaded dill, sometimes inserting `.swap`. Since the active dill will |
| /// never be replaced, there is no risk of writing the file while the embedder is attempting |
| /// to read from it. This also avoids filling up the devfs, if a incrementing counter was |
| /// used instead. |
| /// |
| /// This is only used for hot restart, incremental dills uploaded as part of the hot |
| /// reload process do not have this issue. |
| bool _swap = false; |
| |
| /// Whether the resident runner has correctly attached to the running application. |
| bool _didAttach = false; |
| |
| final Map<String, List<int>> benchmarkData = <String, List<int>>{}; |
| |
| DateTime? firstBuildTime; |
| |
| String? _targetPlatform; |
| String? _sdkName; |
| bool? _emulator; |
| |
| NativeAssetsBuildRunner? _buildRunner; |
| |
| Future<void> _calculateTargetPlatform() async { |
| if (_targetPlatform != null) { |
| return; |
| } |
| |
| 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; |
| } |
| } |
| |
| 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 { |
| 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> definitionTypes, |
| List<String> typeDefinitions, |
| List<String> typeBounds, |
| List<String> typeDefaults, |
| String libraryUri, |
| String? klass, |
| String? method, |
| bool isStatic, |
| ) async { |
| for (final FlutterDevice? device in flutterDevices) { |
| if (device!.generator != null) { |
| final CompilerOutput? compilerOutput = |
| await device.generator!.compileExpression(expression, definitions, |
| definitionTypes, typeDefinitions, typeBounds, typeDefaults, |
| libraryUri, klass, method, isStatic); |
| if (compilerOutput != null && compilerOutput.expressionData != null) { |
| return base64.encode(compilerOutput.expressionData!); |
| } |
| } |
| } |
| throw Exception('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, |
| bool allowExistingDdsInstance = false, |
| bool enableDevTools = false, |
| bool needsFullRestart = true, |
| }) async { |
| _didAttach = true; |
| try { |
| await connectToServiceProtocol( |
| reloadSources: _reloadSourcesService, |
| restart: _restartService, |
| compileExpression: _compileExpressionService, |
| getSkSLMethod: writeSkSL, |
| allowExistingDdsInstance: allowExistingDdsInstance, |
| ); |
| // 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'); |
| return 2; |
| } |
| |
| if (debuggingOptions.serveObservatory) { |
| await enableObservatory(); |
| } |
| |
| if (enableDevTools) { |
| // The method below is guaranteed never to return a failing future. |
| unawaited(residentDevtoolsHandler!.serveAndAnnounceDevTools( |
| devToolsServerAddress: debuggingOptions.devToolsServerAddress, |
| flutterDevices: flutterDevices, |
| isStartPaused: debuggingOptions.startPaused, |
| )); |
| } |
| |
| for (final FlutterDevice? device in flutterDevices) { |
| await device!.initLogReader(); |
| device |
| .developmentShaderCompiler |
| .configureCompiler( |
| device.targetPlatform, |
| impellerStatus: debuggingOptions.enableImpeller, |
| ); |
| } |
| 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 DevFSException catch (error) { |
| globals.printError('Error initializing DevFS: $error'); |
| return 3; |
| } |
| |
| final Stopwatch initialUpdateDevFSsTimer = Stopwatch()..start(); |
| final UpdateFSReport devfsResult = await _updateDevFS(fullRestart: needsFullRestart); |
| _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, |
| reason: 'restart', |
| silent: true, |
| ); |
| } |
| |
| appStartedCompleter?.complete(); |
| |
| if (benchmarkMode) { |
| // Wait multiple seconds for the isolate to have fully started. |
| await Future<void>.delayed(const Duration(seconds: 10)); |
| // 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); |
| // Wait multiple seconds to stabilize benchmark on slower device lab hardware. |
| // Hot restart finishes when the new isolate is started, not when the new isolate |
| // is ready. This process can actually take multiple seconds. |
| await Future<void>.delayed(const Duration(seconds: 10)); |
| |
| globals.printStatus('Benchmarking hot reload'); |
| // Measure time to perform a hot reload. |
| await restart(); |
| 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, |
| bool enableDevTools = false, |
| String? route, |
| }) async { |
| await _calculateTargetPlatform(); |
| |
| final Uri projectUri = Uri.directory(projectRootPath); |
| _buildRunner ??= NativeAssetsBuildRunnerImpl( |
| projectUri, |
| debuggingOptions.buildInfo.packageConfig, |
| fileSystem, |
| globals.logger, |
| ); |
| final Uri? nativeAssetsYaml = await dryRunNativeAssets( |
| projectUri: projectUri, |
| fileSystem: fileSystem, |
| buildRunner: _buildRunner!, |
| flutterDevices: flutterDevices, |
| ); |
| |
| final Stopwatch appStartedTimer = Stopwatch()..start(); |
| final File mainFile = globals.fs.file(mainPath); |
| firstBuildTime = DateTime.now(); |
| |
| Duration totalCompileTime = Duration.zero; |
| Duration totalLaunchAppTime = Duration.zero; |
| |
| final List<Future<bool>> startupTasks = <Future<bool>>[]; |
| 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. |
| await runSourceGenerators(); |
| if (device!.generator != null) { |
| final Stopwatch compileTimer = Stopwatch()..start(); |
| startupTasks.add( |
| device.generator!.recompile( |
| mainFile.uri, |
| <Uri>[], |
| // When running without a provided applicationBinary, the tool will |
| // simultaneously run the initial frontend_server compilation and |
| // the native build step. If there is a Dart compilation error, it |
| // should only be displayed once. |
| suppressErrors: applicationBinary == null, |
| checkDartPluginRegistry: true, |
| dartPluginRegistrant: FlutterProject.current().dartPluginRegistrant, |
| outputPath: dillOutputPath, |
| packageConfig: debuggingOptions.buildInfo.packageConfig, |
| projectRootPath: FlutterProject.current().directory.absolute.path, |
| fs: globals.fs, |
| nativeAssetsYaml: nativeAssetsYaml, |
| ).then((CompilerOutput? output) { |
| compileTimer.stop(); |
| totalCompileTime += compileTimer.elapsed; |
| return output?.errorCount == 0; |
| }) |
| ); |
| } |
| |
| final Stopwatch launchAppTimer = Stopwatch()..start(); |
| startupTasks.add(device.runHot( |
| hotRunner: this, |
| route: route, |
| ).then((int result) { |
| totalLaunchAppTime += launchAppTimer.elapsed; |
| return result == 0; |
| })); |
| } |
| |
| unawaited(appStartedCompleter?.future.then((_) => HotEvent('reload-ready', |
| targetPlatform: _targetPlatform!, |
| sdkName: _sdkName!, |
| emulator: _emulator!, |
| fullRestart: false, |
| overallTimeInMs: appStartedTimer.elapsed.inMilliseconds, |
| compileTimeInMs: totalCompileTime.inMilliseconds, |
| transferTimeInMs: totalLaunchAppTime.inMilliseconds, |
| ).send())); |
| |
| try { |
| final List<bool> results = await Future.wait(startupTasks); |
| if (!results.every((bool passed) => passed)) { |
| appFailedToStart(); |
| return 1; |
| } |
| cacheInitialDillCompilation(); |
| } on Exception catch (err) { |
| globals.printError(err.toString()); |
| appFailedToStart(); |
| return 1; |
| } |
| |
| return attach( |
| connectionInfoCompleter: connectionInfoCompleter, |
| appStartedCompleter: appStartedCompleter, |
| enableDevTools: enableDevTools, |
| needsFullRestart: false, |
| ); |
| } |
| |
| 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), |
| ), |
| ]; |
| } |
| |
| 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(packagesPath: '.packages'); |
| if (result != 0) { |
| return UpdateFSReport(); |
| } |
| } |
| |
| final Stopwatch findInvalidationTimer = _stopwatchFactory.createStopwatch('updateDevFS')..start(); |
| final DevFS devFS = flutterDevices[0].devFS!; |
| final InvalidationResult invalidationResult = await projectFileInvalidator.findInvalidated( |
| lastCompiled: devFS.lastCompiled, |
| urisToMonitor: devFS.sources, |
| packagesPath: packagesFilePath, |
| asyncScanning: hotRunnerConfig!.asyncScanning, |
| packageConfig: devFS.lastPackageConfig |
| ?? debuggingOptions.buildInfo.packageConfig, |
| ); |
| findInvalidationTimer.stop(); |
| final File entrypointFile = globals.fs.file(mainPath); |
| if (!entrypointFile.existsSync()) { |
| globals.printError( |
| 'The entrypoint file (i.e. the file with main()) ${entrypointFile.path} ' |
| 'cannot be found. Moving or renaming this file will prevent changes to ' |
| 'its contents from being discovered during hot reload/restart until ' |
| 'flutter is restarted or the file is restored.' |
| ); |
| } |
| final UpdateFSReport results = UpdateFSReport( |
| success: true, |
| scannedSourcesCount: devFS.sources.length, |
| findInvalidatedDuration: findInvalidationTimer.elapsed, |
| ); |
| for (final FlutterDevice? device in flutterDevices) { |
| results.incorporateResults(await device!.updateDevFS( |
| mainUri: entrypointFile.absolute.uri, |
| target: target, |
| bundle: assetBundle, |
| firstBuildTime: firstBuildTime, |
| bundleFirstUpload: isFirstUpload, |
| bundleDirty: !isFirstUpload && rebuildBundle, |
| fullRestart: fullRestart, |
| projectRootPath: projectRootPath, |
| pathToReload: getReloadPath(fullRestart: fullRestart, swap: _swap), |
| invalidatedFiles: invalidationResult.uris!, |
| packageConfig: invalidationResult.packageConfig!, |
| dillOutputPath: dillOutputPath, |
| )); |
| } |
| return results; |
| } |
| |
| void _resetDirtyAssets() { |
| for (final FlutterDevice device in flutterDevices) { |
| final DevFS? devFS = device.devFS; |
| if (devFS == null) { |
| // This is sometimes null, however we don't know why and have not been |
| // able to reproduce, https://github.com/flutter/flutter/issues/108653 |
| continue; |
| } |
| devFS.assetPathsToEvict.clear(); |
| devFS.shaderPathsToEvict.clear(); |
| devFS.scenePathsToEvict.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)) |
| .then<void>( |
| (Object? _) {}, |
| onError: (Object? error, StackTrace stackTrace) { |
| globals.printTrace('Ignored error while cleaning up DevFS: $error\n$stackTrace'); |
| } |
| ), |
| ); |
| } |
| 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() async { |
| final List<Future<void>> futures = <Future<void>>[]; |
| for (final FlutterDevice? device in flutterDevices) { |
| final Uri deviceEntryUri = device!.devFS!.baseUri!.resolve(_swap ? 'main.dart.swap.dill' : 'main.dart.dill'); |
| final Uri deviceAssetsDirectoryUri = device.devFS!.baseUri!.resolveUri( |
| globals.fs.path.toUri(getAssetBuildDirectory())); |
| futures.add(_launchInView(device, |
| deviceEntryUri, |
| deviceAssetsDirectoryUri)); |
| } |
| await Future.wait(futures); |
| } |
| |
| Future<OperationResult> _restartFromSources({ |
| String? reason, |
| }) async { |
| final Stopwatch restartTimer = Stopwatch()..start(); |
| UpdateFSReport updatedDevFS; |
| try { |
| updatedDevFS = await _updateDevFS(fullRestart: true); |
| } finally { |
| hotRunnerConfig!.updateDevFSComplete(); |
| } |
| 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!)) { |
| // The embedder requires that the isolate is unpaused, because the |
| // runInView method requires interaction with dart engine APIs that |
| // are not thread-safe, and thus must be run on the same thread that |
| // would be blocked by the pause. Simply un-pausing is not sufficient, |
| // because this does not prevent the isolate from immediately hitting |
| // a breakpoint (for example if the breakpoint was placed in a loop |
| // or in a frequently called method) or an exception. Instead, all |
| // breakpoints are first disabled and exception pause mode set to |
| // None, and then the isolate resumed. |
| // These settings to not need restoring as Hot Restart results in |
| // new isolates, which will be configured by the editor as they are |
| // started. |
| final List<Future<void>> breakpointAndExceptionRemoval = <Future<void>>[ |
| device.vmService!.service.setIsolatePauseMode(isolate.id!, |
| exceptionPauseMode: vm_service.ExceptionPauseMode.kNone), |
| for (final vm_service.Breakpoint breakpoint in isolate.breakpoints!) |
| device.vmService!.service.removeBreakpoint(isolate.id!, breakpoint.id!), |
| ]; |
| await Future.wait(breakpointAndExceptionRemoval); |
| await device.vmService!.service.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 restarted, and so they must be manually killed. |
| final vm_service.VM vm = await device.vmService!.service.getVM(); |
| for (final vm_service.IsolateRef isolateRef in vm.isolates!) { |
| if (uiIsolatesIds.contains(isolateRef.id)) { |
| continue; |
| } |
| operations.add( |
| device.vmService!.service.kill(isolateRef.id!) |
| // Since we never check the value of this Future, only await its |
| // completion, make its type nullable so we can return null when |
| // catching errors. |
| .then<vm_service.Success?>( |
| (vm_service.Success success) => success, |
| onError: (Object error, StackTrace stackTrace) { |
| if (error is vm_service.SentinelException || |
| (error is vm_service.RPCError && error.code == 105)) { |
| // Do nothing on a SentinelException since it means the isolate |
| // has already been killed. |
| // Error code 105 indicates the isolate is not yet runnable, and might |
| // be triggered if the tool is attempting to kill the asset parsing |
| // isolate before it has finished starting up. |
| return null; |
| } |
| return Future<vm_service.Success?>.error(error, stackTrace); |
| }, |
| ), |
| ); |
| } |
| } |
| await Future.wait(operations); |
| |
| await _launchFromDevFS(); |
| 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); |
| |
| // Toggle the main dill name after successfully uploading. |
| _swap =! _swap; |
| |
| return OperationResult( |
| OperationResult.ok.code, |
| OperationResult.ok.message, |
| updateFSReport: updatedDevFS, |
| ); |
| } |
| |
| /// Returns [true] if the reload was successful. |
| /// Prints errors if [printErrors] is [true]. |
| static bool validateReloadReport( |
| vm_service.ReloadReport? reloadReport, { |
| bool printErrors = true, |
| }) { |
| if (reloadReport == null) { |
| if (printErrors) { |
| globals.printError('Hot reload did not receive reload report.'); |
| } |
| return false; |
| } |
| final ReloadReportContents contents = ReloadReportContents.fromReloadReport(reloadReport); |
| if (!reloadReport.success!) { |
| if (printErrors) { |
| globals.printError('Hot reload was rejected:'); |
| for (final ReasonForCancelling reason in contents.notices) { |
| globals.printError(reason.toString()); |
| } |
| } |
| return false; |
| } |
| return true; |
| } |
| |
| @override |
| Future<OperationResult> restart({ |
| bool fullRestart = false, |
| String? reason, |
| bool silent = false, |
| bool pause = false, |
| }) async { |
| if (flutterDevices.any((FlutterDevice? device) => device!.devFS == null)) { |
| return OperationResult(1, 'Device initialization has not completed.'); |
| } |
| await _calculateTargetPlatform(); |
| final Stopwatch timer = Stopwatch()..start(); |
| |
| // Run source generation if needed. |
| await runSourceGenerators(); |
| |
| if (fullRestart) { |
| final OperationResult result = await _fullRestartHelper( |
| targetPlatform: _targetPlatform, |
| sdkName: _sdkName, |
| emulator: _emulator, |
| reason: reason, |
| silent: silent, |
| ); |
| if (!silent) { |
| globals.printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.'); |
| } |
| unawaited(residentDevtoolsHandler!.hotRestart(flutterDevices)); |
| 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) { |
| if (result.extraTimings.isNotEmpty) { |
| final String extraTimingsString = result.extraTimings |
| .map((OperationResultExtraTiming e) => '${e.description}: ${e.timeInMs} ms') |
| .join(', '); |
| globals.printStatus('${result.message} in $elapsed ($extraTimingsString).'); |
| } else { |
| globals.printStatus('${result.message} in $elapsed.'); |
| } |
| } |
| } |
| return result; |
| } |
| |
| Future<OperationResult> _fullRestartHelper({ |
| String? targetPlatform, |
| String? sdkName, |
| bool? emulator, |
| String? reason, |
| bool? silent, |
| }) async { |
| if (!supportsRestart) { |
| return OperationResult(1, 'hotRestart not supported'); |
| } |
| Status? status; |
| if (!silent!) { |
| status = globals.logger.startProgress( |
| 'Performing hot restart...', |
| progressId: 'hot.restart', |
| ); |
| } |
| OperationResult result; |
| String? restartEvent; |
| try { |
| final Stopwatch restartTimer = _stopwatchFactory.createStopwatch('fullRestartHelper')..start(); |
| if ((await hotRunnerConfig!.setupHotRestart()) != true) { |
| return OperationResult(1, 'setupHotRestart failed'); |
| } |
| result = await _restartFromSources(reason: reason); |
| restartTimer.stop(); |
| if (!result.isOk) { |
| restartEvent = 'restart-failed'; |
| } else { |
| HotEvent('restart', |
| targetPlatform: targetPlatform!, |
| sdkName: sdkName!, |
| emulator: emulator!, |
| fullRestart: true, |
| reason: reason, |
| overallTimeInMs: restartTimer.elapsed.inMilliseconds, |
| syncedBytes: result.updateFSReport?.syncedBytes, |
| invalidatedSourcesCount: result.updateFSReport?.invalidatedSourcesCount, |
| transferTimeInMs: result.updateFSReport?.transferDuration.inMilliseconds, |
| compileTimeInMs: result.updateFSReport?.compileDuration.inMilliseconds, |
| findInvalidatedTimeInMs: result.updateFSReport?.findInvalidatedDuration.inMilliseconds, |
| scannedSourcesCount: result.updateFSReport?.scannedSourcesCount, |
| ).send(); |
| } |
| } 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 { |
| // The `restartEvent` variable will be null if restart succeeded. We will |
| // only handle the case when it failed here. |
| if (restartEvent != null) { |
| 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...', |
| 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, |
| progressId: 'hot.reload', |
| ); |
| }, |
| ); |
| } on vm_service.RPCError catch (error) { |
| String errorMessage = 'hot reload failed to complete'; |
| int errorCode = 1; |
| if (error.code == kIsolateReloadBarred) { |
| errorCode = error.code; |
| 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' |
| '${error.message} (error code: ${error.code})'; |
| HotEvent('reload-barred', |
| targetPlatform: targetPlatform!, |
| sdkName: sdkName!, |
| emulator: emulator!, |
| fullRestart: false, |
| reason: reason, |
| ).send(); |
| } else { |
| HotEvent('exception', |
| targetPlatform: targetPlatform!, |
| sdkName: sdkName!, |
| emulator: emulator!, |
| fullRestart: false, |
| reason: reason, |
| ).send(); |
| } |
| return OperationResult(errorCode, errorMessage, 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 { |
| final Map<FlutterDevice?, List<FlutterView>> viewCache = <FlutterDevice?, List<FlutterView>>{}; |
| for (final FlutterDevice? device in flutterDevices) { |
| final List<FlutterView> views = await device!.vmService!.getFlutterViews(); |
| viewCache[device] = views; |
| for (final FlutterView view in views) { |
| if (view.uiIsolate == null) { |
| return OperationResult(2, 'Application isolate not found', fatal: true); |
| } |
| } |
| } |
| |
| final Stopwatch reloadTimer = _stopwatchFactory.createStopwatch('reloadSources:reload')..start(); |
| if ((await hotRunnerConfig!.setupHotReload()) != true) { |
| return OperationResult(1, 'setupHotReload failed'); |
| } |
| final Stopwatch devFSTimer = Stopwatch()..start(); |
| UpdateFSReport updatedDevFS; |
| try { |
| updatedDevFS= await _updateDevFS(); |
| } finally { |
| hotRunnerConfig!.updateDevFSComplete(); |
| } |
| // 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'); |
| } |
| |
| final List<OperationResultExtraTiming> extraTimings = <OperationResultExtraTiming>[]; |
| extraTimings.add(OperationResultExtraTiming('compile', updatedDevFS.compileDuration.inMilliseconds)); |
| |
| String reloadMessage = 'Reloaded 0 libraries'; |
| final Stopwatch reloadVMTimer = _stopwatchFactory.createStopwatch('reloadSources:vm')..start(); |
| final Map<String, Object?> firstReloadDetails = <String, Object?>{}; |
| if (updatedDevFS.invalidatedSourcesCount > 0) { |
| final OperationResult result = await _reloadSourcesHelper( |
| this, |
| flutterDevices, |
| pause, |
| firstReloadDetails, |
| targetPlatform, |
| sdkName, |
| emulator, |
| reason, |
| globals.flutterUsage, |
| ); |
| if (result.code != 0) { |
| return result; |
| } |
| reloadMessage = result.message; |
| } else { |
| _addBenchmarkData('hotReloadVMReloadMilliseconds', 0); |
| } |
| reloadVMTimer.stop(); |
| extraTimings.add(OperationResultExtraTiming('reload', reloadVMTimer.elapsedMilliseconds)); |
| |
| await evictDirtyAssets(); |
| |
| final Stopwatch reassembleTimer = _stopwatchFactory.createStopwatch('reloadSources:reassemble')..start(); |
| |
| final ReassembleResult reassembleResult = await _reassembleHelper( |
| flutterDevices, |
| viewCache, |
| onSlow, |
| reloadMessage, |
| ); |
| shouldReportReloadTime = reassembleResult.shouldReportReloadTime; |
| if (reassembleResult.reassembleViews.isEmpty) { |
| return OperationResult(OperationResult.ok.code, reloadMessage); |
| } |
| // Record time it took for Flutter to reassemble the application. |
| reassembleTimer.stop(); |
| _addBenchmarkData('hotReloadFlutterReassembleMilliseconds', reassembleTimer.elapsed.inMilliseconds); |
| extraTimings.add(OperationResultExtraTiming('reassemble', reassembleTimer.elapsedMilliseconds)); |
| |
| 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? ?? 0, |
| syncedLibraryCount: firstReloadDetails['receivedLibraryCount'] as int? ?? 0, |
| syncedClassesCount: firstReloadDetails['receivedClassesCount'] as int? ?? 0, |
| syncedProceduresCount: firstReloadDetails['receivedProceduresCount'] as int? ?? 0, |
| syncedBytes: updatedDevFS.syncedBytes, |
| invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount, |
| transferTimeInMs: updatedDevFS.transferDuration.inMilliseconds, |
| compileTimeInMs: updatedDevFS.compileDuration.inMilliseconds, |
| findInvalidatedTimeInMs: updatedDevFS.findInvalidatedDuration.inMilliseconds, |
| scannedSourcesCount: updatedDevFS.scannedSourcesCount, |
| reassembleTimeInMs: reassembleTimer.elapsed.inMilliseconds, |
| reloadVMTimeInMs: reloadVMTimer.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 ((reassembleResult.reassembleViews.length == 1) && !reassembleResult.failedReassemble && shouldReportReloadTime) { |
| globals.flutterUsage.sendTiming('hot', 'reload', reloadDuration); |
| } |
| return OperationResult( |
| reassembleResult.failedReassemble ? 1 : OperationResult.ok.code, |
| reloadMessage, |
| extraTimings: extraTimings |
| ); |
| } |
| |
| @override |
| void printHelp({ required bool details }) { |
| globals.printStatus('Flutter run key commands.'); |
| commandHelp.r.print(); |
| if (supportsRestart) { |
| commandHelp.R.print(); |
| } |
| if (details) { |
| printHelpDetails(); |
| commandHelp.hWithDetails.print(); |
| } else { |
| commandHelp.hWithoutDetails.print(); |
| } |
| if (_didAttach) { |
| commandHelp.d.print(); |
| } |
| commandHelp.c.print(); |
| commandHelp.q.print(); |
| if (debuggingOptions.buildInfo.nullSafetyMode != NullSafetyMode.sound) { |
| globals.printStatus(''); |
| globals.printStatus( |
| 'Running without sound null safety ⚠️', |
| emphasis: true, |
| ); |
| globals.printStatus( |
| 'Dart 3 will only support sound null safety, see https://dart.dev/null-safety', |
| ); |
| } |
| globals.printStatus(''); |
| printDebuggerList(); |
| } |
| |
| @visibleForTesting |
| Future<void> evictDirtyAssets() async { |
| final List<Future<void>> futures = <Future<void>>[]; |
| for (final FlutterDevice? device in flutterDevices) { |
| if (device!.devFS!.assetPathsToEvict.isEmpty && |
| device.devFS!.shaderPathsToEvict.isEmpty && |
| device.devFS!.scenePathsToEvict.isEmpty) { |
| continue; |
| } |
| final List<FlutterView> views = await device.vmService!.getFlutterViews(); |
| |
| // If this is the first time we update the assets, make sure to call the setAssetDirectory |
| if (!device.devFS!.hasSetAssetDirectory) { |
| final Uri deviceAssetsDirectoryUri = device.devFS!.baseUri!.resolveUri(globals.fs.path.toUri(getAssetBuildDirectory())); |
| await Future.wait<void>(views.map<Future<void>>( |
| (FlutterView view) => device.vmService!.setAssetDirectory( |
| assetsDirectory: deviceAssetsDirectoryUri, |
| uiIsolateId: view.uiIsolate!.id, |
| viewId: view.id, |
| windows: device.targetPlatform == TargetPlatform.windows_x64, |
| ) |
| )); |
| for (final FlutterView view in views) { |
| globals.printTrace('Set asset directory in $view.'); |
| } |
| device.devFS!.hasSetAssetDirectory = true; |
| } |
| |
| if (views.first.uiIsolate == null) { |
| globals.printError('Application isolate not found for $device'); |
| continue; |
| } |
| |
| if (device.devFS!.didUpdateFontManifest) { |
| futures.add(device.vmService!.reloadAssetFonts( |
| isolateId: views.first.uiIsolate!.id!, |
| viewId: views.first.id, |
| )); |
| } |
| |
| for (final String assetPath in device.devFS!.assetPathsToEvict) { |
| futures.add( |
| device.vmService! |
| .flutterEvictAsset( |
| assetPath, |
| isolateId: views.first.uiIsolate!.id!, |
| ) |
| ); |
| } |
| for (final String assetPath in device.devFS!.shaderPathsToEvict) { |
| futures.add( |
| device.vmService! |
| .flutterEvictShader( |
| assetPath, |
| isolateId: views.first.uiIsolate!.id!, |
| ) |
| ); |
| } |
| for (final String assetPath in device.devFS!.scenePathsToEvict) { |
| futures.add( |
| device.vmService! |
| .flutterEvictScene( |
| assetPath, |
| isolateId: views.first.uiIsolate!.id!, |
| ) |
| ); |
| } |
| device.devFS!.assetPathsToEvict.clear(); |
| device.devFS!.shaderPathsToEvict.clear(); |
| device.devFS!.scenePathsToEvict.clear(); |
| } |
| await Future.wait<void>(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 residentDevtoolsHandler!.shutdown(); |
| await stopEchoingDeviceLog(); |
| } |
| } |
| |
| typedef ReloadSourcesHelper = Future<OperationResult> Function( |
| HotRunner hotRunner, |
| List<FlutterDevice?> flutterDevices, |
| bool? pause, |
| Map<String, dynamic> firstReloadDetails, |
| String? targetPlatform, |
| String? sdkName, |
| bool? emulator, |
| String? reason, |
| Usage usage, |
| ); |
| |
| @visibleForTesting |
| Future<OperationResult> defaultReloadSourcesHelper( |
| HotRunner hotRunner, |
| List<FlutterDevice?> flutterDevices, |
| bool? pause, |
| Map<String, dynamic> firstReloadDetails, |
| String? targetPlatform, |
| String? sdkName, |
| bool? emulator, |
| String? reason, |
| Usage usage, |
| ) async { |
| final Stopwatch vmReloadTimer = Stopwatch()..start(); |
| const String entryPath = 'main.dart.incremental.dill'; |
| final List<Future<DeviceReloadReport?>> allReportsFutures = <Future<DeviceReloadReport?>>[]; |
| |
| for (final FlutterDevice? device in flutterDevices) { |
| final List<Future<vm_service.ReloadReport>> reportFutures = await _reloadDeviceSources( |
| device!, |
| entryPath, |
| pause: pause, |
| ); |
| allReportsFutures.add(Future.wait(reportFutures).then<DeviceReloadReport?>( |
| (List<vm_service.ReloadReport> reports) async { |
| // TODO(aam): Investigate why we are validating only first reload report, |
| // which seems to be current behavior |
| if (reports.isEmpty) { |
| return null; |
| } |
| 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( |
| HotRunner.validateReloadReport(firstReport, printErrors: false), |
| ); |
| return DeviceReloadReport(device, reports); |
| }, |
| )); |
| } |
| final Iterable<DeviceReloadReport> reports = (await Future.wait(allReportsFutures)).whereType<DeviceReloadReport>(); |
| final vm_service.ReloadReport? reloadReport = reports.isEmpty ? null : reports.first.reports[0]; |
| if (reloadReport == null || !HotRunner.validateReloadReport(reloadReport)) { |
| // Reload failed. |
| HotEvent('reload-reject', |
| targetPlatform: targetPlatform!, |
| sdkName: sdkName!, |
| emulator: emulator!, |
| fullRestart: false, |
| reason: reason, |
| usage: usage, |
| ).send(); |
| // Reset devFS lastCompileTime to ensure the file will still be marked |
| // as dirty on subsequent reloads. |
| _resetDevFSCompileTime(flutterDevices); |
| if (reloadReport == null) { |
| return OperationResult(1, 'No Dart isolates found'); |
| } |
| final ReloadReportContents contents = ReloadReportContents.fromReloadReport(reloadReport); |
| return OperationResult(1, 'Reload rejected: ${contents.notices.join("\n")}'); |
| } |
| // 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.addAll(castStringKeyedMap(reloadReport.json!['details'])!); |
| final Map<String, dynamic> details = reloadReport.json!['details'] as Map<String, dynamic>; |
| final int? loadedLibraryCount = details['loadedLibraryCount'] as int?; |
| final int? finalLibraryCount = details['finalLibraryCount'] as int?; |
| globals.printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries'); |
| // reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries'; |
| // Record time it took for the VM to reload the sources. |
| hotRunner._addBenchmarkData('hotReloadVMReloadMilliseconds', vmReloadTimer.elapsed.inMilliseconds); |
| return OperationResult(0, 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries'); |
| } |
| |
| Future<List<Future<vm_service.ReloadReport>>> _reloadDeviceSources( |
| FlutterDevice device, |
| String entryPath, { |
| bool? pause = false, |
| }) async { |
| final String deviceEntryUri = device.devFS!.baseUri! |
| .resolve(entryPath).toString(); |
| final vm_service.VM vm = await device.vmService!.service.getVM(); |
| return <Future<vm_service.ReloadReport>>[ |
| for (final vm_service.IsolateRef isolateRef in vm.isolates!) |
| device.vmService!.service.reloadSources( |
| isolateRef.id!, |
| pause: pause, |
| rootLibUri: deviceEntryUri, |
| ), |
| ]; |
| } |
| |
| void _resetDevFSCompileTime(List<FlutterDevice?> flutterDevices) { |
| for (final FlutterDevice? device in flutterDevices) { |
| device!.devFS!.resetLastCompiled(); |
| } |
| } |
| |
| @visibleForTesting |
| class ReassembleResult { |
| ReassembleResult(this.reassembleViews, this.failedReassemble, this.shouldReportReloadTime); |
| final Map<FlutterView?, FlutterVmService?> reassembleViews; |
| final bool failedReassemble; |
| final bool shouldReportReloadTime; |
| } |
| |
| typedef ReassembleHelper = Future<ReassembleResult> Function( |
| List<FlutterDevice?> flutterDevices, |
| Map<FlutterDevice?, List<FlutterView>> viewCache, |
| void Function(String message)? onSlow, |
| String reloadMessage, |
| ); |
| |
| Future<ReassembleResult> _defaultReassembleHelper( |
| List<FlutterDevice?> flutterDevices, |
| Map<FlutterDevice?, List<FlutterView>> viewCache, |
| void Function(String message)? onSlow, |
| String reloadMessage, |
| ) async { |
| // Check if any isolates are paused and reassemble those that aren't. |
| final Map<FlutterView, FlutterVmService?> reassembleViews = <FlutterView, FlutterVmService?>{}; |
| final List<Future<void>> reassembleFutures = <Future<void>>[]; |
| String? serviceEventKind; |
| int pausedIsolatesFound = 0; |
| bool failedReassemble = false; |
| bool shouldReportReloadTime = true; |
| for (final FlutterDevice? device in flutterDevices) { |
| final List<FlutterView> views = viewCache[device]!; |
| 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.Event? pauseEvent = await device!.vmService! |
| .getIsolatePauseEventOrNull(view.uiIsolate!.id!); |
| 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; |
| // If the tool identified a change in a single widget, do a fast instead |
| // of a full reassemble. |
| final Future<void> reassembleWork = device.vmService!.flutterReassemble( |
| isolateId: view.uiIsolate!.id!, |
| ); |
| reassembleFutures.add(reassembleWork.then( |
| (Object? obj) => obj, |
| onError: (Object error, StackTrace stackTrace) { |
| if (error is! Exception) { |
| return Future<Object?>.error(error, stackTrace); |
| } |
| failedReassemble = true; |
| globals.printError('Reassembling ${view.uiIsolate!.name} failed: $error\n$stackTrace'); |
| }, |
| )); |
| } |
| } |
| } |
| 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 ReassembleResult(reassembleViews, failedReassemble, shouldReportReloadTime); |
| } |
| } |
| assert(reassembleViews.isNotEmpty); |
| |
| globals.printTrace('Reassembling application'); |
| |
| final Future<void> reassembleFuture = Future.wait<void>(reassembleFutures).then((void _) => null); |
| 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.Event? pauseEvent = await reassembleViews[view]! |
| .getIsolatePauseEventOrNull(view.uiIsolate!.id!); |
| if (pauseEvent != null && isPauseEvent(pauseEvent.kind!)) { |
| postReloadPausedIsolatesFound += 1; |
| if (serviceEventKind == null) { |
| serviceEventKind = pauseEvent.kind; |
| } else if (serviceEventKind != 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!)}.'); |
| } |
| return; |
| }, |
| ); |
| return ReassembleResult(reassembleViews, failedReassemble, shouldReportReloadTime); |
| } |
| |
| 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; |
| } |
| switch (serviceEventKind) { |
| case vm_service.EventKind.kPauseStart: |
| message.write('paused (probably due to --start-paused)'); |
| case vm_service.EventKind.kPauseExit: |
| message.write('paused because ${ plural ? 'they have' : 'it has' } terminated'); |
| case vm_service.EventKind.kPauseBreakpoint: |
| message.write('paused in the debugger on a breakpoint'); |
| case vm_service.EventKind.kPauseInterrupted: |
| message.write('paused due in the debugger'); |
| case vm_service.EventKind.kPauseException: |
| message.write('paused in the debugger after an exception was thrown'); |
| case vm_service.EventKind.kPausePostRequest: |
| message.write('paused'); |
| case '': |
| message.write('paused for various reasons'); |
| default: |
| message.write('paused'); |
| } |
| return message.toString(); |
| } |
| |
| /// 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 { |
| |
| if (lastCompiled == null) { |
| // Initial load. |
| assert(urisToMonitor.isEmpty); |
| return InvalidationResult( |
| packageConfig: packageConfig, |
| 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>( |
| // Calling fs.stat() is more performant than fs.file().stat(), but |
| // uri.toFilePath() does not work with MultiRootFileSystem. |
| () => (uri.hasScheme && uri.scheme != 'file' |
| ? _fileSystem.file(uri).stat() |
| : _fileSystem.stat(uri.toFilePath(windows: _platform.isWindows))) |
| .then((FileStat stat) { |
| final DateTime updatedAt = stat.modified; |
| if (updatedAt.isAfter(lastCompiled)) { |
| invalidatedFiles.add(uri); |
| } |
| }) |
| )); |
| } |
| await Future.wait<void>(waitList); |
| } else { |
| for (final Uri uri in urisToScan) { |
| // Calling fs.statSync() is more performant than fs.file().statSync(), but |
| // uri.toFilePath() does not work with MultiRootFileSystem. |
| final DateTime updatedAt = uri.hasScheme && uri.scheme != 'file' |
| ? _fileSystem.file(uri).statSync().modified |
| : _fileSystem.statSync(uri.toFilePath(windows: _platform.isWindows)).modified; |
| if (updatedAt.isAfter(lastCompiled)) { |
| invalidatedFiles.add(uri); |
| } |
| } |
| } |
| // We need to check the .packages file too since it is not used in compilation. |
| final File packageFile = _fileSystem.file(packagesPath); |
| final Uri packageUri = packageFile.uri; |
| final DateTime updatedAt = packageFile.statSync().modified; |
| if (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(zanderso): 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(packagesPath), |
| logger: _logger, |
| ); |
| } |
| } |
| |
| /// Additional serialization logic for a hot reload response. |
| class ReloadReportContents { |
| factory ReloadReportContents.fromReloadReport(vm_service.ReloadReport report) { |
| final List<ReasonForCancelling> reasons = <ReasonForCancelling>[]; |
| final Object? notices = report.json!['notices']; |
| if (notices is! List<dynamic>) { |
| return ReloadReportContents._(report.success, reasons, report); |
| } |
| for (final Object? obj in notices) { |
| if (obj is! Map<String, dynamic>) { |
| continue; |
| } |
| final Map<String, dynamic> notice = obj; |
| reasons.add(ReasonForCancelling( |
| message: notice['message'] is String |
| ? notice['message'] as String? |
| : 'Unknown Error', |
| )); |
| } |
| |
| return ReloadReportContents._(report.success, reasons, report); |
| } |
| |
| ReloadReportContents._( |
| this.success, |
| this.notices, |
| this.report, |
| ); |
| |
| final bool? success; |
| final List<ReasonForCancelling> notices; |
| final vm_service.ReloadReport report; |
| } |
| |
| /// A serialization class for hot reload rejection reasons. |
| /// |
| /// Injects an additional error message that a hot restart will |
| /// resolve the issue. |
| class ReasonForCancelling { |
| ReasonForCancelling({ |
| this.message, |
| }); |
| |
| final String? message; |
| |
| @override |
| String toString() { |
| return '$message.\nTry performing a hot restart instead.'; |
| } |
| } |