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

import 'dart:async';

7
import 'package:meta/meta.dart';
8

9
import 'android/gradle.dart';
10
import 'application_package.dart';
11
import 'artifacts.dart';
12
import 'asset.dart';
13
import 'base/common.dart';
14 15
import 'base/file_system.dart';
import 'base/io.dart';
16
import 'base/logger.dart';
17
import 'base/terminal.dart';
18
import 'base/utils.dart';
19
import 'build_info.dart';
20
import 'compile.dart';
21 22 23
import 'dart/dependencies.dart';
import 'dart/package_map.dart';
import 'dependency_checker.dart';
24
import 'devfs.dart';
25 26
import 'device.dart';
import 'globals.dart';
27 28
import 'run_cold.dart';
import 'run_hot.dart';
29
import 'vmservice.dart';
30

31 32 33 34 35 36
class FlutterDevice {
  final Device device;
  List<Uri> observatoryUris;
  List<VMService> vmServices;
  DevFS devFS;
  ApplicationPackage package;
37
  ResidentCompiler generator;
38
  String dillOutputPath;
39 40
  List<String> fileSystemRoots;
  String fileSystemScheme;
41 42 43

  StreamSubscription<String> _loggingSubscription;

44
  FlutterDevice(this.device, {
45 46
    @required bool previewDart2,
    @required bool trackWidgetCreation,
47
    this.dillOutputPath,
48 49
    this.fileSystemRoots,
    this.fileSystemScheme,
50 51 52 53 54
  }) {
    if (previewDart2) {
      generator = new ResidentCompiler(
        artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath),
        trackWidgetCreation: trackWidgetCreation,
55
        fileSystemRoots: fileSystemRoots, fileSystemScheme: fileSystemScheme
56 57
      );
    }
58
  }
59

60
  String viewFilter;
61

62 63 64 65 66 67 68
  /// If the [reloadSources] parameter is not null the 'reloadSources' service
  /// will be registered.
  /// The 'reloadSources' service can be used by other Service Protocol clients
  /// connected to the VM (e.g. Observatory) to request a reload of the source
  /// code of the running application (a.k.a. HotReload).
  /// This ensures that the reload process follows the normal orchestration of
  /// the Flutter Tools and not just the VM internal service.
69
  Future<Null> _connect({ReloadSources reloadSources}) async {
70 71 72 73
    if (vmServices != null)
      return;
    vmServices = new List<VMService>(observatoryUris.length);
    for (int i = 0; i < observatoryUris.length; i++) {
74
      printTrace('Connecting to service protocol: ${observatoryUris[i]}');
75
      vmServices[i] = await VMService.connect(observatoryUris[i],
76
          reloadSources: reloadSources);
77
      printTrace('Successfully connected to service protocol: ${observatoryUris[i]}');
78 79 80 81
    }
  }

  Future<Null> refreshViews() async {
82
    if (vmServices == null || vmServices.isEmpty)
83 84 85 86 87 88
      return;
    for (VMService service in vmServices)
      await service.vm.refreshViews();
  }

  List<FlutterView> get views {
89 90 91 92 93 94 95 96 97
    if (vmServices == null)
      return <FlutterView>[];

    return vmServices
      .where((VMService service) => !service.isClosed)
      .expand((VMService service) => viewFilter != null
          ? service.vm.allViewsWithName(viewFilter)
          : service.vm.views)
      .toList();
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
  }

  Future<Null> getVMs() async {
    for (VMService service in vmServices)
      await service.getVM();
  }

  Future<Null> waitForViews() async {
    // Refresh the view list, and wait a bit for the list to populate.
    for (VMService service in vmServices)
      await service.waitForViews();
  }

  Future<Null> stopApps() async {
    final List<FlutterView> flutterViews = views;
    if (flutterViews == null || flutterViews.isEmpty)
      return;
    for (FlutterView view in flutterViews) {
116 117 118 119
      if (view != null && view.uiIsolate != null) {
        // Manage waits specifically below.
        view.uiIsolate.flutterExit(); // ignore: unawaited_futures
      }
120 121 122 123 124
    }
    await new Future<Null>.delayed(const Duration(milliseconds: 100));
  }

  Future<Uri> setupDevFS(String fsName,
125 126 127
    Directory rootDirectory, {
    String packagesFilePath
  }) {
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
    // One devFS per device. Shared by all running instances.
    devFS = new DevFS(
      vmServices[0],
      fsName,
      rootDirectory,
      packagesFilePath: packagesFilePath
    );
    return devFS.create();
  }

  List<Future<Map<String, dynamic>>> reloadSources(
    String entryPath, {
    bool pause: false
  }) {
    final Uri deviceEntryUri = devFS.baseUri.resolveUri(fs.path.toUri(entryPath));
    final Uri devicePackagesUri = devFS.baseUri.resolve('.packages');
    final List<Future<Map<String, dynamic>>> reports = <Future<Map<String, dynamic>>>[];
    for (FlutterView view in views) {
      final Future<Map<String, dynamic>> report = view.uiIsolate.reloadSources(
        pause: pause,
        rootLibUri: deviceEntryUri,
        packagesUri: devicePackagesUri
      );
      reports.add(report);
    }
    return reports;
  }

