run_hot.dart 28 KB
Newer Older
1 2 3 4 5
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6
import 'dart:convert';
7

8 9
import 'package:json_rpc_2/error_code.dart' as rpc_error_code;
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
10
import 'package:meta/meta.dart';
11

12
import 'base/common.dart';
13
import 'base/context.dart';
14
import 'base/file_system.dart';
15
import 'base/logger.dart';
16
import 'base/terminal.dart';
17
import 'base/utils.dart';
18
import 'build_info.dart';
19
import 'compile.dart';
20
import 'dart/dependencies.dart';
21
import 'dart/pub.dart';
22 23 24
import 'device.dart';
import 'globals.dart';
import 'resident_runner.dart';
25
import 'usage.dart';
26
import 'vmservice.dart';
27

28 29 30 31 32
class HotRunnerConfig {
  /// Should the hot runner compute the minimal Dart dependencies?
  bool computeDartDependencies = true;
  /// Should the hot runner assume that the minimal Dart dependencies do not change?
  bool stableDartDependencies = false;
33 34 35 36 37
  /// 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;
  }
38 39 40 41
}

HotRunnerConfig get hotRunnerConfig => context[HotRunnerConfig];

42 43
const bool kHotReloadDefault = true;

44
// TODO(flutter/flutter#23031): Test this.
45 46
class HotRunner extends ResidentRunner {
  HotRunner(
47
    List<FlutterDevice> devices, {
48 49
    String target,
    DebuggingOptions debuggingOptions,
50 51
    bool usesTerminalUI = true,
    this.benchmarkMode = false,
52
    this.applicationBinary,
53
    this.hostIsIde = false,
54 55
    String projectRootPath,
    String packagesFilePath,
56
    this.dillOutputPath,
57 58
    bool stayResident = true,
    bool ipv6 = false,
59
  }) : super(devices,
60 61
             target: target,
             debuggingOptions: debuggingOptions,
62 63 64
             usesTerminalUI: usesTerminalUI,
             projectRootPath: projectRootPath,
             packagesFilePath: packagesFilePath,
65 66
             stayResident: stayResident,
             ipv6: ipv6);
67

68
  final bool benchmarkMode;
69
  final File applicationBinary;
70
  final bool hostIsIde;
71
  bool _didAttach = false;
72
  Set<String> _dartDependencies;
73
  final String dillOutputPath;
74

75
  final Map<String, List<int>> benchmarkData = <String, List<int>>{};
76 77
  // The initial launch is from a snapshot.
  bool _runningFromSnapshot = true;
78
  DateTime firstBuildTime;
79

80 81 82 83 84
  void _addBenchmarkData(String name, int value) {
    benchmarkData[name] ??= <int>[];
    benchmarkData[name].add(value);
  }

85
  Future<bool> _refreshDartDependencies() async {
86 87 88 89
    if (!hotRunnerConfig.computeDartDependencies) {
      // Disabled.
      return true;
    }
90 91 92 93
    if (_dartDependencies != null) {
      // Already computed.
      return true;
    }
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109

    try {
      // Will return immediately if pubspec.yaml is up-to-date.
      await pubGet(
        context: PubContext.pubGet,
        directory: projectRootPath,
      );
    } on ToolExit catch (error) {
      printError(
        'Unable to reload your application because "flutter packages get" failed to update '
        'package dependencies.\n'
        '$error'
      );
      return false;
    }

110
    final DartDependencySetBuilder dartDependencySetBuilder =
111
        DartDependencySetBuilder(mainPath, packagesFilePath);
112
    try {
113
      _dartDependencies = Set<String>.from(dartDependencySetBuilder.build());
114 115 116 117 118
    } on DartDependencyException catch (error) {
      printError(
        'Your application could not be compiled, because its dependencies could not be established.\n'
        '$error'
      );
119 120 121 122 123
      return false;
    }
    return true;
  }

124
  Future<void> _reloadSourcesService(String isolateId,
125
      { bool force = false, bool pause = false }) async {
126 127
    // TODO(cbernaschina): check that isolateId is the id of the UI isolate.
    final OperationResult result = await restart(pauseAfterRestart: pause);
128
    if (!result.isOk) {
129
      throw rpc.RpcException(
130 131 132 133 134 135
        rpc_error_code.INTERNAL_ERROR,
        'Unable to reload sources',
      );
    }
  }

136 137 138 139 140 141 142 143 144
  Future<String> _compileExpressionService(String isolateId, String expression,
      List<String> definitions, List<String> typeDefinitions,
      String libraryUri, String klass, bool isStatic,
      ) async {
    for (FlutterDevice device in flutterDevices) {
      if (device.generator != null) {
        final CompilerOutput compilerOutput =
            await device.generator.compileExpression(expression, definitions,
                typeDefinitions, libraryUri, klass, isStatic);
145
        if (compilerOutput != null && compilerOutput.outputFilename != null) {
146 147 148 149
          return base64.encode(fs.file(compilerOutput.outputFilename).readAsBytesSync());
        }
      }
    }
150
    throw 'Failed to compile $expression';
151 152
  }

153
  Future<int> attach({
154
    Completer<DebugConnectionInfo> connectionInfoCompleter,
155
    Completer<void> appStartedCompleter,
156
  }) async {
157
    _didAttach = true;
158
    try {
159
      await connectToServiceProtocol(reloadSources: _reloadSourcesService, compileExpression: _compileExpressionService);
160 161 162 163 164
    } catch (error) {
      printError('Error connecting to the service protocol: $error');
      return 2;
    }

165 166
    for (FlutterDevice device in flutterDevices)
      device.initLogReader();
167
    try {
168
      final List<Uri> baseUris = await _initDevFS();
169
      if (connectionInfoCompleter != null) {
170
        // Only handle one debugger connection.
171
        connectionInfoCompleter.complete(
172
          DebugConnectionInfo(
173 174 175
            httpUri: flutterDevices.first.observatoryUris.first,
            wsUri: flutterDevices.first.vmServices.first.wsAddress,
            baseUri: baseUris.first.toString()
176 177 178 179 180 181 182
          )
        );
      }
    } catch (error) {
      printError('Error initializing DevFS: $error');
      return 3;
    }
183
    final Stopwatch initialUpdateDevFSsTimer = Stopwatch()..start();
184
    final bool devfsResult = await _updateDevFS(fullRestart: true);
185 186
    _addBenchmarkData('hotReloadInitialDevFSSyncMilliseconds',
        initialUpdateDevFSsTimer.elapsed.inMilliseconds);
187
    if (!devfsResult)
188 189
      return 3;

190
    await refreshViews();
191
    for (FlutterDevice device in flutterDevices) {
192 193 194 195
      // 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();
196 197 198
      for (FlutterView view in device.views)
        printTrace('Connected to $view.');
    }
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216

    if (stayResident) {
      setupTerminal();
      registerSignalHandlers();
    }

    appStartedCompleter?.complete();

    if (benchmarkMode) {
      // We are running in benchmark mode.
      printStatus('Running in benchmark mode.');
      // Measure time to perform a hot restart.
      printStatus('Benchmarking hot restart');
      await restart(fullRestart: true);
      // TODO(johnmccutchan): Modify script entry point.
      printStatus('Benchmarking hot reload');
      // Measure time to perform a hot reload.
      await restart(fullRestart: false);
217 218 219 220 221 222 223 224
      if (stayResident) {
        await waitForAppToFinish();
      } else {
        printStatus('Benchmark completed. Exiting application.');
        await _cleanupDevFS();
        await stopEchoingDeviceLog();
        await stopApp();
      }
225 226
      final File benchmarkOutput = fs.file('hot_benchmark.json');
      benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData));
227
      return 0;
228 229 230 231 232 233 234 235
    }

    if (stayResident)
      return waitForAppToFinish();
    await cleanupAtFinish();
    return 0;
  }

