resident_runner.dart 29.3 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 'application_package.dart';
10
import 'artifacts.dart';
11
import 'asset.dart';
12
import 'base/common.dart';
13 14
import 'base/file_system.dart';
import 'base/io.dart';
15
import 'base/logger.dart';
16
import 'base/terminal.dart';
17
import 'base/utils.dart';
18
import 'build_info.dart';
19
import 'compile.dart';
20 21 22
import 'dart/dependencies.dart';
import 'dart/package_map.dart';
import 'dependency_checker.dart';
23
import 'devfs.dart';
24 25
import 'device.dart';
import 'globals.dart';
26
import 'project.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
    @required bool previewDart2,
46
    @required bool trackWidgetCreation,
47
    this.dillOutputPath,
48 49
    this.fileSystemRoots,
    this.fileSystemScheme,
50 51 52 53 54 55 56 57 58
  }) {
    if (previewDart2) {
      generator = new ResidentCompiler(
        artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath),
        trackWidgetCreation: trackWidgetCreation,
        fileSystemRoots: fileSystemRoots, fileSystemScheme: fileSystemScheme
      );
    }
  }
59

60
  String viewFilter;
61

62 63 64 65 66
  /// 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).
67 68
  /// The 'compileExpression' service can be used to compile user-provided
  /// expressions requested during debugging of the application.
69 70
  /// This ensures that the reload process follows the normal orchestration of
  /// the Flutter Tools and not just the VM internal service.
71
  Future<Null> _connect({ReloadSources reloadSources, CompileExpression compileExpression}) async {
72 73 74 75
    if (vmServices != null)
      return;
    vmServices = new List<VMService>(observatoryUris.length);
    for (int i = 0; i < observatoryUris.length; i++) {
76
      printTrace('Connecting to service protocol: ${observatoryUris[i]}');
77
      vmServices[i] = await VMService.connect(observatoryUris[i],
78 79
          reloadSources: reloadSources,
          compileExpression: compileExpression);
80
      printTrace('Successfully connected to service protocol: ${observatoryUris[i]}');
81 82 83 84
    }
  }

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

  List<FlutterView> get views {
92 93 94 95 96 97 98 99 100
    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();
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
  }

  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) {
119 120 121 122
      if (view != null && view.uiIsolate != null) {
        // Manage waits specifically below.
        view.uiIsolate.flutterExit(); // ignore: unawaited_futures
      }
123 124 125 126 127
    }
    await new Future<Null>.delayed(const Duration(milliseconds: 100));
  }

  Future<Uri> setupDevFS(String fsName,
128 129 130
    Directory rootDirectory, {
    String packagesFilePath
  }) {
131 132 133 134 135 136 137 138 139 140 141 142
    // 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, {
143
    bool pause = false
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
  }) {
    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;
  }

159 160 161 162 163 164 165 166 167
  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)
    ));
  }

168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
  // 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;
  }

186 187 188 189 190 191 192 193 194 195
  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();
  }

196 197 198 199 200
  Future<Null> debugDumpLayerTree() async {
    for (FlutterView view in views)
      await view.uiIsolate.flutterDebugDumpLayerTree();
  }

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

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

211 212 213 214 215
  Future<Null> toggleDebugPaintSizeEnabled() async {
    for (FlutterView view in views)
      await view.uiIsolate.flutterToggleDebugPaintSizeEnabled();
  }

216 217 218 219 220
  Future<Null> debugTogglePerformanceOverlayOverride() async {
    for (FlutterView view in views)
      await view.uiIsolate.flutterTogglePerformanceOverlayOverride();
  }