156 157 158 159 160 161 162 163 164
  Future<Null> resetAssetDirectory() async {
    final Uri deviceAssetsDirectoryUri = devFS.baseUri.resolveUri(
        fs.path.toUri(getAssetBuildDirectory()));
    assert(deviceAssetsDirectoryUri != null);
    await Future.wait(views.map(
      (FlutterView view) => view.setAssetDirectory(deviceAssetsDirectoryUri)
    ));
  }

165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
  // Lists program elements changed in the most recent reload that have not
  // since executed.
  Future<List<ProgramElement>> unusedChangesInLastReload() async {
    final List<Future<List<ProgramElement>>> reports =
      <Future<List<ProgramElement>>>[];
    for (FlutterView view in views)
      reports.add(view.uiIsolate.getUnusedChangesInLastReload());
    final List<ProgramElement> elements = <ProgramElement>[];
    for (Future<List<ProgramElement>> report in reports) {
      for (ProgramElement element in await report)
        elements.add(new ProgramElement(element.qualifiedName,
                                        devFS.deviceUriToHostUri(element.uri),
                                        element.line,
                                        element.column));
    }
    return elements;
  }

183 184 185 186 187 188 189 190 191 192
  Future<Null> debugDumpApp() async {
    for (FlutterView view in views)
      await view.uiIsolate.flutterDebugDumpApp();
  }

  Future<Null> debugDumpRenderTree() async {
    for (FlutterView view in views)
      await view.uiIsolate.flutterDebugDumpRenderTree();
  }

193 194 195 196 197
  Future<Null> debugDumpLayerTree() async {
    for (FlutterView view in views)
      await view.uiIsolate.flutterDebugDumpLayerTree();
  }

198
  Future<Null> debugDumpSemanticsTreeInTraversalOrder() async {
199
    for (FlutterView view in views)
200
      await view.uiIsolate.flutterDebugDumpSemanticsTreeInTraversalOrder();
201 202 203 204 205
  }

  Future<Null> debugDumpSemanticsTreeInInverseHitTestOrder() async {
    for (FlutterView view in views)
      await view.uiIsolate.flutterDebugDumpSemanticsTreeInInverseHitTestOrder();
206 207
  }

208 209 210 211 212
  Future<Null> toggleDebugPaintSizeEnabled() async {
    for (FlutterView view in views)
      await view.uiIsolate.flutterToggleDebugPaintSizeEnabled();
  }

213 214 215 216 217
  Future<Null> debugTogglePerformanceOverlayOverride() async {
    for (FlutterView view in views)
      await view.uiIsolate.flutterTogglePerformanceOverlayOverride();
  }

218 219 220 221 222
  Future<Null> toggleWidgetInspector() async {
    for (FlutterView view in views)
      await view.uiIsolate.flutterToggleWidgetInspector();
  }