236 237
  @override
  Future<int> run({
238
    Completer<DebugConnectionInfo> connectionInfoCompleter,
239
    Completer<void> appStartedCompleter,
240
    String route,
241
    bool shouldBuild = true
242
  }) async {
243 244
    if (!fs.isFileSync(mainPath)) {
      String message = 'Tried to run $mainPath, but that file does not exist.';
245 246 247 248 249 250
      if (target == null)
        message += '\nConsider using the -t option to specify the Dart file to start.';
      printError(message);
      return 1;
    }

251
    // Determine the Dart dependencies eagerly.
252
    if (!await _refreshDartDependencies()) {
253 254 255 256
      // Some kind of source level error or missing file in the Dart code.
      return 1;
    }

257
    firstBuildTime = DateTime.now();
258

259 260 261 262 263 264 265 266 267
    for (FlutterDevice device in flutterDevices) {
      final int result = await device.runHot(
        hotRunner: this,
        route: route,
        shouldBuild: shouldBuild,
      );
      if (result != 0) {
        return result;
      }
268 269
    }

270 271 272 273
    return attach(
      connectionInfoCompleter: connectionInfoCompleter,
      appStartedCompleter: appStartedCompleter
    );
274 275 276
  }

  @override
277
  Future<void> handleTerminalCommand(String code) async {
278
    final String lower = code.toLowerCase();
279
    if (lower == 'r') {
280
      final OperationResult result = await restart(fullRestart: code == 'R');
281 282 283 284
      if (!result.isOk) {
        // TODO(johnmccutchan): Attempt to determine the number of errors that
        // occurred and tighten this message.
        printStatus('Try again after fixing the above error(s).', emphasis: true);
285
      }
286 287 288 289 290 291
    } else if (lower == 'l') {
      final List<FlutterView> views = flutterDevices.expand((FlutterDevice d) => d.views).toList();
      printStatus('Connected ${pluralize('view', views.length)}:');
      for (FlutterView v in views) {
        printStatus('${v.uiIsolate.name} (${v.uiIsolate.id})', indent: 2);
      }
292 293 294
    }
  }

295
  Future<List<Uri>> _initDevFS() async {
296
    final String fsName = fs.path.basename(projectRootPath);
297 298 299 300 301 302 303 304 305 306
    final List<Uri> devFSUris = <Uri>[];
    for (FlutterDevice device in flutterDevices) {
      final Uri uri = await device.setupDevFS(
        fsName,
        fs.directory(projectRootPath),
        packagesFilePath: packagesFilePath
      );
      devFSUris.add(uri);
    }
    return devFSUris;
307
  }
308

309
  Future<bool> _updateDevFS({ bool fullRestart = false }) async {
310
    if (!await _refreshDartDependencies()) {
311 312 313
      // Did not update DevFS because of a Dart source error.
      return false;
    }
314
    final bool isFirstUpload = assetBundle.wasBuiltOnce() == false;
315
    final bool rebuildBundle = assetBundle.needsBuild();
316
    if (rebuildBundle) {
317
      printTrace('Updating assets');
318
      final int result = await assetBundle.build();
319 320
      if (result != 0)
        return false;
321
    }
322 323 324

    for (FlutterDevice device in flutterDevices) {
      final bool result = await device.updateDevFS(
325 326
        mainPath: mainPath,
        target: target,
327
        bundle: assetBundle,
328 329 330
        firstBuildTime: firstBuildTime,
        bundleFirstUpload: isFirstUpload,
        bundleDirty: isFirstUpload == false && rebuildBundle,
331
        fileFilter: _dartDependencies,
332 333
        fullRestart: fullRestart,
        projectRootPath: projectRootPath,
334
        pathToReload: getReloadPath(fullRestart: fullRestart),
335 336 337
      );
      if (!result)
        return false;
338
    }
339

340 341 342 343
    if (!hotRunnerConfig.stableDartDependencies) {
      // Clear the set after the sync so they are recomputed next time.
      _dartDependencies = null;
    }
344 345 346
    return true;
  }

347
  Future<void> _evictDirtyAssets() async {
348 349 350 351 352 353 354 355 356
    for (FlutterDevice device in flutterDevices) {
      if (device.devFS.assetPathsToEvict.isEmpty)
        return;
      if (device.views.first.uiIsolate == null)
        throw 'Application isolate not found';
      for (String assetPath in device.devFS.assetPathsToEvict)
        await device.views.first.uiIsolate.flutterEvictAsset(assetPath);
      device.devFS.assetPathsToEvict.clear();
    }
357 358
  }

359 360 361 362 363
  void _resetDirtyAssets() {
    for (FlutterDevice device in flutterDevices)
      device.devFS.assetPathsToEvict.clear();
  }

364
  Future<void> _cleanupDevFS() async {
365 366 367 368 369 370 371 372 373 374 375
    for (FlutterDevice device in flutterDevices) {
      if (device.devFS != null) {
        // Cleanup the devFS; don't wait indefinitely, and ignore any errors.
        await device.devFS.destroy()
          .timeout(const Duration(milliseconds: 250))
          .catchError((dynamic error) {
            printTrace('$error');
          });
      }
      device.devFS = null;
    }
376 377
  }

378
  Future<void> _launchInView(FlutterDevice device,
379
                             Uri entryUri,
380 381
                             Uri packagesUri,
                             Uri assetsDirectoryUri) async {
382 383
    for (FlutterView view in device.views)
      await view.runFromSource(entryUri, packagesUri, assetsDirectoryUri);
384 385
  }

386
  Future<void> _launchFromDevFS(String mainScript) async {
387
    final String entryUri = fs.path.relative(mainScript, from: projectRootPath);
388 389 390
    for (FlutterDevice device in flutterDevices) {
      final Uri deviceEntryUri = device.devFS.baseUri.resolveUri(
        fs.path.toUri(entryUri));
391
      final Uri devicePackagesUri = device.devFS.baseUri.resolve('.packages');
392 393 394 395 396 397
      final Uri deviceAssetsDirectoryUri = device.devFS.baseUri.resolveUri(
        fs.path.toUri(getAssetBuildDirectory()));
      await _launchInView(device,
                          deviceEntryUri,
                          devicePackagesUri,
                          deviceAssetsDirectoryUri);
398 399 400 401 402
      if (benchmarkMode) {
        for (FlutterDevice device in flutterDevices)
          for (FlutterView view in device.views)
            await view.flushUIThreadTasks();
      }
403
    }
404 405
  }

406 407 408 409 410 411
  Future<OperationResult> _restartFromSources({ String reason }) async {
    final Map<String, String> analyticsParameters =
      reason == null
        ? null
        : <String, String>{ kEventReloadReasonParameterName: reason };

412 413 414 415 416
    if (!_isPaused()) {
      printTrace('Refreshing active FlutterViews before restarting.');
      await refreshViews();
    }

417
    final Stopwatch restartTimer = Stopwatch()..start();
418 419
    // TODO(aam): Add generator reset logic once we switch to using incremental
    // compiler for full application recompilation on restart.
420
    final bool updatedDevFS = await _updateDevFS(fullRestart: true);
421 422 423 424 425
    if (!updatedDevFS) {
      for (FlutterDevice device in flutterDevices) {
        if (device.generator != null)
          device.generator.reject();
      }
426
      return OperationResult(1, 'DevFS synchronization failed');
427 428 429 430 431 432 433 434
    }
    _resetDirtyAssets();
    for (FlutterDevice device in flutterDevices) {
      // VM must have accepted the kernel binary, there will be no reload
      // report, so we let incremental compiler know that source code was accepted.
      if (device.generator != null)
        device.generator.accept();
    }
435
    // Check if the isolate is paused and resume it.
436 437 438 439 440 441 442 443 444 445 446
    for (FlutterDevice device in flutterDevices) {
      for (FlutterView view in device.views) {
        if (view.uiIsolate != null) {
          // Reload the isolate.
          await view.uiIsolate.reload();
          final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
          if ((pauseEvent != null) && pauseEvent.isPauseEvent) {
            // Resume the isolate so that it can be killed by the embedder.
            await view.uiIsolate.resume();
          }
        }
447 448 449 450
      }
    }
    // We are now running from source.
    _runningFromSnapshot = false;
451
    await _launchFromDevFS(mainPath + '.dill');
452
    restartTimer.stop();
453
    printTrace('Hot restart performed in ${getElapsedAsMilliseconds(restartTimer.elapsed)}.');
454 455
    // We are now running from sources.
    _runningFromSnapshot = false;
456 457
    _addBenchmarkData('hotRestartMillisecondsToFrame',
        restartTimer.elapsed.inMilliseconds);
458
    flutterUsage.sendEvent('hot', 'restart', parameters: analyticsParameters);
459
    flutterUsage.sendTiming('hot', 'restart', restartTimer.elapsed);
460
    return OperationResult.ok;
461 462 463
  }

  /// Returns [true] if the reload was successful.
464 465
  /// Prints errors if [printErrors] is [true].
  static bool validateReloadReport(Map<String, dynamic> reloadReport,
466
      { bool printErrors = true }) {
467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484
    if (reloadReport == null) {
      if (printErrors)
        printError('Hot reload did not receive reload report.');
      return false;
    }
    if (!(reloadReport['type'] == 'ReloadReport' &&
          (reloadReport['success'] == true ||
           (reloadReport['success'] == false &&
            (reloadReport['details'] is Map<String, dynamic> &&
             reloadReport['details']['notices'] is List<dynamic> &&
             reloadReport['details']['notices'].isNotEmpty &&
             reloadReport['details']['notices'].every(
               (dynamic item) => item is Map<String, dynamic> && item['message'] is String
             )
            )
           )
          )
         )) {
485 486
      if (printErrors)
        printError('Hot reload received invalid response: $reloadReport');
487 488
      return false;
    }
489
    if (!reloadReport['success']) {
490 491 492 493 494
      if (printErrors) {
        printError('Hot reload was rejected:');
        for (Map<String, dynamic> notice in reloadReport['details']['notices'])
          printError('${notice['message']}');
      }
495 496 497 498 499
      return false;
    }
    return true;
  }

500 501 502
  @override
  bool get supportsRestart => true;

503
  @override
504
  Future<OperationResult> restart({ bool fullRestart = false, bool pauseAfterRestart = false, String reason }) async {
505
    final Stopwatch timer = Stopwatch()..start();
506
    if (fullRestart) {
507
      final Status status = logger.startProgress(
508
        'Performing hot restart...',
509
        progressId: 'hot.restart',
510
      );
Devon Carew's avatar
Devon Carew committed
511
      try {
512
        if (!(await hotRunnerConfig.setupHotRestart()))
513
          return OperationResult(1, 'setupHotRestart failed');
514
        final OperationResult result = await _restartFromSources(reason: reason);
515 516
        if (!result.isOk)
          return result;
517
      } finally {
518
        status.cancel();
Devon Carew's avatar
Devon Carew committed
519
      }
520 521
      printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.');
      return OperationResult.ok;
522
    } else {
523 524
      final bool reloadOnTopOfSnapshot = _runningFromSnapshot;
      final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing';
525
      final Status status = logger.startProgress(
526 527 528
        '$progressPrefix hot reload...',
        progressId: 'hot.reload'
      );
529
      OperationResult result;
Devon Carew's avatar
Devon Carew committed
530
      try {
531
        result = await _reloadSources(pause: pauseAfterRestart, reason: reason);
532
      } finally {
533
        status.cancel();
Devon Carew's avatar
Devon Carew committed
534
      }
535 536 537 538 539
      if (result.isOk)
        printStatus('${result.message} in ${getElapsedAsMilliseconds(timer.elapsed)}.');
      if (result.hintMessage != null)
        printStatus('\n${result.hintMessage}');
      return result;
540 541
    }
  }
542

543 544 545 546 547
  Future<OperationResult> _reloadSources({ bool pause = false, String reason }) async {
    final Map<String, String> analyticsParameters =
      reason == null
        ? null
        : <String, String>{ kEventReloadReasonParameterName: reason };
548 549 550 551 552 553
    for (FlutterDevice device in flutterDevices) {
      for (FlutterView view in device.views) {
        if (view.uiIsolate == null)
          throw 'Application isolate not found';
      }
    }
554 555

    if (!_isPaused()) {
556
      printTrace('Refreshing active FlutterViews before reloading.');
557 558 559
      await refreshViews();
    }

560 561 562 563 564 565 566
    // The initial launch is from a script snapshot. When we reload from source
    // on top of a script snapshot, the first reload will be a worst case reload
    // because all of the sources will end up being dirty (library paths will
    // change from host path to a device path). Subsequent reloads will
    // not be affected, so we resume reporting reload times on the second
    // reload.
    final bool shouldReportReloadTime = !_runningFromSnapshot;
567
    final Stopwatch reloadTimer = Stopwatch()..start();
568

569
    final Stopwatch devFSTimer = Stopwatch()..start();
570
    final bool updatedDevFS = await _updateDevFS();
571
    // Record time it took to synchronize to DevFS.
572 573
    _addBenchmarkData('hotReloadDevFSSyncMilliseconds',
        devFSTimer.elapsed.inMilliseconds);
574
    if (!updatedDevFS)
575
      return OperationResult(1, 'DevFS synchronization failed');
Devon Carew's avatar
Devon Carew committed
576
    String reloadMessage;
577
    final Stopwatch vmReloadTimer = Stopwatch()..start();
578
    try {
579
      final String entryPath = fs.path.relative(
580
        getReloadPath(fullRestart: false),
581 582
        from: projectRootPath,
      );
583
      final Completer<Map<String, dynamic>> retrieveFirstReloadReport = Completer<Map<String, dynamic>>();
584 585

      int countExpectedReports = 0;
586
      for (FlutterDevice device in flutterDevices) {
587 588 589 590 591
        if (_runningFromSnapshot) {
          // Asset directory has to be set only once when we switch from
          // running from snapshot to running from uploaded files.
          await device.resetAssetDirectory();
        }
592

593
        // List has one report per Flutter view.
594 595 596 597
        final List<Future<Map<String, dynamic>>> reports = device.reloadSources(
          entryPath,
          pause: pause
        );
598
        countExpectedReports += reports.length;
599
        await Future
600
            .wait<Map<String, dynamic>>(reports)
601 602 603
            .catchError((dynamic error) {
              return <Map<String, dynamic>>[error];
            })
604
            .then<void>(
605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620
              (List<Map<String, dynamic>> list) {
                // TODO(aam): Investigate why we are validating only first reload report,
                // which seems to be current behavior
                final Map<String, dynamic> firstReport = list.first;
                // Don't print errors because they will be printed further down when
                // `validateReloadReport` is called again.
                device.updateReloadStatus(
                  validateReloadReport(firstReport, printErrors: false)
                );
                if (!retrieveFirstReloadReport.isCompleted)
                  retrieveFirstReloadReport.complete(firstReport);
              },
              onError: (dynamic error, StackTrace stack) {
                retrieveFirstReloadReport.completeError(error, stack);
              },
            );
621
      }
622 623

      if (countExpectedReports == 0) {
624
        printError('Unable to hot reload. No instance of Flutter is currently running.');
625
        return OperationResult(1, 'No instances running');
626
      }
627
      final Map<String, dynamic> reloadReport = await retrieveFirstReloadReport.future;
628
      if (!validateReloadReport(reloadReport)) {
629
        // Reload failed.
630
        flutterUsage.sendEvent('hot', 'reload-reject');
631
        return OperationResult(1, 'Reload rejected');
632
      } else {
633
        flutterUsage.sendEvent('hot', 'reload', parameters: analyticsParameters);
634 635
        final int loadedLibraryCount = reloadReport['details']['loadedLibraryCount'];
        final int finalLibraryCount = reloadReport['details']['finalLibraryCount'];
636
        printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries');
637
        reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries';
638
      }
639
    } on Map<String, dynamic> catch (error, st) {
640
      printError('Hot reload failed: $error\n$st');
641 642
      final int errorCode = error['code'];
      final String errorMessage = error['message'];
643
      if (errorCode == Isolate.kIsolateReloadBarred) {
644 645 646 647 648
        printError(
          '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.'
        );
649
        flutterUsage.sendEvent('hot', 'reload-barred');
650
        return OperationResult(errorCode, errorMessage);
651
      }
Devon Carew's avatar
Devon Carew committed
652

653
      printError('Hot reload failed:\ncode = $errorCode\nmessage = $errorMessage\n$st');
654
      return OperationResult(errorCode, errorMessage);
655 656
    } catch (error, st) {
      printError('Hot reload failed: $error\n$st');
657
      return OperationResult(1, '$error');
658
    }
659
    // Record time it took for the VM to reload the sources.
660 661
    _addBenchmarkData('hotReloadVMReloadMilliseconds',
        vmReloadTimer.elapsed.inMilliseconds);
662
    final Stopwatch reassembleTimer = Stopwatch()..start();
663
    // Reload the isolate.
664
    for (FlutterDevice device in flutterDevices) {
665 666 667
      printTrace('Sending reload events to ${device.device.name}');
      for (FlutterView view in device.views) {
        printTrace('Sending reload event to "${view.uiIsolate.name}"');
668
        await view.uiIsolate.reload();
669 670
      }
      await device.refreshViews();
671
    }
672 673
    // We are now running from source.
    _runningFromSnapshot = false;
674
    // Check if the isolate is paused.
675

676
    final List<FlutterView> reassembleViews = <FlutterView>[];
677 678
    for (FlutterDevice device in flutterDevices) {
      for (FlutterView view in device.views) {
679 680
        // 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.
681
        final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
682
        if (pauseEvent != null && pauseEvent.isPauseEvent && pauseEvent.kind != ServiceEvent.kPausePostRequest) {
683 684 685
          continue;
        }
        reassembleViews.add(view);
686 687 688 689
      }
    }
    if (reassembleViews.isEmpty) {
      printTrace('Skipping reassemble because all isolates are paused.');
690
      return OperationResult(OperationResult.ok.code, reloadMessage);
691
    }
692
    printTrace('Evicting dirty assets');
693
    await _evictDirtyAssets();
694
    printTrace('Reassembling application');
695
    bool reassembleAndScheduleErrors = false;
696
    bool reassembleTimedOut = false;
697 698 699
    for (FlutterView view in reassembleViews) {
      try {
        await view.uiIsolate.flutterReassemble();
700 701
      } on TimeoutException {
        reassembleTimedOut = true;
702 703
        printTrace('Reassembling ${view.uiIsolate.name} took too long.');
        printStatus('Hot reloading ${view.uiIsolate.name} took too long; the reload may have failed.');
704
        continue;
705 706 707 708 709 710 711 712 713 714 715 716
      } catch (error) {
        reassembleAndScheduleErrors = true;
        printError('Reassembling ${view.uiIsolate.name} failed: $error');
        continue;
      }
      try {
        /* ensure that a frame is scheduled */
        await view.uiIsolate.uiWindowScheduleFrame();
      } catch (error) {
        reassembleAndScheduleErrors = true;
        printError('Scheduling a frame for ${view.uiIsolate.name} failed: $error');
      }
717
    }
718
    // Record time it took for Flutter to reassemble the application.
719 720
    _addBenchmarkData('hotReloadFlutterReassembleMilliseconds',
        reassembleTimer.elapsed.inMilliseconds);
721

722
    reloadTimer.stop();
723
    printTrace('Hot reload performed in ${getElapsedAsMilliseconds(reloadTimer.elapsed)}.');
724
    // Record complete time it took for the reload.
725 726
    _addBenchmarkData('hotReloadMillisecondsToFrame',
        reloadTimer.elapsed.inMilliseconds);
727 728 729 730 731 732
    // Only report timings if we reloaded a single view without any
    // errors or timeouts.
    if ((reassembleViews.length == 1) &&
        !reassembleAndScheduleErrors &&
        !reassembleTimedOut &&
        shouldReportReloadTime)
733
      flutterUsage.sendTiming('hot', 'reload', reloadTimer.elapsed);
734

735
    return OperationResult(
736
      reassembleAndScheduleErrors ? 1 : OperationResult.ok.code,
737
      reloadMessage,
738
    );
739 740
  }

