run.dart 15.9 KB
Newer Older
1 2 3 4 5
// Copyright 2015 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';
6
import 'dart:io';
7 8

import 'package:path/path.dart' as path;
9 10

import '../application_package.dart';
11
import '../base/common.dart';
12
import '../base/logger.dart';
13
import '../base/utils.dart';
14
import '../build_info.dart';
15
import '../device.dart';
16
import '../globals.dart';
17
import '../observatory.dart';
18
import '../runner/flutter_command.dart';
19
import 'build_apk.dart';
20
import 'install.dart';
21
import 'trace.dart';
22

23 24
abstract class RunCommandBase extends FlutterCommand {
  RunCommandBase() {
25 26
    addBuildModeFlags();

27 28 29 30
    argParser.addFlag('trace-startup',
        negatable: true,
        defaultsTo: false,
        help: 'Start tracing during startup.');
31
    argParser.addOption('route',
32
        help: 'Which route to load when running the app.');
33
    usesTargetOption();
34
  }
35 36 37 38

  bool get traceStartup => argResults['trace-startup'];
  String get target => argResults['target'];
  String get route => argResults['route'];
39
}
40

41
class RunCommand extends RunCommandBase {
42
  @override
43
  final String name = 'run';
44 45

  @override
46
  final String description = 'Run your Flutter app on an attached device.';
47 48

  @override
49
  final List<String> aliases = <String>['start'];
50

51
  RunCommand() {
52
    argParser.addFlag('full-restart',
53
        defaultsTo: true,
54
        help: 'Stop any currently running application process before running the app.');
55 56 57 58 59
    argParser.addFlag('start-paused',
        defaultsTo: false,
        negatable: false,
        help: 'Start in a paused mode and wait for a debugger to connect.');
    argParser.addOption('debug-port',
60
        help: 'Listen to the given port for a debug connection (defaults to $kDefaultObservatoryPort).');
61
    usesPubOption();
Devon Carew's avatar
Devon Carew committed
62 63 64 65 66 67 68 69

    // A temporary, hidden flag to experiment with a different run style.
    // TODO(devoncarew): Remove this.
    argParser.addFlag('resident',
        defaultsTo: false,
        negatable: false,
        hide: true,
        help: 'Stay resident after running the app.');
70 71 72 73 74 75

    // Hidden option to enable a benchmarking mode. This will run the given
    // application, measure the startup time and the app restart time, write the
    // results out to 'refresh_benchmark.json', and exit. This flag is intended
    // for use in generating automated flutter benchmarks.
    argParser.addFlag('benchmark', negatable: false, hide: true);
76 77
  }

78
  @override
79 80
  bool get requiresDevice => true;

81 82 83 84 85 86 87 88 89 90 91
  @override
  String get usagePath {
    Device device = deviceForCommand;

    if (device == null)
      return name;

    // Return 'run/ios'.
    return '$name/${getNameForTargetPlatform(device.platform)}';
  }

92 93
  @override
  Future<int> runInProject() async {
94 95
    int debugPort;

Devon Carew's avatar
Devon Carew committed
96 97 98 99 100 101 102
    if (argResults['debug-port'] != null) {
      try {
        debugPort = int.parse(argResults['debug-port']);
      } catch (error) {
        printError('Invalid port for `--debug-port`: $error');
        return 1;
      }
103 104
    }

Devon Carew's avatar
Devon Carew committed
105 106 107
    DebuggingOptions options;

    if (getBuildMode() != BuildMode.debug) {
108
      options = new DebuggingOptions.disabled(getBuildMode());
Devon Carew's avatar
Devon Carew committed
109 110
    } else {
      options = new DebuggingOptions.enabled(
111
        getBuildMode(),
Devon Carew's avatar
Devon Carew committed
112 113 114 115
        startPaused: argResults['start-paused'],
        observatoryPort: debugPort
      );
    }
116

Devon Carew's avatar
Devon Carew committed
117
    if (argResults['resident']) {
118
      _RunAndStayResident runner = new _RunAndStayResident(
Devon Carew's avatar
Devon Carew committed
119 120 121 122 123
        deviceForCommand,
        target: target,
        debuggingOptions: options,
        buildMode: getBuildMode()
      );
124

125
      return runner.run(traceStartup: traceStartup, benchmark: argResults['benchmark']);
Devon Carew's avatar
Devon Carew committed
126
    } else {
127
      return startApp(
Devon Carew's avatar
Devon Carew committed
128 129 130 131 132 133
        deviceForCommand,
        target: target,
        stop: argResults['full-restart'],
        install: true,
        debuggingOptions: options,
        traceStartup: traceStartup,
134
        benchmark: argResults['benchmark'],
Devon Carew's avatar
Devon Carew committed
135 136 137 138
        route: route,
        buildMode: getBuildMode()
      );
    }
Adam Barth's avatar
Adam Barth committed
139 140 141
  }
}

