resident_runner.dart 23.2 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 '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 20 21
import 'dart/dependencies.dart';
import 'dart/package_map.dart';
import 'dependency_checker.dart';
22
import 'devfs.dart';
23 24
import 'device.dart';
import 'globals.dart';
25 26
import 'run_cold.dart';
import 'run_hot.dart';
27
import 'vmservice.dart';
28

29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
class FlutterDevice {
  final Device device;
  List<Uri> observatoryUris;
  List<VMService> vmServices;
  DevFS devFS;
  ApplicationPackage package;

  String _viewFilter;
  StreamSubscription<String> _loggingSubscription;

  FlutterDevice(this.device);

  String get viewFilter => _viewFilter;
  set viewFilter(String filter) {
    _viewFilter = filter;
    _viewsCache = null;
  }

  void connect() {
    if (vmServices != null)
      return;
    vmServices = new List<VMService>(observatoryUris.length);
    for (int i = 0; i < observatoryUris.length; i++) {
      vmServices[i] = VMService.connect(observatoryUris[i]);
      printTrace('Connected to service protocol: ${observatoryUris[i]}');
    }
  }

  Future<Null> refreshViews() async {
    if ((vmServices == null) || vmServices.isEmpty)
      return;
    for (VMService service in vmServices)
      await service.vm.refreshViews();
    _viewsCache = null;
  }

  List<FlutterView> _viewsCache;
  List<FlutterView> get views {
    if (_viewsCache == null) {
      if ((vmServices == null) || vmServices.isEmpty)
        return null;
      final List<FlutterView> result = <FlutterView>[];
      if (_viewFilter == null) {
        for (VMService service in vmServices) {
          if (!service.isClosed)
            result.addAll(service.vm.views.toList());
        }
      } else {
        for (VMService service in vmServices) {
          if (!service.isClosed)
            result.addAll(service.vm.allViewsWithName(_viewFilter));
        }
      }
      _viewsCache = result;
    }
    return _viewsCache;
  }

  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) {
      if (view != null && view.uiIsolate != null)
        view.uiIsolate.flutterExit();
    }
    await new Future<Null>.delayed(const Duration(milliseconds: 100));
  }

  Future<Uri> setupDevFS(String fsName,
                         Directory rootDirectory, {
                         String packagesFilePath
                       }) {
    // 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;
  }

  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();
  }

151 152 153 154 155 156 157 158 159 160
  Future<Null> debugDumpLayerTree() async {
    for (FlutterView view in views)
      await view.uiIsolate.flutterDebugDumpLayerTree();
  }

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

161 162 163 164 165
  Future<Null> toggleDebugPaintSizeEnabled() async {
    for (FlutterView view in views)
      await view.uiIsolate.flutterToggleDebugPaintSizeEnabled();
  }

