run_hot.dart 46.5 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// 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 'package:package_config/package_config.dart';
7
import 'package:vm_service/vm_service.dart' as vm_service;
8
import 'package:meta/meta.dart';
9
import 'package:pool/pool.dart';
10

11
import 'base/async_guard.dart';
12
import 'base/context.dart';
13
import 'base/file_system.dart';
14
import 'base/logger.dart';
15
import 'base/platform.dart';
16
import 'base/utils.dart';
17
import 'build_info.dart';
18
import 'bundle.dart';
19
import 'compile.dart';
20
import 'convert.dart';
21
import 'dart/package_map.dart';
22
import 'devfs.dart';
23
import 'device.dart';
24
import 'globals.dart' as globals;
25
import 'reporting/reporting.dart';
26
import 'resident_runner.dart';
27
import 'vmservice.dart';
28

29
ProjectFileInvalidator get projectFileInvalidator => context.get<ProjectFileInvalidator>() ?? ProjectFileInvalidator(
30 31 32
  fileSystem: globals.fs,
  platform: globals.platform,
  logger: globals.logger,
33
);
34 35 36

HotRunnerConfig get hotRunnerConfig => context.get<HotRunnerConfig>();

37 38 39
class HotRunnerConfig {
  /// Should the hot runner assume that the minimal Dart dependencies do not change?
  bool stableDartDependencies = false;
40 41 42 43

  /// Whether the hot runner should scan for modified files asynchronously.
  bool asyncScanning = false;

44 45 46 47 48
  /// 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;
  }
49 50 51 52 53
  /// A hook for implementations to perform any necessary operations right
  /// before the runner is about to be shut down.
  Future<void> runPreShutdownOperations() async {
    return;
  }
54 55
}

56 57
const bool kHotReloadDefault = true;

58 59 60 61
class DeviceReloadReport {
  DeviceReloadReport(this.device, this.reports);

  FlutterDevice device;
62
  List<vm_service.ReloadReport> reports; // List has one report per Flutter view.
63 64
}

65 66
class HotRunner extends ResidentRunner {
  HotRunner(
67
    List<FlutterDevice> devices, {
68
    String target,
69
    @required DebuggingOptions debuggingOptions,
70
    this.benchmarkMode = false,
71
    this.applicationBinary,
72
    this.hostIsIde = false,
73
    String projectRootPath,
74
    String dillOutputPath,
75 76
    bool stayResident = true,
    bool ipv6 = false,
77 78 79 80 81 82 83 84 85 86 87 88
    bool machine = false,
  }) : super(
          devices,
          target: target,
          debuggingOptions: debuggingOptions,
          projectRootPath: projectRootPath,
          stayResident: stayResident,
          hotMode: true,
          dillOutputPath: dillOutputPath,
          ipv6: ipv6,
          machine: machine,
        );
89

90
  final bool benchmarkMode;
91
  final File applicationBinary;
92
  final bool hostIsIde;
93 94 95 96 97 98 99 100 101 102 103 104 105 106

  /// 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;

107
  bool _didAttach = false;
108

109
  final Map<String, List<int>> benchmarkData = <String, List<int>>{};
110

111
  DateTime firstBuildTime;
112
  bool _shouldResetAssetDirectory = true;
113

114 115 116 117 118
  void _addBenchmarkData(String name, int value) {
    benchmarkData[name] ??= <int>[];
    benchmarkData[name].add(value);
  }

119 120 121 122 123
  Future<void> _reloadSourcesService(
    String isolateId, {
    bool force = false,
    bool pause = false,
  }) async {
124
    // TODO(cbernaschina): check that isolateId is the id of the UI isolate.
125
    final OperationResult result = await restart(pause: pause);
126
    if (!result.isOk) {
127
      throw vm_service.RPCError(
128
        'Unable to reload sources',
129 130
        RPCErrorCodes.kInternalError,
        '',
131 132 133 134
      );
    }
  }

135 136
  Future<void> _restartService({ bool pause = false }) async {
    final OperationResult result =
137
      await restart(fullRestart: true, pause: pause);
138
    if (!result.isOk) {
139
      throw vm_service.RPCError(
140
        'Unable to restart',
141 142
        RPCErrorCodes.kInternalError,
        '',
143 144 145 146
      );
    }
  }

147 148 149 150 151 152 153 154 155
  Future<String> _compileExpressionService(
    String isolateId,
    String expression,
    List<String> definitions,
    List<String> typeDefinitions,
    String libraryUri,
    String klass,
    bool isStatic,
  ) async {
156
    for (final FlutterDevice device in flutterDevices) {
157 158 159 160
      if (device.generator != null) {
        final CompilerOutput compilerOutput =
            await device.generator.compileExpression(expression, definitions,
                typeDefinitions, libraryUri, klass, isStatic);
161
        if (compilerOutput != null && compilerOutput.outputFilename != null) {
162
          return base64.encode(globals.fs.file(compilerOutput.outputFilename).readAsBytesSync());
163 164 165
        }
      }
    }
166
    throw 'Failed to compile $expression';
167 168
  }

169
  @override
170
  Future<OperationResult> reloadMethod({ String libraryId, String classId }) async {
171 172 173 174 175 176
    final OperationResult result = await restart(pause: false);
    if (!result.isOk) {
      throw vm_service.RPCError(
        'Unable to reload sources',
        RPCErrorCodes.kInternalError,
        '',
177 178
      );
    }
179
    return result;
180 181
  }

182
  // Returns the exit code of the flutter tool process, like [run].
183
  @override
184
  Future<int> attach({
185
    Completer<DebugConnectionInfo> connectionInfoCompleter,
186
    Completer<void> appStartedCompleter,
187
  }) async {
188
    _didAttach = true;
189
    try {
190 191
      await connectToServiceProtocol(
        reloadSources: _reloadSourcesService,
192
        restart: _restartService,
193
        compileExpression: _compileExpressionService,
194
        reloadMethod: reloadMethod,
195
        getSkSLMethod: writeSkSL,
196
      );
197 198 199 200 201
    // Catches all exceptions, non-Exception objects are rethrown.
    } catch (error) { // ignore: avoid_catches_without_on_clauses
      if (error is! Exception && error is! String) {
        rethrow;
      }
202
      globals.printError('Error connecting to the service protocol: $error');
203 204 205
      return 2;
    }

206
    for (final FlutterDevice device in flutterDevices) {
207
      await device.initLogReader();
208
    }
209
    try {
210
      final List<Uri> baseUris = await _initDevFS();
211
      if (connectionInfoCompleter != null) {
212
        // Only handle one debugger connection.
213
        connectionInfoCompleter.complete(
214
          DebugConnectionInfo(
215 216
            httpUri: flutterDevices.first.vmService.httpAddress,
            wsUri: flutterDevices.first.vmService.wsAddress,
217
            baseUri: baseUris.first.toString(),
218
          ),
219 220
        );
      }
221
    } on Exception catch (error) {
222
      globals.printError('Error initializing DevFS: $error');
223 224
      return 3;
    }
225
    final Stopwatch initialUpdateDevFSsTimer = Stopwatch()..start();
226
    final UpdateFSReport devfsResult = await _updateDevFS(fullRestart: true);
227 228 229 230
    _addBenchmarkData(
      'hotReloadInitialDevFSSyncMilliseconds',
      initialUpdateDevFSsTimer.elapsed.inMilliseconds,
    );
231
    if (!devfsResult.success) {
232
      return 3;
233
    }
234

235
    for (final FlutterDevice device in flutterDevices) {
236 237
      // VM must have accepted the kernel binary, there will be no reload
      // report, so we let incremental compiler know that source code was accepted.
238
      if (device.generator != null) {
239
        device.generator.accept();
240
      }
241 242
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
243
        globals.printTrace('Connected to $view.');
244
      }
245
    }
246

247 248 249 250 251 252 253 254 255 256 257 258
    // In fast-start mode, apps are initialized from a placeholder splashscreen
    // app. We must do a restart here to load the program and assets for the
    // real app.
    if (debuggingOptions.fastStart) {
      await restart(
        fullRestart: true,
        benchmarkMode: !debuggingOptions.startPaused,
        reason: 'restart',
        silent: true,
      );
    }

259 260 261 262
    appStartedCompleter?.complete();