741 742 743 744 745 746 747 748 749 750 751 752 753 754 755
  bool _isPaused() {
    for (FlutterDevice device in flutterDevices) {
      for (FlutterView view in device.views) {
        if (view.uiIsolate != null) {
          final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
          if (pauseEvent != null && pauseEvent.isPauseEvent) {
            return true;
          }
        }
      }
    }

    return false;
  }

756
  @override
757
  void printHelp({ @required bool details }) {
758
    const String fire = '🔥';
759 760 761 762
    final String message = terminal.color(
      fire + terminal.bolden('  To hot reload changes while running, press "r". '
          'To hot restart (and rebuild state), press "R".'),
      TerminalColor.red,
763
    );
764
    printStatus(message);
765 766 767 768 769
    for (FlutterDevice device in flutterDevices) {
      final String dname = device.device.name;
      for (Uri uri in device.observatoryUris)
        printStatus('An Observatory debugger and profiler on $dname is available at: $uri');
    }
770 771 772
    final String quitMessage = _didAttach
        ? 'To detach, press "d"; to quit, press "q".'
        : 'To quit, press "q".';
773
    if (details) {
774
      printHelpDetails();
775
      printStatus('To repeat this help message, press "h". $quitMessage');
776
    } else {
777
      printStatus('For a more detailed help message, press "h". $quitMessage');
778
    }
779 780 781
  }

  @override
782
  Future<void> cleanupAfterSignal() async {
783
    await stopEchoingDeviceLog();
784 785 786 787 788
    if (_didAttach) {
      appFinished();
    } else {
      await stopApp();
    }
789 790
  }

791
  @override
792
  Future<void> preStop() => _cleanupDevFS();
793

794
  @override
795
  Future<void> cleanupAtFinish() async {
796 797 798 799
    await _cleanupDevFS();
    await stopEchoingDeviceLog();
  }
}