166
  Future<String> togglePlatform({ String from }) async {
167 168 169 170 171 172 173 174 175 176
    String to;
    switch (from) {
      case 'iOS':
        to = 'android';
        break;
      case 'android':
      default:
        to = 'iOS';
        break;
    }
177
    for (FlutterView view in views)
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 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 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
      await view.uiIsolate.flutterPlatformOverride(to);
    return to;
  }

  void startEchoingDeviceLog() {
    if (_loggingSubscription != null)
      return;
    _loggingSubscription = device.getLogReader(app: package).logLines.listen((String line) {
      if (!line.contains('Observatory listening on http') &&
          !line.contains('Diagnostic server listening on http'))
        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;
    final String modeName = getModeName(hotRunner.debuggingOptions.buildMode);
    printStatus('Launching ${getDisplayPath(hotRunner.mainPath)} on ${device.name} in $modeName mode...');

    final TargetPlatform targetPlatform = await device.targetPlatform;
    package = getApplicationPackageForPlatform(
      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,
      hotRunner.debuggingOptions.buildMode,
      mainPath: hotRunner.mainPath,
      debuggingOptions: hotRunner.debuggingOptions,
      platformArgs: platformArgs,
      route: route,
      prebuiltApplication: prebuiltMode,
      kernelPath: hotRunner.kernelFilePath,
      applicationNeedsRebuild: shouldBuild || hasDirtyDependencies
    );

    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;
    package = getApplicationPackageForPlatform(
      targetPlatform,
      applicationBinary: coldRunner.applicationBinary
    );

    final String modeName = getModeName(coldRunner.debuggingOptions.buildMode);
    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;
    }

    Map<String, dynamic> platformArgs;
    if (coldRunner.traceStartup != null)
      platformArgs = <String, dynamic>{ 'trace-startup': coldRunner.traceStartup };

    startEchoingDeviceLog();

    final bool hasDirtyDependencies = coldRunner.hasDirtyDependencies(this);
    final LaunchResult result = await device.startApp(
      package,
      coldRunner.debuggingOptions.buildMode,
      mainPath: coldRunner.mainPath,
      debuggingOptions: coldRunner.debuggingOptions,
      platformArgs: platformArgs,
      route: route,
      prebuiltApplication: prebuiltMode,
      applicationNeedsRebuild: shouldBuild || hasDirtyDependencies
    );

    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({
    DevFSProgressReporter progressReporter,
    AssetBundle bundle,
    bool bundleDirty: false,
    Set<String> fileFilter
  }) async {
    final Status devFSStatus = logger.startProgress(
      'Syncing files to device ${device.name}...',
      expectSlowOperation: true
    );
    int bytes = 0;
    try {
      bytes = await devFS.update(
        progressReporter: progressReporter,
        bundle: bundle,
        bundleDirty: bundleDirty,
        fileFilter: fileFilter
      );
    } on DevFSException {
      devFSStatus.cancel();
      return false;
    }
    devFSStatus.stop();
    printTrace('Synced ${getSizeAsMB(bytes)}.');
    return true;
  }
}

342 343
// Shared code between different resident application runners.
abstract class ResidentRunner {
344
  ResidentRunner(this.flutterDevices, {
345 346
    this.target,
    this.debuggingOptions,
347 348 349 350
    this.usesTerminalUI: true,
    String projectRootPath,
    String packagesFilePath,
    String projectAssets,
351
    this.stayResident,
352 353
  }) {
    _mainPath = findMainDartFile(target);
354
    _projectRootPath = projectRootPath ?? fs.currentDirectory.path;
355
    _packagesFilePath =
356
        packagesFilePath ?? fs.path.absolute(PackageMap.globalPackagesPath);
357 358 359 360 361
    if (projectAssets != null)
      _assetBundle = new AssetBundle.fixed(_projectRootPath, projectAssets);
    else
      _assetBundle = new AssetBundle();
  }
362

363
  final List<FlutterDevice> flutterDevices;
364 365 366
  final String target;
  final DebuggingOptions debuggingOptions;
  final bool usesTerminalUI;
367
  final bool stayResident;
368
  final Completer<int> _finished = new Completer<int>();
369
  bool _stopped = false;
370 371 372 373 374 375 376 377
  String _packagesFilePath;
  String get packagesFilePath => _packagesFilePath;
  String _projectRootPath;
  String get projectRootPath => _projectRootPath;
  String _mainPath;
  String get mainPath => _mainPath;
  AssetBundle _assetBundle;
  AssetBundle get assetBundle => _assetBundle;
378

379 380 381 382 383
  bool get isRunningDebug => debuggingOptions.buildMode == BuildMode.debug;
  bool get isRunningProfile => debuggingOptions.buildMode == BuildMode.profile;
  bool get isRunningRelease => debuggingOptions.buildMode == BuildMode.release;
  bool get supportsServiceProtocol => isRunningDebug || isRunningProfile;

384
  /// Start the app and keep the process running during its lifetime.
385
  Future<int> run({
386
    Completer<DebugConnectionInfo> connectionInfoCompleter,
387
    Completer<Null> appStartedCompleter,
388 389 390
    String route,
    bool shouldBuild: true
  });
391

392 393 394 395 396
  bool get supportsRestart => false;

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

398
  Future<Null> stop() async {
399
    _stopped = true;
400
    await stopEchoingDeviceLog();
401
    await preStop();
402 403 404
    return stopApp();
  }

405 406 407 408 409 410
  Future<Null> detach() async {
    await stopEchoingDeviceLog();
    await preStop();
    appFinished();
  }

411
  Future<Null> refreshViews() async {
412 413
    for (FlutterDevice device in flutterDevices)
      await device.refreshViews();
414 415
  }

416
  Future<Null> _debugDumpApp() async {
417
    await refreshViews();
418 419
    for (FlutterDevice device in flutterDevices)
      await device.debugDumpApp();
420 421
  }

422
  Future<Null> _debugDumpRenderTree() async {
423
    await refreshViews();
424 425
    for (FlutterDevice device in flutterDevices)
      await device.debugDumpRenderTree();
426 427
  }

428 429 430 431 432 433 434 435 436 437 438 439
  Future<Null> _debugDumpLayerTree() async {
    await refreshViews();
    for (FlutterDevice device in flutterDevices)
      await device.debugDumpLayerTree();
  }

  Future<Null> _debugDumpSemanticsTree() async {
    await refreshViews();
    for (FlutterDevice device in flutterDevices)
      await device.debugDumpSemanticsTree();
  }

440
  Future<Null> _debugToggleDebugPaintSizeEnabled() async {
441
    await refreshViews();
442 443
    for (FlutterDevice device in flutterDevices)
      await device.toggleDebugPaintSizeEnabled();
444 445
  }

446 447
  Future<Null> _screenshot(FlutterDevice device) async {
    final Status status = logger.startProgress('Taking screenshot for ${device.device.name}...');
448
    final File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png');
449
    try {
450
      if (supportsServiceProtocol && isRunningDebug) {
451
        await device.refreshViews();
452
        try {
453 454
          for (FlutterView view in device.views)
            await view.uiIsolate.flutterDebugAllowBanner(false);
455 456 457 458
        } catch (error) {
          status.stop();
          printError(error);
        }
459 460
      }
      try {
461
        await device.device.takeScreenshot(outputFile);
462
      } finally {
463 464
        if (supportsServiceProtocol && isRunningDebug) {
          try {
465 466
            for (FlutterView view in device.views)
              await view.uiIsolate.flutterDebugAllowBanner(true);
467 468 469 470
          } catch (error) {
            status.stop();
            printError(error);
          }
471 472
        }
      }
473
      final int sizeKB = (await outputFile.length()) ~/ 1024;
474
      status.stop();
475
      printStatus('Screenshot written to ${fs.path.relative(outputFile.path)} (${sizeKB}kB).');
476
    } catch (error) {
477
      status.stop();
478 479 480 481
      printError('Error taking screenshot: $error');
    }
  }

482
  Future<Null> _debugTogglePlatform() async {
483
    await refreshViews();
484 485 486 487 488
    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');
489 490
  }

491
  void registerSignalHandlers() {
492
    assert(stayResident);
493
    ProcessSignal.SIGINT.watch().listen(_cleanUpAndExit);
494
    ProcessSignal.SIGTERM.watch().listen(_cleanUpAndExit);
495
    if (!supportsServiceProtocol || !supportsRestart)
496
      return;
497 498
    ProcessSignal.SIGUSR1.watch().listen(_handleSignal);
    ProcessSignal.SIGUSR2.watch().listen(_handleSignal);
499 500
  }

501 502 503 504 505 506
  Future<Null> _cleanUpAndExit(ProcessSignal signal) async {
    _resetTerminal();
    await cleanupAfterSignal();
    exit(0);
  }

507
  bool _processingUserRequest = false;
508
  Future<Null> _handleSignal(ProcessSignal signal) async {
509
    if (_processingUserRequest) {
510 511 512
      printTrace('Ignoring signal: "$signal" because we are busy.');
      return;
    }
513
    _processingUserRequest = true;
514

515
    final bool fullRestart = signal == ProcessSignal.SIGUSR2;
516 517 518 519

    try {
      await restart(fullRestart: fullRestart);
    } finally {
520
      _processingUserRequest = false;
521
    }
522 523 524
  }

  Future<Null> stopEchoingDeviceLog() async {
525 526
    for (FlutterDevice device in flutterDevices)
      device.stopEchoingDeviceLog();
527 528
  }

529
  Future<Null> connectToServiceProtocol({String viewFilter}) async {
530
    if (!debuggingOptions.debuggingEnabled)
531
      return new Future<Null>.error('Error the service protocol is not enabled.');
532

533 534 535 536 537 538 539 540 541 542 543 544
    bool viewFound = false;
    for (FlutterDevice device in flutterDevices) {
      device.viewFilter = viewFilter;
      device.connect();
      await device.getVMs();
      await device.waitForViews();
      if (device.views == null)
        printStatus('No Flutter views available on ${device.device.name}');
      else
        viewFound = true;
    }
    if (!viewFound)
545
      throwToolExit('No Flutter view is available');
546

547
    // Listen for service protocol connection to close.
548 549 550 551 552 553 554
    for (FlutterDevice device in flutterDevices) {
      for (VMService service in device.vmServices) {
        service.done.then<Null>(
          _serviceProtocolDone,
          onError: _serviceProtocolError
        ).whenComplete(_serviceDisconnected);
      }
555
    }
556 557 558 559 560 561 562 563 564 565
  }

  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);
566 567 568
  }

  /// Returns [true] if the input has been handled by this function.