    if (benchmarkMode) {
      // We are running in benchmark mode.
263
      globals.printStatus('Running in benchmark mode.');
264
      // Measure time to perform a hot restart.
265
      globals.printStatus('Benchmarking hot restart');
266
      await restart(fullRestart: true, benchmarkMode: true);
267 268 269
      // Wait for notifications to finish. attempt to work around
      // timing issue caused by sentinel.
      await Future<void>.delayed(const Duration(seconds: 1));
270
      globals.printStatus('Benchmarking hot reload');
271 272
      // Measure time to perform a hot reload.
      await restart(fullRestart: false);
273 274 275
      if (stayResident) {
        await waitForAppToFinish();
      } else {
276
        globals.printStatus('Benchmark completed. Exiting application.');
277 278
        await _cleanupDevFS();
        await stopEchoingDeviceLog();
279
        await exitApp();
280
      }
281
      final File benchmarkOutput = globals.fs.file('hot_benchmark.json');
282
      benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData));
283
      return 0;
284
    }
285
    writeVmserviceFile();
286

287
    int result = 0;
288
    if (stayResident) {
289
      result = await waitForAppToFinish();
290
    }
291
    await cleanupAtFinish();
292
    return result;
293 294
  }

295 296
  @override
  Future<int> run({
297
    Completer<DebugConnectionInfo> connectionInfoCompleter,
298
    Completer<void> appStartedCompleter,
299
    String route,
300
  }) async {
301
    if (!globals.fs.isFileSync(mainPath)) {
302
      String message = 'Tried to run $mainPath, but that file does not exist.';
303
      if (target == null) {
304
        message += '\nConsider using the -t option to specify the Dart file to start.';
305
      }
306
      globals.printError(message);
307 308 309
      return 1;
    }

310
    firstBuildTime = DateTime.now();
311

312
    final List<Future<bool>> startupTasks = <Future<bool>>[];
313
    final PackageConfig packageConfig = await loadPackageConfigWithLogging(
314
      globals.fs.file(debuggingOptions.buildInfo.packagesPath),
315
      logger: globals.logger,
316
    );
317
    for (final FlutterDevice device in flutterDevices) {
318 319 320 321
      // 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.
322
      await runSourceGenerators();
323 324 325
      if (device.generator != null) {
        startupTasks.add(
          device.generator.recompile(
326
            globals.fs.file(mainPath).uri,
327
            <Uri>[],
328 329 330 331 332
            // 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,
333
            outputPath: dillOutputPath ??
334
              getDefaultApplicationKernelPath(trackWidgetCreation: debuggingOptions.buildInfo.trackWidgetCreation),
335
            packageConfig: packageConfig,
336 337 338 339
          ).then((CompilerOutput output) => output?.errorCount == 0)
        );
      }
      startupTasks.add(device.runHot(
340 341
        hotRunner: this,
        route: route,
342 343 344 345 346
      ).then((int result) => result == 0));
    }
    try {
      final List<bool> results = await Future.wait(startupTasks);
      if (!results.every((bool passed) => passed)) {
347
        appFailedToStart();
348
        return 1;
349
      }
350
      cacheInitialDillCompilation();
351 352
    } on Exception catch (err) {
      globals.printError(err.toString());
353
      appFailedToStart();
354
      return 1;
355 356
    }

357 358
    return attach(
      connectionInfoCompleter: connectionInfoCompleter,
359
      appStartedCompleter: appStartedCompleter,
360
    );
361 362
  }