223
  Future<String> togglePlatform({ String from }) async {
224 225 226 227 228 229 230 231 232 233
    String to;
    switch (from) {
      case 'iOS':
        to = 'android';
        break;
      case 'android':
      default:
        to = 'iOS';
        break;
    }
234
    for (FlutterView view in views)
235 236 237 238 239 240 241 242
      await view.uiIsolate.flutterPlatformOverride(to);
    return to;
  }

  void startEchoingDeviceLog() {
    if (_loggingSubscription != null)
      return;
    _loggingSubscription = device.getLogReader(app: package).logLines.listen((String line) {
243
      if (!line.contains('Observatory listening on http'))
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
        printStatus(line);
    });
  }

  Future<Null> stopEchoingDeviceLog() async {
    if (_loggingSubscription == null)
      return;
    await _loggingSubscription.cancel();
    _loggingSubscription = null;
  }

  void initLogReader() {
    device.getLogReader(app: package).appPid = vmServices.first.vm.pid;
  }

  Future<int> runHot({
    HotRunner hotRunner,
    String route,
    bool shouldBuild,
  }) async {
    final bool prebuiltMode = hotRunner.applicationBinary != null;
265
    final String modeName = hotRunner.debuggingOptions.buildInfo.modeName;
266 267 268
    printStatus('Launching ${getDisplayPath(hotRunner.mainPath)} on ${device.name} in $modeName mode...');

    final TargetPlatform targetPlatform = await device.targetPlatform;
269
    package = await getApplicationPackageForPlatform(
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
      targetPlatform,
      applicationBinary: hotRunner.applicationBinary
    );

    if (package == null) {
      String message = 'No application found for $targetPlatform.';
      final String hint = getMissingPackageHintForPlatform(targetPlatform);
      if (hint != null)
        message += '\n$hint';
      printError(message);
      return 1;
    }

    final Map<String, dynamic> platformArgs = <String, dynamic>{};

    startEchoingDeviceLog();

    // Start the application.
    final bool hasDirtyDependencies = hotRunner.hasDirtyDependencies(this);
    final Future<LaunchResult> futureResult = device.startApp(
      package,
      mainPath: hotRunner.mainPath,
      debuggingOptions: hotRunner.debuggingOptions,
      platformArgs: platformArgs,
      route: route,
      prebuiltApplication: prebuiltMode,
296 297
      applicationNeedsRebuild: shouldBuild || hasDirtyDependencies,
      usesTerminalUi: hotRunner.usesTerminalUI,
298
      ipv6: hotRunner.ipv6,
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
    );

    final LaunchResult result = await futureResult;

    if (!result.started) {
      printError('Error launching application on ${device.name}.');
      await stopEchoingDeviceLog();
      return 2;
    }
    observatoryUris = <Uri>[result.observatoryUri];
    return 0;
  }


  Future<int> runCold({
    ColdRunner coldRunner,
    String route,
    bool shouldBuild: true,
  }) async {
    final TargetPlatform targetPlatform = await device.targetPlatform;
319
    package = await getApplicationPackageForPlatform(
320 321 322 323
      targetPlatform,
      applicationBinary: coldRunner.applicationBinary
    );

324
    final String modeName = coldRunner.debuggingOptions.buildInfo.modeName;
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
    final bool prebuiltMode = coldRunner.applicationBinary != null;
    if (coldRunner.mainPath == null) {
      assert(prebuiltMode);
      printStatus('Launching ${package.displayName} on ${device.name} in $modeName mode...');
    } else {
      printStatus('Launching ${getDisplayPath(coldRunner.mainPath)} on ${device.name} in $modeName mode...');
    }

    if (package == null) {
      String message = 'No application found for $targetPlatform.';
      final String hint = getMissingPackageHintForPlatform(targetPlatform);
      if (hint != null)
        message += '\n$hint';
      printError(message);
      return 1;
    }

342
    final Map<String, dynamic> platformArgs = <String, dynamic>{};
343
    if (coldRunner.traceStartup != null)
344
      platformArgs['trace-startup'] = coldRunner.traceStartup;
345 346 347 348 349 350 351 352 353 354 355

    startEchoingDeviceLog();

    final bool hasDirtyDependencies = coldRunner.hasDirtyDependencies(this);
    final LaunchResult result = await device.startApp(
      package,
      mainPath: coldRunner.mainPath,
      debuggingOptions: coldRunner.debuggingOptions,
      platformArgs: platformArgs,
      route: route,
      prebuiltApplication: prebuiltMode,
356 357
      applicationNeedsRebuild: shouldBuild || hasDirtyDependencies,
      usesTerminalUi: coldRunner.usesTerminalUI,
358
      ipv6: coldRunner.ipv6,
359 360 361 362 363 364 365 366 367 368 369 370 371
    );

    if (!result.started) {
      printError('Error running application on ${device.name}.');
      await stopEchoingDeviceLog();
      return 2;
    }
    if (result.hasObservatory)
      observatoryUris = <Uri>[result.observatoryUri];
    return 0;
  }

  Future<bool> updateDevFS({
372 373
    String mainPath,
    String target,
374
    AssetBundle bundle,
375 376
    DateTime firstBuildTime,
    bool bundleFirstUpload: false,
377
    bool bundleDirty: false,
378
    Set<String> fileFilter,
379 380
    bool fullRestart: false,
    String projectRootPath,
381 382 383 384 385 386 387 388
  }) async {
    final Status devFSStatus = logger.startProgress(
      'Syncing files to device ${device.name}...',
      expectSlowOperation: true
    );
    int bytes = 0;
    try {
      bytes = await devFS.update(
389 390
        mainPath: mainPath,
        target: target,
391
        bundle: bundle,
392 393
        firstBuildTime: firstBuildTime,
        bundleFirstUpload: bundleFirstUpload,
394
        bundleDirty: bundleDirty,
395
        fileFilter: fileFilter,
396
        generator: generator,
397 398
        fullRestart: fullRestart,
        dillOutputPath: dillOutputPath,
399
        projectRootPath: projectRootPath,
400 401 402 403 404 405 406 407 408
      );
    } on DevFSException {
      devFSStatus.cancel();
      return false;
    }
    devFSStatus.stop();
    printTrace('Synced ${getSizeAsMB(bytes)}.');
    return true;
  }