569
  Future<bool> _commonTerminalInputHandler(String character) async {
570 571 572 573
    final String lower = character.toLowerCase();

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

574 575
    if (lower == 'h' || lower == '?') {
      // help
576
      printHelp(details: true);
577 578
      return true;
    } else if (lower == 'w') {
579 580
      if (supportsServiceProtocol) {
        await _debugDumpApp();
581
        return true;
582
      }
583
    } else if (lower == 't') {
584 585
      if (supportsServiceProtocol) {
        await _debugDumpRenderTree();
586
        return true;
587
      }
588 589 590 591 592 593 594 595 596 597
    } else if (character == 'L') {
      if (supportsServiceProtocol) {
        await _debugDumpLayerTree();
        return true;
      }
    } else if (character == 'S') {
      if (supportsServiceProtocol) {
        await _debugDumpSemanticsTree();
        return true;
      }
598
    } else if (lower == 'p') {
599 600
      if (supportsServiceProtocol && isRunningDebug) {
        await _debugToggleDebugPaintSizeEnabled();
601
        return true;
602
      }
603
    } else if (character == 's') {
604 605 606
      for (FlutterDevice device in flutterDevices) {
        if (device.device.supportsScreenshot)
          await _screenshot(device);
607
      }
608
      return true;
609 610
    } else if (lower == 'o') {
      if (supportsServiceProtocol && isRunningDebug) {
611
        await _debugTogglePlatform();
612 613
        return true;
      }
614 615
    } else if (lower == 'q') {
      // exit
616
      await stop();
617
      return true;
618 619 620
    } else if (lower == 'd') {
      await detach();
      return true;
621 622 623 624 625
    }

    return false;
  }