142
Future<int> startApp(
143
  Device device, {
144
  String target,
145
  bool stop: true,
146
  bool install: true,
Devon Carew's avatar
Devon Carew committed
147
  DebuggingOptions debuggingOptions,
148
  bool traceStartup: false,
149
  bool benchmark: false,
150
  String route,
151
  BuildMode buildMode: BuildMode.debug
152 153 154 155 156 157
}) async {
  String mainPath = findMainDartFile(target);
  if (!FileSystemEntity.isFileSync(mainPath)) {
    String message = 'Tried to run $mainPath, but that file does not exist.';
    if (target == null)
      message += '\nConsider using the -t option to specify the Dart file to start.';
158
    printError(message);
159 160 161
    return 1;
  }

162
  ApplicationPackage package = getApplicationPackageForPlatform(device.platform);
163 164

  if (package == null) {
Adam Barth's avatar
Adam Barth committed
165 166 167 168 169
    String message = 'No application found for ${device.platform}.';
    String hint = _getMissingPackageHintForPlatform(device.platform);
    if (hint != null)
      message += '\n$hint';
    printError(message);
170 171 172
    return 1;
  }

173 174
  Stopwatch stopwatch = new Stopwatch()..start();

175 176
  // TODO(devoncarew): We shouldn't have to do type checks here.
  if (install && device is AndroidDevice) {
177
    printTrace('Running build command.');
178 179

    int result = await buildApk(
Devon Carew's avatar
Devon Carew committed
180
      device.platform,
181
      target: target,
182
      buildMode: buildMode
183
    );
184

185 186
    if (result != 0)
      return result;
187 188
  }

Devon Carew's avatar
Devon Carew committed
189 190 191 192
  // TODO(devoncarew): Move this into the device.startApp() impls. They should
  // wait on the stop command to complete before (re-)starting the app. We could
  // plumb a Future through the start command from here, but that seems a little
  // messy.
193
  if (stop) {
194 195 196 197
    if (package != null) {
      printTrace("Stopping app '${package.name}' on ${device.name}.");
      // We don't wait for the stop command to complete.
      device.stopApp(package);
198
    }
199 200
  }

201
  // Allow any stop commands from above to start work.
Ian Hickson's avatar
Ian Hickson committed
202
  await new Future<Duration>.delayed(Duration.ZERO);
203

204 205
  // TODO(devoncarew): This fails for ios devices - we haven't built yet.
  if (install && device is AndroidDevice) {
206
    printStatus('Installing $package to $device...');
207

208 209
    if (!(await installApp(device, package)))
      return 1;
210 211
  }

212
  Map<String, dynamic> platformArgs = <String, dynamic>{};
213

214 215
  if (traceStartup != null)
    platformArgs['trace-startup'] = traceStartup;
216

217
  printStatus('Running ${_getDisplayPath(mainPath)} on ${device.name}...');
218

Devon Carew's avatar
Devon Carew committed
219
  LaunchResult result = await device.startApp(
220 221 222
    package,
    mainPath: mainPath,
    route: route,
Devon Carew's avatar
Devon Carew committed
223 224 225 226
    debuggingOptions: debuggingOptions,
    platformArgs: platformArgs
  );

227 228 229
  stopwatch.stop();

  if (!result.started) {
Devon Carew's avatar
Devon Carew committed
230
    printError('Error running application on ${device.name}.');
231 232 233 234 235 236 237 238 239 240 241 242
  } else if (traceStartup) {
    try {
      Observatory observatory = await Observatory.connect(result.observatoryPort);
      await _downloadStartupTrace(observatory);
    } catch (error) {
      printError('Error connecting to observatory: $error');
      return 1;
    }
  }

  if (benchmark)
    _writeBenchmark(stopwatch);
Devon Carew's avatar
Devon Carew committed
243 244 245 246 247 248 249 250 251 252 253 254 255 256

  return result.started ? 0 : 2;
}

/// 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]) {
  if (target == null)
    target = '';
  String targetPath = path.absolute(target);
  if (FileSystemEntity.isDirectorySync(targetPath))
    return path.join(targetPath, 'lib', 'main.dart');
  else
    return targetPath;
257
}
258