409 410 411 412 413 414 415

  void updateReloadStatus(bool wasReloadSuccessful) {
    if (wasReloadSuccessful)
      generator?.accept();
    else
      generator?.reject();
  }
416 417
}

418 419
// Shared code between different resident application runners.
abstract class ResidentRunner {
420
  ResidentRunner(this.flutterDevices, {
421 422
    this.target,
    this.debuggingOptions,
423 424 425
    this.usesTerminalUI: true,
    String projectRootPath,
    String packagesFilePath,
426
    this.stayResident,
427
    this.ipv6,
428 429
  }) {
    _mainPath = findMainDartFile(target);
430
    _projectRootPath = projectRootPath ?? fs.currentDirectory.path;
431
    _packagesFilePath =
432
        packagesFilePath ?? fs.path.absolute(PackageMap.globalPackagesPath);
433
    _assetBundle = AssetBundleFactory.instance.createBundle();
434
  }
435

436
  final List<FlutterDevice> flutterDevices;
437 438 439
  final String target;
  final DebuggingOptions debuggingOptions;
  final bool usesTerminalUI;
440
  final bool stayResident;
441
  final bool ipv6;
442
  final Completer<int> _finished = new Completer<int>();
443
  bool _stopped = false;
444 445 446 447 448 449 450 451
  String _packagesFilePath;
  String get packagesFilePath => _packagesFilePath;
  String _projectRootPath;
  String get projectRootPath => _projectRootPath;
  String _mainPath;
  String get mainPath => _mainPath;
  AssetBundle _assetBundle;
  AssetBundle get assetBundle => _assetBundle;
452

453 454 455
  bool get isRunningDebug => debuggingOptions.buildInfo.isDebug;
  bool get isRunningProfile => debuggingOptions.buildInfo.isProfile;
  bool get isRunningRelease => debuggingOptions.buildInfo.isRelease;
456 457
  bool get supportsServiceProtocol => isRunningDebug || isRunningProfile;

458
  /// Start the app and keep the process running during its lifetime.
459
  Future<int> run({
460
    Completer<DebugConnectionInfo> connectionInfoCompleter,
461
    Completer<Null> appStartedCompleter,
462 463 464
    String route,
    bool shouldBuild: true
  });
465

466 467 468 469 470
  bool get supportsRestart => false;