363
  Future<List<Uri>> _initDevFS() async {
364
    final String fsName = globals.fs.path.basename(projectRootPath);
365
    return <Uri>[
366
      for (final FlutterDevice device in flutterDevices)
367 368
        await device.setupDevFS(
          fsName,
369
          globals.fs.directory(projectRootPath),
370 371 372
          packagesFilePath: packagesFilePath,
        ),
    ];
373
  }
374

375
  Future<UpdateFSReport> _updateDevFS({ bool fullRestart = false }) async {
376
    final bool isFirstUpload = !assetBundle.wasBuiltOnce();
377
    final bool rebuildBundle = assetBundle.needsBuild();
378
    if (rebuildBundle) {
379
      globals.printTrace('Updating assets');
380
      final int result = await assetBundle.build(packagesPath: '.packages');
381
      if (result != 0) {
382
        return UpdateFSReport(success: false);
383
      }
384
    }
385

386
    final InvalidationResult invalidationResult = await projectFileInvalidator.findInvalidated(
387
      lastCompiled: flutterDevices[0].devFS.lastCompiled,
388 389
      urisToMonitor: flutterDevices[0].devFS.sources,
      packagesPath: packagesFilePath,
390
      asyncScanning: hotRunnerConfig.asyncScanning,
391
      packageConfig: flutterDevices[0].devFS.lastPackageConfig,
392
    );
393 394 395 396 397 398 399 400 401
    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.'
      );
    }
402
    final UpdateFSReport results = UpdateFSReport(success: true);
403
    for (final FlutterDevice device in flutterDevices) {
404
      results.incorporateResults(await device.updateDevFS(
405
        mainUri: entrypointFile.absolute.uri,
406
        target: target,
407
        bundle: assetBundle,
408 409
        firstBuildTime: firstBuildTime,
        bundleFirstUpload: isFirstUpload,
410
        bundleDirty: !isFirstUpload && rebuildBundle,
411 412
        fullRestart: fullRestart,
        projectRootPath: projectRootPath,
413
        pathToReload: getReloadPath(fullRestart: fullRestart, swap: _swap),
414 415
        invalidatedFiles: invalidationResult.uris,
        packageConfig: invalidationResult.packageConfig,
416
        dillOutputPath: dillOutputPath,
417 418
      ));
    }
419
    return results;
420 421
  }

422
  void _resetDirtyAssets() {
423
    for (final FlutterDevice device in flutterDevices) {
424
      device.devFS.assetPathsToEvict.clear();
425
    }
426 427
  }

428
  Future<void> _cleanupDevFS() async {
429
    final List<Future<void>> futures = <Future<void>>[];
430
    for (final FlutterDevice device in flutterDevices) {
431
      if (device.devFS != null) {
432 433
        // Cleanup the devFS, but don't wait indefinitely.
        // We ignore any errors, because it's not clear what we would do anyway.
434
        futures.add(device.devFS.destroy()
435 436
          .timeout(const Duration(milliseconds: 250))
          .catchError((dynamic error) {
437
            globals.printTrace('Ignored error while cleaning up DevFS: $error');
438
          }));
439 440 441
      }
      device.devFS = null;
    }
442
    await Future.wait(futures);
443 444
  }

445 446
  Future<void> _launchInView(
    FlutterDevice device,
447 448 449
    Uri main,
    Uri assetsDirectory,
  ) async {
450
    final List<FlutterView> views = await device.vmService.getFlutterViews();
451
    await Future.wait(<Future<void>>[
452
      for (final FlutterView view in views)
453 454 455 456 457
        device.vmService.runInView(
          viewId: view.id,
          main: main,
          assetsDirectory: assetsDirectory,
        ),
458
    ]);
459 460
  }

461
  Future<void> _launchFromDevFS(String mainScript) async {
462
    final String entryUri = globals.fs.path.relative(mainScript, from: projectRootPath);
463
    final List<Future<void>> futures = <Future<void>>[];
464
    for (final FlutterDevice device in flutterDevices) {
465
      final Uri deviceEntryUri = device.devFS.baseUri.resolveUri(
466
        globals.fs.path.toUri(entryUri));
467
      final Uri deviceAssetsDirectoryUri = device.devFS.baseUri.resolveUri(
468
        globals.fs.path.toUri(getAssetBuildDirectory()));
469
      futures.add(_launchInView(device,
470
                          deviceEntryUri,
471
                          deviceAssetsDirectoryUri));
472
    }
473 474 475
    await Future.wait(futures);
    if (benchmarkMode) {
      futures.clear();
476
      for (final FlutterDevice device in flutterDevices) {
477 478
        final List<FlutterView> views = await device.vmService.getFlutterViews();
        for (final FlutterView view in views) {
479 480
          futures.add(device.vmService
            .flushUIThreadTasks(uiIsolateId: view.uiIsolate.id));
481 482
        }
      }
483 484
      await Future.wait(futures);
    }
485 486
  }

487 488
  Future<OperationResult> _restartFromSources({
    String reason,
489
    bool benchmarkMode = false,
490
  }) async {
491
    final Stopwatch restartTimer = Stopwatch()..start();
492 493
    // TODO(aam): Add generator reset logic once we switch to using incremental
    // compiler for full application recompilation on restart.
494 495
    final UpdateFSReport updatedDevFS = await _updateDevFS(fullRestart: true);
    if (!updatedDevFS.success) {
496
      for (final FlutterDevice device in flutterDevices) {
497
        if (device.generator != null) {
498
          await device.generator.reject();
499
        }
500
      }
501
      return OperationResult(1, 'DevFS synchronization failed');
502 503
    }
    _resetDirtyAssets();
504
    for (final FlutterDevice device in flutterDevices) {
505 506
      // VM must have accepted the kernel binary, there will be no reload
      // report, so we let incremental compiler know that source code was accepted.
507
      if (device.generator != null) {
508
        device.generator.accept();
509
      }
510
    }
511
    // Check if the isolate is paused and resume it.
512
    final List<Future<void>> operations = <Future<void>>[];
513
    for (final FlutterDevice device in flutterDevices) {
514
      final Set<String> uiIsolatesIds = <String>{};
515 516
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
517 518
        if (view.uiIsolate == null) {
          continue;
519
        }
520
        uiIsolatesIds.add(view.uiIsolate.id);
521
        // Reload the isolate.
522 523 524 525
        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)) {
526 527 528 529 530 531 532 533 534 535 536 537 538
            // 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 unpausing 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. Instead, all breakpoints are first
            // disabled and then the isolate resumed.
            final List<Future<void>> breakpointRemoval = <Future<void>>[
              for (final vm_service.Breakpoint breakpoint in isolate.breakpoints)
                device.vmService.removeBreakpoint(isolate.id, breakpoint.id)
            ];
            await Future.wait(breakpointRemoval);
539
            await device.vmService.resume(view.uiIsolate.id);
540 541
          }
        }));
