blob: d155741825b042c7080f5e6da3f809ae2dde5b2c [file]
// 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';
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
import '../application_package.dart';
import '../base/common.dart';
import '../base/logger.dart';
import '../build_configuration.dart';
import '../device.dart';
import '../globals.dart';
import '../runner/flutter_command.dart';
import '../toolchain.dart';
import 'build_apk.dart';
import 'install.dart';
abstract class RunCommandBase extends FlutterCommand {
RunCommandBase() {
addBuildModeFlags();
// TODO(devoncarew): Remove in favor of --debug/--profile/--release.
argParser.addFlag('checked',
negatable: true,
defaultsTo: true,
help: 'Run the application in checked ("slow") mode.\n'
'Note: this flag will be removed in favor of the --debug/--profile/--release flags.');
argParser.addFlag('trace-startup',
negatable: true,
defaultsTo: false,
help: 'Start tracing during startup.');
argParser.addOption('route',
help: 'Which route to load when running the app.');
usesTargetOption();
}
bool get traceStartup => argResults['trace-startup'];
String get target => argResults['target'];
String get route => argResults['route'];
}
class RunCommand extends RunCommandBase {
@override
final String name = 'run';
@override
final String description = 'Run your Flutter app on an attached device.';
@override
final List<String> aliases = <String>['start'];
RunCommand() {
argParser.addFlag('full-restart',
defaultsTo: true,
help: 'Stop any currently running application process before running the app.');
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',
help: 'Listen to the given port for a debug connection (defaults to $defaultObservatoryPort).');
usesPubOption();
// 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.');
}
@override
bool get requiresDevice => true;
@override
String get usagePath {
Device device = deviceForCommand;
if (device == null)
return name;
// Return 'run/ios'.
return '$name/${getNameForTargetPlatform(device.platform)}';
}
@override
Future<int> runInProject() async {
int debugPort;
if (argResults['debug-port'] != null) {
try {
debugPort = int.parse(argResults['debug-port']);
} catch (error) {
printError('Invalid port for `--debug-port`: $error');
return 1;
}
}
DebuggingOptions options;
if (getBuildMode() != BuildMode.debug) {
options = new DebuggingOptions.disabled();
} else {
options = new DebuggingOptions.enabled(
// TODO(devoncarew): Check this to 'getBuildMode() == BuildMode.debug'.
checked: argResults['checked'],
startPaused: argResults['start-paused'],
observatoryPort: debugPort
);
}
if (argResults['resident']) {
_RunAndStayResident runner = new _RunAndStayResident(
deviceForCommand,
toolchain,
target: target,
debuggingOptions: options,
traceStartup: traceStartup,
buildMode: getBuildMode()
);
return runner.run();
} else {
return startApp(
deviceForCommand,
toolchain,
target: target,
stop: argResults['full-restart'],
install: true,
debuggingOptions: options,
traceStartup: traceStartup,
route: route,
buildMode: getBuildMode()
);
}
}
}
Future<int> startApp(
Device device,
Toolchain toolchain, {
String target,
bool stop: true,
bool install: true,
DebuggingOptions debuggingOptions,
bool traceStartup: false,
String route,
BuildMode buildMode: BuildMode.debug
}) 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.';
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;
}
// TODO(devoncarew): We shouldn't have to do type checks here.
if (install && device is AndroidDevice) {
printTrace('Running build command.');
int result = await buildApk(
device.platform,
toolchain,
target: target,
buildMode: buildMode
);
if (result != 0)
return result;
}
// 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.
if (stop) {
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 (install && device is AndroidDevice) {
printStatus('Installing $package to $device...');
if (!(await installApp(device, package)))
return 1;
}
Map<String, dynamic> platformArgs = <String, dynamic>{};
if (traceStartup != null)
platformArgs['trace-startup'] = traceStartup;
printStatus('Running ${_getDisplayPath(mainPath)} on ${device.name}...');
LaunchResult result = await device.startApp(
package,
toolchain,
mainPath: mainPath,
route: route,
debuggingOptions: debuggingOptions,
platformArgs: platformArgs
);
if (!result.started)
printError('Error running application on ${device.name}.');
else if (traceStartup)
await _downloadStartupTrace(result.observatoryPort, device);
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;
}
Future<Null> _downloadStartupTrace(int observatoryPort, Device device) async {
Map<String, dynamic> timeline = await device.stopTracingAndDownloadTimeline(
observatoryPort,
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);
if (event == null)
return null;
return event['ts'];
}
int engineEnterTimestampMicros = extractInstantEventTimestamp(flutterEngineMainEnterEventName);
int frameworkInitTimestampMicros = extractInstantEventTimestamp(frameworkInitEventName);
int firstFrameTimestampMicros = extractInstantEventTimestamp(firstUsefulFrameEventName);
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;
}
await traceInfoFile.writeAsString(JSON.encode(traceInfo));
String timeToFirstFrameMessage;
if (timeToFirstFrameMicros > 1000000) {
timeToFirstFrameMessage = '${(timeToFirstFrameMicros / 1000000).toStringAsFixed(2)} seconds';
} else {
timeToFirstFrameMessage = '${timeToFirstFrameMicros ~/ 1000} milliseconds';
}
printStatus('Time to first frame $timeToFirstFrameMessage');
printStatus('Saved startup trace info in ${traceInfoFile.path}');
}
/// Delay until the Observatory / service protocol is available.
///
/// This does not fail if we're unable to connect, and times out after the given
/// [timeout].
Future<Null> delayUntilObservatoryAvailable(String host, int port, {
Duration timeout: const Duration(seconds: 10)
}) async {
printTrace('Waiting until Observatory is available (port $port).');
final String url = 'ws://$host:$port/ws';
printTrace('Looking for the observatory at $url.');
Stopwatch stopwatch = new Stopwatch()..start();
while (stopwatch.elapsed <= timeout) {
try {
WebSocket ws = await WebSocket.connect(url);
printTrace('Connected to the observatory port.');
ws.close().catchError((dynamic error) => null);
return;
} catch (error) {
await new Future<Null>.delayed(new Duration(milliseconds: 250));
}
}
printTrace('Unable to connect to the observatory.');
}
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;
}
}
/// 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;
return fullPath.startsWith(cwd) ? fullPath.substring(cwd.length) : fullPath;
}
class _RunAndStayResident {
_RunAndStayResident(
this.device,
this.toolchain, {
this.target,
this.debuggingOptions,
this.traceStartup : false,
this.buildMode : BuildMode.debug
});
final Device device;
final Toolchain toolchain;
final String target;
final DebuggingOptions debuggingOptions;
final bool traceStartup;
final BuildMode buildMode;
Completer<int> _exitCompleter;
StreamSubscription<String> _loggingSubscription;
WebSocket _observatoryConnection;
String _isolateId;
int _messageId = 0;
/// Start the app and keep the process running during its lifetime.
Future<int> run() 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.';
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;
}
// 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,
toolchain,
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,
toolchain,
mainPath: mainPath,
debuggingOptions: debuggingOptions,
platformArgs: platformArgs
);
if (!result.started) {
printError('Error running application on ${device.name}.');
await _loggingSubscription.cancel();
return 2;
}
_exitCompleter = new Completer<int>();
// Connect to observatory.
if (debuggingOptions.debuggingEnabled) {
final String localhost = InternetAddress.LOOPBACK_IP_V4.address;
final String url = 'ws://$localhost:${result.observatoryPort}/ws';
_observatoryConnection = await WebSocket.connect(url);
printTrace('Connected to observatory port: ${result.observatoryPort}.');
// Listen for observatory connection close.
_observatoryConnection.listen((dynamic data) {
if (data is String) {
Map<String, dynamic> json = JSON.decode(data);
if (json['method'] == 'streamNotify') {
Map<String, dynamic> event = json['params']['event'];
if (event['isolate'] != null && _isolateId == null)
_isolateId = event['isolate']['id'];
} else if (json['result'] != null && json['result']['type'] == 'VM') {
// isolates: [{
// type: @Isolate, fixedId: true, id: isolates/724543296, name: dev.flx$main, number: 724543296
// }]
List<dynamic> isolates = json['result']['isolates'];
if (isolates.isNotEmpty)
_isolateId = isolates.first['id'];
} else if (json['error'] != null) {
printError('Error: ${json['error']['message']}.');
printTrace(data);
}
}
}, onDone: () {
_handleExit();
});
_observatoryConnection.add(JSON.encode(<String, dynamic>{
'method': 'streamListen',
'params': <String, dynamic>{ 'streamId': 'Isolate' },
'id': _messageId++
}));
_observatoryConnection.add(JSON.encode(<String, dynamic>{
'method': 'getVM',
'id': _messageId++
}));
}
printStatus('Application running.');
_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();
}
});
ProcessSignal.SIGINT.watch().listen((ProcessSignal signal) {
_handleExit();
});
ProcessSignal.SIGTERM.watch().listen((ProcessSignal signal) {
_handleExit();
});
return _exitCompleter.future.then((int exitCode) async {
if (_observatoryConnection != null &&
_observatoryConnection.readyState == WebSocket.OPEN &&
_isolateId != null) {
_observatoryConnection.add(JSON.encode(<String, dynamic>{
'method': 'ext.flutter.exit',
'params': <String, dynamic>{ 'isolateId': _isolateId },
'id': _messageId++
}));
// 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() {
if (_observatoryConnection == null) {
printError('Debugging is not enabled.');
} else {
printStatus('Re-starting application...');
// TODO(devoncarew): Show an error if the isolate reload fails.
_observatoryConnection.add(JSON.encode(<String, dynamic>{
'method': 'isolateReload',
'params': <String, dynamic>{ 'isolateId': _isolateId },
'id': _messageId++
}));
}
}
void _handleExit() {
if (!_exitCompleter.isCompleted) {
_loggingSubscription?.cancel();
printStatus('');
printStatus('Application finished.');
terminal.singleCharMode = false;
_exitCompleter.complete(0);
}
}
}