diff --git a/packages/flutter_tools/lib/src/base/logger.dart b/packages/flutter_tools/lib/src/base/logger.dart index c066dcb1e1..5b2ee21f7a 100644 --- a/packages/flutter_tools/lib/src/base/logger.dart +++ b/packages/flutter_tools/lib/src/base/logger.dart @@ -3,15 +3,16 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert' show ASCII; import 'dart:io'; -final _AnsiTerminal _terminal = new _AnsiTerminal(); +final AnsiTerminal terminal = new AnsiTerminal(); abstract class Logger { bool get isVerbose => false; set supportsColor(bool value) { - _terminal.supportsColor = value; + terminal.supportsColor = value; } /// Display an error level message to the user. Commands should use this if they @@ -59,7 +60,7 @@ class StdoutLogger extends Logger { _status?.cancel(); _status = null; - print(emphasis ? _terminal.writeBold(message) : message); + print(emphasis ? terminal.writeBold(message) : message); } @override @@ -70,7 +71,7 @@ class StdoutLogger extends Logger { _status?.cancel(); _status = null; - if (_terminal.supportsColor) { + if (terminal.supportsColor) { _status = new _AnsiStatus(message); return _status; } else { @@ -177,34 +178,48 @@ class _LogMessage { String prefix = '${millis.toString().padLeft(4)} ms • '; String indent = ''.padLeft(prefix.length); if (millis >= 100) - prefix = _terminal.writeBold(prefix.substring(0, prefix.length - 3)) + ' • '; + prefix = terminal.writeBold(prefix.substring(0, prefix.length - 3)) + ' • '; String indentMessage = message.replaceAll('\n', '\n$indent'); if (type == _LogType.error) { - stderr.writeln(prefix + _terminal.writeBold(indentMessage)); + stderr.writeln(prefix + terminal.writeBold(indentMessage)); if (stackTrace != null) stderr.writeln(indent + stackTrace.toString().replaceAll('\n', '\n$indent')); } else if (type == _LogType.status) { - print(prefix + _terminal.writeBold(indentMessage)); + print(prefix + terminal.writeBold(indentMessage)); } else { print(prefix + indentMessage); } } } -class _AnsiTerminal { - _AnsiTerminal() { +class AnsiTerminal { + AnsiTerminal() { // TODO(devoncarew): This detection does not work for Windows. String term = Platform.environment['TERM']; supportsColor = term != null && term != 'dumb'; } - static const String _bold = '\u001B[1m'; + static const String KEY_F1 = '\u001BOP'; + static const String KEY_F5 = '\u001B[15~'; + static const String KEY_F10 = '\u001B[21~'; + + static const String _bold = '\u001B[1m'; static const String _reset = '\u001B[0m'; bool supportsColor; String writeBold(String str) => supportsColor ? '$_bold$str$_reset' : str; + + set singleCharMode(bool value) { + stdin.echoMode = !value; + stdin.lineMode = !value; + } + + /// Return keystrokes from the console. + /// + /// Useful when the console is in [singleCharMode]. + Stream get onCharInput => stdin.transform(ASCII.decoder); } class _AnsiStatus extends Status { diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 24a7236786..de6773416d 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -10,6 +10,7 @@ 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'; @@ -98,7 +99,6 @@ class RunCommand extends RunCommandBase { } } - int result; DebuggingOptions options; if (getBuildMode() != BuildMode.debug) { @@ -112,7 +112,7 @@ class RunCommand extends RunCommandBase { } if (argResults['resident']) { - result = await startAppStayResident( + _RunAndStayResident runner = new _RunAndStayResident( deviceForCommand, toolchain, target: target, @@ -120,8 +120,10 @@ class RunCommand extends RunCommandBase { traceStartup: traceStartup, buildMode: getBuildMode() ); + + return runner.run(); } else { - result = await startApp( + return startApp( deviceForCommand, toolchain, target: target, @@ -133,8 +135,6 @@ class RunCommand extends RunCommandBase { buildMode: getBuildMode() ); } - - return result; } } @@ -231,141 +231,6 @@ Future startApp( return result.started ? 0 : 2; } -// start logging -// start the app -// scrape obs. port -// connect via obs. -// stay alive as long as obs. is alive -// intercept SIG_QUIT; kill the launched app - -Future startAppStayResident( - Device device, - Toolchain toolchain, { - String target, - DebuggingOptions debuggingOptions, - bool traceStartup: false, - 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 (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.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 platformArgs; - if (traceStartup != null) - platformArgs = { 'trace-startup': traceStartup }; - - printStatus('Running ${_getDisplayPath(mainPath)} on ${device.name}...'); - - StreamSubscription 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; - } - - Completer exitCompleter = new Completer(); - - void complete(int exitCode) { - if (!exitCompleter.isCompleted) - exitCompleter.complete(0); - }; - - // Connect to observatory. - WebSocket observatoryConnection; - - 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) { - // Ignore observatory messages. - }, onDone: () { - loggingSubscription.cancel(); - printStatus('Application finished.'); - complete(0); - }); - } - - printStatus('Application running.'); - - // When terminating, close down the log reader. - ProcessSignal.SIGINT.watch().listen((ProcessSignal signal) { - loggingSubscription.cancel(); - printStatus(''); - complete(0); - }); - ProcessSignal.SIGTERM.watch().listen((ProcessSignal signal) { - loggingSubscription.cancel(); - complete(0); - }); - - return exitCompleter.future; -} - /// 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]) { @@ -477,3 +342,227 @@ 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 _exitCompleter; + StreamSubscription _loggingSubscription; + + WebSocket _observatoryConnection; + String _isolateId; + int _messageId = 0; + + /// Start the app and keep the process running during its lifetime. + Future 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.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 platformArgs; + if (traceStartup != null) + platformArgs = { '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(); + + // 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 json = JSON.decode(data); + + if (json['method'] == 'streamNotify') { + Map 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 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({ + 'method': 'streamListen', + 'params': { 'streamId': 'Isolate' }, + 'id': _messageId++ + })); + + _observatoryConnection.add(JSON.encode({ + '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({ + 'method': 'ext.flutter.exit', + 'params': { 'isolateId': _isolateId }, + 'id': _messageId++ + })); + // WebSockets do not have a flush() method. + await new Future.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({ + 'method': 'isolateReload', + 'params': { 'isolateId': _isolateId }, + 'id': _messageId++ + })); + } + } + + void _handleExit() { + if (!_exitCompleter.isCompleted) { + _loggingSubscription?.cancel(); + printStatus(''); + printStatus('Application finished.'); + terminal.singleCharMode = false; + _exitCompleter.complete(0); + } + } +} diff --git a/packages/flutter_tools/lib/src/commands/trace.dart b/packages/flutter_tools/lib/src/commands/trace.dart index 4cbe38fc6c..6e4e437fd1 100644 --- a/packages/flutter_tools/lib/src/commands/trace.dart +++ b/packages/flutter_tools/lib/src/commands/trace.dart @@ -3,8 +3,8 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:io'; import 'dart:convert'; +import 'dart:io'; import '../base/common.dart'; import '../base/utils.dart'; diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 9dbebed3ee..44bdd7fa16 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -12,8 +12,8 @@ import 'package:web_socket_channel/io.dart'; import 'android/android_device.dart'; import 'application_package.dart'; import 'base/common.dart'; -import 'base/utils.dart'; import 'base/os.dart'; +import 'base/utils.dart'; import 'build_configuration.dart'; import 'globals.dart'; import 'ios/devices.dart';