221 222 223 224 225
  Future<Null> toggleWidgetInspector() async {
    for (FlutterView view in views)
      await view.uiIsolate.flutterToggleWidgetInspector();
  }

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

  void startEchoingDeviceLog() {
    if (_loggingSubscription != null)
      return;
    _loggingSubscription = device.getLogReader(app: package).logLines.listen((String line) {
246
      if (!line.contains('Observatory listening on http'))
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
        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;
268
    final String modeName = hotRunner.debuggingOptions.buildInfo.modeName;
269 270 271
    printStatus('Launching ${getDisplayPath(hotRunner.mainPath)} on ${device.name} in $modeName mode...');

    final TargetPlatform targetPlatform = await device.targetPlatform;
272
    package = await getApplicationPackageForPlatform(
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
      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,
299 300
      applicationNeedsRebuild: shouldBuild || hasDirtyDependencies,
      usesTerminalUi: hotRunner.usesTerminalUI,
301
      ipv6: hotRunner.ipv6,
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,
319
    bool shouldBuild = true,
320 321
  }) async {
    final TargetPlatform targetPlatform = await device.targetPlatform;
322
    package = await getApplicationPackageForPlatform(
323 324 325 326
      targetPlatform,
      applicationBinary: coldRunner.applicationBinary
    );

327
    final String modeName = coldRunner.debuggingOptions.buildInfo.modeName;
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
    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;
    }

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

    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,
359 360
      applicationNeedsRebuild: shouldBuild || hasDirtyDependencies,
      usesTerminalUi: coldRunner.usesTerminalUI,
361
      ipv6: coldRunner.ipv6,
362 363 364 365 366 367 368 369 370 371 372 373 374
    );

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

  void updateReloadStatus(bool wasReloadSuccessful) {
    if (wasReloadSuccessful)
417
      generator?.accept();
418
    else
419
      generator?.reject();
420
  }
421 422
}

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

441
  final List<FlutterDevice> flutterDevices;
442 443 444
  final String target;
  final DebuggingOptions debuggingOptions;
  final bool usesTerminalUI;
445
  final bool stayResident;
446
  final bool ipv6;
447
  final Completer<int> _finished = new Completer<int>();
448
  bool _stopped = false;
449 450 451 452 453 454
  String _packagesFilePath;
  String get packagesFilePath => _packagesFilePath;
  String _projectRootPath;
  String get projectRootPath => _projectRootPath;
  String _mainPath;
  String get mainPath => _mainPath;
455 456 457 458
  String getReloadPath({bool fullRestart}) =>
      debuggingOptions.buildInfo.previewDart2
          ? mainPath + (fullRestart? '' : '.incremental') + '.dill'
          : mainPath;
459 460
  AssetBundle _assetBundle;
  AssetBundle get assetBundle => _assetBundle;
461

462 463 464
  bool get isRunningDebug => debuggingOptions.buildInfo.isDebug;
  bool get isRunningProfile => debuggingOptions.buildInfo.isProfile;
  bool get isRunningRelease => debuggingOptions.buildInfo.isRelease;
465 466
  bool get supportsServiceProtocol => isRunningDebug || isRunningProfile;

467
  /// Start the app and keep the process running during its lifetime.
468
  Future<int> run({
469
    Completer<DebugConnectionInfo> connectionInfoCompleter,
470
    Completer<void> appStartedCompleter,
471
    String route,
472
    bool shouldBuild = true
473
  });
474

475 476
  bool get supportsRestart => false;

477
  Future<OperationResult> restart({ bool fullRestart = false, bool pauseAfterRestart = false }) {
478 479
    throw 'unsupported';
  }
480

481
  Future<Null> stop() async {
482
    _stopped = true;
483
    await stopEchoingDeviceLog();
484
    await preStop();
485 486 487
    return stopApp();
  }

488 489 490 491 492 493
  Future<Null> detach() async {
    await stopEchoingDeviceLog();
    await preStop();
    appFinished();
  }

494
  Future<Null> refreshViews() async {
495 496
    for (FlutterDevice device in flutterDevices)
      await device.refreshViews();
497 498
  }

499
  Future<Null> _debugDumpApp() async {
500
    await refreshViews();
501 502
    for (FlutterDevice device in flutterDevices)
      await device.debugDumpApp();
503 504
  }

505
  Future<Null> _debugDumpRenderTree() async {
506
    await refreshViews();
507 508
    for (FlutterDevice device in flutterDevices)
      await device.debugDumpRenderTree();
509 510
  }

511 512 513 514 515 516
  Future<Null> _debugDumpLayerTree() async {
    await refreshViews();
    for (FlutterDevice device in flutterDevices)
      await device.debugDumpLayerTree();
  }

517
  Future<Null> _debugDumpSemanticsTreeInTraversalOrder() async {
518 519
    await refreshViews();
    for (FlutterDevice device in flutterDevices)
520
      await device.debugDumpSemanticsTreeInTraversalOrder();
521 522 523 524 525 526
  }

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