  Future<OperationResult> restart({ bool fullRestart: false, bool pauseAfterRestart: false }) {
    throw 'unsupported';
  }
471

472
  Future<Null> stop() async {
473
    _stopped = true;
474
    await stopEchoingDeviceLog();
475
    await preStop();
476 477 478
    return stopApp();
  }

479 480 481 482 483 484
  Future<Null> detach() async {
    await stopEchoingDeviceLog();
    await preStop();
    appFinished();
  }

485
  Future<Null> refreshViews() async {
486 487
    for (FlutterDevice device in flutterDevices)
      await device.refreshViews();
488 489
  }

490
  Future<Null> _debugDumpApp() async {
491
    await refreshViews();
492 493
    for (FlutterDevice device in flutterDevices)
      await device.debugDumpApp();
494 495
  }

496
  Future<Null> _debugDumpRenderTree() async {
497
    await refreshViews();
498 499
    for (FlutterDevice device in flutterDevices)
      await device.debugDumpRenderTree();
500 501
  }

502 503 504 505 506 507
  Future<Null> _debugDumpLayerTree() async {
    await refreshViews();
    for (FlutterDevice device in flutterDevices)
      await device.debugDumpLayerTree();
  }

508
  Future<Null> _debugDumpSemanticsTreeInTraversalOrder() async {
509 510
    await refreshViews();
    for (FlutterDevice device in flutterDevices)
511
      await device.debugDumpSemanticsTreeInTraversalOrder();
512 513 514 515 516 517
  }

  Future<Null> _debugDumpSemanticsTreeInInverseHitTestOrder() async {
    await refreshViews();
    for (FlutterDevice device in flutterDevices)
      await device.debugDumpSemanticsTreeInInverseHitTestOrder();
518 519
  }

520
  Future<Null> _debugToggleDebugPaintSizeEnabled() async {
521
    await refreshViews();
522 523
    for (FlutterDevice device in flutterDevices)
      await device.toggleDebugPaintSizeEnabled();
524 525
  }

526 527 528 529 530 531
  Future<Null> _debugTogglePerformanceOverlayOverride() async {
    await refreshViews();
    for (FlutterDevice device in flutterDevices)
      await device.debugTogglePerformanceOverlayOverride();
  }

532 533 534 535 536 537
  Future<Null> _debugToggleWidgetInspector() async {
    await refreshViews();
    for (FlutterDevice device in flutterDevices)
      await device.toggleWidgetInspector();
  }

538 539
  Future<Null> _screenshot(FlutterDevice device) async {
    final Status status = logger.startProgress('Taking screenshot for ${device.device.name}...');
540
    final File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png');
541
    try {
542
      if (supportsServiceProtocol && isRunningDebug) {
543
        await device.refreshViews();
544
        try {
545 546
          for (FlutterView view in device.views)
            await view.uiIsolate.flutterDebugAllowBanner(false);
547 548
        } catch (error) {
          status.stop();
549
          printError('Error communicating with Flutter on the device: $error');
550
        }
551 552
      }
      try {
553
        await device.device.takeScreenshot(outputFile);
554
      } finally {
555 556
        if (supportsServiceProtocol && isRunningDebug) {
          try {
557 558
            for (FlutterView view in device.views)
              await view.uiIsolate.flutterDebugAllowBanner(true);
559 560
          } catch (error) {
            status.stop();
561
            printError('Error communicating with Flutter on the device: $error');
562
          }
563 564
        }
      }
565
      final int sizeKB = (await outputFile.length()) ~/ 1024;
566
      status.stop();
567
      printStatus('Screenshot written to ${fs.path.relative(outputFile.path)} (${sizeKB}kB).');
568
    } catch (error) {
569
      status.stop();
570 571 572 573
      printError('Error taking screenshot: $error');
    }
  }

574
  Future<Null> _debugTogglePlatform() async {
575
    await refreshViews();
576 577 578 579 580
    final String from = await flutterDevices[0].views[0].uiIsolate.flutterPlatformOverride();
    String to;
    for (FlutterDevice device in flutterDevices)
      to = await device.togglePlatform(from: from);
    printStatus('Switched operating system to $to');
581 582
  }

583
  void registerSignalHandlers() {
584
    assert(stayResident);
585
    ProcessSignal.SIGINT.watch().listen(_cleanUpAndExit);
586
    ProcessSignal.SIGTERM.watch().listen(_cleanUpAndExit);
587
    if (!supportsServiceProtocol || !supportsRestart)
588
      return;
589 590
    ProcessSignal.SIGUSR1.watch().listen(_handleSignal);
    ProcessSignal.SIGUSR2.watch().listen(_handleSignal);
591 592
  }

593 594 595 596 597 598
  Future<Null> _cleanUpAndExit(ProcessSignal signal) async {
    _resetTerminal();
    await cleanupAfterSignal();
    exit(0);
  }

599
  bool _processingUserRequest = false;
600
  Future<Null> _handleSignal(ProcessSignal signal) async {
601
    if (_processingUserRequest) {
602 603 604
      printTrace('Ignoring signal: "$signal" because we are busy.');
      return;
    }
605
    _processingUserRequest = true;
606

607
    final bool fullRestart = signal == ProcessSignal.SIGUSR2;
608 609 610 611

    try {
      await restart(fullRestart: fullRestart);
    } finally {
612
      _processingUserRequest = false;
613
    }
614 615 616
  }