626
  Future<Null> processTerminalInput(String command) async {
627 628
    // When terminal doesn't support line mode, '\n' can sneak into the input.
    command = command.trim();
629
    if (_processingUserRequest) {
630 631 632
      printTrace('Ignoring terminal input: "$command" because we are busy.');
      return;
    }
633
    _processingUserRequest = true;
634
    try {
635
      final bool handled = await _commonTerminalInputHandler(command);
636 637 638
      if (!handled)
        await handleTerminalCommand(command);
    } finally {
639
      _processingUserRequest = false;
640
    }
641 642
  }

643 644 645 646 647 648 649 650 651 652 653 654
  void _serviceDisconnected() {
    if (_stopped) {
      // User requested the application exit.
      return;
    }
    if (_finished.isCompleted)
      return;
    printStatus('Lost connection to device.');
    _resetTerminal();
    _finished.complete(0);
  }

655 656 657 658 659 660 661 662 663 664 665 666 667 668
  void appFinished() {
    if (_finished.isCompleted)
      return;
    printStatus('Application finished.');
    _resetTerminal();
    _finished.complete(0);
  }

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

  void setupTerminal() {
669
    assert(stayResident);
670
    if (usesTerminalUI) {
671 672
      if (!logger.quiet) {
        printStatus('');
673
        printHelp(details: false);
674
      }
675
      terminal.singleCharMode = true;
676
      terminal.onCharInput.listen(processTerminalInput);
677 678 679 680
    }
  }

  Future<int> waitForAppToFinish() async {
681
    final int exitCode = await _finished.future;
682 683 684 685
    await cleanupAtFinish();
    return exitCode;
  }