542
      }
543

544 545 546
      // The engine handles killing and recreating isolates that it has spawned
      // ("uiIsolates"). The isolates that were spawned from these uiIsolates
      // will not be restared, and so they must be manually killed.
547 548 549 550
      final vm_service.VM vm = await device.vmService.getVM();
      for (final vm_service.IsolateRef isolateRef in vm.isolates) {
        if (uiIsolatesIds.contains(isolateRef.id)) {
          continue;
551
        }
552 553 554 555 556
        operations.add(device.vmService.kill(isolateRef.id)
          .catchError((dynamic error, StackTrace stackTrace) {
            // Do nothing on a SentinelException since it means the isolate
            // has already been killed.
          }, test: (dynamic error) => error is vm_service.SentinelException));
557
      }
558
    }
559
    await Future.wait(operations);
560

561
    await _launchFromDevFS('$mainPath${_swap ? '.swap' : ''}.dill');
562
    restartTimer.stop();
563
    globals.printTrace('Hot restart performed in ${getElapsedAsMilliseconds(restartTimer.elapsed)}.');
564 565
    _addBenchmarkData('hotRestartMillisecondsToFrame',
        restartTimer.elapsed.inMilliseconds);
566 567

    // Send timing analytics.
568
    globals.flutterUsage.sendTiming('hot', 'restart', restartTimer.elapsed);
569 570 571 572

    // In benchmark mode, make sure all stream notifications have finished.
    if (benchmarkMode) {
      final List<Future<void>> isolateNotifications = <Future<void>>[];
573
      for (final FlutterDevice device in flutterDevices) {
574 575 576 577 578
        try {
          await device.vmService.streamListen('Isolate');
        } on vm_service.RPCError {
          // Do nothing, we're already subcribed.
        }
579 580
        isolateNotifications.add(
          device.vmService.onIsolateEvent.firstWhere((vm_service.Event event) {
581
            return event.kind == vm_service.EventKind.kIsolateRunnable;
582 583
          }),
        );
584 585 586
      }
      await Future.wait(isolateNotifications);
    }
587 588 589
    // Toggle the main dill name after successfully uploading.
    _swap =! _swap;

590
    return OperationResult.ok;
591 592 593
  }

  /// Returns [true] if the reload was successful.
594
  /// Prints errors if [printErrors] is [true].
595 596 597 598
  static bool validateReloadReport(
    Map<String, dynamic> reloadReport, {
    bool printErrors = true,
  }) {
599
    if (reloadReport == null) {
600
      if (printErrors) {
601
        globals.printError('Hot reload did not receive reload report.');
602
      }
603 604 605 606 607 608 609
      return false;
    }
    if (!(reloadReport['type'] == 'ReloadReport' &&
          (reloadReport['success'] == true ||
           (reloadReport['success'] == false &&
            (reloadReport['details'] is Map<String, dynamic> &&
             reloadReport['details']['notices'] is List<dynamic> &&
610 611
             (reloadReport['details']['notices'] as List<dynamic>).isNotEmpty &&
             (reloadReport['details']['notices'] as List<dynamic>).every(
612 613 614 615 616 617
               (dynamic item) => item is Map<String, dynamic> && item['message'] is String
             )
            )
           )
          )
         )) {
618
      if (printErrors) {
619
        globals.printError('Hot reload received invalid response: $reloadReport');
620
      }
621 622
      return false;
    }
623
    if (!(reloadReport['success'] as bool)) {
624
      if (printErrors) {
625
        globals.printError('Hot reload was rejected:');
626
        for (final Map<String, dynamic> notice in (reloadReport['details']['notices'] as List<dynamic>).cast<Map<String, dynamic>>()) {
627
          globals.printError('${notice['message']}');
628
        }
629
      }
630 631 632 633 634
      return false;
    }
    return true;
  }

635 636 637
  @override
  bool get supportsRestart => true;

638
  @override