  Future<Null> stopEchoingDeviceLog() async {
617 618 619
    await Future.wait(
      flutterDevices.map((FlutterDevice device) => device.stopEchoingDeviceLog())
    );
620 621
  }

622 623 624 625
  /// If the [reloadSources] parameter is not null the 'reloadSources' service
  /// will be registered
  Future<Null> connectToServiceProtocol({String viewFilter,
      ReloadSources reloadSources}) async {
626
    if (!debuggingOptions.debuggingEnabled)
627
      return new Future<Null>.error('Error the service protocol is not enabled.');
628

629 630 631
    bool viewFound = false;
    for (FlutterDevice device in flutterDevices) {
      device.viewFilter = viewFilter;
632
      await device._connect(reloadSources: reloadSources);
633 634 635 636 637 638 639 640
      await device.getVMs();
      await device.waitForViews();
      if (device.views == null)
        printStatus('No Flutter views available on ${device.device.name}');
      else
        viewFound = true;
    }
    if (!viewFound)
641
      throwToolExit('No Flutter view is available');
642

643
    // Listen for service protocol connection to close.
644 645
    for (FlutterDevice device in flutterDevices) {
      for (VMService service in device.vmServices) {
646 647 648 649
        // This hooks up callbacks for when the connection stops in the future.
        // We don't want to wait for them. We don't handle errors in those callbacks'
        // futures either because they just print to logger and is not critical.
        service.done.then<Null>( // ignore: unawaited_futures
650 651 652 653
          _serviceProtocolDone,
          onError: _serviceProtocolError
        ).whenComplete(_serviceDisconnected);
      }
654
    }
655 656 657 658 659 660 661 662 663 664
  }

  Future<Null> _serviceProtocolDone(dynamic object) {
    printTrace('Service protocol connection closed.');
    return new Future<Null>.value(object);
  }

  Future<Null> _serviceProtocolError(dynamic error, StackTrace stack) {
    printTrace('Service protocol connection closed with an error: $error\n$stack');
    return new Future<Null>.error(error, stack);
665 666 667
  }