686
  bool hasDirtyDependencies(FlutterDevice device) {
687
    final DartDependencySetBuilder dartDependencySetBuilder =
688
        new DartDependencySetBuilder(mainPath, packagesFilePath);
689
    final DependencyChecker dependencyChecker =
690
        new DependencyChecker(dartDependencySetBuilder, assetBundle);
691
    final String path = device.package.packagePath;
692
    if (path == null)
693 694
      return true;
    final FileStat stat = fs.file(path).statSync();
695
    if (stat.type != FileSystemEntityType.FILE)
696
      return true;
697
    if (!fs.file(path).existsSync())
698 699 700 701 702
      return true;
    final DateTime lastBuildTime = stat.modified;
    return dependencyChecker.check(lastBuildTime);
  }

703 704 705
  Future<Null> preStop() async { }

  Future<Null> stopApp() async {
706 707
    for (FlutterDevice device in flutterDevices)
      await device.stopApps();
708 709 710
    appFinished();
  }

711 712 713 714
  /// Called to print help to the terminal.
  void printHelp({ @required bool details });

  void printHelpDetails() {
715
    if (supportsServiceProtocol) {
716
      printStatus('You can dump the widget hierarchy of the app (debugDumpApp) by pressing "w".');
717
      printStatus('To dump the rendering tree of the app (debugDumpRenderTree), press "t".');
718
      printStatus('For layers (debugDumpLayerTree), use "L"; accessibility (debugDumpSemantics), "S".');
719 720 721 722
      if (isRunningDebug) {
        printStatus('To toggle the display of construction lines (debugPaintSizeEnabled), press "p".');
        printStatus('To simulate different operating systems, (defaultTargetPlatform), press "o".');
      }
723
    }
724
    if (flutterDevices.any((FlutterDevice d) => d.device.supportsScreenshot))
725
      printStatus('To save a screenshot to flutter.png, press "s".');
726 727
  }

728 729 730 731 732
  /// 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.
733
  Future<Null> handleTerminalCommand(String code);
734 735
}

Devon Carew's avatar
Devon Carew committed
736 737 738 739 740 741 742 743 744 745 746
class OperationResult {
  static final OperationResult ok = new OperationResult(0, '');

  OperationResult(this.code, this.message);

  final int code;
  final String message;

  bool get isOk => code == 0;
}

747 748 749
/// 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]) {
750
  target ??= '';
751
  final String targetPath = fs.path.absolute(target);
752
  if (fs.isDirectorySync(targetPath))
753
    return fs.path.join(targetPath, 'lib', 'main.dart');
754 755 756 757 758 759 760 761
  else
    return targetPath;
}

String getMissingPackageHintForPlatform(TargetPlatform platform) {
  switch (platform) {
    case TargetPlatform.android_arm:
    case TargetPlatform.android_x64:
762 763 764 765 766 767
    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.';
768 769 770 771 772 773
    case TargetPlatform.ios:
      return 'Is your project missing an ios/Runner/Info.plist?\nConsider running "flutter create ." to create one.';
    default:
      return null;
  }
}
774 775

class DebugConnectionInfo {
776
  DebugConnectionInfo({ this.httpUri, this.wsUri, this.baseUri });
777

778 779 780 781
  // TODO(danrubel): the httpUri field should be removed as part of
  // https://github.com/flutter/flutter/issues/7050
  final Uri httpUri;
  final Uri wsUri;
782 783
  final String baseUri;
}