529
  Future<Null> _debugToggleDebugPaintSizeEnabled() async {
530
    await refreshViews();
531 532
    for (FlutterDevice device in flutterDevices)
      await device.toggleDebugPaintSizeEnabled();
533 534
  }

535 536 537 538 539 540
  Future<Null> _debugTogglePerformanceOverlayOverride() async {
    await refreshViews();
    for (FlutterDevice device in flutterDevices)
      await device.debugTogglePerformanceOverlayOverride();
  }

541 542 543 544 545 546
  Future<Null> _debugToggleWidgetInspector() async {
    await refreshViews();
    for (FlutterDevice device in flutterDevices)
      await device.toggleWidgetInspector();
  }

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

583
  Future<Null> _debugTogglePlatform() async {
584
    await refreshViews();
585 586 587 588 589
    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');
590 591
  }

592
  void registerSignalHandlers() {
593
    assert(stayResident);
594
    ProcessSignal.SIGINT.watch().listen(_cleanUpAndExit);
595
    ProcessSignal.SIGTERM.watch().listen(_cleanUpAndExit);
596
    if (!supportsServiceProtocol || !supportsRestart)
597
      return;
598 599
    ProcessSignal.SIGUSR1.watch().listen(_handleSignal);
    ProcessSignal.SIGUSR2.watch().listen(_handleSignal);
600 601
  }

602 603 604 605 606 607
  Future<Null> _cleanUpAndExit(ProcessSignal signal) async {
    _resetTerminal();
    await cleanupAfterSignal();
    exit(0);
  }

608
  bool _processingUserRequest = false;
609
  Future<Null> _handleSignal(ProcessSignal signal) async {
610
    if (_processingUserRequest) {
611 612 613
      printTrace('Ignoring signal: "$signal" because we are busy.');
      return;
    }
614
    _processingUserRequest = true;
615

616
    final bool fullRestart = signal == ProcessSignal.SIGUSR2;
617 618 619 620

    try {
      await restart(fullRestart: fullRestart);
    } finally {
621
      _processingUserRequest = false;
622
    }
623 624 625
  }

  Future<Null> stopEchoingDeviceLog() async {
626 627 628
    await Future.wait(
      flutterDevices.map((FlutterDevice device) => device.stopEchoingDeviceLog())
    );
629 630
  }

631 632 633
  /// If the [reloadSources] parameter is not null the 'reloadSources' service
  /// will be registered
  Future<Null> connectToServiceProtocol({String viewFilter,
634
      ReloadSources reloadSources, CompileExpression compileExpression}) async {
635
    if (!debuggingOptions.debuggingEnabled)
636
      return new Future<Null>.error('Error the service protocol is not enabled.');
637

638 639 640
    bool viewFound = false;
    for (FlutterDevice device in flutterDevices) {
      device.viewFilter = viewFilter;
641 642
      await device._connect(reloadSources: reloadSources,
          compileExpression: compileExpression);
643 644
      await device.getVMs();
      await device.waitForViews();
645
      if (device.views.isEmpty)
646 647 648 649 650
        printStatus('No Flutter views available on ${device.device.name}');
      else
        viewFound = true;
    }
    if (!viewFound)
651
      throwToolExit('No Flutter view is available');
652

653
    // Listen for service protocol connection to close.
654 655
    for (FlutterDevice device in flutterDevices) {
      for (VMService service in device.vmServices) {
656 657 658 659
        // 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
660 661 662 663
          _serviceProtocolDone,
          onError: _serviceProtocolError
        ).whenComplete(_serviceDisconnected);
      }
664
    }
665 666 667 668 669 670 671 672 673 674
  }

  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);
675 676 677
  }

  /// Returns [true] if the input has been handled by this function.
678
  Future<bool> _commonTerminalInputHandler(String character) async {
679 680 681 682
    final String lower = character.toLowerCase();

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

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

    return false;
  }

749
  Future<Null> processTerminalInput(String command) async {
750 751
    // When terminal doesn't support line mode, '\n' can sneak into the input.
    command = command.trim();
752
    if (_processingUserRequest) {
753 754 755
      printTrace('Ignoring terminal input: "$command" because we are busy.');
      return;
    }
756
    _processingUserRequest = true;
757
    try {
758
      final bool handled = await _commonTerminalInputHandler(command);
759 760
      if (!handled)
        await handleTerminalCommand(command);
761 762 763
    } catch (error, st) {
      printError('$error\n$st');
      _cleanUpAndExit(null);
764
    } finally {
765
      _processingUserRequest = false;
766
    }
767 768
  }

