• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:json_rpc_2/error_code.dart' as rpc_error_code;
8import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
9import 'package:meta/meta.dart';
10
11import 'base/async_guard.dart';
12import 'base/common.dart';
13import 'base/context.dart';
14import 'base/file_system.dart';
15import 'base/logger.dart';
16import 'base/platform.dart';
17import 'base/terminal.dart';
18import 'base/utils.dart';
19import 'build_info.dart';
20import 'compile.dart';
21import 'convert.dart';
22import 'devfs.dart';
23import 'device.dart';
24import 'globals.dart';
25import 'reporting/reporting.dart';
26import 'resident_runner.dart';
27import 'vmservice.dart';
28
29class HotRunnerConfig {
30  /// Should the hot runner assume that the minimal Dart dependencies do not change?
31  bool stableDartDependencies = false;
32  /// A hook for implementations to perform any necessary initialization prior
33  /// to a hot restart. Should return true if the hot restart should continue.
34  Future<bool> setupHotRestart() async {
35    return true;
36  }
37  /// A hook for implementations to perform any necessary operations right
38  /// before the runner is about to be shut down.
39  Future<void> runPreShutdownOperations() async {
40    return;
41  }
42}
43
44HotRunnerConfig get hotRunnerConfig => context.get<HotRunnerConfig>();
45
46const bool kHotReloadDefault = true;
47
48class DeviceReloadReport {
49  DeviceReloadReport(this.device, this.reports);
50
51  FlutterDevice device;
52  List<Map<String, dynamic>> reports; // List has one report per Flutter view.
53}
54
55// TODO(mklim): Test this, flutter/flutter#23031.
56class HotRunner extends ResidentRunner {
57  HotRunner(
58    List<FlutterDevice> devices, {
59    String target,
60    DebuggingOptions debuggingOptions,
61    bool usesTerminalUi = true,
62    this.benchmarkMode = false,
63    this.applicationBinary,
64    this.hostIsIde = false,
65    String projectRootPath,
66    String packagesFilePath,
67    String dillOutputPath,
68    bool stayResident = true,
69    bool ipv6 = false,
70  }) : super(devices,
71             target: target,
72             debuggingOptions: debuggingOptions,
73             usesTerminalUi: usesTerminalUi,
74             projectRootPath: projectRootPath,
75             packagesFilePath: packagesFilePath,
76             stayResident: stayResident,
77             hotMode: true,
78             dillOutputPath: dillOutputPath,
79             ipv6: ipv6);
80
81  final bool benchmarkMode;
82  final File applicationBinary;
83  final bool hostIsIde;
84  bool _didAttach = false;
85
86  final Map<String, List<int>> benchmarkData = <String, List<int>>{};
87  // The initial launch is from a snapshot.
88  bool _runningFromSnapshot = true;
89  DateTime firstBuildTime;
90
91  void _addBenchmarkData(String name, int value) {
92    benchmarkData[name] ??= <int>[];
93    benchmarkData[name].add(value);
94  }
95
96  Future<void> _reloadSourcesService(
97    String isolateId, {
98    bool force = false,
99    bool pause = false,
100  }) async {
101    // TODO(cbernaschina): check that isolateId is the id of the UI isolate.
102    final OperationResult result = await restart(pauseAfterRestart: pause);
103    if (!result.isOk) {
104      throw rpc.RpcException(
105        rpc_error_code.INTERNAL_ERROR,
106        'Unable to reload sources',
107      );
108    }
109  }
110
111  Future<void> _restartService({ bool pause = false }) async {
112    final OperationResult result =
113      await restart(fullRestart: true, pauseAfterRestart: pause);
114    if (!result.isOk) {
115      throw rpc.RpcException(
116        rpc_error_code.INTERNAL_ERROR,
117        'Unable to restart',
118      );
119    }
120  }
121
122  Future<String> _compileExpressionService(
123    String isolateId,
124    String expression,
125    List<String> definitions,
126    List<String> typeDefinitions,
127    String libraryUri,
128    String klass,
129    bool isStatic,
130  ) async {
131    for (FlutterDevice device in flutterDevices) {
132      if (device.generator != null) {
133        final CompilerOutput compilerOutput =
134            await device.generator.compileExpression(expression, definitions,
135                typeDefinitions, libraryUri, klass, isStatic);
136        if (compilerOutput != null && compilerOutput.outputFilename != null) {
137          return base64.encode(fs.file(compilerOutput.outputFilename).readAsBytesSync());
138        }
139      }
140    }
141    throw 'Failed to compile $expression';
142  }
143
144  // Returns the exit code of the flutter tool process, like [run].
145  @override
146  Future<int> attach({
147    Completer<DebugConnectionInfo> connectionInfoCompleter,
148    Completer<void> appStartedCompleter,
149  }) async {
150    _didAttach = true;
151    try {
152      await connectToServiceProtocol(
153        reloadSources: _reloadSourcesService,
154        restart: _restartService,
155        compileExpression: _compileExpressionService,
156      );
157    } catch (error) {
158      printError('Error connecting to the service protocol: $error');
159      // https://github.com/flutter/flutter/issues/33050
160      // TODO(blasten): Remove this check once https://issuetracker.google.com/issues/132325318 has been fixed.
161      if (await hasDeviceRunningAndroidQ(flutterDevices) &&
162          error.toString().contains(kAndroidQHttpConnectionClosedExp)) {
163        printStatus('�� If you are using an emulator running Android Q Beta, consider using an emulator running API level 29 or lower.');
164        printStatus('Learn more about the status of this issue on https://issuetracker.google.com/issues/132325318.');
165      }
166      return 2;
167    }
168
169    for (FlutterDevice device in flutterDevices)
170      device.initLogReader();
171    try {
172      final List<Uri> baseUris = await _initDevFS();
173      if (connectionInfoCompleter != null) {
174        // Only handle one debugger connection.
175        connectionInfoCompleter.complete(
176          DebugConnectionInfo(
177            httpUri: flutterDevices.first.observatoryUris.first,
178            wsUri: flutterDevices.first.vmServices.first.wsAddress,
179            baseUri: baseUris.first.toString(),
180          )
181        );
182      }
183    } catch (error) {
184      printError('Error initializing DevFS: $error');
185      return 3;
186    }
187    final Stopwatch initialUpdateDevFSsTimer = Stopwatch()..start();
188    final UpdateFSReport devfsResult = await _updateDevFS(fullRestart: true);
189    _addBenchmarkData(
190      'hotReloadInitialDevFSSyncMilliseconds',
191      initialUpdateDevFSsTimer.elapsed.inMilliseconds,
192    );
193    if (!devfsResult.success)
194      return 3;
195
196    await refreshViews();
197    for (FlutterDevice device in flutterDevices) {
198      // VM must have accepted the kernel binary, there will be no reload
199      // report, so we let incremental compiler know that source code was accepted.
200      if (device.generator != null)
201        device.generator.accept();
202      for (FlutterView view in device.views)
203        printTrace('Connected to $view.');
204    }
205
206    appStartedCompleter?.complete();
207
208    if (benchmarkMode) {
209      // We are running in benchmark mode.
210      printStatus('Running in benchmark mode.');
211      // Measure time to perform a hot restart.
212      printStatus('Benchmarking hot restart');
213      await restart(fullRestart: true, benchmarkMode: true);
214      printStatus('Benchmarking hot reload');
215      // Measure time to perform a hot reload.
216      await restart(fullRestart: false);
217      if (stayResident) {
218        await waitForAppToFinish();
219      } else {
220        printStatus('Benchmark completed. Exiting application.');
221        await _cleanupDevFS();
222        await stopEchoingDeviceLog();
223        await exitApp();
224      }
225      final File benchmarkOutput = fs.file('hot_benchmark.json');
226      benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData));
227      return 0;
228    }
229
230    int result = 0;
231    if (stayResident)
232      result = await waitForAppToFinish();
233    await cleanupAtFinish();
234    return result;
235  }
236
237  @override
238  Future<int> run({
239    Completer<DebugConnectionInfo> connectionInfoCompleter,
240    Completer<void> appStartedCompleter,
241    String route,
242  }) async {
243    if (!fs.isFileSync(mainPath)) {
244      String message = 'Tried to run $mainPath, but that file does not exist.';
245      if (target == null)
246        message += '\nConsider using the -t option to specify the Dart file to start.';
247      printError(message);
248      return 1;
249    }
250
251    firstBuildTime = DateTime.now();
252
253    for (FlutterDevice device in flutterDevices) {
254      final int result = await device.runHot(
255        hotRunner: this,
256        route: route,
257      );
258      if (result != 0) {
259        return result;
260      }
261    }
262
263    return attach(
264      connectionInfoCompleter: connectionInfoCompleter,
265      appStartedCompleter: appStartedCompleter,
266    );
267  }
268
269  Future<List<Uri>> _initDevFS() async {
270    final String fsName = fs.path.basename(projectRootPath);
271    final List<Uri> devFSUris = <Uri>[];
272    for (FlutterDevice device in flutterDevices) {
273      final Uri uri = await device.setupDevFS(
274        fsName,
275        fs.directory(projectRootPath),
276        packagesFilePath: packagesFilePath,
277      );
278      devFSUris.add(uri);
279    }
280    return devFSUris;
281  }
282
283  Future<UpdateFSReport> _updateDevFS({ bool fullRestart = false }) async {
284    final bool isFirstUpload = assetBundle.wasBuiltOnce() == false;
285    final bool rebuildBundle = assetBundle.needsBuild();
286    if (rebuildBundle) {
287      printTrace('Updating assets');
288      final int result = await assetBundle.build();
289      if (result != 0)
290        return UpdateFSReport(success: false);
291    }
292
293    // Picking up first device's compiler as a source of truth - compilers
294    // for all devices should be in sync.
295    final List<Uri> invalidatedFiles = ProjectFileInvalidator.findInvalidated(
296      lastCompiled: flutterDevices[0].devFS.lastCompiled,
297      urisToMonitor: flutterDevices[0].devFS.sources,
298      packagesPath: packagesFilePath,
299    );
300    final UpdateFSReport results = UpdateFSReport(success: true);
301    for (FlutterDevice device in flutterDevices) {
302      results.incorporateResults(await device.updateDevFS(
303        mainPath: mainPath,
304        target: target,
305        bundle: assetBundle,
306        firstBuildTime: firstBuildTime,
307        bundleFirstUpload: isFirstUpload,
308        bundleDirty: isFirstUpload == false && rebuildBundle,
309        fullRestart: fullRestart,
310        projectRootPath: projectRootPath,
311        pathToReload: getReloadPath(fullRestart: fullRestart),
312        invalidatedFiles: invalidatedFiles,
313        dillOutputPath: dillOutputPath,
314      ));
315    }
316    return results;
317  }
318
319  void _resetDirtyAssets() {
320    for (FlutterDevice device in flutterDevices)
321      device.devFS.assetPathsToEvict.clear();
322  }
323
324  Future<void> _cleanupDevFS() async {
325    final List<Future<void>> futures = <Future<void>>[];
326    for (FlutterDevice device in flutterDevices) {
327      if (device.devFS != null) {
328        // Cleanup the devFS, but don't wait indefinitely.
329        // We ignore any errors, because it's not clear what we would do anyway.
330        futures.add(device.devFS.destroy()
331          .timeout(const Duration(milliseconds: 250))
332          .catchError((dynamic error) {
333            printTrace('Ignored error while cleaning up DevFS: $error');
334          }));
335      }
336      device.devFS = null;
337    }
338    await Future.wait(futures);
339  }
340
341  Future<void> _launchInView(
342    FlutterDevice device,
343    Uri entryUri,
344    Uri packagesUri,
345    Uri assetsDirectoryUri,
346  ) {
347    final List<Future<void>> futures = <Future<void>>[];
348    for (FlutterView view in device.views)
349      futures.add(view.runFromSource(entryUri, packagesUri, assetsDirectoryUri));
350    final Completer<void> completer = Completer<void>();
351    Future.wait(futures).whenComplete(() { completer.complete(null); });
352    return completer.future;
353  }
354
355  Future<void> _launchFromDevFS(String mainScript) async {
356    final String entryUri = fs.path.relative(mainScript, from: projectRootPath);
357    final List<Future<void>> futures = <Future<void>>[];
358    for (FlutterDevice device in flutterDevices) {
359      final Uri deviceEntryUri = device.devFS.baseUri.resolveUri(
360        fs.path.toUri(entryUri));
361      final Uri devicePackagesUri = device.devFS.baseUri.resolve('.packages');
362      final Uri deviceAssetsDirectoryUri = device.devFS.baseUri.resolveUri(
363        fs.path.toUri(getAssetBuildDirectory()));
364      futures.add(_launchInView(device,
365                          deviceEntryUri,
366                          devicePackagesUri,
367                          deviceAssetsDirectoryUri));
368    }
369    await Future.wait(futures);
370    if (benchmarkMode) {
371      futures.clear();
372      for (FlutterDevice device in flutterDevices)
373        for (FlutterView view in device.views)
374          futures.add(view.flushUIThreadTasks());
375      await Future.wait(futures);
376    }
377  }
378
379  Future<OperationResult> _restartFromSources({
380    String reason,
381    bool benchmarkMode = false
382  }) async {
383    if (!_isPaused()) {
384      printTrace('Refreshing active FlutterViews before restarting.');
385      await refreshViews();
386    }
387
388    final Stopwatch restartTimer = Stopwatch()..start();
389    // TODO(aam): Add generator reset logic once we switch to using incremental
390    // compiler for full application recompilation on restart.
391    final UpdateFSReport updatedDevFS = await _updateDevFS(fullRestart: true);
392    if (!updatedDevFS.success) {
393      for (FlutterDevice device in flutterDevices) {
394        if (device.generator != null) {
395          await device.generator.reject();
396        }
397      }
398      return OperationResult(1, 'DevFS synchronization failed');
399    }
400    _resetDirtyAssets();
401    for (FlutterDevice device in flutterDevices) {
402      // VM must have accepted the kernel binary, there will be no reload
403      // report, so we let incremental compiler know that source code was accepted.
404      if (device.generator != null) {
405        device.generator.accept();
406      }
407    }
408    // Check if the isolate is paused and resume it.
409    final List<Future<void>> futures = <Future<void>>[];
410    for (FlutterDevice device in flutterDevices) {
411      for (FlutterView view in device.views) {
412        if (view.uiIsolate == null) {
413          continue;
414        }
415        // Reload the isolate.
416        final Completer<void> completer = Completer<void>();
417        futures.add(completer.future);
418        unawaited(view.uiIsolate.reload().then(
419          (ServiceObject _) {
420            final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
421            if ((pauseEvent != null) && pauseEvent.isPauseEvent) {
422              // Resume the isolate so that it can be killed by the embedder.
423              return view.uiIsolate.resume();
424            }
425            return null;
426          },
427        ).whenComplete(
428          () { completer.complete(null); },
429        ));
430      }
431    }
432    await Future.wait(futures);
433    // We are now running from source.
434    _runningFromSnapshot = false;
435    await _launchFromDevFS(mainPath + '.dill');
436    restartTimer.stop();
437    printTrace('Hot restart performed in ${getElapsedAsMilliseconds(restartTimer.elapsed)}.');
438    // We are now running from sources.
439    _runningFromSnapshot = false;
440    _addBenchmarkData('hotRestartMillisecondsToFrame',
441        restartTimer.elapsed.inMilliseconds);
442
443    // Send timing analytics.
444    flutterUsage.sendTiming('hot', 'restart', restartTimer.elapsed);
445
446    // In benchmark mode, make sure all stream notifications have finished.
447    if (benchmarkMode) {
448      final List<Future<void>> isolateNotifications = <Future<void>>[];
449      for (FlutterDevice device in flutterDevices) {
450        for (FlutterView view in device.views) {
451          isolateNotifications.add(
452            view.owner.vm.vmService.onIsolateEvent.then((Stream<ServiceEvent> serviceEvents) async {
453              await for (ServiceEvent serviceEvent in serviceEvents) {
454                if (serviceEvent.owner.name.contains('_spawn') && serviceEvent.kind == ServiceEvent.kIsolateExit) {
455                  return;
456                }
457              }
458            }),
459          );
460        }
461      }
462      await Future.wait(isolateNotifications);
463    }
464    return OperationResult.ok;
465  }
466
467  /// Returns [true] if the reload was successful.
468  /// Prints errors if [printErrors] is [true].
469  static bool validateReloadReport(
470    Map<String, dynamic> reloadReport, {
471    bool printErrors = true,
472  }) {
473    if (reloadReport == null) {
474      if (printErrors)
475        printError('Hot reload did not receive reload report.');
476      return false;
477    }
478    if (!(reloadReport['type'] == 'ReloadReport' &&
479          (reloadReport['success'] == true ||
480           (reloadReport['success'] == false &&
481            (reloadReport['details'] is Map<String, dynamic> &&
482             reloadReport['details']['notices'] is List<dynamic> &&
483             reloadReport['details']['notices'].isNotEmpty &&
484             reloadReport['details']['notices'].every(
485               (dynamic item) => item is Map<String, dynamic> && item['message'] is String
486             )
487            )
488           )
489          )
490         )) {
491      if (printErrors)
492        printError('Hot reload received invalid response: $reloadReport');
493      return false;
494    }
495    if (!reloadReport['success']) {
496      if (printErrors) {
497        printError('Hot reload was rejected:');
498        for (Map<String, dynamic> notice in reloadReport['details']['notices'])
499          printError('${notice['message']}');
500      }
501      return false;
502    }
503    return true;
504  }
505
506  @override
507  bool get supportsRestart => true;
508
509  @override
510  Future<OperationResult> restart({
511    bool fullRestart = false,
512    bool pauseAfterRestart = false,
513    String reason,
514    bool benchmarkMode = false
515  }) async {
516    String targetPlatform;
517    String sdkName;
518    bool emulator;
519    if (flutterDevices.length == 1) {
520      final Device device = flutterDevices.first.device;
521      targetPlatform = getNameForTargetPlatform(await device.targetPlatform);
522      sdkName = await device.sdkNameAndVersion;
523      emulator = await device.isLocalEmulator;
524    } else if (flutterDevices.length > 1) {
525      targetPlatform = 'multiple';
526      sdkName = 'multiple';
527      emulator = false;
528    } else {
529      targetPlatform = 'unknown';
530      sdkName = 'unknown';
531      emulator = false;
532    }
533    final Stopwatch timer = Stopwatch()..start();
534    if (fullRestart) {
535      final OperationResult result = await _fullRestartHelper(
536        targetPlatform: targetPlatform,
537        sdkName: sdkName,
538        emulator: emulator,
539        reason: reason,
540        benchmarkMode: benchmarkMode,
541      );
542      printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.');
543      return result;
544    }
545    final OperationResult result = await _hotReloadHelper(
546      targetPlatform: targetPlatform,
547      sdkName: sdkName,
548      emulator: emulator,
549      reason: reason,
550      pauseAfterRestart: pauseAfterRestart,
551    );
552    if (result.isOk) {
553      final String elapsed = getElapsedAsMilliseconds(timer.elapsed);
554      printStatus('${result.message} in $elapsed.');
555    }
556    return result;
557  }
558
559  Future<OperationResult> _fullRestartHelper({
560    String targetPlatform,
561    String sdkName,
562    bool emulator,
563    String reason,
564    bool benchmarkMode,
565  }) async {
566    if (!canHotRestart) {
567      return OperationResult(1, 'hotRestart not supported');
568    }
569    final Status status = logger.startProgress(
570      'Performing hot restart...',
571      timeout: timeoutConfiguration.fastOperation,
572      progressId: 'hot.restart',
573    );
574    OperationResult result;
575    String restartEvent = 'restart';
576    try {
577      if (!(await hotRunnerConfig.setupHotRestart())) {
578        return OperationResult(1, 'setupHotRestart failed');
579      }
580      // The current implementation of the vmservice and JSON rpc may throw
581      // unhandled exceptions into the zone that cannot be caught with a regular
582      // try catch. The usage is [asyncGuard] is required to normalize the error
583      // handling, at least until we can refactor the underlying code.
584      result = await asyncGuard(() => _restartFromSources(
585        reason: reason,
586        benchmarkMode: benchmarkMode,
587      ));
588      if (!result.isOk) {
589        restartEvent = 'restart-failed';
590      }
591    } on rpc.RpcException {
592      restartEvent = 'exception';
593      return OperationResult(1, 'hot restart failed to complete', fatal: true);
594    } finally {
595      HotEvent(restartEvent,
596        targetPlatform: targetPlatform,
597        sdkName: sdkName,
598        emulator: emulator,
599        fullRestart: true,
600        reason: reason).send();
601      status.cancel();
602    }
603    return result;
604  }
605
606  Future<OperationResult> _hotReloadHelper({
607    String targetPlatform,
608    String sdkName,
609    bool emulator,
610    String reason,
611    bool pauseAfterRestart = false,
612  }) async {
613    final bool reloadOnTopOfSnapshot = _runningFromSnapshot;
614    final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing';
615    Status status = logger.startProgress(
616      '$progressPrefix hot reload...',
617      timeout: timeoutConfiguration.fastOperation,
618      progressId: 'hot.reload',
619    );
620    OperationResult result;
621    try {
622      result = await _reloadSources(
623        targetPlatform: targetPlatform,
624        sdkName: sdkName,
625        emulator: emulator,
626        pause: pauseAfterRestart,
627        reason: reason,
628        onSlow: (String message) {
629          status?.cancel();
630          status = logger.startProgress(
631            message,
632            timeout: timeoutConfiguration.slowOperation,
633            progressId: 'hot.reload',
634          );
635        },
636      );
637    } on rpc.RpcException {
638      HotEvent('exception',
639        targetPlatform: targetPlatform,
640        sdkName: sdkName,
641        emulator: emulator,
642        fullRestart: false,
643        reason: reason).send();
644      return OperationResult(1, 'hot reload failed to complete', fatal: true);
645    } finally {
646      status.cancel();
647    }
648    return result;
649  }
650
651  Future<OperationResult> _reloadSources({
652    String targetPlatform,
653    String sdkName,
654    bool emulator,
655    bool pause = false,
656    String reason,
657    void Function(String message) onSlow
658  }) async {
659    for (FlutterDevice device in flutterDevices) {
660      for (FlutterView view in device.views) {
661        if (view.uiIsolate == null) {
662          return OperationResult(2, 'Application isolate not found', fatal: true);
663        }
664      }
665    }
666
667    // The initial launch is from a script snapshot. When we reload from source
668    // on top of a script snapshot, the first reload will be a worst case reload
669    // because all of the sources will end up being dirty (library paths will
670    // change from host path to a device path). Subsequent reloads will
671    // not be affected, so we resume reporting reload times on the second
672    // reload.
673    bool shouldReportReloadTime = !_runningFromSnapshot;
674    final Stopwatch reloadTimer = Stopwatch()..start();
675
676    if (!_isPaused()) {
677      printTrace('Refreshing active FlutterViews before reloading.');
678      await refreshViews();
679    }
680
681    final Stopwatch devFSTimer = Stopwatch()..start();
682    final UpdateFSReport updatedDevFS = await _updateDevFS();
683    // Record time it took to synchronize to DevFS.
684    _addBenchmarkData('hotReloadDevFSSyncMilliseconds', devFSTimer.elapsed.inMilliseconds);
685    if (!updatedDevFS.success) {
686      return OperationResult(1, 'DevFS synchronization failed');
687    }
688    String reloadMessage;
689    final Stopwatch vmReloadTimer = Stopwatch()..start();
690    Map<String, dynamic> firstReloadDetails;
691    try {
692      final String entryPath = fs.path.relative(
693        getReloadPath(fullRestart: false),
694        from: projectRootPath,
695      );
696      final List<Future<DeviceReloadReport>> allReportsFutures = <Future<DeviceReloadReport>>[];
697      for (FlutterDevice device in flutterDevices) {
698        if (_runningFromSnapshot) {
699          // Asset directory has to be set only once when we switch from
700          // running from snapshot to running from uploaded files.
701          await device.resetAssetDirectory();
702        }
703        final Completer<DeviceReloadReport> completer = Completer<DeviceReloadReport>();
704        allReportsFutures.add(completer.future);
705        final List<Future<Map<String, dynamic>>> reportFutures = device.reloadSources(
706          entryPath, pause: pause,
707        );
708        unawaited(Future.wait(reportFutures).then(
709          (List<Map<String, dynamic>> reports) async {
710            // TODO(aam): Investigate why we are validating only first reload report,
711            // which seems to be current behavior
712            final Map<String, dynamic> firstReport = reports.first;
713            // Don't print errors because they will be printed further down when
714            // `validateReloadReport` is called again.
715            await device.updateReloadStatus(
716              validateReloadReport(firstReport, printErrors: false),
717            );
718            completer.complete(DeviceReloadReport(device, reports));
719          },
720        ));
721      }
722      final List<DeviceReloadReport> reports = await Future.wait(allReportsFutures);
723      for (DeviceReloadReport report in reports) {
724        final Map<String, dynamic> reloadReport = report.reports[0];
725        if (!validateReloadReport(reloadReport)) {
726          // Reload failed.
727          HotEvent('reload-reject',
728            targetPlatform: targetPlatform,
729            sdkName: sdkName,
730            emulator: emulator,
731            fullRestart: false,
732            reason: reason,
733          ).send();
734          return OperationResult(1, 'Reload rejected');
735        }
736        // Collect stats only from the first device. If/when run -d all is
737        // refactored, we'll probably need to send one hot reload/restart event
738        // per device to analytics.
739        firstReloadDetails ??= reloadReport['details'];
740        final int loadedLibraryCount = reloadReport['details']['loadedLibraryCount'];
741        final int finalLibraryCount = reloadReport['details']['finalLibraryCount'];
742        printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries');
743        reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries';
744      }
745    } on Map<String, dynamic> catch (error, stackTrace) {
746      printTrace('Hot reload failed: $error\n$stackTrace');
747      final int errorCode = error['code'];
748      String errorMessage = error['message'];
749      if (errorCode == Isolate.kIsolateReloadBarred) {
750        errorMessage = 'Unable to hot reload application due to an unrecoverable error in '
751                       'the source code. Please address the error and then use "R" to '
752                       'restart the app.\n'
753                       '$errorMessage (error code: $errorCode)';
754        HotEvent('reload-barred',
755          targetPlatform: targetPlatform,
756          sdkName: sdkName,
757          emulator: emulator,
758          fullRestart: false,
759          reason: reason,
760        ).send();
761        return OperationResult(errorCode, errorMessage);
762      }
763      return OperationResult(errorCode, '$errorMessage (error code: $errorCode)');
764    } catch (error, stackTrace) {
765      printTrace('Hot reload failed: $error\n$stackTrace');
766      return OperationResult(1, '$error');
767    }
768    // Record time it took for the VM to reload the sources.
769    _addBenchmarkData('hotReloadVMReloadMilliseconds', vmReloadTimer.elapsed.inMilliseconds);
770    final Stopwatch reassembleTimer = Stopwatch()..start();
771    // Reload the isolate.
772    final List<Future<void>> allDevices = <Future<void>>[];
773    for (FlutterDevice device in flutterDevices) {
774      printTrace('Sending reload events to ${device.device.name}');
775      final List<Future<ServiceObject>> futuresViews = <Future<ServiceObject>>[];
776      for (FlutterView view in device.views) {
777        printTrace('Sending reload event to "${view.uiIsolate.name}"');
778        futuresViews.add(view.uiIsolate.reload());
779      }
780      final Completer<void> deviceCompleter = Completer<void>();
781      unawaited(Future.wait(futuresViews).whenComplete(() {
782        deviceCompleter.complete(device.refreshViews());
783      }));
784      allDevices.add(deviceCompleter.future);
785    }
786    await Future.wait(allDevices);
787    // We are now running from source.
788    _runningFromSnapshot = false;
789    // Check if any isolates are paused.
790    final List<FlutterView> reassembleViews = <FlutterView>[];
791    String serviceEventKind;
792    int pausedIsolatesFound = 0;
793    for (FlutterDevice device in flutterDevices) {
794      for (FlutterView view in device.views) {
795        // Check if the isolate is paused, and if so, don't reassemble. Ignore the
796        // PostPauseEvent event - the client requesting the pause will resume the app.
797        final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
798        if (pauseEvent != null && pauseEvent.isPauseEvent && pauseEvent.kind != ServiceEvent.kPausePostRequest) {
799          pausedIsolatesFound += 1;
800          if (serviceEventKind == null) {
801            serviceEventKind = pauseEvent.kind;
802          } else if (serviceEventKind != pauseEvent.kind) {
803            serviceEventKind = ''; // many kinds
804          }
805        } else {
806          reassembleViews.add(view);
807        }
808      }
809    }
810    if (pausedIsolatesFound > 0) {
811      if (onSlow != null)
812        onSlow('${_describePausedIsolates(pausedIsolatesFound, serviceEventKind)}; interface might not update.');
813      if (reassembleViews.isEmpty) {
814        printTrace('Skipping reassemble because all isolates are paused.');
815        return OperationResult(OperationResult.ok.code, reloadMessage);
816      }
817    }
818    printTrace('Evicting dirty assets');
819    await _evictDirtyAssets();
820    assert(reassembleViews.isNotEmpty);
821    printTrace('Reassembling application');
822    bool failedReassemble = false;
823    final List<Future<void>> futures = <Future<void>>[];
824    for (FlutterView view in reassembleViews) {
825      futures.add(() async {
826        try {
827          await view.uiIsolate.flutterReassemble();
828        } catch (error) {
829          failedReassemble = true;
830          printError('Reassembling ${view.uiIsolate.name} failed: $error');
831          return;
832        }
833      }());
834    }
835    final Future<void> reassembleFuture = Future.wait<void>(futures).then<void>((List<void> values) { });
836    await reassembleFuture.timeout(
837      const Duration(seconds: 2),
838      onTimeout: () async {
839        if (pausedIsolatesFound > 0) {
840          shouldReportReloadTime = false;
841          return; // probably no point waiting, they're probably deadlocked and we've already warned.
842        }
843        // Check if any isolate is newly paused.
844        printTrace('This is taking a long time; will now check for paused isolates.');
845        int postReloadPausedIsolatesFound = 0;
846        String serviceEventKind;
847        for (FlutterView view in reassembleViews) {
848          await view.uiIsolate.reload();
849          final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
850          if (pauseEvent != null && pauseEvent.isPauseEvent) {
851            postReloadPausedIsolatesFound += 1;
852            if (serviceEventKind == null) {
853              serviceEventKind = pauseEvent.kind;
854            } else if (serviceEventKind != pauseEvent.kind) {
855              serviceEventKind = ''; // many kinds
856            }
857          }
858        }
859        printTrace('Found $postReloadPausedIsolatesFound newly paused isolate(s).');
860        if (postReloadPausedIsolatesFound == 0) {
861          await reassembleFuture; // must just be taking a long time... keep waiting!
862          return;
863        }
864        shouldReportReloadTime = false;
865        if (onSlow != null)
866          onSlow('${_describePausedIsolates(postReloadPausedIsolatesFound, serviceEventKind)}.');
867      },
868    );
869    // Record time it took for Flutter to reassemble the application.
870    _addBenchmarkData('hotReloadFlutterReassembleMilliseconds', reassembleTimer.elapsed.inMilliseconds);
871
872    reloadTimer.stop();
873    final Duration reloadDuration = reloadTimer.elapsed;
874    final int reloadInMs = reloadDuration.inMilliseconds;
875
876    // Collect stats that help understand scale of update for this hot reload request.
877    // For example, [syncedLibraryCount]/[finalLibraryCount] indicates how
878    // many libraries were affected by the hot reload request.
879    // Relation of [invalidatedSourcesCount] to [syncedLibraryCount] should help
880    // understand sync/transfer "overhead" of updating this number of source files.
881    HotEvent('reload',
882      targetPlatform: targetPlatform,
883      sdkName: sdkName,
884      emulator: emulator,
885      fullRestart: false,
886      reason: reason,
887      overallTimeInMs: reloadInMs,
888      finalLibraryCount: firstReloadDetails['finalLibraryCount'],
889      syncedLibraryCount: firstReloadDetails['receivedLibraryCount'],
890      syncedClassesCount: firstReloadDetails['receivedClassesCount'],
891      syncedProceduresCount: firstReloadDetails['receivedProceduresCount'],
892      syncedBytes: updatedDevFS.syncedBytes,
893      invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount,
894      transferTimeInMs: devFSTimer.elapsed.inMilliseconds,
895    ).send();
896
897    if (shouldReportReloadTime) {
898      printTrace('Hot reload performed in ${getElapsedAsMilliseconds(reloadDuration)}.');
899      // Record complete time it took for the reload.
900      _addBenchmarkData('hotReloadMillisecondsToFrame', reloadInMs);
901    }
902    // Only report timings if we reloaded a single view without any errors.
903    if ((reassembleViews.length == 1) && !failedReassemble && shouldReportReloadTime)
904      flutterUsage.sendTiming('hot', 'reload', reloadDuration);
905    return OperationResult(
906      failedReassemble ? 1 : OperationResult.ok.code,
907      reloadMessage,
908    );
909  }
910
911  String _describePausedIsolates(int pausedIsolatesFound, String serviceEventKind) {
912    assert(pausedIsolatesFound > 0);
913    final StringBuffer message = StringBuffer();
914    bool plural;
915    if (pausedIsolatesFound == 1) {
916      if (flutterDevices.length == 1 && flutterDevices.single.views.length == 1) {
917        message.write('The application is ');
918      } else {
919        message.write('An isolate is ');
920      }
921      plural = false;
922    } else {
923      message.write('$pausedIsolatesFound isolates are ');
924      plural = true;
925    }
926    assert(serviceEventKind != null);
927    switch (serviceEventKind) {
928      case ServiceEvent.kPauseStart: message.write('paused (probably due to --start-paused)'); break;
929      case ServiceEvent.kPauseExit: message.write('paused because ${ plural ? 'they have' : 'it has' } terminated'); break;
930      case ServiceEvent.kPauseBreakpoint: message.write('paused in the debugger on a breakpoint'); break;
931      case ServiceEvent.kPauseInterrupted: message.write('paused due in the debugger'); break;
932      case ServiceEvent.kPauseException: message.write('paused in the debugger after an exception was thrown'); break;
933      case ServiceEvent.kPausePostRequest: message.write('paused'); break;
934      case '': message.write('paused for various reasons'); break;
935      default:
936        message.write('paused');
937    }
938    return message.toString();
939  }
940
941  bool _isPaused() {
942    for (FlutterDevice device in flutterDevices) {
943      for (FlutterView view in device.views) {
944        if (view.uiIsolate != null) {
945          final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
946          if (pauseEvent != null && pauseEvent.isPauseEvent) {
947            return true;
948          }
949        }
950      }
951    }
952    return false;
953  }
954
955  @override
956  void printHelp({ @required bool details }) {
957    const String fire = '��';
958    String rawMessage = '  To hot reload changes while running, press "r". ';
959    if (canHotRestart) {
960      rawMessage += 'To hot restart (and rebuild state), press "R".';
961    }
962    final String message = terminal.color(
963      fire + terminal.bolden(rawMessage),
964      TerminalColor.red,
965    );
966    printStatus(message);
967    for (FlutterDevice device in flutterDevices) {
968      final String dname = device.device.name;
969      for (Uri uri in device.observatoryUris)
970        printStatus('An Observatory debugger and profiler on $dname is available at: $uri');
971    }
972    final String quitMessage = _didAttach
973        ? 'To detach, press "d"; to quit, press "q".'
974        : 'To quit, press "q".';
975    if (details) {
976      printHelpDetails();
977      printStatus('To repeat this help message, press "h". $quitMessage');
978    } else {
979      printStatus('For a more detailed help message, press "h". $quitMessage');
980    }
981  }
982
983  Future<void> _evictDirtyAssets() {
984    final List<Future<Map<String, dynamic>>> futures = <Future<Map<String, dynamic>>>[];
985    for (FlutterDevice device in flutterDevices) {
986      if (device.devFS.assetPathsToEvict.isEmpty)
987        continue;
988      if (device.views.first.uiIsolate == null) {
989        printError('Application isolate not found for $device');
990        continue;
991      }
992      for (String assetPath in device.devFS.assetPathsToEvict) {
993        futures.add(device.views.first.uiIsolate.flutterEvictAsset(assetPath));
994      }
995      device.devFS.assetPathsToEvict.clear();
996    }
997    return Future.wait<Map<String, dynamic>>(futures);
998  }
999
1000  @override
1001  Future<void> cleanupAfterSignal() async {
1002    await stopEchoingDeviceLog();
1003    await hotRunnerConfig.runPreShutdownOperations();
1004    if (_didAttach) {
1005      appFinished();
1006    } else {
1007      await exitApp();
1008    }
1009  }
1010
1011  @override
1012  Future<void> preExit() async {
1013    await _cleanupDevFS();
1014    await hotRunnerConfig.runPreShutdownOperations();
1015  }
1016
1017  @override
1018  Future<void> cleanupAtFinish() async {
1019    await _cleanupDevFS();
1020    await stopEchoingDeviceLog();
1021  }
1022}
1023
1024class ProjectFileInvalidator {
1025  static const String _pubCachePathLinuxAndMac = '.pub-cache';
1026  static const String _pubCachePathWindows = 'Pub/Cache';
1027
1028  static List<Uri> findInvalidated({
1029    @required DateTime lastCompiled,
1030    @required List<Uri> urisToMonitor,
1031    @required String packagesPath,
1032  }) {
1033    final List<Uri> invalidatedFiles = <Uri>[];
1034    int scanned = 0;
1035    final Stopwatch stopwatch = Stopwatch()..start();
1036    for (Uri uri in urisToMonitor) {
1037      if ((platform.isWindows && uri.path.contains(_pubCachePathWindows))
1038          || uri.path.contains(_pubCachePathLinuxAndMac)) {
1039        // Don't watch pub cache directories to speed things up a little.
1040        continue;
1041      }
1042      final DateTime updatedAt = fs.statSync(
1043          uri.toFilePath(windows: platform.isWindows)).modified;
1044      scanned++;
1045      if (updatedAt == null) {
1046        continue;
1047      }
1048      if (updatedAt.millisecondsSinceEpoch > lastCompiled.millisecondsSinceEpoch) {
1049        invalidatedFiles.add(uri);
1050      }
1051    }
1052    // we need to check the .packages file too since it is not used in compilation.
1053    final DateTime packagesUpdatedAt = fs.statSync(packagesPath).modified;
1054    if (lastCompiled != null && packagesUpdatedAt != null
1055        && packagesUpdatedAt.millisecondsSinceEpoch > lastCompiled.millisecondsSinceEpoch) {
1056      invalidatedFiles.add(fs.file(packagesPath).uri);
1057      scanned++;
1058    }
1059    printTrace('Scanned through $scanned files in ${stopwatch.elapsedMilliseconds}ms');
1060    return invalidatedFiles;
1061  }
1062}
1063