639 640 641
  Future<OperationResult> restart({
    bool fullRestart = false,
    String reason,
642
    bool benchmarkMode = false,
643 644
    bool silent = false,
    bool pause = false,
645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662
  }) async {
    String targetPlatform;
    String sdkName;
    bool emulator;
    if (flutterDevices.length == 1) {
      final Device device = flutterDevices.first.device;
      targetPlatform = getNameForTargetPlatform(await device.targetPlatform);
      sdkName = await device.sdkNameAndVersion;
      emulator = await device.isLocalEmulator;
    } else if (flutterDevices.length > 1) {
      targetPlatform = 'multiple';
      sdkName = 'multiple';
      emulator = false;
    } else {
      targetPlatform = 'unknown';
      sdkName = 'unknown';
      emulator = false;
    }
663
    final Stopwatch timer = Stopwatch()..start();
664 665 666 667

    // Run source generation if needed.
    await runSourceGenerators();

668
    if (fullRestart) {
669 670 671 672 673 674
      final OperationResult result = await _fullRestartHelper(
        targetPlatform: targetPlatform,
        sdkName: sdkName,
        emulator: emulator,
        reason: reason,
        benchmarkMode: benchmarkMode,
675
        silent: silent,
676
      );
677
      if (!silent) {
678
        globals.printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.');
679
      }
680 681 682 683 684 685 686
      return result;
    }
    final OperationResult result = await _hotReloadHelper(
      targetPlatform: targetPlatform,
      sdkName: sdkName,
      emulator: emulator,
      reason: reason,
687
      pause: pause,
688 689 690
    );
    if (result.isOk) {
      final String elapsed = getElapsedAsMilliseconds(timer.elapsed);
691
      if (!silent) {
692
        globals.printStatus('${result.message} in $elapsed.');
693
      }
694 695 696 697 698 699 700 701 702 703
    }
    return result;
  }

  Future<OperationResult> _fullRestartHelper({
    String targetPlatform,
    String sdkName,
    bool emulator,
    String reason,
    bool benchmarkMode,
704
    bool silent,
705 706 707 708
  }) async {
    if (!canHotRestart) {
      return OperationResult(1, 'hotRestart not supported');
    }
709 710
    Status status;
    if (!silent) {
711
      status = globals.logger.startProgress(
712 713 714 715 716
        'Performing hot restart...',
        timeout: timeoutConfiguration.fastOperation,
        progressId: 'hot.restart',
      );
    }
717 718 719 720 721
    OperationResult result;
    String restartEvent = 'restart';
    try {
      if (!(await hotRunnerConfig.setupHotRestart())) {
        return OperationResult(1, 'setupHotRestart failed');
Devon Carew's avatar
Devon Carew committed
722
      }
723 724 725 726 727
      // The current implementation of the vmservice and JSON rpc may throw
      // unhandled exceptions into the zone that cannot be caught with a regular
      // try catch. The usage is [asyncGuard] is required to normalize the error
      // handling, at least until we can refactor the underlying code.
      result = await asyncGuard(() => _restartFromSources(
728 729
        reason: reason,
        benchmarkMode: benchmarkMode,
730
      ));
731 732
      if (!result.isOk) {
        restartEvent = 'restart-failed';
733
      }
734 735 736 737
    } 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) {
738
      restartEvent = 'exception';
739
      return OperationResult(1, 'hot restart failed to complete: $err\n$st', fatal: true);
740 741 742 743 744 745
    } finally {
      HotEvent(restartEvent,
        targetPlatform: targetPlatform,
        sdkName: sdkName,
        emulator: emulator,
        fullRestart: true,
746
        nullSafety: usageNullSafety,
747
        reason: reason).send();
748
      status?.cancel();
749
    }
750
    return result;
751
  }
752

753 754 755 756 757
  Future<OperationResult> _hotReloadHelper({
    String targetPlatform,
    String sdkName,
    bool emulator,
    String reason,
758
    bool pause,
759
  }) async {
760
    Status status = globals.logger.startProgress(
761
      'Performing hot reload...',
762 763 764 765 766 767 768 769 770 771
      timeout: timeoutConfiguration.fastOperation,
      progressId: 'hot.reload',
    );
    OperationResult result;
    try {
      result = await _reloadSources(
        targetPlatform: targetPlatform,
        sdkName: sdkName,
        emulator: emulator,
        reason: reason,
772
        pause: pause,
773 774
        onSlow: (String message) {
          status?.cancel();
775
          status = globals.logger.startProgress(
776 777 778 779 780 781
            message,
            timeout: timeoutConfiguration.slowOperation,
            progressId: 'hot.reload',
          );
        },
      );
782
    } on vm_service.RPCError {
783 784 785 786 787
      HotEvent('exception',
        targetPlatform: targetPlatform,
        sdkName: sdkName,
        emulator: emulator,
        fullRestart: false,
788 789 790
        nullSafety: usageNullSafety,
        reason: reason,
      ).send();
791 792 793
      return OperationResult(1, 'hot reload failed to complete', fatal: true);
    } finally {
      status.cancel();
794
    }
795 796 797 798 799 800 801 802 803
    return result;
  }

  Future<OperationResult> _reloadSources({
    String targetPlatform,
    String sdkName,
    bool emulator,
    bool pause = false,
    String reason,
804
    void Function(String message) onSlow,
805
  }) async {
806
    for (final FlutterDevice device in flutterDevices) {
807 808
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
809
        if (view.uiIsolate == null) {
810
          return OperationResult(2, 'Application isolate not found', fatal: true);
811
        }
812 813
      }
    }
814

815 816
    final Stopwatch reloadTimer = Stopwatch()..start();
    final Stopwatch devFSTimer = Stopwatch()..start();
817
    final UpdateFSReport updatedDevFS = await _updateDevFS();
818
    // Record time it took to synchronize to DevFS.
819
    bool shouldReportReloadTime = true;
820
    _addBenchmarkData('hotReloadDevFSSyncMilliseconds', devFSTimer.elapsed.inMilliseconds);
821
    if (!updatedDevFS.success) {
822
      return OperationResult(1, 'DevFS synchronization failed');
823
    }
Devon Carew's avatar
Devon Carew committed
824
    String reloadMessage;
825
    final Stopwatch vmReloadTimer = Stopwatch()..start();
826
    Map<String, dynamic> firstReloadDetails;
827
    try {
828
      final String entryPath = globals.fs.path.relative(
829
        getReloadPath(fullRestart: false, swap: _swap),
830 831
        from: projectRootPath,
      );
832
      final List<Future<DeviceReloadReport>> allReportsFutures = <Future<DeviceReloadReport>>[];
833
      for (final FlutterDevice device in flutterDevices) {
834
        if (_shouldResetAssetDirectory) {
835
          // Asset directory has to be set only once when we switch from
836
          // running from bundle to uploaded files.
837
          await device.resetAssetDirectory();
838
          _shouldResetAssetDirectory = false;
839
        }
840
        final List<Future<vm_service.ReloadReport>> reportFutures = await device.reloadSources(
841
          entryPath, pause: pause,
842
        );
843
        allReportsFutures.add(Future.wait(reportFutures).then(
844
          (List<vm_service.ReloadReport> reports) async {
845 846
            // TODO(aam): Investigate why we are validating only first reload report,
            // which seems to be current behavior
847
            final vm_service.ReloadReport firstReport = reports.first;
848 849 850
            // Don't print errors because they will be printed further down when
            // `validateReloadReport` is called again.
            await device.updateReloadStatus(
851
              validateReloadReport(firstReport.json, printErrors: false),
852
            );
853
            return DeviceReloadReport(device, reports);
854 855
          },
        ));
856
      }
857
      final List<DeviceReloadReport> reports = await Future.wait(allReportsFutures);
858
      for (final DeviceReloadReport report in reports) {
859 860
        final vm_service.ReloadReport reloadReport = report.reports[0];
        if (!validateReloadReport(reloadReport.json)) {
861
          // Reload failed.
862 863 864 865 866 867
          HotEvent('reload-reject',
            targetPlatform: targetPlatform,
            sdkName: sdkName,
            emulator: emulator,
            fullRestart: false,
            reason: reason,
868
            nullSafety: usageNullSafety,
869
          ).send();
870 871
          return OperationResult(1, 'Reload rejected');
        }
872 873 874
        // 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.
875 876 877
        firstReloadDetails ??= castStringKeyedMap(reloadReport.json['details']);
        final int loadedLibraryCount = reloadReport.json['details']['loadedLibraryCount'] as int;
        final int finalLibraryCount = reloadReport.json['details']['finalLibraryCount'] as int;
878
        globals.printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries');
879
        reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries';
880
      }
881
    } on Map<String, dynamic> catch (error, stackTrace) {
882
      globals.printTrace('Hot reload failed: $error\n$stackTrace');
883 884
      final int errorCode = error['code'] as int;
      String errorMessage = error['message'] as String;
885
      if (errorCode == kIsolateReloadBarred) {
886 887 888 889
        errorMessage = 'Unable to hot reload application due to an unrecoverable error in '
                       'the source code. Please address the error and then use "R" to '
                       'restart the app.\n'
                       '$errorMessage (error code: $errorCode)';
890 891 892 893 894 895
        HotEvent('reload-barred',
          targetPlatform: targetPlatform,
          sdkName: sdkName,
          emulator: emulator,
          fullRestart: false,
          reason: reason,
896
          nullSafety: usageNullSafety,
897
        ).send();
898
        return OperationResult(errorCode, errorMessage);
899
      }
900
      return OperationResult(errorCode, '$errorMessage (error code: $errorCode)');
901
    } on Exception catch (error, stackTrace) {
902
      globals.printTrace('Hot reload failed: $error\n$stackTrace');
903
      return OperationResult(1, '$error');
904
    }
905
    // Record time it took for the VM to reload the sources.
906
    _addBenchmarkData('hotReloadVMReloadMilliseconds', vmReloadTimer.elapsed.inMilliseconds);
907
    final Stopwatch reassembleTimer = Stopwatch()..start();
908 909 910 911
    await _evictDirtyAssets();

    // Check if any isolates are paused and reassemble those
    // that aren't.
912
    final Map<FlutterView, vm_service.VmService> reassembleViews = <FlutterView, vm_service.VmService>{};
913
    final List<Future<void>> reassembleFutures = <Future<void>>[];
914 915
    String serviceEventKind;
    int pausedIsolatesFound = 0;
916
    bool failedReassemble = false;
917
    for (final FlutterDevice device in flutterDevices) {
918 919
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
920 921
        // 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.
922 923 924 925 926 927
        final vm_service.Isolate isolate = await device.vmService
          .getIsolateOrNull(view.uiIsolate.id);
        final vm_service.Event pauseEvent = isolate?.pauseEvent;
        if (pauseEvent != null
          && isPauseEvent(pauseEvent.kind)
          && pauseEvent.kind != vm_service.EventKind.kPausePostRequest) {
928 929 930 931 932 933 934
          pausedIsolatesFound += 1;
          if (serviceEventKind == null) {
            serviceEventKind = pauseEvent.kind;
          } else if (serviceEventKind != pauseEvent.kind) {
            serviceEventKind = ''; // many kinds
          }
        } else {
935
          reassembleViews[view] = device.vmService;
936 937 938 939 940 941 942 943 944 945 946 947 948
          // If the tool identified a change in a single widget, do a fast instead
          // of a full reassemble.
          Future<void> reassembleWork;
          if (updatedDevFS.fastReassemble == true) {
            reassembleWork = device.vmService.flutterFastReassemble(
              isolateId: view.uiIsolate.id,
            );
          } else {
            reassembleWork = device.vmService.flutterReassemble(
              isolateId: view.uiIsolate.id,
            );
          }
          reassembleFutures.add(reassembleWork.catchError((dynamic error) {
949 950 951
            failedReassemble = true;
            globals.printError('Reassembling ${view.uiIsolate.name} failed: $error');
          }, test: (dynamic error) => error is Exception));
952
        }
953 954
      }
    }
955
    if (pausedIsolatesFound > 0) {
956
      if (onSlow != null) {
957
        onSlow('${_describePausedIsolates(pausedIsolatesFound, serviceEventKind)}; interface might not update.');
958
      }
959
      if (reassembleViews.isEmpty) {
960
        globals.printTrace('Skipping reassemble because all isolates are paused.');
961 962
        return OperationResult(OperationResult.ok.code, reloadMessage);
      }
963
    }
964
    assert(reassembleViews.isNotEmpty);
965

966
    globals.printTrace('Reassembling application');
967

968
    final Future<void> reassembleFuture = Future.wait<void>(reassembleFutures);
969 970 971 972 973 974 975 976
    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.
977
        globals.printTrace('This is taking a long time; will now check for paused isolates.');
978 979
        int postReloadPausedIsolatesFound = 0;
        String serviceEventKind;
980 981 982 983 984 985 986
        for (final FlutterView view in reassembleViews.keys) {
          final vm_service.Isolate isolate = await reassembleViews[view]
            .getIsolateOrNull(view.uiIsolate.id);
          if (isolate == null) {
            continue;
          }
          if (isolate.pauseEvent != null && isPauseEvent(isolate.pauseEvent.kind)) {
987 988
            postReloadPausedIsolatesFound += 1;
            if (serviceEventKind == null) {
989 990
              serviceEventKind = isolate.pauseEvent.kind;
            } else if (serviceEventKind != isolate.pauseEvent.kind) {
991 992 993 994
              serviceEventKind = ''; // many kinds
            }
          }
        }
995
        globals.printTrace('Found $postReloadPausedIsolatesFound newly paused isolate(s).');
996 997 998 999 1000
        if (postReloadPausedIsolatesFound == 0) {
          await reassembleFuture; // must just be taking a long time... keep waiting!
          return;
        }
        shouldReportReloadTime = false;
1001
        if (onSlow != null) {
1002
          onSlow('${_describePausedIsolates(postReloadPausedIsolatesFound, serviceEventKind)}.');
1003
        }
1004 1005
      },
    );
1006
    // Record time it took for Flutter to reassemble the application.
1007
    _addBenchmarkData('hotReloadFlutterReassembleMilliseconds', reassembleTimer.elapsed.inMilliseconds);
1008

1009
    reloadTimer.stop();
1010 1011 1012
    final Duration reloadDuration = reloadTimer.elapsed;
    final int reloadInMs = reloadDuration.inMilliseconds;

1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024
    // 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,
1025 1026 1027 1028
      finalLibraryCount: firstReloadDetails['finalLibraryCount'] as int,
      syncedLibraryCount: firstReloadDetails['receivedLibraryCount'] as int,
      syncedClassesCount: firstReloadDetails['receivedClassesCount'] as int,
      syncedProceduresCount: firstReloadDetails['receivedProceduresCount'] as int,
1029 1030 1031
      syncedBytes: updatedDevFS.syncedBytes,
      invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount,
      transferTimeInMs: devFSTimer.elapsed.inMilliseconds,
1032
      nullSafety: usageNullSafety,
1033
    ).send();
1034

1035
    if (shouldReportReloadTime) {
1036
      globals.printTrace('Hot reload performed in ${getElapsedAsMilliseconds(reloadDuration)}.');
1037 1038 1039 1040
      // Record complete time it took for the reload.
      _addBenchmarkData('hotReloadMillisecondsToFrame', reloadInMs);
    }
    // Only report timings if we reloaded a single view without any errors.
1041
    if ((reassembleViews.length == 1) && !failedReassemble && shouldReportReloadTime) {
1042
      globals.flutterUsage.sendTiming('hot', 'reload', reloadDuration);
1043
    }
1044
    return OperationResult(
1045
      failedReassemble ? 1 : OperationResult.ok.code,
1046
      reloadMessage,
1047
    );
1048 1049
  }

1050 1051 1052 1053 1054
  String _describePausedIsolates(int pausedIsolatesFound, String serviceEventKind) {
    assert(pausedIsolatesFound > 0);
    final StringBuffer message = StringBuffer();
    bool plural;
    if (pausedIsolatesFound == 1) {
1055
      message.write('The application is ');
1056 1057 1058 1059 1060 1061 1062
      plural = false;
    } else {
      message.write('$pausedIsolatesFound isolates are ');
      plural = true;
    }
    assert(serviceEventKind != null);
    switch (serviceEventKind) {
1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083
      case vm_service.EventKind.kPauseStart:
        message.write('paused (probably due to --start-paused)');
        break;
      case vm_service.EventKind.kPauseExit:
        message.write('paused because ${ plural ? 'they have' : 'it has' } terminated');
        break;
      case vm_service.EventKind.kPauseBreakpoint:
        message.write('paused in the debugger on a breakpoint');
        break;
      case vm_service.EventKind.kPauseInterrupted:
        message.write('paused due in the debugger');
        break;
      case vm_service.EventKind.kPauseException:
        message.write('paused in the debugger after an exception was thrown');
        break;
      case vm_service.EventKind.kPausePostRequest:
        message.write('paused');
        break;
      case '':
        message.write('paused for various reasons');
        break;
1084 1085 1086 1087 1088 1089
      default:
        message.write('paused');
    }
    return message.toString();
  }

1090

1091
  @override
1092
  void printHelp({ @required bool details }) {
1093
    globals.printStatus('Flutter run key commands.');
1094
    commandHelp.r.print();
1095
    if (canHotRestart) {
1096
      commandHelp.R.print();
1097
    }
1098
    commandHelp.h.print();
1099
    if (_didAttach) {
1100
      commandHelp.d.print();
1101
    }
1102
    commandHelp.c.print();
1103
    commandHelp.q.print();
1104 1105
    if (details) {
      printHelpDetails();
1106 1107 1108 1109 1110 1111
    }
    for (final FlutterDevice device in flutterDevices) {
      final String dname = device.device.name;
      // Caution: This log line is parsed by device lab tests.
      globals.printStatus(
        'An Observatory debugger and profiler on $dname is available at: '
1112
        '${device.vmService.httpAddress}',
1113
      );
1114
    }
1115 1116
  }

1117
  Future<void> _evictDirtyAssets() async {
1118
    final List<Future<Map<String, dynamic>>> futures = <Future<Map<String, dynamic>>>[];
1119
    for (final FlutterDevice device in flutterDevices) {
1120
      if (device.devFS.assetPathsToEvict.isEmpty) {
1121
        continue;
1122
      }
1123 1124
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      if (views.first.uiIsolate == null) {
1125
        globals.printError('Application isolate not found for $device');
1126 1127
        continue;
      }
1128
      for (final String assetPath in device.devFS.assetPathsToEvict) {
1129
        futures.add(
1130
          device.vmService
1131 1132
            .flutterEvictAsset(
              assetPath,
1133
              isolateId: views.first.uiIsolate.id,
1134 1135
            )
        );
1136 1137 1138 1139 1140 1141
      }
      device.devFS.assetPathsToEvict.clear();
    }
    return Future.wait<Map<String, dynamic>>(futures);
  }