  /// Returns [true] if the input has been handled by this function.
668
  Future<bool> _commonTerminalInputHandler(String character) async {
669 670 671 672
    final String lower = character.toLowerCase();

    printStatus(''); // the key the user tapped might be on this line

673 674
    if (lower == 'h' || lower == '?') {
      // help
675
      printHelp(details: true);
676 677
      return true;
    } else if (lower == 'w') {
678 679
      if (supportsServiceProtocol) {
        await _debugDumpApp();
680
        return true;
681
      }
682
    } else if (lower == 't') {
683 684
      if (supportsServiceProtocol) {
        await _debugDumpRenderTree();
685
        return true;
686
      }
687 688 689 690 691 692 693
    } else if (character == 'L') {
      if (supportsServiceProtocol) {
        await _debugDumpLayerTree();
        return true;
      }
    } else if (character == 'S') {
      if (supportsServiceProtocol) {
694
        await _debugDumpSemanticsTreeInTraversalOrder();
695 696
        return true;
      }
697
    } else if (character == 'U') {
698 699
      if (supportsServiceProtocol) {
        await _debugDumpSemanticsTreeInInverseHitTestOrder();
700 701
        return true;
      }
702
    } else if (character == 'p') {
703 704
      if (supportsServiceProtocol && isRunningDebug) {
        await _debugToggleDebugPaintSizeEnabled();
705
        return true;
706
      }
707 708 709
    } else if (character == 'P') {
      if (supportsServiceProtocol) {
        await _debugTogglePerformanceOverlayOverride();
710 711 712 713
      }
    } else if (lower == 'i') {
      if (supportsServiceProtocol) {
        await _debugToggleWidgetInspector();
714 715
        return true;
      }
716
    } else if (character == 's') {
717 718 719
      for (FlutterDevice device in flutterDevices) {
        if (device.device.supportsScreenshot)
          await _screenshot(device);
720
      }
721
      return true;
722 723
    } else if (lower == 'o') {
      if (supportsServiceProtocol && isRunningDebug) {
724
        await _debugTogglePlatform();
725 726
        return true;
      }
727 728
    } else if (lower == 'q') {
      // exit
729
      await stop();
730
      return true;
731 732 733
    } else if (lower == 'd') {
      await detach();
      return true;
734 735 736 737 738
    }

    return false;
  }

739
  Future<Null> processTerminalInput(String command) async {
740 741
    // When terminal doesn't support line mode, '\n' can sneak into the input.
    command = command.trim();
742
    if (_processingUserRequest) {
743 744 745
      printTrace('Ignoring terminal input: "$command" because we are busy.');
      return;
    }
746
    _processingUserRequest = true;
747
    try {
748
      final bool handled = await _commonTerminalInputHandler(command);
749 750
      if (!handled)
        await handleTerminalCommand(command);
751 752 753
    } catch (error, st) {
      printError('$error\n$st');
      _cleanUpAndExit(null);
754
    } finally {
755
      _processingUserRequest = false;
756
    }
757 758
  }

759 760 761 762 763 764 765 766 767 768 769 770
  void _serviceDisconnected() {
    if (_stopped) {
      // User requested the application exit.
      return;
    }
    if (_finished.isCompleted)
      return;
    printStatus('Lost connection to device.');
    _resetTerminal();
    _finished.complete(0);
  }

771 772 773 774 775 776 777 778 779 780 781 782 783 784
  void appFinished() {
    if (_finished.isCompleted)
      return;
    printStatus('Application finished.');
    _resetTerminal();
    _finished.complete(0);
  }

  void _resetTerminal() {
    if (usesTerminalUI)
      terminal.singleCharMode = false;
  }

  void setupTerminal() {
785
    assert(stayResident);
786
    if (usesTerminalUI) {
787 788
      if (!logger.quiet) {
        printStatus('');
789
        printHelp(details: false);
790
      }
791
      terminal.singleCharMode = true;
792
      terminal.onCharInput.listen(processTerminalInput);
793 794 795 796
    }
  }

  Future<int> waitForAppToFinish() async {
797
    final int exitCode = await _finished.future;
798 799 800 801
    await cleanupAtFinish();
    return exitCode;
  }

802
  bool hasDirtyDependencies(FlutterDevice device) {
803
    final DartDependencySetBuilder dartDependencySetBuilder =
804
        new DartDependencySetBuilder(mainPath, packagesFilePath);
805
    final DependencyChecker dependencyChecker =
806
        new DependencyChecker(dartDependencySetBuilder, assetBundle);
807
    final String path = device.package.packagePath;
808
    if (path == null)
809 810
      return true;
    final FileStat stat = fs.file(path).statSync();
811
    if (stat.type != FileSystemEntityType.FILE) // ignore: deprecated_member_use
812
      return true;
813
    if (!fs.file(path).existsSync())
814 815 816 817 818
      return true;
    final DateTime lastBuildTime = stat.modified;
    return dependencyChecker.check(lastBuildTime);
  }

819 820 821
  Future<Null> preStop() async { }

  Future<Null> stopApp() async {
822 823
    for (FlutterDevice device in flutterDevices)
      await device.stopApps();
824 825 826
    appFinished();
  }

827 828 829 830
  /// Called to print help to the terminal.
  void printHelp({ @required bool details });

