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