Commit b0dca796 authored by Devon Carew's avatar Devon Carew

Flutter run (#3553)

* rework flutter run

* fix npe with --debug-port

* connect to obs and exit when that conneciton closes

* update todos
parent cbe650a7
......@@ -12,7 +12,6 @@ import 'package:web_socket_channel/io.dart';
import '../android/android_sdk.dart';
import '../application_package.dart';
import '../base/common.dart';
import '../base/os.dart';
import '../base/process.dart';
import '../base/utils.dart';
......@@ -189,49 +188,38 @@ class AndroidDevice extends Device {
}
Future<Null> _forwardPort(String service, int devicePort, int port) async {
bool portWasZero = (port == null) || (port == 0);
try {
// Set up port forwarding for observatory.
port = await portForwarder.forward(devicePort,
hostPort: port);
if (portWasZero)
printStatus('$service listening on http://127.0.0.1:$port');
port = await portForwarder.forward(devicePort, hostPort: port);
printStatus('$service listening on http://127.0.0.1:$port');
} catch (e) {
printError('Unable to forward port $port: $e');
}
}
Future<bool> startBundle(AndroidApk apk, String bundlePath, {
bool checked: true,
Future<LaunchResult> startBundle(AndroidApk apk, String bundlePath, {
bool traceStartup: false,
String route,
bool clearLogs: false,
bool startPaused: false,
int observatoryPort: observatoryDefaultPort,
int diagnosticPort: diagnosticDefaultPort
DebuggingOptions options
}) async {
printTrace('$this startBundle');
if (!FileSystemEntity.isFileSync(bundlePath)) {
printError('Cannot find $bundlePath');
return false;
return new LaunchResult.failed();
}
if (clearLogs)
this.clearLogs();
runCheckedSync(adbCommandForDevice(<String>['push', bundlePath, _deviceBundlePath]));
ServiceProtocolDiscovery observatoryDiscovery =
new ServiceProtocolDiscovery(logReader, ServiceProtocolDiscovery.kObservatoryService);
ServiceProtocolDiscovery diagnosticDiscovery =
new ServiceProtocolDiscovery(logReader, ServiceProtocolDiscovery.kDiagnosticService);
ServiceProtocolDiscovery observatoryDiscovery;
ServiceProtocolDiscovery diagnosticDiscovery;
// We take this future here but do not wait for completion until *after* we
// start the bundle.
Future<List<int>> scrapeServicePorts = Future.wait(
<Future<int>>[observatoryDiscovery.nextPort(), diagnosticDiscovery.nextPort()]);
if (options.debuggingEnabled) {
observatoryDiscovery = new ServiceProtocolDiscovery(
logReader, ServiceProtocolDiscovery.kObservatoryService);
diagnosticDiscovery = new ServiceProtocolDiscovery(
logReader, ServiceProtocolDiscovery.kDiagnosticService);
}
List<String> cmd = adbCommandForDevice(<String>[
'shell', 'am', 'start',
......@@ -240,61 +228,76 @@ class AndroidDevice extends Device {
'-f', '0x20000000', // FLAG_ACTIVITY_SINGLE_TOP
'--ez', 'enable-background-compilation', 'true',
]);
if (checked)
cmd.addAll(<String>['--ez', 'enable-checked-mode', 'true']);
if (traceStartup)
cmd.addAll(<String>['--ez', 'trace-startup', 'true']);
if (startPaused)
cmd.addAll(<String>['--ez', 'start-paused', 'true']);
if (route != null)
cmd.addAll(<String>['--es', 'route', route]);
if (options.debuggingEnabled) {
if (options.checked)
cmd.addAll(<String>['--ez', 'enable-checked-mode', 'true']);
if (options.startPaused)
cmd.addAll(<String>['--ez', 'start-paused', 'true']);
}
cmd.add(apk.launchActivity);
String result = runCheckedSync(cmd);
// This invocation returns 0 even when it fails.
if (result.contains('Error: ')) {
printError(result.trim());
return false;
return new LaunchResult.failed();
}
// Wait for the service protocol port here. This will complete once the
// device has printed "Observatory is listening on...".
printTrace('Waiting for observatory port to be available...');
try {
List<int> devicePorts = await scrapeServicePorts.timeout(new Duration(seconds: 12));
int observatoryDevicePort = devicePorts[0];
int diagnosticDevicePort = devicePorts[1];
printTrace('observatory port = $observatoryDevicePort');
await _forwardPort(ServiceProtocolDiscovery.kObservatoryService,
observatoryDevicePort, observatoryPort);
await _forwardPort(ServiceProtocolDiscovery.kDiagnosticService,
diagnosticDevicePort, diagnosticPort);
return true;
} catch (error) {
if (error is TimeoutException)
printError('Timed out while waiting for a debug connection.');
else
printError('Error waiting for a debug connection: $error');
if (!options.debuggingEnabled) {
return new LaunchResult.succeeded();
} else {
// Wait for the service protocol port here. This will complete once the
// device has printed "Observatory is listening on...".
printTrace('Waiting for observatory port to be available...');
return false;
try {
Future<List<int>> scrapeServicePorts = Future.wait(
<Future<int>>[observatoryDiscovery.nextPort(), diagnosticDiscovery.nextPort()]
);
List<int> devicePorts = await scrapeServicePorts.timeout(new Duration(seconds: 20));
int observatoryDevicePort = devicePorts[0];
printTrace('observatory port = $observatoryDevicePort');
int observatoryLocalPort = await options.findBestObservatoryPort();
// TODO(devoncarew): Remember the forwarding information (so we can later remove the
// port forwarding).
await _forwardPort(ServiceProtocolDiscovery.kObservatoryService,
observatoryDevicePort, observatoryLocalPort);
int diagnosticDevicePort = devicePorts[1];
printTrace('diagnostic port = $diagnosticDevicePort');
int diagnosticLocalPort = await options.findBestDiagnosticPort();
await _forwardPort(ServiceProtocolDiscovery.kDiagnosticService,
diagnosticDevicePort, diagnosticLocalPort);
return new LaunchResult.succeeded(
observatoryPort: observatoryLocalPort,
diagnosticPort: diagnosticLocalPort
);
} catch (error) {
if (error is TimeoutException)
printError('Timed out while waiting for a debug connection.');
else
printError('Error waiting for a debug connection: $error');
return new LaunchResult.failed();
} finally {
observatoryDiscovery.cancel();
diagnosticDiscovery.cancel();
}
}
}
@override
Future<bool> startApp(
Future<LaunchResult> startApp(
ApplicationPackage package,
Toolchain toolchain, {
String mainPath,
String route,
bool checked: true,
bool clearLogs: false,
bool startPaused: false,
int observatoryPort: observatoryDefaultPort,
int diagnosticPort: diagnosticDefaultPort,
DebuggingOptions debuggingOptions,
Map<String, dynamic> platformArgs
}) async {
if (!_checkForSupportedAdbVersion() || !_checkForSupportedAndroidVersion())
return false;
return new LaunchResult.failed();
String localBundlePath = await flx.buildFlx(
toolchain,
......@@ -304,21 +307,13 @@ class AndroidDevice extends Device {
printTrace('Starting bundle for $this.');
if (await startBundle(
return startBundle(
package,
localBundlePath,
checked: checked,
traceStartup: platformArgs['trace-startup'],
route: route,
clearLogs: clearLogs,
startPaused: startPaused,
observatoryPort: observatoryPort,
diagnosticPort: diagnosticPort
)) {
return true;
} else {
return false;
}
options: debuggingOptions
);
}
@override
......@@ -468,7 +463,8 @@ List<AndroidDevice> getAdbDevices() {
// 0149947A0D01500C device usb:340787200X
// emulator-5612 host features:shell_2
RegExp deviceRegExShort = new RegExp(r'^(\S+)\s+(\S+)\s+\S+$');
// emulator-5554 offline
RegExp deviceRegExShort = new RegExp(r'^(\S+)\s+(\S+)(\s+\S+)?$');
for (String line in output) {
// Skip lines like: * daemon started successfully *
......@@ -522,34 +518,25 @@ List<AndroidDevice> getAdbDevices() {
/// A log reader that logs from `adb logcat`.
class _AdbLogReader extends DeviceLogReader {
_AdbLogReader(this.device);
_AdbLogReader(this.device) {
_linesController = new StreamController<String>.broadcast(
onListen: _start,
onCancel: _stop
);
}
final AndroidDevice device;
final StreamController<String> _linesStreamController =
new StreamController<String>.broadcast();
StreamController<String> _linesController;
Process _process;
StreamSubscription<String> _stdoutSubscription;
StreamSubscription<String> _stderrSubscription;
@override
Stream<String> get lines => _linesStreamController.stream;
Stream<String> get logLines => _linesController.stream;
@override
String get name => device.name;
@override
bool get isReading => _process != null;
@override
Future<int> get finished => _process != null ? _process.exitCode : new Future<int>.value(0);
@override
Future<Null> start() async {
if (_process != null)
throw new StateError('_AdbLogReader must be stopped before it can be started.');
void _start() {
// Start the adb logcat process.
List<String> args = <String>['logcat', '-v', 'tag'];
String lastTimestamp = device.lastLogcatTimestamp;
......@@ -558,55 +545,29 @@ class _AdbLogReader extends DeviceLogReader {
args.addAll(<String>[
'-s', 'flutter:V', 'FlutterMain:V', 'FlutterView:V', 'AndroidRuntime:W', 'ActivityManager:W', 'System.err:W', '*:F'
]);
_process = await runCommand(device.adbCommandForDevice(args));
_stdoutSubscription =
_process.stdout.transform(UTF8.decoder)
.transform(const LineSplitter()).listen(_onLine);
_stderrSubscription =
_process.stderr.transform(UTF8.decoder)
.transform(const LineSplitter()).listen(_onLine);
_process.exitCode.then(_onExit);
}
runCommand(device.adbCommandForDevice(args)).then((Process process) {
_process = process;
_process.stdout.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine);
_process.stderr.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine);
@override
Future<Null> stop() async {
if (_process == null)
throw new StateError('_AdbLogReader must be started before it can be stopped.');
_stdoutSubscription?.cancel();
_stdoutSubscription = null;
_stderrSubscription?.cancel();
_stderrSubscription = null;
await _process.kill();
_process = null;
}
void _onExit(int exitCode) {
_stdoutSubscription?.cancel();
_stdoutSubscription = null;
_stderrSubscription?.cancel();
_stderrSubscription = null;
_process = null;
_process.exitCode.then((int code) {
if (_linesController.hasListener)
_linesController.close();
});
});
}
void _onLine(String line) {
// Filter out some noisy ActivityManager notifications.
if (line.startsWith('W/ActivityManager: getRunningAppProcesses'))
return;
_linesStreamController.add(line);
_linesController.add(line);
}
@override
int get hashCode => name.hashCode;
void _stop() {
// TODO(devoncarew): We should remove adb port forwarding here.
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other is! _AdbLogReader)
return false;
return other.device.id == device.id;
_process?.kill();
}
}
......
......@@ -2,5 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const int observatoryDefaultPort = 8181;
const int diagnosticDefaultPort = 8182;
const int defaultObservatoryPort = 8100;
const int defaultDiagnosticPort = 8101;
const int defaultDrivePort = 8183;
......@@ -109,3 +109,30 @@ Future<int> findAvailablePort() async {
await socket.close();
return port;
}
const int _kMaxSearchIterations = 5;
/// This method will attempt to return a port close to or the same as
/// [defaultPort]. Failing that, it will return any available port.
Future<int> findPreferredPort(int defaultPort, { int searchStep: 2 }) async {
int iterationCount = 0;
while (iterationCount < _kMaxSearchIterations) {
int port = defaultPort + iterationCount * searchStep;
if (await _isPortAvailable(port))
return port;
iterationCount++;
}
return findAvailablePort();
}
Future<bool> _isPortAvailable(int port) async {
try {
ServerSocket socket = await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, port);
await socket.close();
return true;
} catch (error) {
return false;
}
}
......@@ -267,8 +267,7 @@ class AppDomain extends Domain {
command.toolchain,
stop: true,
target: args['target'],
route: args['route'],
checked: args['checked'] ?? true
route: args['route']
);
if (result != 0)
......
......@@ -11,6 +11,7 @@ import 'package:test/src/executable.dart' as executable; // ignore: implementati
import '../android/android_device.dart' show AndroidDevice;
import '../application_package.dart';
import '../base/file_system.dart';
import '../base/common.dart';
import '../base/os.dart';
import '../device.dart';
import '../globals.dart';
......@@ -60,8 +61,9 @@ class DriveCommand extends RunCommandBase {
);
argParser.addOption('debug-port',
defaultsTo: '8183',
help: 'Listen to the given port for a debug connection.');
defaultsTo: defaultDrivePort.toString(),
help: 'Listen to the given port for a debug connection.'
);
}
@override
......@@ -261,25 +263,22 @@ Future<int> startApp(DriveCommand command) async {
await command.device.installApp(package);
printTrace('Starting application.');
bool started = await command.device.startApp(
LaunchResult result = await command.device.startApp(
package,
command.toolchain,
mainPath: mainPath,
route: command.route,
checked: command.checked,
clearLogs: true,
startPaused: true,
observatoryPort: command.debugPort,
debuggingOptions: new DebuggingOptions.enabled(
checked: command.checked,
startPaused: true,
observatoryPort: command.debugPort
),
platformArgs: <String, dynamic>{
'trace-startup': command.traceStartup,
}
);
if (started && command.device.supportsStartPaused) {
await delayUntilObservatoryAvailable('localhost', command.debugPort);
}
return started ? 0 : 2;
return result.started ? 0 : 2;
}
/// Runs driver tests.
......
......@@ -7,6 +7,7 @@ import 'dart:io';
import '../base/os.dart';
import '../base/process.dart';
import '../device.dart';
import '../globals.dart';
import 'run.dart';
......@@ -62,7 +63,7 @@ class ListenCommand extends RunCommandBase {
target: target,
install: firstTime,
stop: true,
checked: checked,
debuggingOptions: new DebuggingOptions.enabled(checked: checked),
traceStartup: traceStartup,
route: route
);
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import '../device.dart';
import '../globals.dart';
......@@ -40,17 +41,33 @@ class LogsCommand extends FlutterCommand {
printStatus('Showing $logReader logs:');
Completer<int> exitCompleter = new Completer<int>();
// Start reading.
if (!logReader.isReading)
await logReader.start();
StreamSubscription<String> subscription = logReader.logLines.listen(
printStatus,
onDone: () {
exitCompleter.complete(0);
},
onError: (dynamic error) {
exitCompleter.complete(error is int ? error : 1);
}
);
StreamSubscription<String> subscription = logReader.lines.listen(printStatus);
// When terminating, close down the log reader.
ProcessSignal.SIGINT.watch().listen((ProcessSignal signal) {
subscription.cancel();
printStatus('');
exitCompleter.complete(0);
});
ProcessSignal.SIGTERM.watch().listen((ProcessSignal signal) {
subscription.cancel();
exitCompleter.complete(0);
});
// Wait for the log reader to be finished.
int result = await logReader.finished;
int result = await exitCompleter.future;
subscription.cancel();
if (result != 0)
printError('Error listening to $logReader logs.');
return result;
......
......@@ -39,7 +39,8 @@ class RefreshCommand extends FlutterCommand {
String snapshotPath = path.join(tempDir.path, 'snapshot_blob.bin');
int result = await toolchain.compiler.createSnapshot(
mainPath: argResults['target'], snapshotPath: snapshotPath
mainPath: argResults['target'],
snapshotPath: snapshotPath
);
if (result != 0) {
printError('Failed to run the Flutter compiler. Exit code: $result');
......
......@@ -17,19 +17,6 @@ import '../toolchain.dart';
import 'build_apk.dart';
import 'install.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;
}
}
abstract class RunCommandBase extends FlutterCommand {
RunCommandBase() {
argParser.addFlag('checked',
......@@ -66,17 +53,21 @@ class RunCommand extends RunCommandBase {
argParser.addFlag('full-restart',
defaultsTo: true,
help: 'Stop any currently running application process before running the app.');
argParser.addFlag('clear-logs',
defaultsTo: true,
help: 'Clear log history 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',
defaultsTo: observatoryDefaultPort.toString(),
help: 'Listen to the given port for a debug connection.');
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
......@@ -95,45 +86,54 @@ class RunCommand extends RunCommandBase {
@override
Future<int> runInProject() async {
bool clearLogs = argResults['clear-logs'];
int debugPort;
try {
debugPort = int.parse(argResults['debug-port']);
} catch (error) {
printError('Invalid port for `--debug-port`: $error');
return 1;
if (argResults['debug-port'] != null) {
try {
debugPort = int.parse(argResults['debug-port']);
} catch (error) {
printError('Invalid port for `--debug-port`: $error');
return 1;
}
}
int result = await startApp(
deviceForCommand,
toolchain,
target: target,
enginePath: runner.enginePath,
install: true,
stop: argResults['full-restart'],
checked: checked,
traceStartup: traceStartup,
route: route,
clearLogs: clearLogs,
startPaused: argResults['start-paused'],
debugPort: debugPort,
buildMode: getBuildMode()
);
int result;
DebuggingOptions options;
if (getBuildMode() != BuildMode.debug) {
options = new DebuggingOptions.disabled();
} else {
options = new DebuggingOptions.enabled(
checked: checked,
startPaused: argResults['start-paused'],
observatoryPort: debugPort
);
}
return result;
}
}
if (argResults['resident']) {
result = await startAppStayResident(
deviceForCommand,
toolchain,
target: target,
debuggingOptions: options,
traceStartup: traceStartup,
buildMode: getBuildMode()
);
} else {
result = await startApp(
deviceForCommand,
toolchain,
target: target,
stop: argResults['full-restart'],
install: true,
debuggingOptions: options,
traceStartup: traceStartup,
route: route,
buildMode: getBuildMode()
);
}
String _getMissingPackageHintForPlatform(TargetPlatform platform) {
switch (platform) {
case TargetPlatform.android_arm:
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 result;
}
}
......@@ -141,15 +141,11 @@ Future<int> startApp(
Device device,
Toolchain toolchain, {
String target,
String enginePath,
bool stop: true,
bool install: true,
bool checked: true,
DebuggingOptions debuggingOptions,
bool traceStartup: false,
String route,
bool clearLogs: false,
bool startPaused: false,
int debugPort: observatoryDefaultPort,
BuildMode buildMode: BuildMode.debug
}) async {
String mainPath = findMainDartFile(target);
......@@ -216,22 +212,164 @@ Future<int> startApp(
printStatus('Running ${_getDisplayPath(mainPath)} on ${device.name}...');
bool result = await device.startApp(
LaunchResult result = await device.startApp(
package,
toolchain,
mainPath: mainPath,
route: route,
checked: checked,
clearLogs: clearLogs,
startPaused: startPaused,
observatoryPort: debugPort,
debuggingOptions: debuggingOptions,
platformArgs: platformArgs
);
if (!result.started)
printError('Error running application on ${device.name}.');
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<int> 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<Duration>.delayed(Duration.ZERO);
printTrace('Running install command.');
// TODO(devoncarew): This fails for ios devices - we haven't built yet.
await installApp(device, package);
Map<String, dynamic> platformArgs;
if (traceStartup != null)
platformArgs = <String, dynamic>{ 'trace-startup': traceStartup };
printStatus('Running ${_getDisplayPath(mainPath)} on ${device.name}...');
StreamSubscription<String> 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)
if (!result.started) {
printError('Error running application on ${device.name}.');
await loggingSubscription.cancel();
return 2;
}
return result ? 0 : 2;
Completer<int> exitCompleter = new Completer<int>();
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]) {
if (target == null)
target = '';
String targetPath = path.absolute(target);
if (FileSystemEntity.isDirectorySync(targetPath))
return path.join(targetPath, 'lib', 'main.dart');
else
return targetPath;
}
/// Delay until the Observatory / service protocol is available.
......@@ -243,10 +381,9 @@ Future<Null> delayUntilObservatoryAvailable(String host, int port, {
}) async {
printTrace('Waiting until Observatory is available (port $port).');
Stopwatch stopwatch = new Stopwatch()..start();
final String url = 'ws://$host:$port/ws';
printTrace('Looking for the observatory at $url.');
Stopwatch stopwatch = new Stopwatch()..start();
while (stopwatch.elapsed <= timeout) {
try {
......@@ -262,11 +399,21 @@ Future<Null> delayUntilObservatoryAvailable(String host, int port, {
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;
if (fullPath.startsWith(cwd))
return fullPath.substring(cwd.length);
return fullPath;
return fullPath.startsWith(cwd) ? fullPath.substring(cwd.length) : fullPath;
}
......@@ -16,7 +16,7 @@ class SkiaCommand extends FlutterCommand {
argParser.addOption('output-file', help: 'Write the Skia picture file to this path.');
argParser.addOption('skiaserve', help: 'Post the picture to a skiaserve debugger at this URL.');
argParser.addOption('diagnostic-port',
defaultsTo: diagnosticDefaultPort.toString(),
defaultsTo: defaultDiagnosticPort.toString(),
help: 'Local port where the diagnostic server is listening.');
}
......
......@@ -18,7 +18,7 @@ class TraceCommand extends FlutterCommand {
argParser.addOption('duration',
defaultsTo: '10', abbr: 'd', help: 'Duration in seconds to trace.');
argParser.addOption('debug-port',
defaultsTo: observatoryDefaultPort.toString(),
defaultsTo: defaultObservatoryPort.toString(),
help: 'Local port where the observatory is listening.');
}
......
......@@ -10,6 +10,7 @@ import 'android/android_device.dart';
import 'application_package.dart';
import 'base/common.dart';
import 'base/utils.dart';
import 'base/os.dart';
import 'build_configuration.dart';
import 'globals.dart';
import 'ios/devices.dart';
......@@ -176,16 +177,12 @@ abstract class Device {
///
/// [platformArgs] allows callers to pass platform-specific arguments to the
/// start call.
Future<bool> startApp(
Future<LaunchResult> startApp(
ApplicationPackage package,
Toolchain toolchain, {
String mainPath,
String route,
bool checked: true,
bool clearLogs: false,
bool startPaused: false,
int observatoryPort: observatoryDefaultPort,
int diagnosticPort: diagnosticDefaultPort,
DebuggingOptions debuggingOptions,
Map<String, dynamic> platformArgs
});
......@@ -229,6 +226,68 @@ abstract class Device {
}
}
class DebuggingOptions {
DebuggingOptions.enabled({
this.checked: true,
this.startPaused: false,
this.observatoryPort,
this.diagnosticPort
}) : debuggingEnabled = true;
DebuggingOptions.disabled() :
debuggingEnabled = false,
checked = false,
startPaused = false,
observatoryPort = null,
diagnosticPort = null;
final bool debuggingEnabled;
final bool checked;
final bool startPaused;
final int observatoryPort;
final int diagnosticPort;
bool get hasObservatoryPort => observatoryPort != null;
/// Return the user specified observatory port. If that isn't available,
/// return [defaultObservatoryPort], or a port close to that one.
Future<int> findBestObservatoryPort() {
if (hasObservatoryPort)
return new Future<int>.value(observatoryPort);
return findPreferredPort(observatoryPort ?? defaultObservatoryPort);
}
bool get hasDiagnosticPort => diagnosticPort != null;
/// Return the user specified diagnostic port. If that isn't available,
/// return [defaultObservatoryPort], or a port close to that one.
Future<int> findBestDiagnosticPort() {
return findPreferredPort(diagnosticPort ?? defaultDiagnosticPort);
}
}
class LaunchResult {
LaunchResult.succeeded({ this.observatoryPort, this.diagnosticPort }) : started = true;
LaunchResult.failed() : started = false, observatoryPort = null, diagnosticPort = null;
bool get hasObservatory => observatoryPort != null;
final bool started;
final int observatoryPort;
final int diagnosticPort;
@override
String toString() {
StringBuffer buf = new StringBuffer('started=$started');
if (observatoryPort != null)
buf.write(', observatory=$observatoryPort');
if (diagnosticPort != null)
buf.write(', diagnostic=$diagnosticPort');
return buf.toString();
}
}
class ForwardedPort {
ForwardedPort(this.hostPort, this.devicePort);
......@@ -248,40 +307,18 @@ 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: null });
/// Stops forwarding [forwardedPort].
Future<Null> unforward(ForwardedPort forwardedPort);
}
/// Read the log for a particular device. Subclasses must implement `hashCode`
/// and `operator ==` so that log readers that read from the same location can be
/// de-duped. For example, two Android devices will both try and log using
/// `adb logcat`; we don't want to display two identical log streams.
/// Read the log for a particular device.
abstract class DeviceLogReader {
String get name;
/// A broadcast stream where each element in the string is a line of log
/// output.
Stream<String> get lines;
/// Start reading logs from the device.
Future<Null> start();
/// Actively reading lines from the log?
bool get isReading;
/// Actively stop reading logs from the device.
Future<Null> stop();
/// Completes when the log is finished.
Future<int> get finished;
@override
int get hashCode;
@override
bool operator ==(dynamic other);
/// A broadcast stream where each element in the string is a line of log output.
Stream<String> get logLines;
@override
String toString() => name;
......
......@@ -9,7 +9,6 @@ import 'dart:io';
import 'package:path/path.dart' as path;
import '../application_package.dart';
import '../base/common.dart';
import '../base/os.dart';
import '../base/process.dart';
import '../build_configuration.dart';
......@@ -154,19 +153,15 @@ class IOSDevice extends Device {
}
@override
Future<bool> startApp(
Future<LaunchResult> startApp(
ApplicationPackage app,
Toolchain toolchain, {
String mainPath,
String route,
bool checked: true,
bool clearLogs: false,
bool startPaused: false,
int observatoryPort: observatoryDefaultPort,
int diagnosticPort: diagnosticDefaultPort,
DebuggingOptions debuggingOptions,
Map<String, dynamic> platformArgs
}) async {
// TODO(chinmaygarde): Use checked, mainPath, route, clearLogs.
// TODO(chinmaygarde): Use checked, mainPath, route.
// TODO(devoncarew): Handle startPaused, debugPort.
printTrace('Building ${app.name} for $id');
......@@ -174,7 +169,7 @@ class IOSDevice extends Device {
bool buildResult = await buildIOSXcodeProject(app, buildForDevice: true);
if (!buildResult) {
printError('Could not build the precompiled application for the device.');
return false;
return new LaunchResult.failed();
}
// Step 2: Check that the application exists at the specified path.
......@@ -182,7 +177,7 @@ class IOSDevice extends Device {
bool bundleExists = bundle.existsSync();
if (!bundleExists) {
printError('Could not find the built application bundle at ${bundle.path}.');
return false;
return new LaunchResult.failed();
}
// Step 3: Attempt to install the application on the device.
......@@ -197,10 +192,10 @@ class IOSDevice extends Device {
if (installationResult != 0) {
printError('Could not install ${bundle.path} on $id.');
return false;
return new LaunchResult.failed();
}
return true;
return new LaunchResult.succeeded();
}
@override
......@@ -266,90 +261,46 @@ class IOSDevice extends Device {
}
class _IOSDeviceLogReader extends DeviceLogReader {
_IOSDeviceLogReader(this.device);
_IOSDeviceLogReader(this.device) {
_linesController = new StreamController<String>.broadcast(
onListen: _start,
onCancel: _stop
);
}
final IOSDevice device;
final StreamController<String> _linesStreamController =
new StreamController<String>.broadcast();
StreamController<String> _linesController;
Process _process;
StreamSubscription<String> _stdoutSubscription;
StreamSubscription<String> _stderrSubscription;
@override
Stream<String> get lines => _linesStreamController.stream;
Stream<String> get logLines => _linesController.stream;
@override
String get name => device.name;
@override
bool get isReading => _process != null;
@override
Future<int> get finished {
return _process != null ? _process.exitCode : new Future<int>.value(0);
}
@override
Future<Null> start() async {
if (_process != null) {
throw new StateError(
'_IOSDeviceLogReader must be stopped before it can be started.'
);
}
_process = await runCommand(<String>[device.loggerPath]);
_stdoutSubscription =
_process.stdout.transform(UTF8.decoder)
.transform(const LineSplitter()).listen(_onLine);
_stderrSubscription =
_process.stderr.transform(UTF8.decoder)
.transform(const LineSplitter()).listen(_onLine);
_process.exitCode.then(_onExit);
}
@override
Future<Null> stop() async {
if (_process == null) {
throw new StateError(
'_IOSDeviceLogReader must be started before it can be stopped.'
);
}
_stdoutSubscription?.cancel();
_stdoutSubscription = null;
_stderrSubscription?.cancel();
_stderrSubscription = null;
await _process.kill();
_process = null;
}
void _start() {
runCommand(<String>[device.loggerPath]).then((Process process) {
_process = process;
_process.stdout.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine);
_process.stderr.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine);
void _onExit(int exitCode) {
_stdoutSubscription?.cancel();
_stdoutSubscription = null;
_stderrSubscription?.cancel();
_stderrSubscription = null;
_process = null;
_process.exitCode.then((int code) {
if (_linesController.hasListener)
_linesController.close();
});
});
}
RegExp _runnerRegex = new RegExp(r'Runner');
static final RegExp _runnerRegex = new RegExp(r'FlutterRunner');
void _onLine(String line) {
if (!_runnerRegex.hasMatch(line))
return;
_linesStreamController.add(line);
if (_runnerRegex.hasMatch(line))
_linesController.add(line);
}
@override
int get hashCode => name.hashCode;
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other is! _IOSDeviceLogReader)
return false;
return other.name == name;
void _stop() {
_process?.kill();
}
}
......
......@@ -9,7 +9,6 @@ import 'dart:io';
import 'package:path/path.dart' as path;
import '../application_package.dart';
import '../base/common.dart';
import '../base/context.dart';
import '../base/process.dart';
import '../build_configuration.dart';
......@@ -437,32 +436,25 @@ class IOSSimulator extends Device {
}
@override
Future<bool> startApp(
Future<LaunchResult> startApp(
ApplicationPackage app,
Toolchain toolchain, {
String mainPath,
String route,
bool checked: true,
bool clearLogs: false,
bool startPaused: false,
int observatoryPort: observatoryDefaultPort,
int diagnosticPort: diagnosticDefaultPort,
DebuggingOptions debuggingOptions,
Map<String, dynamic> platformArgs
}) async {
printTrace('Building ${app.name} for $id.');
if (clearLogs)
this.clearLogs();
if (!(await _setupUpdatedApplicationBundle(app, toolchain)))
return false;
return new LaunchResult.failed();
ServiceProtocolDiscovery serviceProtocolDiscovery =
new ServiceProtocolDiscovery(logReader, ServiceProtocolDiscovery.kObservatoryService);
ServiceProtocolDiscovery observatoryDiscovery;
// We take this future here but do not wait for completion until *after* we
// start the application.
Future<int> scrapeServicePort = serviceProtocolDiscovery.nextPort();
if (debuggingOptions.debuggingEnabled) {
observatoryDiscovery = new ServiceProtocolDiscovery(
logReader, ServiceProtocolDiscovery.kObservatoryService);
}
// Prepare launch arguments.
List<String> args = <String>[
......@@ -471,39 +463,47 @@ class IOSSimulator extends Device {
"--packages=${path.absolute('.packages')}",
];
if (checked)
args.add("--enable-checked-mode");
if (debuggingOptions.debuggingEnabled) {
if (debuggingOptions.checked)
args.add("--enable-checked-mode");
if (debuggingOptions.startPaused)
args.add("--start-paused");
if (startPaused)
args.add("--start-paused");
if (observatoryPort != observatoryDefaultPort)
int observatoryPort = await debuggingOptions.findBestObservatoryPort();
args.add("--observatory-port=$observatoryPort");
}
// Launch the updated application in the simulator.
try {
SimControl.instance.launch(id, app.id, args);
} catch (error) {
printError('$error');
return false;
return new LaunchResult.failed();
}
// Wait for the service protocol port here. This will complete once the
// device has printed "Observatory is listening on..."
printTrace('Waiting for observatory port to be available...');
try {
int devicePort = await scrapeServicePort.timeout(new Duration(seconds: 12));
printTrace('service protocol port = $devicePort');
printStatus('Observatory listening on http://127.0.0.1:$devicePort');
return true;
} catch (error) {
if (error is TimeoutException)
printError('Timed out while waiting for a debug connection.');
else
printError('Error waiting for a debug connection: $error');
return false;
if (!debuggingOptions.debuggingEnabled) {
return new LaunchResult.succeeded();
} else {
// Wait for the service protocol port here. This will complete once the
// device has printed "Observatory is listening on..."
printTrace('Waiting for observatory port to be available...');
try {
int devicePort = await observatoryDiscovery
.nextPort()
.timeout(new Duration(seconds: 20));
printTrace('service protocol port = $devicePort');
printStatus('Observatory listening on http://127.0.0.1:$devicePort');
return new LaunchResult.succeeded(observatoryPort: devicePort);
} catch (error) {
if (error is TimeoutException)
printError('Timed out while waiting for a debug connection.');
else
printError('Error waiting for a debug connection: $error');
return new LaunchResult.failed();
} finally {
observatoryDiscovery.cancel();
}
}
}
......@@ -662,118 +662,57 @@ class IOSSimulator extends Device {
}
class _IOSSimulatorLogReader extends DeviceLogReader {
_IOSSimulatorLogReader(this.device);
_IOSSimulatorLogReader(this.device) {
_linesController = new StreamController<String>.broadcast(
onListen: () {
_start();
},
onCancel: _stop
);
}
final IOSSimulator device;
final StreamController<String> _linesStreamController =
new StreamController<String>.broadcast();
StreamController<String> _linesController;
bool _lastWasFiltered = false;
// We log from two logs: the device and the system log.
// We log from two files: the device and the system log.
Process _deviceProcess;
StreamSubscription<String> _deviceStdoutSubscription;
StreamSubscription<String> _deviceStderrSubscription;
Process _systemProcess;
StreamSubscription<String> _systemStdoutSubscription;
StreamSubscription<String> _systemStderrSubscription;
@override
Stream<String> get lines => _linesStreamController.stream;
Stream<String> get logLines => _linesController.stream;
@override
String get name => device.name;
@override
bool get isReading => (_deviceProcess != null) && (_systemProcess != null);
@override
Future<int> get finished {
return (_deviceProcess != null) ? _deviceProcess.exitCode : new Future<int>.value(0);
}
@override
Future<Null> start() async {
if (isReading) {
throw new StateError(
'_IOSSimulatorLogReader must be stopped before it can be started.'
);
}
// TODO(johnmccutchan): Add a ProcessSet abstraction that handles running
// N processes and merging their output.
Future<Null> _start() async {
// Device log.
device.ensureLogsExists();
_deviceProcess = await runCommand(
<String>['tail', '-n', '+0', '-F', device.logFilePath]);
_deviceStdoutSubscription =
_deviceProcess.stdout.transform(UTF8.decoder)
.transform(const LineSplitter()).listen(_onDeviceLine);
_deviceStderrSubscription =
_deviceProcess.stderr.transform(UTF8.decoder)
.transform(const LineSplitter()).listen(_onDeviceLine);
_deviceProcess.exitCode.then(_onDeviceExit);
_deviceProcess = await runCommand(<String>['tail', '-n', '0', '-F', device.logFilePath]);
_deviceProcess.stdout.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onDeviceLine);
_deviceProcess.stderr.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onDeviceLine);
// Track system.log crashes.
// ReportCrash[37965]: Saved crash report for FlutterRunner[37941]...
_systemProcess = await runCommand(
<String>['tail', '-F', '/private/var/log/system.log']);
_systemStdoutSubscription =
_systemProcess.stdout.transform(UTF8.decoder)
.transform(const LineSplitter()).listen(_onSystemLine);
_systemStderrSubscription =
_systemProcess.stderr.transform(UTF8.decoder)
.transform(const LineSplitter()).listen(_onSystemLine);
_systemProcess.exitCode.then(_onSystemExit);
}
@override
Future<Null> stop() async {
if (!isReading) {
throw new StateError(
'_IOSSimulatorLogReader must be started before it can be stopped.'
);
}
if (_deviceProcess != null) {
await _deviceProcess.kill();
_deviceProcess = null;
}
_onDeviceExit(0);
if (_systemProcess != null) {
await _systemProcess.kill();
_systemProcess = null;
}
_onSystemExit(0);
}
_systemProcess = await runCommand(<String>['tail', '-n', '0', '-F', '/private/var/log/system.log']);
_systemProcess.stdout.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onSystemLine);
_systemProcess.stderr.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onSystemLine);
void _onDeviceExit(int exitCode) {
_deviceStdoutSubscription?.cancel();
_deviceStdoutSubscription = null;
_deviceStderrSubscription?.cancel();
_deviceStderrSubscription = null;
_deviceProcess = null;
}
void _onSystemExit(int exitCode) {
_systemStdoutSubscription?.cancel();
_systemStdoutSubscription = null;
_systemStderrSubscription?.cancel();
_systemStderrSubscription = null;
_systemProcess = null;
_deviceProcess.exitCode.then((int code) {
if (_linesController.hasListener)
_linesController.close();
});
}
// Match the log prefix (in order to shorten it):
// 'Jan 29 01:31:44 devoncarew-macbookpro3 SpringBoard[96648]: ...'
final RegExp _mapRegex =
new RegExp(r'\S+ +\S+ +\S+ \S+ (.+)\[\d+\]\)?: (.*)$');
static final RegExp _mapRegex = new RegExp(r'\S+ +\S+ +\S+ \S+ (.+)\[\d+\]\)?: (.*)$');
// Jan 31 19:23:28 --- last message repeated 1 time ---
final RegExp _lastMessageRegex = new RegExp(r'\S+ +\S+ +\S+ --- (.*) ---$');
static final RegExp _lastMessageRegex = new RegExp(r'\S+ +\S+ +\S+ --- (.*) ---$');
final RegExp _flutterRunnerRegex = new RegExp(r' FlutterRunner\[\d+\] ');
static final RegExp _flutterRunnerRegex = new RegExp(r' FlutterRunner\[\d+\] ');
String _filterDeviceLine(String string) {
Match match = _mapRegex.matchAsPrefix(string);
......@@ -808,8 +747,7 @@ class _IOSSimulatorLogReader extends DeviceLogReader {
String filteredLine = _filterDeviceLine(line);
if (filteredLine == null)
return;
_linesStreamController.add(filteredLine);
_linesController.add(filteredLine);
}
String _filterSystemLog(String string) {
......@@ -825,19 +763,12 @@ class _IOSSimulatorLogReader extends DeviceLogReader {
if (filteredLine == null)
return;
_linesStreamController.add(filteredLine);
_linesController.add(filteredLine);
}
@override
int get hashCode => device.logFilePath.hashCode;
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other is! _IOSSimulatorLogReader)
return false;
return other.device.logFilePath == device.logFilePath;
void _stop() {
_deviceProcess?.kill();
_systemProcess?.kill();
}
}
......
......@@ -8,28 +8,30 @@ import 'device.dart';
/// Discover service protocol ports on devices.
class ServiceProtocolDiscovery {
static const String kObservatoryService = 'Observatory';
static const String kDiagnosticService = 'Diagnostic server';
/// [logReader] A [DeviceLogReader] to look for service messages in.
/// [logReader] - a [DeviceLogReader] to look for service messages in.
ServiceProtocolDiscovery(DeviceLogReader logReader, String serviceName)
: _logReader = logReader,
_serviceName = serviceName {
: _logReader = logReader, _serviceName = serviceName {
assert(_logReader != null);
if (!_logReader.isReading)
_logReader.start();
_logReader.lines.listen(_onLine);
_subscription = _logReader.logLines.listen(_onLine);
}
static const String kObservatoryService = 'Observatory';
static const String kDiagnosticService = 'Diagnostic server';
final DeviceLogReader _logReader;
final String _serviceName;
Completer<int> _completer = new Completer<int>();
StreamSubscription<String> _subscription;
/// The [Future] returned by this function will complete when the next service
/// protocol port is found.
Future<int> nextPort() => _completer.future;
void cancel() {
_subscription.cancel();
}
void _onLine(String line) {
int portNumber = 0;
if (line.contains('$_serviceName listening on http://')) {
......@@ -48,6 +50,7 @@ class ServiceProtocolDiscovery {
void _located(int port) {
assert(_completer != null);
assert(!_completer.isCompleted);
_completer.complete(port);
_completer = new Completer<int>();
}
......
......@@ -26,6 +26,9 @@ class SnapshotCompiler {
String depfilePath,
String buildOutputPath
}) {
assert(mainPath != null);
assert(snapshotPath != null);
final List<String> args = [
_path,
mainPath,
......
......@@ -15,36 +15,39 @@ void main() {
MockDeviceLogReader logReader = new MockDeviceLogReader();
ServiceProtocolDiscovery discoverer =
new ServiceProtocolDiscovery(logReader, ServiceProtocolDiscovery.kObservatoryService);
// Get next port future.
Future<int> nextPort = discoverer.nextPort();
expect(nextPort, isNotNull);
// Inject some lines.
logReader.addLine('HELLO WORLD');
logReader.addLine(
'Observatory listening on http://127.0.0.1:9999');
logReader.addLine('Observatory listening on http://127.0.0.1:9999');
// Await the port.
expect(await nextPort, 9999);
// Get next port future.
nextPort = discoverer.nextPort();
logReader.addLine(
'Observatory listening on http://127.0.0.1:3333');
logReader.addLine('Observatory listening on http://127.0.0.1:3333');
expect(await nextPort, 3333);
// Get next port future.
nextPort = discoverer.nextPort();
// Inject some bad lines.
logReader.addLine('Observatory listening on http://127.0.0.1');
logReader.addLine('Observatory listening on http://127.0.0.1:');
logReader.addLine(
'Observatory listening on http://127.0.0.1:apple');
logReader.addLine('Observatory listening on http://127.0.0.1:apple');
int port = await nextPort.timeout(
const Duration(milliseconds: 100), onTimeout: () => 77);
// Expect the timeout port.
expect(port, 77);
// Get next port future.
nextPort = discoverer.nextPort();
logReader.addLine(
'I/flutter : Observatory listening on http://127.0.0.1:52584');
logReader.addLine('I/flutter : Observatory listening on http://127.0.0.1:52584');
expect(await nextPort, 52584);
discoverer.cancel();
});
});
}
......@@ -63,38 +63,12 @@ class MockDeviceLogReader extends DeviceLogReader {
@override
String get name => 'MockLogReader';
final StreamController<String> _linesStreamController =
new StreamController<String>.broadcast();
final Completer<int> _finishedCompleter = new Completer<int>();
@override
Stream<String> get lines => _linesStreamController.stream;
void addLine(String line) {
_linesStreamController.add(line);
}
bool _started = false;
final StreamController<String> _linesController = new StreamController<String>.broadcast();
@override
Future<Null> start() async {
assert(!_started);
_started = true;
}
Stream<String> get logLines => _linesController.stream;
@override
bool get isReading => _started;
@override
Future<Null> stop() {
assert(_started);
_started = false;
return new Future<Null>.value();
}
@override
Future<int> get finished => _finishedCompleter.future;
void addLine(String line) => _linesController.add(line);
}
void applyMocksToCommand(FlutterCommand command) {
......
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