Devon Carew's avatar
Devon Carew committed
259 260 261 262 263 264 265 266 267 268 269 270
String _getMissingPackageHintForPlatform(TargetPlatform platform) {
  switch (platform) {
    case TargetPlatform.android_arm:
    case TargetPlatform.android_x64:
      return 'Is your project missing an android/AndroidManifest.xml?';
    case TargetPlatform.ios:
      return 'Is your project missing an ios/Info.plist?';
    default:
      return null;
  }
}

271 272 273 274
/// Return a relative path if [fullPath] is contained by the cwd, else return an
/// absolute path.
String _getDisplayPath(String fullPath) {
  String cwd = Directory.current.path + Platform.pathSeparator;
Devon Carew's avatar
Devon Carew committed
275
  return fullPath.startsWith(cwd) ?  fullPath.substring(cwd.length) : fullPath;
276
}
277 278 279

class _RunAndStayResident {
  _RunAndStayResident(
280
    this.device, {
281 282 283 284 285 286 287 288 289 290 291 292 293
    this.target,
    this.debuggingOptions,
    this.buildMode : BuildMode.debug
  });

  final Device device;
  final String target;
  final DebuggingOptions debuggingOptions;
  final BuildMode buildMode;

  Completer<int> _exitCompleter;
  StreamSubscription<String> _loggingSubscription;

294
  Observatory observatory;
295 296 297
  String _isolateId;

  /// Start the app and keep the process running during its lifetime.
298
  Future<int> run({ bool traceStartup: false, bool benchmark: false }) async {
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
    String mainPath = findMainDartFile(target);
    if (!FileSystemEntity.isFileSync(mainPath)) {
      String message = 'Tried to run $mainPath, but that file does not exist.';
      if (target == null)
        message += '\nConsider using the -t option to specify the Dart file to start.';
      printError(message);
      return 1;
    }

    ApplicationPackage package = getApplicationPackageForPlatform(device.platform);

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

319 320
    Stopwatch stopwatch = new Stopwatch()..start();

321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375
    // TODO(devoncarew): We shouldn't have to do type checks here.
    if (device is AndroidDevice) {
      printTrace('Running build command.');

      int result = await buildApk(
        device.platform,
        target: target,
        buildMode: buildMode
      );

      if (result != 0)
        return result;
    }

    // TODO(devoncarew): Move this into the device.startApp() impls.
    if (package != null) {
      printTrace("Stopping app '${package.name}' on ${device.name}.");
      // We don't wait for the stop command to complete.
      device.stopApp(package);
    }

    // Allow any stop commands from above to start work.
    await new Future<Duration>.delayed(Duration.ZERO);

    // TODO(devoncarew): This fails for ios devices - we haven't built yet.
    if (device is AndroidDevice) {
      printTrace('Running install command.');
      if (!(await installApp(device, package)))
        return 1;
    }

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

    printStatus('Running ${_getDisplayPath(mainPath)} on ${device.name}...');

    _loggingSubscription = device.logReader.logLines.listen((String line) {
      if (!line.contains('Observatory listening on http') && !line.contains('Diagnostic server listening on http'))
        printStatus(line);
    });

    LaunchResult result = await device.startApp(
      package,
      mainPath: mainPath,
      debuggingOptions: debuggingOptions,
      platformArgs: platformArgs
    );

    if (!result.started) {
      printError('Error running application on ${device.name}.');
      await _loggingSubscription.cancel();
      return 2;
    }

376 377
    stopwatch.stop();

378 379 380 381
    _exitCompleter = new Completer<int>();

    // Connect to observatory.
    if (debuggingOptions.debuggingEnabled) {
382
      observatory = await Observatory.connect(result.observatoryPort);
383 384
      printTrace('Connected to observatory port: ${result.observatoryPort}.');

385 386 387 388 389 390
      observatory.onIsolateEvent.listen((Event event) {
        if (event['isolate'] != null)
          _isolateId = event['isolate']['id'];
      });
      observatory.streamListen('Isolate');

391
      // Listen for observatory connection close.
392
      observatory.done.whenComplete(() {
393 394 395
        _handleExit();
      });

396 397 398 399
      observatory.getVM().then((VM vm) {
        if (vm.isolates.isNotEmpty)
          _isolateId = vm.isolates.first['id'];
      });
400 401 402 403
    }

    printStatus('Application running.');

404 405 406 407
    if (observatory != null && traceStartup) {
      printStatus('Downloading startup trace info...');

      await _downloadStartupTrace(observatory);
408

409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
      _handleExit();
    } else {
      _printHelp();

      terminal.singleCharMode = true;

      terminal.onCharInput.listen((String code) {
        String lower = code.toLowerCase();

        if (lower == 'h' || code == AnsiTerminal.KEY_F1) {
          // F1, help
          _printHelp();
        } else if (lower == 'r' || code == AnsiTerminal.KEY_F5) {
          // F5, refresh
          _handleRefresh();
        } else if (lower == 'q' || code == AnsiTerminal.KEY_F10) {
          // F10, exit
          _handleExit();
        }
      });
429

430
      ProcessSignal.SIGINT.watch().listen((ProcessSignal signal) {
431
        _handleExit();
432 433 434 435 436
      });
      ProcessSignal.SIGTERM.watch().listen((ProcessSignal signal) {
        _handleExit();
      });
    }
437

438 439 440 441 442 443
    if (benchmark) {
      _writeBenchmark(stopwatch);
      new Future<Null>.delayed(new Duration(seconds: 2)).then((_) {
        _handleExit();
      });
    }
444 445

    return _exitCompleter.future.then((int exitCode) async {
446 447 448
      if (observatory != null && !observatory.isClosed && _isolateId != null) {
        observatory.flutterExit(_isolateId);

449 450 451 452 453 454 455 456 457 458 459 460 461
        // WebSockets do not have a flush() method.
        await new Future<Null>.delayed(new Duration(milliseconds: 100));
      }

      return exitCode;
    });
  }

