blob: 01316c7702a4a1763dbca0d0f2e2d01470db8e3f [file] [log] [blame]
// 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:io';
import '../application_package.dart';
import '../base/common.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../cache.dart';
import '../device.dart';
import '../globals.dart';
import '../hot.dart';
import '../ios/mac.dart';
import '../vmservice.dart';
import '../resident_runner.dart';
import '../run.dart';
import '../runner/flutter_command.dart';
import 'build_apk.dart';
import 'daemon.dart';
import 'install.dart';
import 'trace.dart';
abstract class RunCommandBase extends FlutterCommand {
RunCommandBase() {
addBuildModeFlags(defaultToRelease: false);
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 route => argResults['route'];
}
class RunCommand extends RunCommandBase {
@override
final String name = 'run';
@override
final String description = 'Run your Flutter app on an attached device.';
RunCommand({ bool verboseHelp: false }) {
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 $kDefaultObservatoryPort).');
argParser.addFlag('build',
defaultsTo: true,
help: 'If necessary, build the app before running.');
argParser.addOption('use-application-binary',
hide: !verboseHelp,
help: 'Specify a pre-built application binary to use when running.');
argParser.addFlag('machine',
hide: !verboseHelp,
help: 'Handle machine structured JSON command input\n'
'and provide output and progress in machine friendly format.');
usesPubOption();
// Option to enable hot reloading.
argParser.addFlag(
'hot',
negatable: true,
defaultsTo: kHotReloadDefault,
help: 'Run with support for hot reloading.'
);
// Option to write the pid to a file.
argParser.addOption(
'pid-file',
help: 'Specify a file to write the process id to.\n'
'You can send SIGUSR1 to trigger a hot reload\n'
'and SIGUSR2 to trigger a full restart.'
);
// 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: !verboseHelp);
commandValidator = () {
if (!runningWithPrebuiltApplication) {
return commonCommandValidator();
}
// When running with a prebuilt application, no command validation is
// necessary.
return true;
};
}
Device device;
@override
String get usagePath {
String command = shouldUseHotMode() ? 'hotrun' : name;
if (device == null)
return command;
// Return 'run/ios'.
return '$command/${getNameForTargetPlatform(device.platform)}';
}
@override
void printNoConnectedDevices() {
super.printNoConnectedDevices();
if (getCurrentHostPlatform() == HostPlatform.darwin_x64 &&
XCode.instance.isInstalledAndMeetsVersionCheck) {
printStatus('');
printStatus('To run on a simulator, launch it first:');
printStatus('open -a Simulator.app');
printStatus('');
printStatus('If you expected your device to be detected, please run "flutter doctor" to diagnose');
printStatus('potential issues, or visit https://flutter.io/setup/ for troubleshooting tips.');
}
}
@override
bool get shouldRunPub {
// If we are running with a prebuilt application, do not run pub.
if (runningWithPrebuiltApplication)
return false;
return super.shouldRunPub;
}
bool shouldUseHotMode() {
bool hotArg = argResults['hot'] ?? false;
final bool shouldUseHotMode = hotArg && !runningWithPrebuiltApplication;
return (getBuildMode() == BuildMode.debug) && shouldUseHotMode;
}
bool get runningWithPrebuiltApplication =>
argResults['use-application-binary'] != null;
@override
Future<int> verifyThenRunCommand() async {
if (!commandValidator())
return 1;
device = await findTargetDevice();
if (device == null)
return 1;
return super.verifyThenRunCommand();
}
@override
Future<int> runCommand() async {
Cache.releaseLockEarly();
// Enable hot mode by default if `--no-hot` was not passed and we are in
// debug mode.
final bool hotMode = shouldUseHotMode();
if (argResults['machine']) {
Daemon daemon = new Daemon(stdinCommandStream, stdoutCommandResponse,
notifyingLogger: new NotifyingLogger());
AppInstance app = daemon.appDomain.startApp(
device, Directory.current.path, targetFile, route,
getBuildMode(), argResults['start-paused'], hotMode);
return app.runner.waitForAppToFinish();
}
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;
}
}
if (device.isLocalEmulator && !isEmulatorBuildMode(getBuildMode())) {
printError('${toTitleCase(getModeName(getBuildMode()))} mode is not supported for emulators.');
return 1;
}
DebuggingOptions options;
if (getBuildMode() == BuildMode.release) {
options = new DebuggingOptions.disabled(getBuildMode());
} else {
options = new DebuggingOptions.enabled(
getBuildMode(),
startPaused: argResults['start-paused'],
observatoryPort: debugPort
);
}
if (hotMode) {
if (!device.supportsHotMode) {
printError('Hot mode is not supported by this device. '
'Run with --no-hot.');
return 1;
}
}
String pidFile = argResults['pid-file'];
if (pidFile != null) {
// Write our pid to the file.
new File(pidFile).writeAsStringSync(pid.toString());
}
ResidentRunner runner;
if (hotMode) {
runner = new HotRunner(
device,
target: targetFile,
debuggingOptions: options,
benchmarkMode: argResults['benchmark']
);
} else {
runner = new RunAndStayResident(
device,
target: targetFile,
debuggingOptions: options,
traceStartup: traceStartup,
benchmark: argResults['benchmark'],
applicationBinary: argResults['use-application-binary']
);
}
return runner.run(route: route, shouldBuild: !runningWithPrebuiltApplication && argResults['build']);
}
}
Future<int> startApp(
Device device, {
String target,
bool stop: true,
bool install: true,
DebuggingOptions debuggingOptions,
bool traceStartup: false,
bool benchmark: 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;
}
Stopwatch stopwatch = new Stopwatch()..start();
// 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,
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}.');
await device.stopApp(package);
}
}
// TODO(devoncarew): This fails for ios devices - we haven't built yet.
if (install && device is AndroidDevice) {
printStatus('Installing $package to $device...');
if (!(installApp(device, package, uninstall: false)))
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,
buildMode,
mainPath: mainPath,
route: route,
debuggingOptions: debuggingOptions,
platformArgs: platformArgs
);
stopwatch.stop();
if (!result.started) {
printError('Error running application on ${device.name}.');
} else if (traceStartup) {
try {
VMService observatory = await VMService.connect(result.observatoryPort);
await downloadStartupTrace(observatory);
} catch (error) {
printError('Error downloading trace from observatory: $error');
return 1;
}
}
if (benchmark)
writeRunBenchmarkFile(stopwatch);
return result.started ? 0 : 2;
}