769 770 771 772 773 774 775 776 777 778 779 780
  void _serviceDisconnected() {
    if (_stopped) {
      // User requested the application exit.
      return;
    }
    if (_finished.isCompleted)
      return;
    printStatus('Lost connection to device.');
    _resetTerminal();
    _finished.complete(0);
  }

781 782 783 784 785 786 787 788 789 790 791 792 793 794
  void appFinished() {
    if (_finished.isCompleted)
      return;
    printStatus('Application finished.');
    _resetTerminal();
    _finished.complete(0);
  }

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

  void setupTerminal() {
795
    assert(stayResident);
796
    if (usesTerminalUI) {
797 798
      if (!logger.quiet) {
        printStatus('');
799
        printHelp(details: false);
800
      }
801
      terminal.singleCharMode = true;
802
      terminal.onCharInput.listen(processTerminalInput);
803 804 805 806
    }
  }

  Future<int> waitForAppToFinish() async {
807
    final int exitCode = await _finished.future;
808 809 810 811
    await cleanupAtFinish();
    return exitCode;
  }

812
  bool hasDirtyDependencies(FlutterDevice device) {
813
    final DartDependencySetBuilder dartDependencySetBuilder =
814
        new DartDependencySetBuilder(mainPath, packagesFilePath);
815
    final DependencyChecker dependencyChecker =
816
        new DependencyChecker(dartDependencySetBuilder, assetBundle);
817
    if (device.package.packagesFile == null || !device.package.packagesFile.existsSync()) {
818
      return true;
819 820 821
    }
    final DateTime lastBuildTime = device.package.packagesFile.statSync().modified;

822 823 824
    return dependencyChecker.check(lastBuildTime);
  }

825 826 827
  Future<Null> preStop() async { }

  Future<Null> stopApp() async {
828 829
    for (FlutterDevice device in flutterDevices)
      await device.stopApps();
830 831 832
    appFinished();
  }

833 834 835 836
  /// Called to print help to the terminal.
  void printHelp({ @required bool details });

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

855 856 857 858 859
  /// 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.
860
  Future<Null> handleTerminalCommand(String code);
861 862
}

Devon Carew's avatar
Devon Carew committed
863
class OperationResult {
864
  OperationResult(this.code, this.message, { this.hintMessage, this.hintId });
Devon Carew's avatar
Devon Carew committed
865

866
  /// The result of the operation; a non-zero code indicates a failure.
Devon Carew's avatar
Devon Carew committed
867
  final int code;
868 869

  /// A user facing message about the results of the operation.
Devon Carew's avatar
Devon Carew committed
870
  final String message;
871 872 873 874 875 876 877 878 879 880 881

  /// 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
882 883

  bool get isOk => code == 0;
884 885

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

888 889 890
/// 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]) {
891
  target ??= '';
892
  final String targetPath = fs.path.absolute(target);
893
  if (fs.isDirectorySync(targetPath))
894
    return fs.path.join(targetPath, 'lib', 'main.dart');
895 896 897 898 899 900 901
  else
    return targetPath;
}

String getMissingPackageHintForPlatform(TargetPlatform platform) {
  switch (platform) {
    case TargetPlatform.android_arm:
902
    case TargetPlatform.android_arm64:
903
    case TargetPlatform.android_x64:
904
    case TargetPlatform.android_x86:
905 906 907
      final FlutterProject project = new FlutterProject(fs.currentDirectory);
      final String manifestPath = fs.path.relative(project.android.gradleManifestFile.path);
      return 'Is your project missing an $manifestPath?\nConsider running "flutter create ." to create one.';
908 909 910 911 912 913
    case TargetPlatform.ios:
      return 'Is your project missing an ios/Runner/Info.plist?\nConsider running "flutter create ." to create one.';
    default:
      return null;
  }
}
914 915

class DebugConnectionInfo {
916
  DebugConnectionInfo({ this.httpUri, this.wsUri, this.baseUri });
917

918 919 920 921
  // TODO(danrubel): the httpUri field should be removed as part of
  // https://github.com/flutter/flutter/issues/7050
  final Uri httpUri;
  final Uri wsUri;
922 923
  final String baseUri;
}