  void _printHelp() {
    printStatus('Type "h" or F1 for help, "r" or F5 to restart the app, and "q", F10, or ctrl-c to quit.');
  }

  void _handleRefresh() {
462
    if (observatory == null) {
463 464 465 466
      printError('Debugging is not enabled.');
    } else {
      printStatus('Re-starting application...');

467 468 469
      observatory.isolateReload(_isolateId).catchError((dynamic error) {
        printError('Error restarting app: $error');
      });
470 471 472 473
    }
  }

  void _handleExit() {
474 475
    terminal.singleCharMode = false;

476 477 478 479 480 481 482
    if (!_exitCompleter.isCompleted) {
      _loggingSubscription?.cancel();
      printStatus('Application finished.');
      _exitCompleter.complete(0);
    }
  }
}
483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538

Future<Null> _downloadStartupTrace(Observatory observatory) async {
  Tracing tracing = new Tracing(observatory);

  Map<String, dynamic> timeline = await tracing.stopTracingAndDownloadTimeline(
    waitForFirstFrame: true
  );

  int extractInstantEventTimestamp(String eventName) {
    List<Map<String, dynamic>> events = timeline['traceEvents'];
    Map<String, dynamic> event = events.firstWhere(
      (Map<String, dynamic> event) => event['name'] == eventName, orElse: () => null
    );
    return event == null ? null : event['ts'];
  }

  int engineEnterTimestampMicros = extractInstantEventTimestamp(kFlutterEngineMainEnterEventName);
  int frameworkInitTimestampMicros = extractInstantEventTimestamp(kFrameworkInitEventName);
  int firstFrameTimestampMicros = extractInstantEventTimestamp(kFirstUsefulFrameEventName);

  if (engineEnterTimestampMicros == null) {
    printError('Engine start event is missing in the timeline. Cannot compute startup time.');
    return null;
  }

  if (firstFrameTimestampMicros == null) {
    printError('First frame event is missing in the timeline. Cannot compute startup time.');
    return null;
  }

  File traceInfoFile = new File('build/start_up_info.json');
  int timeToFirstFrameMicros = firstFrameTimestampMicros - engineEnterTimestampMicros;
  Map<String, dynamic> traceInfo = <String, dynamic>{
    'engineEnterTimestampMicros': engineEnterTimestampMicros,
    'timeToFirstFrameMicros': timeToFirstFrameMicros,
  };

  if (frameworkInitTimestampMicros != null) {
    traceInfo['timeToFrameworkInitMicros'] = frameworkInitTimestampMicros - engineEnterTimestampMicros;
    traceInfo['timeAfterFrameworkInitMicros'] = firstFrameTimestampMicros - frameworkInitTimestampMicros;
  }

  traceInfoFile.writeAsStringSync(toPrettyJson(traceInfo));

  printStatus('Time to first frame: ${timeToFirstFrameMicros ~/ 1000}ms.');
  printStatus('Saved startup trace info in ${traceInfoFile.path}.');
}

void _writeBenchmark(Stopwatch stopwatch) {
  final String benchmarkOut = 'refresh_benchmark.json';
  Map<String, dynamic> data = <String, dynamic>{
    'time': stopwatch.elapsedMilliseconds
  };
  new File(benchmarkOut).writeAsStringSync(toPrettyJson(data));
  printStatus('Run benchmark written to $benchmarkOut ($data).');
}