  void printHelpDetails() {
831
    if (supportsServiceProtocol) {
832
      printStatus('You can dump the widget hierarchy of the app (debugDumpApp) by pressing "w".');
833
      printStatus('To dump the rendering tree of the app (debugDumpRenderTree), press "t".');
834
      if (isRunningDebug) {
835
        printStatus('For layers (debugDumpLayerTree), use "L"; for accessibility (debugDumpSemantics), use "S" (for traversal order) or "U" (for inverse hit test order).');
836
        printStatus('To toggle the widget inspector (WidgetsApp.showWidgetInspectorOverride), press "i".');
837 838
        printStatus('To toggle the display of construction lines (debugPaintSizeEnabled), press "p".');
        printStatus('To simulate different operating systems, (defaultTargetPlatform), press "o".');
839
      } else {
840
        printStatus('To dump the accessibility tree (debugDumpSemantics), press "S" (for traversal order) or "U" (for inverse hit test order).');
841
      }
842
      printStatus('To display the performance overlay (WidgetsApp.showPerformanceOverlay), press "P".');
843
    }
844
    if (flutterDevices.any((FlutterDevice d) => d.device.supportsScreenshot))
845
      printStatus('To save a screenshot to flutter.png, press "s".');
846 847
  }

848 849 850 851 852
  /// Called when a signal has requested we exit.
  Future<Null> cleanupAfterSignal();
  /// Called right before we exit.
  Future<Null> cleanupAtFinish();
  /// Called when the runner should handle a terminal command.
853
  Future<Null> handleTerminalCommand(String code);
854 855
}

Devon Carew's avatar
Devon Carew committed
856
class OperationResult {
857
  OperationResult(this.code, this.message, { this.hintMessage, this.hintId });
Devon Carew's avatar
Devon Carew committed
858

859
  /// The result of the operation; a non-zero code indicates a failure.
Devon Carew's avatar
Devon Carew committed
860
  final int code;
861 862

  /// A user facing message about the results of the operation.
Devon Carew's avatar
Devon Carew committed
863
  final String message;
864 865 866 867 868 869 870 871 872 873 874

  /// An optional hint about the results of the operation. This is used to provide
  /// sidecar data about the operation results. For example, this is used when
  /// a reload is successful but some changed program elements where not run after a
  /// reassemble.
  final String hintMessage;

  /// A key used by tools to discriminate between different kinds of operation results.
  /// For example, a successful reload might have a [code] of 0 and a [hintId] of
  /// `'restartRecommended'`.
  final String hintId;
Devon Carew's avatar
Devon Carew committed
875 876

  bool get isOk => code == 0;
877 878

  static final OperationResult ok = new OperationResult(0, '');
Devon Carew's avatar
Devon Carew committed
879 880
}

881 882 883
/// Given the value of the --target option, return the path of the Dart file
/// where the app's main function should be.
String findMainDartFile([String target]) {
884
  target ??= '';
885
  final String targetPath = fs.path.absolute(target);
886
  if (fs.isDirectorySync(targetPath))
887
    return fs.path.join(targetPath, 'lib', 'main.dart');
888 889 890 891 892 893 894
  else
    return targetPath;
}

String getMissingPackageHintForPlatform(TargetPlatform platform) {
  switch (platform) {
    case TargetPlatform.android_arm:
895
    case TargetPlatform.android_arm64:
896
    case TargetPlatform.android_x64:
897 898 899 900 901 902
    case TargetPlatform.android_x86:
      String manifest = 'android/AndroidManifest.xml';
      if (isProjectUsingGradle()) {
        manifest = gradleManifestPath;
      }
      return 'Is your project missing an $manifest?\nConsider running "flutter create ." to create one.';
903 904 905 906 907 908
    case TargetPlatform.ios:
      return 'Is your project missing an ios/Runner/Info.plist?\nConsider running "flutter create ." to create one.';
    default:
      return null;
  }
}
909 910

class DebugConnectionInfo {
911
  DebugConnectionInfo({ this.httpUri, this.wsUri, this.baseUri });
912

913 914 915 916
  // TODO(danrubel): the httpUri field should be removed as part of
  // https://github.com/flutter/flutter/issues/7050
  final Uri httpUri;
  final Uri wsUri;
917 918
  final String baseUri;
}