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:meta/meta.dart'; 8 9import 'application_package.dart'; 10import 'artifacts.dart'; 11import 'asset.dart'; 12import 'base/common.dart'; 13import 'base/file_system.dart'; 14import 'base/io.dart' as io; 15import 'base/logger.dart'; 16import 'base/terminal.dart'; 17import 'base/utils.dart'; 18import 'build_info.dart'; 19import 'codegen.dart'; 20import 'compile.dart'; 21import 'dart/package_map.dart'; 22import 'devfs.dart'; 23import 'device.dart'; 24import 'globals.dart'; 25import 'project.dart'; 26import 'run_cold.dart'; 27import 'run_hot.dart'; 28import 'vmservice.dart'; 29 30class FlutterDevice { 31 FlutterDevice( 32 this.device, { 33 @required this.trackWidgetCreation, 34 this.fileSystemRoots, 35 this.fileSystemScheme, 36 this.viewFilter, 37 TargetModel targetModel = TargetModel.flutter, 38 List<String> experimentalFlags, 39 ResidentCompiler generator, 40 @required BuildMode buildMode, 41 }) : assert(trackWidgetCreation != null), 42 generator = generator ?? ResidentCompiler( 43 artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath, mode: buildMode), 44 trackWidgetCreation: trackWidgetCreation, 45 fileSystemRoots: fileSystemRoots, 46 fileSystemScheme: fileSystemScheme, 47 targetModel: targetModel, 48 experimentalFlags: experimentalFlags, 49 ); 50 51 /// Create a [FlutterDevice] with optional code generation enabled. 52 static Future<FlutterDevice> create( 53 Device device, { 54 @required FlutterProject flutterProject, 55 @required bool trackWidgetCreation, 56 @required String target, 57 @required BuildMode buildMode, 58 List<String> fileSystemRoots, 59 String fileSystemScheme, 60 String viewFilter, 61 TargetModel targetModel = TargetModel.flutter, 62 List<String> experimentalFlags, 63 ResidentCompiler generator, 64 }) async { 65 ResidentCompiler generator; 66 if (flutterProject.hasBuilders) { 67 generator = await CodeGeneratingResidentCompiler.create( 68 flutterProject: flutterProject, 69 ); 70 } else { 71 generator = ResidentCompiler( 72 artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath, mode: buildMode), 73 trackWidgetCreation: trackWidgetCreation, 74 fileSystemRoots: fileSystemRoots, 75 fileSystemScheme: fileSystemScheme, 76 targetModel: targetModel, 77 experimentalFlags: experimentalFlags, 78 ); 79 } 80 return FlutterDevice( 81 device, 82 trackWidgetCreation: trackWidgetCreation, 83 fileSystemRoots: fileSystemRoots, 84 fileSystemScheme:fileSystemScheme, 85 viewFilter: viewFilter, 86 experimentalFlags: experimentalFlags, 87 targetModel: targetModel, 88 generator: generator, 89 buildMode: buildMode, 90 ); 91 } 92 93 final Device device; 94 final ResidentCompiler generator; 95 List<Uri> observatoryUris; 96 List<VMService> vmServices; 97 DevFS devFS; 98 ApplicationPackage package; 99 List<String> fileSystemRoots; 100 String fileSystemScheme; 101 StreamSubscription<String> _loggingSubscription; 102 final String viewFilter; 103 final bool trackWidgetCreation; 104 105 /// If the [reloadSources] parameter is not null the 'reloadSources' service 106 /// will be registered. 107 /// The 'reloadSources' service can be used by other Service Protocol clients 108 /// connected to the VM (e.g. Observatory) to request a reload of the source 109 /// code of the running application (a.k.a. HotReload). 110 /// The 'compileExpression' service can be used to compile user-provided 111 /// expressions requested during debugging of the application. 112 /// This ensures that the reload process follows the normal orchestration of 113 /// the Flutter Tools and not just the VM internal service. 114 Future<void> connect({ 115 ReloadSources reloadSources, 116 Restart restart, 117 CompileExpression compileExpression, 118 }) async { 119 if (vmServices != null) 120 return; 121 final List<VMService> localVmServices = List<VMService>(observatoryUris.length); 122 for (int i = 0; i < observatoryUris.length; i += 1) { 123 printTrace('Connecting to service protocol: ${observatoryUris[i]}'); 124 localVmServices[i] = await VMService.connect( 125 observatoryUris[i], 126 reloadSources: reloadSources, 127 restart: restart, 128 compileExpression: compileExpression, 129 ); 130 printTrace('Successfully connected to service protocol: ${observatoryUris[i]}'); 131 } 132 vmServices = localVmServices; 133 } 134 135 Future<void> refreshViews() async { 136 if (vmServices == null || vmServices.isEmpty) 137 return Future<void>.value(null); 138 final List<Future<void>> futures = <Future<void>>[]; 139 for (VMService service in vmServices) 140 futures.add(service.vm.refreshViews(waitForViews: true)); 141 await Future.wait(futures); 142 } 143 144 List<FlutterView> get views { 145 if (vmServices == null) 146 return <FlutterView>[]; 147 148 return vmServices 149 .where((VMService service) => !service.isClosed) 150 .expand<FlutterView>( 151 (VMService service) { 152 return viewFilter != null 153 ? service.vm.allViewsWithName(viewFilter) 154 : service.vm.views; 155 }, 156 ) 157 .toList(); 158 } 159 160 Future<void> getVMs() async { 161 for (VMService service in vmServices) 162 await service.getVM(); 163 } 164 165 Future<void> exitApps() async { 166 if (!device.supportsFlutterExit) { 167 await device.stopApp(package); 168 return; 169 } 170 final List<FlutterView> flutterViews = views; 171 if (flutterViews == null || flutterViews.isEmpty) 172 return; 173 final List<Future<void>> futures = <Future<void>>[]; 174 // If any of the flutter views are paused, we might not be able to 175 // cleanly exit since the service extension may not have been registered. 176 if (flutterViews.any((FlutterView view) { 177 return view != null && 178 view.uiIsolate != null && 179 view.uiIsolate.pauseEvent.isPauseEvent; 180 } 181 )) { 182 await device.stopApp(package); 183 return; 184 } 185 for (FlutterView view in flutterViews) { 186 if (view != null && view.uiIsolate != null) { 187 assert(!view.uiIsolate.pauseEvent.isPauseEvent); 188 futures.add(view.uiIsolate.flutterExit()); 189 } 190 } 191 // The flutterExit message only returns if it fails, so just wait a few 192 // seconds then assume it worked. 193 // TODO(ianh): We should make this return once the VM service disconnects. 194 await Future.wait(futures).timeout(const Duration(seconds: 2), onTimeout: () => <void>[]); 195 } 196 197 Future<Uri> setupDevFS( 198 String fsName, 199 Directory rootDirectory, { 200 String packagesFilePath, 201 }) { 202 // One devFS per device. Shared by all running instances. 203 devFS = DevFS( 204 vmServices[0], 205 fsName, 206 rootDirectory, 207 packagesFilePath: packagesFilePath, 208 ); 209 return devFS.create(); 210 } 211 212 List<Future<Map<String, dynamic>>> reloadSources( 213 String entryPath, { 214 bool pause = false, 215 }) { 216 final Uri deviceEntryUri = devFS.baseUri.resolveUri(fs.path.toUri(entryPath)); 217 final Uri devicePackagesUri = devFS.baseUri.resolve('.packages'); 218 final List<Future<Map<String, dynamic>>> reports = <Future<Map<String, dynamic>>>[]; 219 for (FlutterView view in views) { 220 final Future<Map<String, dynamic>> report = view.uiIsolate.reloadSources( 221 pause: pause, 222 rootLibUri: deviceEntryUri, 223 packagesUri: devicePackagesUri, 224 ); 225 reports.add(report); 226 } 227 return reports; 228 } 229 230 Future<void> resetAssetDirectory() async { 231 final Uri deviceAssetsDirectoryUri = devFS.baseUri.resolveUri( 232 fs.path.toUri(getAssetBuildDirectory())); 233 assert(deviceAssetsDirectoryUri != null); 234 await Future.wait<void>(views.map<Future<void>>( 235 (FlutterView view) => view.setAssetDirectory(deviceAssetsDirectoryUri) 236 )); 237 } 238 239 // Lists program elements changed in the most recent reload that have not 240 // since executed. 241 Future<List<ProgramElement>> unusedChangesInLastReload() async { 242 final List<Future<List<ProgramElement>>> reports = 243 <Future<List<ProgramElement>>>[]; 244 for (FlutterView view in views) 245 reports.add(view.uiIsolate.getUnusedChangesInLastReload()); 246 final List<ProgramElement> elements = <ProgramElement>[]; 247 for (Future<List<ProgramElement>> report in reports) { 248 for (ProgramElement element in await report) 249 elements.add(ProgramElement(element.qualifiedName, 250 devFS.deviceUriToHostUri(element.uri), 251 element.line, 252 element.column)); 253 } 254 return elements; 255 } 256 257 Future<void> debugDumpApp() async { 258 for (FlutterView view in views) 259 await view.uiIsolate.flutterDebugDumpApp(); 260 } 261 262 Future<void> debugDumpRenderTree() async { 263 for (FlutterView view in views) 264 await view.uiIsolate.flutterDebugDumpRenderTree(); 265 } 266 267 Future<void> debugDumpLayerTree() async { 268 for (FlutterView view in views) 269 await view.uiIsolate.flutterDebugDumpLayerTree(); 270 } 271 272 Future<void> debugDumpSemanticsTreeInTraversalOrder() async { 273 for (FlutterView view in views) 274 await view.uiIsolate.flutterDebugDumpSemanticsTreeInTraversalOrder(); 275 } 276 277 Future<void> debugDumpSemanticsTreeInInverseHitTestOrder() async { 278 for (FlutterView view in views) 279 await view.uiIsolate.flutterDebugDumpSemanticsTreeInInverseHitTestOrder(); 280 } 281 282 Future<void> toggleDebugPaintSizeEnabled() async { 283 for (FlutterView view in views) 284 await view.uiIsolate.flutterToggleDebugPaintSizeEnabled(); 285 } 286 287 Future<void> toggleDebugCheckElevationsEnabled() async { 288 for (FlutterView view in views) 289 await view.uiIsolate.flutterToggleDebugCheckElevationsEnabled(); 290 } 291 292 Future<void> debugTogglePerformanceOverlayOverride() async { 293 for (FlutterView view in views) 294 await view.uiIsolate.flutterTogglePerformanceOverlayOverride(); 295 } 296 297 Future<void> toggleWidgetInspector() async { 298 for (FlutterView view in views) 299 await view.uiIsolate.flutterToggleWidgetInspector(); 300 } 301 302 Future<void> toggleProfileWidgetBuilds() async { 303 for (FlutterView view in views) { 304 await view.uiIsolate.flutterToggleProfileWidgetBuilds(); 305 } 306 } 307 308 Future<String> togglePlatform({ String from }) async { 309 String to; 310 switch (from) { 311 case 'iOS': 312 to = 'android'; 313 break; 314 case 'android': 315 default: 316 to = 'iOS'; 317 break; 318 } 319 for (FlutterView view in views) 320 await view.uiIsolate.flutterPlatformOverride(to); 321 return to; 322 } 323 324 void startEchoingDeviceLog() { 325 if (_loggingSubscription != null) { 326 return; 327 } 328 final Stream<String> logStream = device.getLogReader(app: package).logLines; 329 if (logStream == null) { 330 printError('Failed to read device log stream'); 331 return; 332 } 333 _loggingSubscription = logStream.listen((String line) { 334 if (!line.contains('Observatory listening on http')) 335 printStatus(line, wrap: false); 336 }); 337 } 338 339 Future<void> stopEchoingDeviceLog() async { 340 if (_loggingSubscription == null) 341 return; 342 await _loggingSubscription.cancel(); 343 _loggingSubscription = null; 344 } 345 346 void initLogReader() { 347 device.getLogReader(app: package).appPid = vmServices.first.vm.pid; 348 } 349 350 Future<int> runHot({ 351 HotRunner hotRunner, 352 String route, 353 }) async { 354 final bool prebuiltMode = hotRunner.applicationBinary != null; 355 final String modeName = hotRunner.debuggingOptions.buildInfo.friendlyModeName; 356 printStatus('Launching ${getDisplayPath(hotRunner.mainPath)} on ${device.name} in $modeName mode...'); 357 358 final TargetPlatform targetPlatform = await device.targetPlatform; 359 package = await ApplicationPackageFactory.instance.getPackageForPlatform( 360 targetPlatform, 361 applicationBinary: hotRunner.applicationBinary, 362 ); 363 364 if (package == null) { 365 String message = 'No application found for $targetPlatform.'; 366 final String hint = await getMissingPackageHintForPlatform(targetPlatform); 367 if (hint != null) 368 message += '\n$hint'; 369 printError(message); 370 return 1; 371 } 372 373 final Map<String, dynamic> platformArgs = <String, dynamic>{}; 374 375 startEchoingDeviceLog(); 376 377 // Start the application. 378 final Future<LaunchResult> futureResult = device.startApp( 379 package, 380 mainPath: hotRunner.mainPath, 381 debuggingOptions: hotRunner.debuggingOptions, 382 platformArgs: platformArgs, 383 route: route, 384 prebuiltApplication: prebuiltMode, 385 usesTerminalUi: hotRunner.usesTerminalUi, 386 ipv6: hotRunner.ipv6, 387 ); 388 389 final LaunchResult result = await futureResult; 390 391 if (!result.started) { 392 printError('Error launching application on ${device.name}.'); 393 await stopEchoingDeviceLog(); 394 return 2; 395 } 396 if (result.hasObservatory) { 397 observatoryUris = <Uri>[result.observatoryUri]; 398 } else { 399 observatoryUris = <Uri>[]; 400 } 401 return 0; 402 } 403 404 405 Future<int> runCold({ 406 ColdRunner coldRunner, 407 String route, 408 }) async { 409 final TargetPlatform targetPlatform = await device.targetPlatform; 410 package = await ApplicationPackageFactory.instance.getPackageForPlatform( 411 targetPlatform, 412 applicationBinary: coldRunner.applicationBinary, 413 ); 414 415 final String modeName = coldRunner.debuggingOptions.buildInfo.friendlyModeName; 416 final bool prebuiltMode = coldRunner.applicationBinary != null; 417 if (coldRunner.mainPath == null) { 418 assert(prebuiltMode); 419 printStatus('Launching ${package.displayName} on ${device.name} in $modeName mode...'); 420 } else { 421 printStatus('Launching ${getDisplayPath(coldRunner.mainPath)} on ${device.name} in $modeName mode...'); 422 } 423 424 if (package == null) { 425 String message = 'No application found for $targetPlatform.'; 426 final String hint = await getMissingPackageHintForPlatform(targetPlatform); 427 if (hint != null) 428 message += '\n$hint'; 429 printError(message); 430 return 1; 431 } 432 433 final Map<String, dynamic> platformArgs = <String, dynamic>{}; 434 if (coldRunner.traceStartup != null) 435 platformArgs['trace-startup'] = coldRunner.traceStartup; 436 437 startEchoingDeviceLog(); 438 439 final LaunchResult result = await device.startApp( 440 package, 441 mainPath: coldRunner.mainPath, 442 debuggingOptions: coldRunner.debuggingOptions, 443 platformArgs: platformArgs, 444 route: route, 445 prebuiltApplication: prebuiltMode, 446 usesTerminalUi: coldRunner.usesTerminalUi, 447 ipv6: coldRunner.ipv6, 448 ); 449 450 if (!result.started) { 451 printError('Error running application on ${device.name}.'); 452 await stopEchoingDeviceLog(); 453 return 2; 454 } 455 if (result.hasObservatory) { 456 observatoryUris = <Uri>[result.observatoryUri]; 457 } else { 458 observatoryUris = <Uri>[]; 459 } 460 return 0; 461 } 462 463 Future<UpdateFSReport> updateDevFS({ 464 String mainPath, 465 String target, 466 AssetBundle bundle, 467 DateTime firstBuildTime, 468 bool bundleFirstUpload = false, 469 bool bundleDirty = false, 470 bool fullRestart = false, 471 String projectRootPath, 472 String pathToReload, 473 @required String dillOutputPath, 474 @required List<Uri> invalidatedFiles, 475 }) async { 476 final Status devFSStatus = logger.startProgress( 477 'Syncing files to device ${device.name}...', 478 timeout: timeoutConfiguration.fastOperation, 479 ); 480 UpdateFSReport report; 481 try { 482 report = await devFS.update( 483 mainPath: mainPath, 484 target: target, 485 bundle: bundle, 486 firstBuildTime: firstBuildTime, 487 bundleFirstUpload: bundleFirstUpload, 488 generator: generator, 489 fullRestart: fullRestart, 490 dillOutputPath: dillOutputPath, 491 trackWidgetCreation: trackWidgetCreation, 492 projectRootPath: projectRootPath, 493 pathToReload: pathToReload, 494 invalidatedFiles: invalidatedFiles, 495 ); 496 } on DevFSException { 497 devFSStatus.cancel(); 498 return UpdateFSReport(success: false); 499 } 500 devFSStatus.stop(); 501 printTrace('Synced ${getSizeAsMB(report.syncedBytes)}.'); 502 return report; 503 } 504 505 Future<void> updateReloadStatus(bool wasReloadSuccessful) async { 506 if (wasReloadSuccessful) 507 generator?.accept(); 508 else 509 await generator?.reject(); 510 } 511} 512 513// Issue: https://github.com/flutter/flutter/issues/33050 514// Matches the following patterns: 515// HttpException: Connection closed before full header was received, uri = * 516// HttpException: , uri = * 517final RegExp kAndroidQHttpConnectionClosedExp = RegExp(r'^HttpException\:.+\, uri \=.+$'); 518 519/// Returns `true` if any of the devices is running Android Q. 520Future<bool> hasDeviceRunningAndroidQ(List<FlutterDevice> flutterDevices) async { 521 for (FlutterDevice flutterDevice in flutterDevices) { 522 final String sdkNameAndVersion = await flutterDevice.device.sdkNameAndVersion; 523 if (sdkNameAndVersion != null && sdkNameAndVersion.startsWith('Android 10')) { 524 return true; 525 } 526 } 527 return false; 528} 529 530// Shared code between different resident application runners. 531abstract class ResidentRunner { 532 ResidentRunner( 533 this.flutterDevices, { 534 this.target, 535 this.debuggingOptions, 536 String projectRootPath, 537 String packagesFilePath, 538 this.ipv6, 539 this.usesTerminalUi = true, 540 this.stayResident = true, 541 this.hotMode = true, 542 this.dillOutputPath, 543 }) : mainPath = findMainDartFile(target), 544 projectRootPath = projectRootPath ?? fs.currentDirectory.path, 545 packagesFilePath = packagesFilePath ?? fs.path.absolute(PackageMap.globalPackagesPath), 546 assetBundle = AssetBundleFactory.instance.createBundle() { 547 // TODO(jonahwilliams): this is transitionary logic to allow us to support 548 // platforms that are not yet using flutter assemble. In the "new world", 549 // builds are isolated based on a number of factors. Thus, we cannot assume 550 // that a debug build will create the expected `build/app.dill` file. For 551 // now, I'm working around this by just creating it if it is missing here. 552 // In the future, once build & run are more strongly separated, the build 553 // environment will be plumbed through so that it all comes from a single 554 // source of truth, the [Environment]. 555 final File dillOutput = fs.file(dillOutputPath ?? fs.path.join('build', 'app.dill')); 556 if (!dillOutput.existsSync()) { 557 dillOutput.createSync(recursive: true); 558 } 559 } 560 561 @protected 562 @visibleForTesting 563 final List<FlutterDevice> flutterDevices; 564 final String target; 565 final DebuggingOptions debuggingOptions; 566 final bool usesTerminalUi; 567 final bool stayResident; 568 final bool ipv6; 569 final Completer<int> _finished = Completer<int>(); 570 final String dillOutputPath; 571 final String packagesFilePath; 572 final String projectRootPath; 573 final String mainPath; 574 final AssetBundle assetBundle; 575 576 bool _exited = false; 577 bool hotMode ; 578 String getReloadPath({ bool fullRestart }) => mainPath + (fullRestart ? '' : '.incremental') + '.dill'; 579 580 bool get isRunningDebug => debuggingOptions.buildInfo.isDebug; 581 bool get isRunningProfile => debuggingOptions.buildInfo.isProfile; 582 bool get isRunningRelease => debuggingOptions.buildInfo.isRelease; 583 bool get supportsServiceProtocol => isRunningDebug || isRunningProfile; 584 585 /// Whether this runner can hot restart. 586 /// 587 /// To prevent scenarios where only a subset of devices are hot restarted, 588 /// the runner requires that all attached devices can support hot restart 589 /// before enabling it. 590 bool get canHotRestart { 591 return flutterDevices.every((FlutterDevice device) { 592 return device.device.supportsHotRestart; 593 }); 594 } 595 596 /// Invoke an RPC extension method on the first attached ui isolate of the first device. 597 // TODO(jonahwilliams): Update/Remove this method when refactoring the resident 598 // runner to support a single flutter device. 599 Future<Map<String, dynamic>> invokeFlutterExtensionRpcRawOnFirstIsolate( 600 String method, { 601 Map<String, dynamic> params, 602 }) { 603 return flutterDevices.first.views.first.uiIsolate 604 .invokeFlutterExtensionRpcRaw(method, params: params); 605 } 606 607 /// Whether this runner can hot reload. 608 bool get canHotReload => hotMode; 609 610 /// Start the app and keep the process running during its lifetime. 611 /// 612 /// Returns the exit code that we should use for the flutter tool process; 0 613 /// for success, 1 for user error (e.g. bad arguments), 2 for other failures. 614 Future<int> run({ 615 Completer<DebugConnectionInfo> connectionInfoCompleter, 616 Completer<void> appStartedCompleter, 617 String route, 618 }); 619 620 Future<int> attach({ 621 Completer<DebugConnectionInfo> connectionInfoCompleter, 622 Completer<void> appStartedCompleter, 623 }); 624 625 bool get supportsRestart => false; 626 627 Future<OperationResult> restart({ bool fullRestart = false, bool pauseAfterRestart = false, String reason }) { 628 final String mode = isRunningProfile ? 'profile' : 629 isRunningRelease ? 'release' : 'this'; 630 throw '${fullRestart ? 'Restart' : 'Reload'} is not supported in $mode mode'; 631 } 632 633 Future<void> exit() async { 634 _exited = true; 635 await stopEchoingDeviceLog(); 636 await preExit(); 637 await exitApp(); 638 } 639 640 Future<void> detach() async { 641 await stopEchoingDeviceLog(); 642 await preExit(); 643 appFinished(); 644 } 645 646 Future<void> refreshViews() async { 647 final List<Future<void>> futures = <Future<void>>[]; 648 for (FlutterDevice device in flutterDevices) 649 futures.add(device.refreshViews()); 650 await Future.wait(futures); 651 } 652 653 Future<void> debugDumpApp() async { 654 await refreshViews(); 655 for (FlutterDevice device in flutterDevices) 656 await device.debugDumpApp(); 657 } 658 659 Future<void> debugDumpRenderTree() async { 660 await refreshViews(); 661 for (FlutterDevice device in flutterDevices) 662 await device.debugDumpRenderTree(); 663 } 664 665 Future<void> debugDumpLayerTree() async { 666 await refreshViews(); 667 for (FlutterDevice device in flutterDevices) 668 await device.debugDumpLayerTree(); 669 } 670 671 Future<void> debugDumpSemanticsTreeInTraversalOrder() async { 672 await refreshViews(); 673 for (FlutterDevice device in flutterDevices) 674 await device.debugDumpSemanticsTreeInTraversalOrder(); 675 } 676 677 Future<void> debugDumpSemanticsTreeInInverseHitTestOrder() async { 678 await refreshViews(); 679 for (FlutterDevice device in flutterDevices) 680 await device.debugDumpSemanticsTreeInInverseHitTestOrder(); 681 } 682 683 Future<void> debugToggleDebugPaintSizeEnabled() async { 684 await refreshViews(); 685 for (FlutterDevice device in flutterDevices) 686 await device.toggleDebugPaintSizeEnabled(); 687 } 688 689 Future<void> debugToggleDebugCheckElevationsEnabled() async { 690 await refreshViews(); 691 for (FlutterDevice device in flutterDevices) 692 await device.toggleDebugCheckElevationsEnabled(); 693 } 694 695 Future<void> debugTogglePerformanceOverlayOverride() async { 696 await refreshViews(); 697 for (FlutterDevice device in flutterDevices) 698 await device.debugTogglePerformanceOverlayOverride(); 699 } 700 701 Future<void> debugToggleWidgetInspector() async { 702 await refreshViews(); 703 for (FlutterDevice device in flutterDevices) 704 await device.toggleWidgetInspector(); 705 } 706 707 Future<void> debugToggleProfileWidgetBuilds() async { 708 await refreshViews(); 709 for (FlutterDevice device in flutterDevices) { 710 await device.toggleProfileWidgetBuilds(); 711 } 712 } 713 714 /// Take a screenshot on the provided [device]. 715 /// 716 /// If the device has a connected vmservice, this method will attempt to hide 717 /// and restore the debug banner before taking the screenshot. 718 /// 719 /// Throws an [AssertionError] if [Devce.supportsScreenshot] is not true. 720 Future<void> screenshot(FlutterDevice device) async { 721 assert(device.device.supportsScreenshot); 722 final Status status = logger.startProgress('Taking screenshot for ${device.device.name}...', timeout: timeoutConfiguration.fastOperation); 723 final File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png'); 724 try { 725 if (supportsServiceProtocol && isRunningDebug) { 726 await device.refreshViews(); 727 try { 728 for (FlutterView view in device.views) 729 await view.uiIsolate.flutterDebugAllowBanner(false); 730 } catch (error) { 731 status.cancel(); 732 printError('Error communicating with Flutter on the device: $error'); 733 return; 734 } 735 } 736 try { 737 await device.device.takeScreenshot(outputFile); 738 } finally { 739 if (supportsServiceProtocol && isRunningDebug) { 740 try { 741 for (FlutterView view in device.views) 742 await view.uiIsolate.flutterDebugAllowBanner(true); 743 } catch (error) { 744 status.cancel(); 745 printError('Error communicating with Flutter on the device: $error'); 746 return; 747 } 748 } 749 } 750 final int sizeKB = (await outputFile.length()) ~/ 1024; 751 status.stop(); 752 printStatus('Screenshot written to ${fs.path.relative(outputFile.path)} (${sizeKB}kB).'); 753 } catch (error) { 754 status.cancel(); 755 printError('Error taking screenshot: $error'); 756 } 757 } 758 759 Future<void> debugTogglePlatform() async { 760 await refreshViews(); 761 final String from = await flutterDevices[0].views[0].uiIsolate.flutterPlatformOverride(); 762 String to; 763 for (FlutterDevice device in flutterDevices) 764 to = await device.togglePlatform(from: from); 765 printStatus('Switched operating system to $to'); 766 } 767 768 Future<void> stopEchoingDeviceLog() async { 769 await Future.wait<void>( 770 flutterDevices.map<Future<void>>((FlutterDevice device) => device.stopEchoingDeviceLog()) 771 ); 772 } 773 774 /// If the [reloadSources] parameter is not null the 'reloadSources' service 775 /// will be registered. 776 // 777 // Failures should be indicated by completing the future with an error, using 778 // a string as the error object, which will be used by the caller (attach()) 779 // to display an error message. 780 Future<void> connectToServiceProtocol({ 781 ReloadSources reloadSources, 782 Restart restart, 783 CompileExpression compileExpression, 784 }) async { 785 if (!debuggingOptions.debuggingEnabled) 786 throw 'The service protocol is not enabled.'; 787 788 bool viewFound = false; 789 for (FlutterDevice device in flutterDevices) { 790 await device.connect( 791 reloadSources: reloadSources, 792 restart: restart, 793 compileExpression: compileExpression, 794 ); 795 await device.getVMs(); 796 await device.refreshViews(); 797 if (device.views.isNotEmpty) 798 viewFound = true; 799 } 800 if (!viewFound) { 801 if (flutterDevices.length == 1) 802 throw 'No Flutter view is available on ${flutterDevices.first.device.name}.'; 803 throw 'No Flutter view is available on any device ' 804 '(${flutterDevices.map<String>((FlutterDevice device) => device.device.name).join(', ')}).'; 805 } 806 807 // Listen for service protocol connection to close. 808 for (FlutterDevice device in flutterDevices) { 809 for (VMService service in device.vmServices) { 810 // This hooks up callbacks for when the connection stops in the future. 811 // We don't want to wait for them. We don't handle errors in those callbacks' 812 // futures either because they just print to logger and is not critical. 813 unawaited(service.done.then<void>( 814 _serviceProtocolDone, 815 onError: _serviceProtocolError, 816 ).whenComplete(_serviceDisconnected)); 817 } 818 } 819 } 820 821 Future<void> _serviceProtocolDone(dynamic object) { 822 printTrace('Service protocol connection closed.'); 823 return Future<void>.value(object); 824 } 825 826 Future<void> _serviceProtocolError(dynamic error, StackTrace stack) { 827 printTrace('Service protocol connection closed with an error: $error\n$stack'); 828 return Future<void>.error(error, stack); 829 } 830 831 void _serviceDisconnected() { 832 if (_exited) { 833 // User requested the application exit. 834 return; 835 } 836 if (_finished.isCompleted) 837 return; 838 printStatus('Lost connection to device.'); 839 _finished.complete(0); 840 } 841 842 void appFinished() { 843 if (_finished.isCompleted) 844 return; 845 printStatus('Application finished.'); 846 _finished.complete(0); 847 } 848 849 Future<int> waitForAppToFinish() async { 850 final int exitCode = await _finished.future; 851 assert(exitCode != null); 852 await cleanupAtFinish(); 853 return exitCode; 854 } 855 856 Future<void> preExit() async { } 857 858 Future<void> exitApp() async { 859 final List<Future<void>> futures = <Future<void>>[]; 860 for (FlutterDevice device in flutterDevices) 861 futures.add(device.exitApps()); 862 await Future.wait(futures); 863 appFinished(); 864 } 865 866 /// Called to print help to the terminal. 867 void printHelp({ @required bool details }); 868 869 void printHelpDetails() { 870 if (supportsServiceProtocol) { 871 printStatus('You can dump the widget hierarchy of the app (debugDumpApp) by pressing "w".'); 872 printStatus('To dump the rendering tree of the app (debugDumpRenderTree), press "t".'); 873 if (isRunningDebug) { 874 printStatus('For layers (debugDumpLayerTree), use "L"; for accessibility (debugDumpSemantics), use "S" (for traversal order) or "U" (for inverse hit test order).'); 875 printStatus('To toggle the widget inspector (WidgetsApp.showWidgetInspectorOverride), press "i".'); 876 printStatus('To toggle the display of construction lines (debugPaintSizeEnabled), press "p".'); 877 printStatus('To simulate different operating systems, (defaultTargetPlatform), press "o".'); 878 printStatus('To toggle the elevation checker, press "z".'); 879 } else { 880 printStatus('To dump the accessibility tree (debugDumpSemantics), press "S" (for traversal order) or "U" (for inverse hit test order).'); 881 } 882 printStatus('To display the performance overlay (WidgetsApp.showPerformanceOverlay), press "P".'); 883 printStatus('To enable timeline events for all widget build methods, (debugProfileWidgetBuilds), press "a"'); 884 } 885 if (flutterDevices.any((FlutterDevice d) => d.device.supportsScreenshot)) { 886 printStatus('To save a screenshot to flutter.png, press "s".'); 887 } 888 } 889 890 /// Called when a signal has requested we exit. 891 Future<void> cleanupAfterSignal(); 892 893 /// Called right before we exit. 894 Future<void> cleanupAtFinish(); 895} 896 897class OperationResult { 898 OperationResult(this.code, this.message, { this.fatal = false }); 899 900 /// The result of the operation; a non-zero code indicates a failure. 901 final int code; 902 903 /// A user facing message about the results of the operation. 904 final String message; 905 906 /// Whether this error should cause the runner to exit. 907 final bool fatal; 908 909 bool get isOk => code == 0; 910 911 static final OperationResult ok = OperationResult(0, ''); 912} 913 914/// Given the value of the --target option, return the path of the Dart file 915/// where the app's main function should be. 916String findMainDartFile([ String target ]) { 917 target ??= ''; 918 final String targetPath = fs.path.absolute(target); 919 if (fs.isDirectorySync(targetPath)) 920 return fs.path.join(targetPath, 'lib', 'main.dart'); 921 else 922 return targetPath; 923} 924 925Future<String> getMissingPackageHintForPlatform(TargetPlatform platform) async { 926 switch (platform) { 927 case TargetPlatform.android_arm: 928 case TargetPlatform.android_arm64: 929 case TargetPlatform.android_x64: 930 case TargetPlatform.android_x86: 931 final FlutterProject project = FlutterProject.current(); 932 final String manifestPath = fs.path.relative(project.android.appManifestFile.path); 933 return 'Is your project missing an $manifestPath?\nConsider running "flutter create ." to create one.'; 934 case TargetPlatform.ios: 935 return 'Is your project missing an ios/Runner/Info.plist?\nConsider running "flutter create ." to create one.'; 936 default: 937 return null; 938 } 939} 940 941/// Redirects terminal commands to the correct resident runner methods. 942class TerminalHandler { 943 TerminalHandler(this.residentRunner); 944 945 final ResidentRunner residentRunner; 946 bool _processingUserRequest = false; 947 StreamSubscription<void> subscription; 948 949 @visibleForTesting 950 String lastReceivedCommand; 951 952 void setupTerminal() { 953 if (!logger.quiet) { 954 printStatus(''); 955 residentRunner.printHelp(details: false); 956 } 957 terminal.singleCharMode = true; 958 subscription = terminal.keystrokes.listen(processTerminalInput); 959 } 960 961 void registerSignalHandlers() { 962 assert(residentRunner.stayResident); 963 io.ProcessSignal.SIGINT.watch().listen((io.ProcessSignal signal) { 964 _cleanUp(signal); 965 io.exit(0); 966 }); 967 io.ProcessSignal.SIGTERM.watch().listen((io.ProcessSignal signal) { 968 _cleanUp(signal); 969 io.exit(0); 970 }); 971 if (!residentRunner.supportsServiceProtocol || !residentRunner.supportsRestart) 972 return; 973 io.ProcessSignal.SIGUSR1.watch().listen(_handleSignal); 974 io.ProcessSignal.SIGUSR2.watch().listen(_handleSignal); 975 } 976 977 /// Returns [true] if the input has been handled by this function. 978 Future<bool> _commonTerminalInputHandler(String character) async { 979 printStatus(''); // the key the user tapped might be on this line 980 switch(character) { 981 case 'a': 982 if (residentRunner.supportsServiceProtocol) { 983 await residentRunner.debugToggleProfileWidgetBuilds(); 984 return true; 985 } 986 return false; 987 case 'd': 988 case 'D': 989 await residentRunner.detach(); 990 return true; 991 case 'h': 992 case 'H': 993 case '?': 994 // help 995 residentRunner.printHelp(details: true); 996 return true; 997 case 'i': 998 case 'I': 999 if (residentRunner.supportsServiceProtocol) { 1000 await residentRunner.debugToggleWidgetInspector(); 1001 return true; 1002 } 1003 return false; 1004 case 'l': 1005 final List<FlutterView> views = residentRunner.flutterDevices 1006 .expand((FlutterDevice d) => d.views).toList(); 1007 printStatus('Connected ${pluralize('view', views.length)}:'); 1008 for (FlutterView v in views) { 1009 printStatus('${v.uiIsolate.name} (${v.uiIsolate.id})', indent: 2); 1010 } 1011 return true; 1012 case 'L': 1013 if (residentRunner.supportsServiceProtocol) { 1014 await residentRunner.debugDumpLayerTree(); 1015 return true; 1016 } 1017 return false; 1018 case 'o': 1019 case 'O': 1020 if (residentRunner.supportsServiceProtocol && residentRunner.isRunningDebug) { 1021 await residentRunner.debugTogglePlatform(); 1022 return true; 1023 } 1024 return false; 1025 case 'p': 1026 if (residentRunner.supportsServiceProtocol && residentRunner.isRunningDebug) { 1027 await residentRunner.debugToggleDebugPaintSizeEnabled(); 1028 return true; 1029 } 1030 return false; 1031 case 'P': 1032 if (residentRunner.supportsServiceProtocol) { 1033 await residentRunner.debugTogglePerformanceOverlayOverride(); 1034 return true; 1035 } 1036 return false; 1037 case 'q': 1038 case 'Q': 1039 // exit 1040 await residentRunner.exit(); 1041 return true; 1042 case 's': 1043 for (FlutterDevice device in residentRunner.flutterDevices) { 1044 if (device.device.supportsScreenshot) 1045 await residentRunner.screenshot(device); 1046 } 1047 return true; 1048 case 'r': 1049 if (!residentRunner.canHotReload) { 1050 return false; 1051 } 1052 final OperationResult result = await residentRunner.restart(fullRestart: false); 1053 if (result.fatal) { 1054 throwToolExit(result.message); 1055 } 1056 if (!result.isOk) { 1057 printStatus('Try again after fixing the above error(s).', emphasis: true); 1058 } 1059 return true; 1060 case 'R': 1061 // If hot restart is not supported for all devices, ignore the command. 1062 if (!residentRunner.canHotRestart || !residentRunner.hotMode) { 1063 return false; 1064 } 1065 final OperationResult result = await residentRunner.restart(fullRestart: true); 1066 if (result.fatal) { 1067 throwToolExit(result.message); 1068 } 1069 if (!result.isOk) { 1070 printStatus('Try again after fixing the above error(s).', emphasis: true); 1071 } 1072 return true; 1073 case 'S': 1074 if (residentRunner.supportsServiceProtocol) { 1075 await residentRunner.debugDumpSemanticsTreeInTraversalOrder(); 1076 return true; 1077 } 1078 return false; 1079 case 't': 1080 case 'T': 1081 if (residentRunner.supportsServiceProtocol) { 1082 await residentRunner.debugDumpRenderTree(); 1083 return true; 1084 } 1085 return false; 1086 case 'U': 1087 if (residentRunner.supportsServiceProtocol) { 1088 await residentRunner.debugDumpSemanticsTreeInInverseHitTestOrder(); 1089 return true; 1090 } 1091 return false; 1092 case 'w': 1093 case 'W': 1094 if (residentRunner.supportsServiceProtocol) { 1095 await residentRunner.debugDumpApp(); 1096 return true; 1097 } 1098 return false; 1099 case 'z': 1100 case 'Z': 1101 await residentRunner.debugToggleDebugCheckElevationsEnabled(); 1102 return true; 1103 } 1104 return false; 1105 } 1106 1107 Future<void> processTerminalInput(String command) async { 1108 // When terminal doesn't support line mode, '\n' can sneak into the input. 1109 command = command.trim(); 1110 if (_processingUserRequest) { 1111 printTrace('Ignoring terminal input: "$command" because we are busy.'); 1112 return; 1113 } 1114 _processingUserRequest = true; 1115 try { 1116 lastReceivedCommand = command; 1117 await _commonTerminalInputHandler(command); 1118 } catch (error, st) { 1119 // Don't print stack traces for known error types. 1120 if (error is! ToolExit) { 1121 printError('$error\n$st'); 1122 } 1123 await _cleanUp(null); 1124 rethrow; 1125 } finally { 1126 _processingUserRequest = false; 1127 } 1128 } 1129 1130 Future<void> _handleSignal(io.ProcessSignal signal) async { 1131 if (_processingUserRequest) { 1132 printTrace('Ignoring signal: "$signal" because we are busy.'); 1133 return; 1134 } 1135 _processingUserRequest = true; 1136 1137 final bool fullRestart = signal == io.ProcessSignal.SIGUSR2; 1138 1139 try { 1140 await residentRunner.restart(fullRestart: fullRestart); 1141 } finally { 1142 _processingUserRequest = false; 1143 } 1144 } 1145 1146 Future<void> _cleanUp(io.ProcessSignal signal) async { 1147 terminal.singleCharMode = false; 1148 await subscription?.cancel(); 1149 await residentRunner.cleanupAfterSignal(); 1150 } 1151} 1152 1153class DebugConnectionInfo { 1154 DebugConnectionInfo({ this.httpUri, this.wsUri, this.baseUri }); 1155 1156 // TODO(danrubel): the httpUri field should be removed as part of 1157 // https://github.com/flutter/flutter/issues/7050 1158 final Uri httpUri; 1159 final Uri wsUri; 1160 final String baseUri; 1161} 1162