1142
  @override
1143
  Future<void> cleanupAfterSignal() async {
1144
    await stopEchoingDeviceLog();
1145
    await hotRunnerConfig.runPreShutdownOperations();
1146 1147 1148
    if (_didAttach) {
      appFinished();
    } else {
1149
      await exitApp();
1150
    }
1151 1152
  }

1153
  @override
1154
  Future<void> preExit() async {
1155 1156
    await _cleanupDevFS();
    await hotRunnerConfig.runPreShutdownOperations();
1157
    await super.preExit();
1158
  }
1159

1160
  @override
1161
  Future<void> cleanupAtFinish() async {
1162
    for (final FlutterDevice flutterDevice in flutterDevices) {
1163
      await flutterDevice.device.dispose();
1164
    }
1165 1166 1167 1168
    await _cleanupDevFS();
    await stopEchoingDeviceLog();
  }
}
1169

1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180
/// The result of an invalidation check from [ProjectFileInvalidator].
class InvalidationResult {
  const InvalidationResult({
    this.uris,
    this.packageConfig,
  });

  final List<Uri> uris;
  final PackageConfig packageConfig;
}

1181 1182
/// The [ProjectFileInvalidator] track the dependencies for a running
/// application to determine when they are dirty.
1183
class ProjectFileInvalidator {
1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194
  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;
1195

1196
  static const String _pubCachePathLinuxAndMac = '.pub-cache';
1197 1198
  static const String _pubCachePathWindows = 'Pub/Cache';

1199
  // As of writing, Dart supports up to 32 asynchronous I/O threads per
1200
  // isolate. We also want to avoid hitting platform limits on open file
1201 1202 1203 1204 1205 1206
  // handles/descriptors.
  //
  // This value was chosen based on empirical tests scanning a set of
  // ~2000 files.
  static const int _kMaxPendingStats = 8;

1207
  Future<InvalidationResult> findInvalidated({
1208 1209 1210
    @required DateTime lastCompiled,
    @required List<Uri> urisToMonitor,
    @required String packagesPath,
1211
    @required PackageConfig packageConfig,
1212 1213
    bool asyncScanning = false,
  }) async {
1214 1215 1216 1217
    assert(urisToMonitor != null);
    assert(packagesPath != null);

    if (lastCompiled == null) {
1218
      // Initial load.
1219
      assert(urisToMonitor.isEmpty);
1220 1221 1222 1223
      return InvalidationResult(
        packageConfig: await _createPackageConfig(packagesPath),
        uris: <Uri>[]
      );
1224 1225
    }

1226
    final Stopwatch stopwatch = Stopwatch()..start();
1227 1228
    final List<Uri> urisToScan = <Uri>[
      // Don't watch pub cache directories to speed things up a little.
1229
      for (final Uri uri in urisToMonitor)
1230
        if (_isNotInPubCache(uri)) uri,
1231 1232
    ];
    final List<Uri> invalidatedFiles = <Uri>[];
1233 1234 1235 1236 1237
    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>(
1238 1239
          () => _fileSystem
            .stat(uri.toFilePath(windows: _platform.isWindows))
1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250
            .then((FileStat stat) {
              final DateTime updatedAt = stat.modified;
              if (updatedAt != null && updatedAt.isAfter(lastCompiled)) {
                invalidatedFiles.add(uri);
              }
            })
        ));
      }
      await Future.wait<void>(waitList);
    } else {
      for (final Uri uri in urisToScan) {
1251 1252
        final DateTime updatedAt = _fileSystem.statSync(
            uri.toFilePath(windows: _platform.isWindows)).modified;
1253 1254 1255
        if (updatedAt != null && updatedAt.isAfter(lastCompiled)) {
          invalidatedFiles.add(uri);
        }
1256 1257
      }
    }
1258 1259 1260 1261 1262 1263 1264
    // We need to check the .packages file too since it is not used in compilation.
    final Uri packageUri = _fileSystem.file(packagesPath).uri;
    final DateTime updatedAt = _fileSystem.statSync(
      packageUri.toFilePath(windows: _platform.isWindows)).modified;
    if (updatedAt != null && updatedAt.isAfter(lastCompiled)) {
      invalidatedFiles.add(packageUri);
      packageConfig = await _createPackageConfig(packagesPath);
1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275
      // The frontend_server might be monitoring the package_config.json file,
      // Pub should always produce both files.
      // TODO(jonahwilliams): remove after https://github.com/flutter/flutter/issues/55249
      if (_fileSystem.path.basename(packagesPath) == '.packages') {
        final File packageConfigFile = _fileSystem.file(packagesPath)
          .parent.childDirectory('.dart_tool')
          .childFile('package_config.json');
        if (packageConfigFile.existsSync()) {
          invalidatedFiles.add(packageConfigFile.uri);
        }
      }
1276 1277
    }

1278
    _logger.printTrace(
1279
      'Scanned through ${urisToScan.length} files in '
1280 1281
      '${stopwatch.elapsedMilliseconds}ms'
      '${asyncScanning ? " (async)" : ""}',
1282
    );
1283 1284 1285 1286
    return InvalidationResult(
      packageConfig: packageConfig,
      uris: invalidatedFiles,
    );
1287
  }
1288

1289 1290
  bool _isNotInPubCache(Uri uri) {
    return !(_platform.isWindows && uri.path.contains(_pubCachePathWindows))
1291 1292
        && !uri.path.contains(_pubCachePathLinuxAndMac);
  }
1293 1294

  Future<PackageConfig> _createPackageConfig(String packagesPath) {
1295
    return loadPackageConfigWithLogging(
1296
      _fileSystem.file(packagesPath),
1297
      logger: _logger,
1298 1299
    );
  }
1300
}