Commit 3ba17136 authored by Devon Carew's avatar Devon Carew

add a restart command to the daemon protocol (#4385)

* refactor the --resident run option into a separate file

* update daemon to run --resident apps

* re-plumbing daemon start

* send app logs

* update tests

* review changes

* fix test runner

* remove PackageMap.createGlobalInstance; rely on the ctor

* review comments
parent 68ba5bfd
......@@ -6,7 +6,7 @@ import 'dart:async';
final AppContext _defaultContext = new AppContext();
typedef void ErrorHandler(dynamic error);
typedef void ErrorHandler(dynamic error, StackTrace stackTrace);
/// A singleton for application functionality. This singleton can be different
/// on a per-Zone basis.
......@@ -17,6 +17,7 @@ AppContext get context {
class AppContext {
Map<Type, dynamic> _instances = <Type, dynamic>{};
Zone _zone;
bool isSet(Type type) {
if (_instances.containsKey(type))
......@@ -30,7 +31,7 @@ class AppContext {
if (_instances.containsKey(type))
return _instances[type];
AppContext parent = _calcParent(Zone.current);
AppContext parent = _calcParent(_zone ?? Zone.current);
return parent?.getVariable(type);
}
......@@ -58,11 +59,22 @@ class AppContext {
}
}
dynamic runInZone(dynamic method(), { ErrorHandler onError }) {
dynamic runInZone(dynamic method(), {
ZoneBinaryCallback<dynamic, dynamic, StackTrace> onError
}) {
return runZoned(
method,
() => _run(method),
zoneValues: <String, dynamic>{ 'context': this },
onError: onError
);
}
dynamic _run(dynamic method()) async {
try {
_zone = Zone.current;
return await method();
} finally {
_zone = null;
}
}
}
......@@ -214,7 +214,6 @@ class AnsiTerminal {
String writeBold(String str) => supportsColor ? '$_bold$str$_reset' : str;
set singleCharMode(bool value) {
stdin.echoMode = !value;
stdin.lineMode = !value;
}
......
......@@ -78,6 +78,13 @@ String getSizeAsMB(int bytesLength) {
return '${(bytesLength / (1024 * 1024)).toStringAsFixed(1)}MB';
}
/// 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;
}
/// A class to maintain a list of items, fire events when items are added or
/// removed, and calculate a diff of changes when a new list of items is
/// available.
......
......@@ -22,6 +22,16 @@ enum BuildMode {
String getModeName(BuildMode mode) => getEnumName(mode);
BuildMode getBuildModeForName(String mode) {
if (mode == 'debug')
return BuildMode.debug;
if (mode == 'profile')
return BuildMode.profile;
if (mode == 'release')
return BuildMode.release;
return null;
}
// Returns true if the selected build mode uses ahead-of-time compilation.
bool isAotBuildMode(BuildMode mode) {
return mode == BuildMode.profile || mode == BuildMode.release;
......
......@@ -13,8 +13,8 @@ import '../base/utils.dart';
import '../build_info.dart';
import '../dart/sdk.dart';
import '../globals.dart';
import '../run.dart';
import '../runner/flutter_command.dart';
import 'run.dart';
const String _kDefaultAotOutputDir = 'build/aot';
......
......@@ -10,17 +10,17 @@ import 'package:path/path.dart' as path;
import '../android/android_sdk.dart';
import '../base/file_system.dart' show ensureDirectoryExists;
import '../base/os.dart';
import '../base/logger.dart';
import '../base/os.dart';
import '../base/process.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../flx.dart' as flx;
import '../globals.dart';
import '../run.dart';
import '../runner/flutter_command.dart';
import '../services.dart';
import 'build_aot.dart';
import 'run.dart';
export '../android/android_device.dart' show AndroidDevice;
......
......@@ -20,6 +20,7 @@ import '../dart/sdk.dart';
import '../device.dart';
import '../globals.dart';
import '../ios/simulators.dart' show SimControl, IOSSimulatorUtils;
import '../run.dart';
import 'build_apk.dart' as build_apk;
import 'run.dart';
......
......@@ -11,8 +11,8 @@ import '../base/process.dart';
import '../build_info.dart';
import '../flx.dart' as flx;
import '../globals.dart';
import '../run.dart';
import '../runner/flutter_command.dart';
import 'run.dart';
const String _kDefaultBundlePath = 'build/app.flx';
......
......@@ -5,6 +5,7 @@
import 'dart:async';
import '../application_package.dart';
import '../build_info.dart';
import '../device.dart';
import '../globals.dart';
import '../runner/flutter_command.dart';
......@@ -23,6 +24,11 @@ class StopCommand extends FlutterCommand {
Future<int> runInProject() async {
Device device = deviceForCommand;
ApplicationPackage app = applicationPackages.getPackageForPlatform(device.platform);
if (app == null) {
String platformName = getNameForTargetPlatform(device.platform);
printError('No Flutter application for $platformName found in the current directory.');
return 1;
}
printStatus('Stopping apps on ${device.name}.');
return await device.stopApp(app) ? 0 : 1;
}
......
......@@ -8,6 +8,7 @@ import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:test/src/executable.dart' as executable; // ignore: implementation_imports
import '../dart/package_map.dart';
import '../globals.dart';
import '../runner/flutter_command.dart';
import '../test/flutter_platform.dart' as loader;
......@@ -58,6 +59,7 @@ class TestCommand extends FlutterCommand {
try {
if (testDirectory != null) {
printTrace('switching to directory $testDirectory to run tests');
PackageMap.globalPackagesPath = path.normalize(path.absolute(PackageMap.globalPackagesPath));
Directory.current = testDirectory;
}
printTrace('running test package with arguments: $testArgs');
......
......@@ -147,3 +147,52 @@ class Tracing {
return timeline.response;
}
}
/// Download the startup trace information from the given observatory client and
/// store it to build/start_up_info.json.
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}.');
}
......@@ -17,6 +17,14 @@ Map<String, Uri> _parse(String packagesPath) {
class PackageMap {
PackageMap(this.packagesPath);
static String get globalPackagesPath => _globalPackagesPath ?? kPackagesFileName;
static set globalPackagesPath(String value) {
_globalPackagesPath = value;
}
static String _globalPackagesPath;
final String packagesPath;
Map<String, Uri> get map {
......@@ -26,8 +34,6 @@ class PackageMap {
}
Map<String, Uri> _map;
static PackageMap instance;
String checkValid() {
if (FileSystemEntity.isFileSync(packagesPath))
return null;
......
......@@ -321,7 +321,7 @@ abstract class DevicePortForwarder {
/// Forward [hostPort] on the host to [devicePort] on the device.
/// If [hostPort] is null, will auto select a host port.
/// Returns a Future that completes with the host port.
Future<int> forward(int devicePort, { int hostPort: null });
Future<int> forward(int devicePort, { int hostPort });
/// Stops forwarding [forwardedPort].
Future<Null> unforward(ForwardedPort forwardedPort);
......
......@@ -46,7 +46,7 @@ Future<int> createSnapshot({
final List<String> args = <String>[
tools.getHostToolPath(HostTool.SkySnapshot),
mainPath,
'--packages=${PackageMap.instance.packagesPath}',
'--packages=${path.absolute(PackageMap.globalPackagesPath)}',
'--snapshot=$snapshotPath'
];
if (depfilePath != null)
......
// 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';
import 'dart:io';
import 'package:path/path.dart' as path;
import 'application_package.dart';
import 'base/logger.dart';
import 'base/utils.dart';
import 'build_info.dart';
import 'commands/build_apk.dart';
import 'commands/install.dart';
import 'commands/trace.dart';
import 'device.dart';
import 'globals.dart';
import 'observatory.dart';
/// 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;
}
// TODO: split out the cli part of the UI from this class
class RunAndStayResident {
RunAndStayResident(
this.device, {
this.target,
this.debuggingOptions,
this.usesTerminalUI: true
});
final Device device;
final String target;
final DebuggingOptions debuggingOptions;
final bool usesTerminalUI;
ApplicationPackage _package;
String _mainPath;
LaunchResult _result;
Completer<int> _exitCompleter = new Completer<int>();
StreamSubscription<String> _loggingSubscription;
Observatory observatory;
/// Start the app and keep the process running during its lifetime.
Future<int> run({
bool traceStartup: false,
bool benchmark: false,
Completer<int> observatoryPortCompleter
}) {
// Don't let uncaught errors kill the process.
return runZoned(() {
return _run(
traceStartup: traceStartup,
benchmark: benchmark,
observatoryPortCompleter: observatoryPortCompleter
);
}, onError: (dynamic error, StackTrace stackTrace) {
printError('Exception from flutter run: $error', stackTrace);
});
}
Future<bool> restart() async {
if (observatory == null) {
printError('Debugging is not enabled.');
return false;
} else {
Status status = logger.startProgress('Re-starting application...');
Future<Event> extensionAddedEvent = observatory.onExtensionEvent
.where((Event event) => event.extensionKind == 'Flutter.FrameworkInitialization')
.first;
bool restartResult = await device.restartApp(
_package,
_result,
mainPath: _mainPath,
observatory: observatory
);
status.stop(showElapsedTime: true);
if (restartResult) {
// TODO(devoncarew): We should restore the route here.
await extensionAddedEvent;
}
return restartResult;
}
}
Future<Null> stop() {
_stopLogger();
return _stopApp();
}
Future<int> _run({
bool traceStartup: false,
bool benchmark: false,
Completer<int> observatoryPortCompleter
}) async {
_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;
}
_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 startTime = new Stopwatch()..start();
// 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: debuggingOptions.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 (!(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);
});
_result = await device.startApp(
_package,
debuggingOptions.buildMode,
mainPath: _mainPath,
debuggingOptions: debuggingOptions,
platformArgs: platformArgs
);
if (!_result.started) {
printError('Error running application on ${device.name}.');
await _loggingSubscription.cancel();
return 2;
}
startTime.stop();
if (observatoryPortCompleter != null && _result.hasObservatory)
observatoryPortCompleter.complete(_result.observatoryPort);
// Connect to observatory.
if (debuggingOptions.debuggingEnabled) {
observatory = await Observatory.connect(_result.observatoryPort);
printTrace('Connected to observatory port: ${_result.observatoryPort}.');
observatory.populateIsolateInfo();
observatory.onExtensionEvent.listen((Event event) {
printTrace(event.toString());
});
observatory.onIsolateEvent.listen((Event event) {
printTrace(event.toString());
});
if (benchmark)
await observatory.waitFirstIsolate;
// Listen for observatory connection close.
observatory.done.whenComplete(() {
if (!_exitCompleter.isCompleted) {
printStatus('Application finished.');
_exitCompleter.complete(0);
}
});
}
printStatus('Application running.');
if (observatory != null && traceStartup) {
printStatus('Downloading startup trace info...');
await downloadStartupTrace(observatory);
if (!_exitCompleter.isCompleted)
_exitCompleter.complete(0);
} else {
if (usesTerminalUI) {
if (!logger.quiet)
_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, restart
restart();
} else if (lower == 'q' || code == AnsiTerminal.KEY_F10) {
// F10, exit
_stopApp();
}
});
}
ProcessSignal.SIGINT.watch().listen((ProcessSignal signal) {
_resetTerminal();
_stopLogger();
_stopApp();
});
ProcessSignal.SIGTERM.watch().listen((ProcessSignal signal) {
_resetTerminal();
_stopLogger();
_stopApp();
});
}
if (benchmark) {
await new Future<Null>.delayed(new Duration(seconds: 4));
// Touch the file.
File mainFile = new File(_mainPath);
mainFile.writeAsBytesSync(mainFile.readAsBytesSync());
Stopwatch restartTime = new Stopwatch()..start();
bool restarted = await restart();
restartTime.stop();
writeRunBenchmarkFile(startTime, restarted ? restartTime : null);
await new Future<Null>.delayed(new Duration(seconds: 2));
stop();
}
return _exitCompleter.future.then((int exitCode) async {
_resetTerminal();
_stopLogger();
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 _stopLogger() {
_loggingSubscription?.cancel();
}
void _resetTerminal() {
if (usesTerminalUI)
terminal.singleCharMode = false;
}
Future<Null> _stopApp() {
if (observatory != null && !observatory.isClosed) {
if (observatory.isolates.isNotEmpty) {
observatory.flutterExit(observatory.firstIsolateId);
return new Future<Null>.delayed(new Duration(milliseconds: 100));
}
}
if (!_exitCompleter.isCompleted)
_exitCompleter.complete(0);
return new Future<Null>.value();
}
}
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;
}
}
void writeRunBenchmarkFile(Stopwatch startTime, [Stopwatch restartTime]) {
final String benchmarkOut = 'refresh_benchmark.json';
Map<String, dynamic> data = <String, dynamic>{
'start': startTime.elapsedMilliseconds,
'time': (restartTime ?? startTime).elapsedMilliseconds // time and restart are the same
};
if (restartTime != null)
data['restart'] = restartTime.elapsedMilliseconds;
new File(benchmarkOut).writeAsStringSync(toPrettyJson(data));
printStatus('Run benchmark written to $benchmarkOut ($data).');
}
......@@ -193,7 +193,7 @@ abstract class FlutterCommand extends Command {
// Validate the current package map only if we will not be running "pub get" later.
if (!(_usesPubOption && argResults['pub'])) {
String error = PackageMap.instance.checkValid();
String error = new PackageMap(PackageMap.globalPackagesPath).checkValid();
if (error != null) {
printError(error);
return false;
......
......@@ -148,9 +148,8 @@ class FlutterCommandRunner extends CommandRunner {
if (!_checkFlutterCopy())
return new Future<int>.value(1);
PackageMap.instance = new PackageMap(path.normalize(path.absolute(
globalResults.wasParsed('packages') ? globalResults['packages'] : kPackagesFileName
)));
if (globalResults.wasParsed('packages'))
PackageMap.globalPackagesPath = path.normalize(path.absolute(globalResults['packages']));
// See if the user specified a specific device.
deviceManager.specifiedDeviceId = globalResults['device-id'];
......@@ -191,7 +190,7 @@ class FlutterCommandRunner extends CommandRunner {
if (engineSourcePath == null && globalResults['local-engine'] != null) {
try {
Uri engineUri = PackageMap.instance.map[kFlutterEnginePackageName];
Uri engineUri = new PackageMap(PackageMap.globalPackagesPath).map[kFlutterEnginePackageName];
engineSourcePath = path.dirname(path.dirname(path.dirname(path.dirname(engineUri.path))));
bool dirExists = FileSystemEntity.isDirectorySync(path.join(engineSourcePath, 'out'));
if (engineSourcePath == '/' || engineSourcePath.isEmpty || !dirExists)
......
......@@ -30,7 +30,7 @@ Future<Null> parseServiceConfigs(
) async {
Map<String, Uri> packageMap;
try {
packageMap = PackageMap.instance.map;
packageMap = new PackageMap(PackageMap.globalPackagesPath).map;
} on FormatException catch(e) {
printTrace('Invalid ".packages" file while parsing service configs:\n$e');
return;
......
......@@ -15,6 +15,7 @@ import 'package:test/src/runner/plugin/platform.dart'; // ignore: implementation
import 'package:test/src/runner/plugin/hack_register_platform.dart' as hack; // ignore: implementation_imports
import '../dart/package_map.dart';
import '../globals.dart';
final String _kSkyShell = Platform.environment['SKY_SHELL'];
const String _kHost = '127.0.0.1';
......@@ -46,12 +47,15 @@ Future<_ServerInfo> _startServer() async {
Future<Process> _startProcess(String mainPath, { String packages }) {
assert(shellPath != null || _kSkyShell != null); // Please provide the path to the shell in the SKY_SHELL environment variable.
return Process.start(shellPath ?? _kSkyShell, <String>[
String executable = shellPath ?? _kSkyShell;
List<String> arguments = <String>[
'--enable-checked-mode',
'--non-interactive',
'--packages=$packages',
mainPath,
], environment: <String, String>{ 'FLUTTER_TEST': 'true' });
mainPath
];
printTrace('$executable ${arguments.join(' ')}');
return Process.start(executable, arguments, environment: <String, String>{ 'FLUTTER_TEST': 'true' });
}
void _attachStandardStreams(Process process) {
......@@ -102,7 +106,7 @@ void main() {
''');
Process process = await _startProcess(
listenerFile.path, packages: PackageMap.instance.packagesPath
listenerFile.path, packages: PackageMap.globalPackagesPath
);
_attachStandardStreams(process);
......
......@@ -14,7 +14,7 @@ void main() {
BufferLogger mockLogger = new BufferLogger();
context[Logger] = mockLogger;
context.runInZone(() {
await context.runInZone(() {
printError('foo bar');
});
......@@ -28,7 +28,7 @@ void main() {
BufferLogger mockLogger = new BufferLogger();
context[Logger] = mockLogger;
context.runInZone(() {
await context.runInZone(() {
printStatus('foo bar');
});
......@@ -42,7 +42,7 @@ void main() {
BufferLogger mockLogger = new BufferLogger();
context[Logger] = mockLogger;
context.runInZone(() {
await context.runInZone(() {
printTrace('foo bar');
});
......
......@@ -94,6 +94,44 @@ void main() {
});
});
_testUsingContext('daemon.start', () async {
DaemonCommand command = new DaemonCommand();
applyMocksToCommand(command);
StreamController<Map<String, dynamic>> commands = new StreamController<Map<String, dynamic>>();
StreamController<Map<String, dynamic>> responses = new StreamController<Map<String, dynamic>>();
daemon = new Daemon(
commands.stream,
(Map<String, dynamic> result) => responses.add(result),
daemonCommand: command,
notifyingLogger: notifyingLogger
);
commands.add(<String, dynamic>{ 'id': 0, 'method': 'app.start' });
Map<String, dynamic> response = await responses.stream.where(_notEvent).first;
expect(response['id'], 0);
expect(response['error'], contains('deviceId is required'));
});
_testUsingContext('daemon.restart', () async {
DaemonCommand command = new DaemonCommand();
applyMocksToCommand(command);
StreamController<Map<String, dynamic>> commands = new StreamController<Map<String, dynamic>>();
StreamController<Map<String, dynamic>> responses = new StreamController<Map<String, dynamic>>();
daemon = new Daemon(
commands.stream,
(Map<String, dynamic> result) => responses.add(result),
daemonCommand: command,
notifyingLogger: notifyingLogger
);
commands.add(<String, dynamic>{ 'id': 0, 'method': 'app.restart' });
Map<String, dynamic> response = await responses.stream.where(_notEvent).first;
expect(response['id'], 0);
expect(response['error'], contains('appId is required'));
});
_testUsingContext('daemon.stop', () async {
DaemonCommand command = new DaemonCommand();
applyMocksToCommand(command);
......@@ -110,7 +148,7 @@ void main() {
commands.add(<String, dynamic>{ 'id': 0, 'method': 'app.stop' });
Map<String, dynamic> response = await responses.stream.where(_notEvent).first;
expect(response['id'], 0);
expect(response['error'], contains('deviceId is required'));
expect(response['error'], contains('appId is required'));
});
_testUsingContext('device.getDevices', () async {
......
......@@ -12,7 +12,7 @@ Process daemon;
// version: print version
// shutdown: terminate the server
// start: start an app
// stopAll: stop any running app
// stop: stop a running app
// devices: list devices
Future<Null> main() async {
......@@ -27,18 +27,44 @@ Future<Null> main() async {
stdout.write('> ');
stdin.transform(UTF8.decoder).transform(const LineSplitter()).listen((String line) {
List<String> words = line.split(' ');
if (line == 'version' || line == 'v') {
_send(<String, dynamic>{'method': 'daemon.version'});
} else if (line == 'shutdown' || line == 'q') {
_send(<String, dynamic>{'method': 'daemon.shutdown'});
} else if (line == 'start') {
_send(<String, dynamic>{'method': 'app.start'});
} else if (line == 'stopAll') {
_send(<String, dynamic>{'method': 'app.stopAll'});
} else if (words.first == 'start') {
_send(<String, dynamic>{
'method': 'app.start',
'params': <String, dynamic> {
'deviceId': words[1],
'projectDirectory': words[2]
}
});
} else if (words.first == 'stop') {
if (words.length > 1) {
_send(<String, dynamic>{
'method': 'app.stop',
'params': <String, dynamic> { 'appId': words[1] }
});
} else {
_send(<String, dynamic>{'method': 'app.stop'});
}
} else if (words.first == 'restart') {
if (words.length > 1) {
_send(<String, dynamic>{
'method': 'app.restart',
'params': <String, dynamic> { 'appId': words[1] }
});
} else {
_send(<String, dynamic>{'method': 'app.restart'});
}
} else if (line == 'devices') {
_send(<String, dynamic>{'method': 'device.getDevices'});
} else if (line == 'enable') {
_send(<String, dynamic>{'method': 'device.enable'});
} else {
print('command not understood: $line');
_send(<String, dynamic>{'method': line.trim()});
}
stdout.write('> ');
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment