Chris Bracken b2909a245a Revert use of Xcode instruments for device lookup (#10806)
* Revert "Make device discovery asynchronous (#10803)"
This reverts commit 972be9c8b4048e18ecfb8ab582159c8d78abace8.

* Revert "Use Xcode instruments to list devices (#10801)"
This reverts commit 37bb5f1300e67fe590c44bb9ecda653b2967e347.

This is to resolve a failure that looks related to a bad install of Xcode 8.0
on our build bots and should be reinstated when the infra issue is diagnosed
and resolved.

Instruments worked well when this was originally landed, and on the
following commit, but started failing two commits after this originally
landed. Manual invocation of instruments on the build host currently
results in:

```
dyld: Library not loaded: @rpath/InstrumentsAnalysisCore.framework/Versions/A/InstrumentsAnalysisCore
  Referenced from: /Applications/Xcode8.0.app/Contents/Developer/usr/bin/instruments
  Reason: image not found
Abort trap: 6
```

It appears the /Applications/Xcode8.0.app/Contents/Applications
directory (which contains Instruments) is missing on the host.
2017-06-16 19:00:31 -07:00

805 lines
26 KiB
Dart

// 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:convert';
import '../android/android_sdk.dart';
import '../android/android_workflow.dart';
import '../application_package.dart';
import '../base/common.dart' show throwToolExit;
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/port_scanner.dart';
import '../base/process.dart';
import '../base/process_manager.dart';
import '../build_info.dart';
import '../commands/build_apk.dart';
import '../device.dart';
import '../globals.dart';
import '../protocol_discovery.dart';
import 'adb.dart';
import 'android.dart';
import 'android_sdk.dart';
const String _defaultAdbPath = 'adb';
enum _HardwareType { emulator, physical }
/// Map to help our `isLocalEmulator` detection.
const Map<String, _HardwareType> _knownHardware = const <String, _HardwareType>{
'goldfish': _HardwareType.emulator,
'qcom': _HardwareType.physical,
'ranchu': _HardwareType.emulator,
'samsungexynos7420': _HardwareType.physical,
'samsungexynos8895': _HardwareType.physical,
};
class AndroidDevices extends PollingDeviceDiscovery {
AndroidDevices() : super('Android devices');
@override
bool get supportsPlatform => true;
@override
bool get canListAnything => androidWorkflow.canListDevices;
@override
List<Device> pollingGetDevices() => getAdbDevices();
}
class AndroidDevice extends Device {
AndroidDevice(
String id, {
this.productID,
this.modelID,
this.deviceCodeName
}) : super(id);
final String productID;
final String modelID;
final String deviceCodeName;
Map<String, String> _properties;
bool _isLocalEmulator;
TargetPlatform _platform;
Future<String> _getProperty(String name) async {
if (_properties == null) {
_properties = <String, String>{};
final List<String> propCommand = adbCommandForDevice(<String>['shell', 'getprop']);
printTrace(propCommand.join(' '));
try {
// We pass an encoding of LATIN1 so that we don't try and interpret the
// `adb shell getprop` result as UTF8.
final ProcessResult result = await processManager.run(
propCommand,
stdoutEncoding: LATIN1,
stderrEncoding: LATIN1,
).timeout(const Duration(seconds: 5));
if (result.exitCode == 0) {
_properties = parseAdbDeviceProperties(result.stdout);
} else {
printError('Error retrieving device properties for $name:');
printError(result.stderr);
}
} on TimeoutException catch (_) {
throwToolExit('adb not responding');
} on ProcessException catch (error) {
printError('Error retrieving device properties for $name: $error');
}
}
return _properties[name];
}
@override
Future<bool> get isLocalEmulator async {
if (_isLocalEmulator == null) {
final String hardware = await _getProperty('ro.hardware');
printTrace('ro.hardware = $hardware');
if (_knownHardware.containsKey(hardware)) {
// Look for known hardware models.
_isLocalEmulator = _knownHardware[hardware] == _HardwareType.emulator;
} else {
// Fall back to a best-effort heuristic-based approach.
final String characteristics = await _getProperty('ro.build.characteristics');
printTrace('ro.build.characteristics = $characteristics');
_isLocalEmulator = characteristics != null && characteristics.contains('emulator');
}
}
return _isLocalEmulator;
}
@override
Future<TargetPlatform> get targetPlatform async {
if (_platform == null) {
// http://developer.android.com/ndk/guides/abis.html (x86, armeabi-v7a, ...)
switch (await _getProperty('ro.product.cpu.abi')) {
case 'x86_64':
_platform = TargetPlatform.android_x64;
break;
case 'x86':
_platform = TargetPlatform.android_x86;
break;
default:
_platform = TargetPlatform.android_arm;
break;
}
}
return _platform;
}
@override
Future<String> get sdkNameAndVersion async =>
'Android ${await _sdkVersion} (API ${await _apiVersion})';
Future<String> get _sdkVersion => _getProperty('ro.build.version.release');
Future<String> get _apiVersion => _getProperty('ro.build.version.sdk');
_AdbLogReader _logReader;
_AndroidDevicePortForwarder _portForwarder;
List<String> adbCommandForDevice(List<String> args) {
return <String>[getAdbPath(androidSdk), '-s', id]..addAll(args);
}
bool _isValidAdbVersion(String adbVersion) {
// Sample output: 'Android Debug Bridge version 1.0.31'
final Match versionFields = new RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(adbVersion);
if (versionFields != null) {
final int majorVersion = int.parse(versionFields[1]);
final int minorVersion = int.parse(versionFields[2]);
final int patchVersion = int.parse(versionFields[3]);
if (majorVersion > 1) {
return true;
}
if (majorVersion == 1 && minorVersion > 0) {
return true;
}
if (majorVersion == 1 && minorVersion == 0 && patchVersion >= 32) {
return true;
}
return false;
}
printError(
'Unrecognized adb version string $adbVersion. Skipping version check.');
return true;
}
Future<bool> _checkForSupportedAdbVersion() async {
if (androidSdk == null)
return false;
try {
final RunResult adbVersion = await runCheckedAsync(<String>[getAdbPath(androidSdk), 'version']);
if (_isValidAdbVersion(adbVersion.stdout))
return true;
printError('The ADB at "${getAdbPath(androidSdk)}" is too old; please install version 1.0.32 or later.');
} catch (error, trace) {
printError('Error running ADB: $error', stackTrace: trace);
}
return false;
}
Future<bool> _checkForSupportedAndroidVersion() async {
try {
// If the server is automatically restarted, then we get irrelevant
// output lines like this, which we want to ignore:
// adb server is out of date. killing..
// * daemon started successfully *
await runCheckedAsync(<String>[getAdbPath(androidSdk), 'start-server']);
// Sample output: '22'
final String sdkVersion = await _getProperty('ro.build.version.sdk');
final int sdkVersionParsed = int.parse(sdkVersion, onError: (String source) => null);
if (sdkVersionParsed == null) {
printError('Unexpected response from getprop: "$sdkVersion"');
return false;
}
if (sdkVersionParsed < minApiLevel) {
printError(
'The Android version ($sdkVersion) on the target device is too old. Please '
'use a $minVersionName (version $minApiLevel / $minVersionText) device or later.');
return false;
}
return true;
} catch (e) {
printError('Unexpected failure from adb: $e');
return false;
}
}
String _getDeviceSha1Path(ApplicationPackage app) {
return '/data/local/tmp/sky.${app.id}.sha1';
}
Future<String> _getDeviceApkSha1(ApplicationPackage app) async {
final RunResult result = await runAsync(adbCommandForDevice(<String>['shell', 'cat', _getDeviceSha1Path(app)]));
return result.stdout;
}
String _getSourceSha1(ApplicationPackage app) {
final AndroidApk apk = app;
final File shaFile = fs.file('${apk.apkPath}.sha1');
return shaFile.existsSync() ? shaFile.readAsStringSync() : '';
}
@override
String get name => modelID;
@override
Future<bool> isAppInstalled(ApplicationPackage app) async {
// This call takes 400ms - 600ms.
final RunResult listOut = await runCheckedAsync(adbCommandForDevice(<String>['shell', 'pm', 'list', 'packages', app.id]));
return LineSplitter.split(listOut.stdout).contains("package:${app.id}");
}
@override
Future<bool> isLatestBuildInstalled(ApplicationPackage app) async {
final String installedSha1 = await _getDeviceApkSha1(app);
return installedSha1.isNotEmpty && installedSha1 == _getSourceSha1(app);
}
@override
Future<bool> installApp(ApplicationPackage app) async {
final AndroidApk apk = app;
if (!fs.isFileSync(apk.apkPath)) {
printError('"${apk.apkPath}" does not exist.');
return false;
}
if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion())
return false;
final Status status = logger.startProgress('Installing ${apk.apkPath}...', expectSlowOperation: true);
final RunResult installResult = await runAsync(adbCommandForDevice(<String>['install', '-r', apk.apkPath]));
status.stop();
// Some versions of adb exit with exit code 0 even on failure :(
// Parsing the output to check for failures.
final RegExp failureExp = new RegExp(r'^Failure.*$', multiLine: true);
final String failure = failureExp.stringMatch(installResult.stdout);
if (failure != null) {
printError('Package install error: $failure');
return false;
}
if (installResult.exitCode != 0) {
printError('Error: ADB exited with exit code ${installResult.exitCode}');
return false;
}
await runCheckedAsync(adbCommandForDevice(<String>[
'shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app)
]));
return true;
}
@override
Future<bool> uninstallApp(ApplicationPackage app) async {
if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion())
return false;
final String uninstallOut = (await runCheckedAsync(adbCommandForDevice(<String>['uninstall', app.id]))).stdout;
final RegExp failureExp = new RegExp(r'^Failure.*$', multiLine: true);
final String failure = failureExp.stringMatch(uninstallOut);
if (failure != null) {
printError('Package uninstall error: $failure');
return false;
}
return true;
}
Future<bool> _installLatestApp(ApplicationPackage package) async {
final bool wasInstalled = await isAppInstalled(package);
if (wasInstalled) {
if (await isLatestBuildInstalled(package)) {
printTrace('Latest build already installed.');
return true;
}
}
printTrace('Installing APK.');
if (!await installApp(package)) {
printTrace('Warning: Failed to install APK.');
if (wasInstalled) {
printStatus('Uninstalling old version...');
if (!await uninstallApp(package)) {
printError('Error: Uninstalling old version failed.');
return false;
}
if (!await installApp(package)) {
printError('Error: Failed to install APK again.');
return false;
}
return true;
}
return false;
}
return true;
}
@override
Future<LaunchResult> startApp(
ApplicationPackage package,
BuildMode mode, {
String mainPath,
String route,
DebuggingOptions debuggingOptions,
Map<String, dynamic> platformArgs,
String kernelPath,
bool prebuiltApplication: false,
bool applicationNeedsRebuild: false,
}) async {
if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion())
return new LaunchResult.failed();
if (await targetPlatform != TargetPlatform.android_arm && mode != BuildMode.debug) {
printError('Profile and release builds are only supported on ARM targets.');
return new LaunchResult.failed();
}
if (!prebuiltApplication) {
printTrace('Building APK');
await buildApk(
target: mainPath,
buildMode: debuggingOptions.buildMode,
kernelPath: kernelPath,
);
// Package has been built, so we can get the updated application ID and
// activity name from the .apk.
package = new AndroidApk.fromCurrentDirectory();
}
printTrace("Stopping app '${package.name}' on $name.");
await stopApp(package);
if (!await _installLatestApp(package))
return new LaunchResult.failed();
final bool traceStartup = platformArgs['trace-startup'] ?? false;
final AndroidApk apk = package;
printTrace('$this startApp');
ProtocolDiscovery observatoryDiscovery;
ProtocolDiscovery diagnosticDiscovery;
if (debuggingOptions.debuggingEnabled) {
// TODO(devoncarew): Remember the forwarding information (so we can later remove the
// port forwarding or set it up again when adb fails on us).
observatoryDiscovery = new ProtocolDiscovery.observatory(
getLogReader(), portForwarder: portForwarder, hostPort: debuggingOptions.observatoryPort);
diagnosticDiscovery = new ProtocolDiscovery.diagnosticService(
getLogReader(), portForwarder: portForwarder, hostPort: debuggingOptions.diagnosticPort);
}
List<String> cmd;
cmd = adbCommandForDevice(<String>[
'shell', 'am', 'start',
'-a', 'android.intent.action.RUN',
'-f', '0x20000000', // FLAG_ACTIVITY_SINGLE_TOP
'--ez', 'enable-background-compilation', 'true',
'--ez', 'enable-dart-profiling', 'true',
]);
if (traceStartup)
cmd.addAll(<String>['--ez', 'trace-startup', 'true']);
if (route != null)
cmd.addAll(<String>['--es', 'route', route]);
if (debuggingOptions.enableSoftwareRendering)
cmd.addAll(<String>['--ez', 'enable-software-rendering', 'true']);
if (debuggingOptions.debuggingEnabled) {
if (debuggingOptions.buildMode == BuildMode.debug)
cmd.addAll(<String>['--ez', 'enable-checked-mode', 'true']);
if (debuggingOptions.startPaused)
cmd.addAll(<String>['--ez', 'start-paused', 'true']);
if (debuggingOptions.useTestFonts)
cmd.addAll(<String>['--ez', 'use-test-fonts', 'true']);
}
cmd.add(apk.launchActivity);
final String result = (await runCheckedAsync(cmd)).stdout;
// This invocation returns 0 even when it fails.
if (result.contains('Error: ')) {
printError(result.trim());
return new LaunchResult.failed();
}
if (!debuggingOptions.debuggingEnabled)
return new LaunchResult.succeeded();
// 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...');
// TODO(danrubel) Waiting for observatory and diagnostic services
// can be made common across all devices.
try {
Uri observatoryUri, diagnosticUri;
if (debuggingOptions.buildMode == BuildMode.debug) {
final List<Uri> deviceUris = await Future.wait(
<Future<Uri>>[observatoryDiscovery.uri, diagnosticDiscovery.uri]
);
observatoryUri = deviceUris[0];
diagnosticUri = deviceUris[1];
} else if (debuggingOptions.buildMode == BuildMode.profile) {
observatoryUri = await observatoryDiscovery.uri;
}
return new LaunchResult.succeeded(
observatoryUri: observatoryUri,
diagnosticUri: diagnosticUri,
);
} catch (error) {
printError('Error waiting for a debug connection: $error');
return new LaunchResult.failed();
} finally {
observatoryDiscovery.cancel();
diagnosticDiscovery.cancel();
}
}
@override
bool get supportsHotMode => true;
@override
Future<bool> stopApp(ApplicationPackage app) {
final List<String> command = adbCommandForDevice(<String>['shell', 'am', 'force-stop', app.id]);
return runCommandAndStreamOutput(command).then((int exitCode) => exitCode == 0);
}
@override
void clearLogs() {
runSync(adbCommandForDevice(<String>['logcat', '-c']));
}
@override
DeviceLogReader getLogReader({ApplicationPackage app}) {
// The Android log reader isn't app-specific.
_logReader ??= new _AdbLogReader(this);
return _logReader;
}
@override
DevicePortForwarder get portForwarder => _portForwarder ??= new _AndroidDevicePortForwarder(this);
static final RegExp _timeRegExp = new RegExp(r'^\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}', multiLine: true);
/// Return the most recent timestamp in the Android log or null if there is
/// no available timestamp. The format can be passed to logcat's -T option.
String get lastLogcatTimestamp {
final String output = runCheckedSync(adbCommandForDevice(<String>[
'logcat', '-v', 'time', '-t', '1'
]));
final Match timeMatch = _timeRegExp.firstMatch(output);
return timeMatch?.group(0);
}
@override
bool isSupported() => true;
@override
bool get supportsScreenshot => true;
@override
Future<Null> takeScreenshot(File outputFile) async {
const String remotePath = '/data/local/tmp/flutter_screenshot.png';
await runCheckedAsync(adbCommandForDevice(<String>['shell', 'screencap', '-p', remotePath]));
await runCheckedAsync(adbCommandForDevice(<String>['pull', remotePath, outputFile.path]));
await runCheckedAsync(adbCommandForDevice(<String>['shell', 'rm', remotePath]));
}
@override
Future<List<DiscoveredApp>> discoverApps() async {
final RegExp discoverExp = new RegExp(r'DISCOVER: (.*)');
final List<DiscoveredApp> result = <DiscoveredApp>[];
final StreamSubscription<String> logs = getLogReader().logLines.listen((String line) {
final Match match = discoverExp.firstMatch(line);
if (match != null) {
final Map<String, dynamic> app = JSON.decode(match.group(1));
result.add(new DiscoveredApp(app['id'], app['observatoryPort'], app['diagnosticPort']));
}
});
await runCheckedAsync(adbCommandForDevice(<String>[
'shell', 'am', 'broadcast', '-a', 'io.flutter.view.DISCOVER'
]));
await new Future<Null>.delayed(const Duration(seconds: 1));
logs.cancel();
return result;
}
}
Map<String, String> parseAdbDeviceProperties(String str) {
final Map<String, String> properties = <String, String>{};
final RegExp propertyExp = new RegExp(r'\[(.*?)\]: \[(.*?)\]');
for (Match match in propertyExp.allMatches(str))
properties[match.group(1)] = match.group(2);
return properties;
}
// 015d172c98400a03 device usb:340787200X product:nakasi model:Nexus_7 device:grouper
final RegExp _kDeviceRegex = new RegExp(r'^(\S+)\s+(\S+)(.*)');
/// Return the list of connected ADB devices.
///
/// [mockAdbOutput] is public for testing.
List<AndroidDevice> getAdbDevices({ String mockAdbOutput }) {
final List<AndroidDevice> devices = <AndroidDevice>[];
String text;
if (mockAdbOutput == null) {
final String adbPath = getAdbPath(androidSdk);
printTrace('Listing devices using $adbPath');
if (adbPath == null)
return <AndroidDevice>[];
text = runSync(<String>[adbPath, 'devices', '-l']);
} else {
text = mockAdbOutput;
}
// Check for error messages from adb
if (!text.contains('List of devices')) {
printError(text);
return <AndroidDevice>[];
}
for (String line in text.trim().split('\n')) {
// Skip lines like: * daemon started successfully *
if (line.startsWith('* daemon '))
continue;
// Skip lines about adb server and client version not matching
if (line.startsWith(new RegExp(r'adb server (version|is out of date)'))) {
printStatus(line);
continue;
}
if (line.startsWith('List of devices'))
continue;
if (_kDeviceRegex.hasMatch(line)) {
final Match match = _kDeviceRegex.firstMatch(line);
final String deviceID = match[1];
final String deviceState = match[2];
String rest = match[3];
final Map<String, String> info = <String, String>{};
if (rest != null && rest.isNotEmpty) {
rest = rest.trim();
for (String data in rest.split(' ')) {
if (data.contains(':')) {
final List<String> fields = data.split(':');
info[fields[0]] = fields[1];
}
}
}
if (info['model'] != null)
info['model'] = cleanAdbDeviceName(info['model']);
if (deviceState == 'unauthorized') {
printError(
'Device $deviceID is not authorized.\n'
'You might need to check your device for an authorization dialog.'
);
} else if (deviceState == 'offline') {
printError('Device $deviceID is offline.');
} else {
devices.add(new AndroidDevice(
deviceID,
productID: info['product'],
modelID: info['model'] ?? deviceID,
deviceCodeName: info['device']
));
}
} else {
printError(
'Unexpected failure parsing device information from adb output:\n'
'$line\n'
'Please report a bug at https://github.com/flutter/flutter/issues/new');
}
}
return devices;
}
/// A log reader that logs from `adb logcat`.
class _AdbLogReader extends DeviceLogReader {
_AdbLogReader(this.device) {
_linesController = new StreamController<String>.broadcast(
onListen: _start,
onCancel: _stop
);
}
final AndroidDevice device;
StreamController<String> _linesController;
Process _process;
@override
Stream<String> get logLines => _linesController.stream;
@override
String get name => device.name;
DateTime _timeOrigin;
DateTime _adbTimestampToDateTime(String adbTimestamp) {
// The adb timestamp format is: mm-dd hours:minutes:seconds.milliseconds
// Dart's DateTime parse function accepts this format so long as we provide
// the year, resulting in:
// yyyy-mm-dd hours:minutes:seconds.milliseconds.
return DateTime.parse('${new DateTime.now().year}-$adbTimestamp');
}
void _start() {
// Start the adb logcat process.
final List<String> args = <String>['logcat', '-v', 'time'];
final String lastTimestamp = device.lastLogcatTimestamp;
if (lastTimestamp != null)
_timeOrigin = _adbTimestampToDateTime(lastTimestamp);
else
_timeOrigin = null;
runCommand(device.adbCommandForDevice(args)).then<Null>((Process process) {
_process = process;
final Utf8Decoder decoder = const Utf8Decoder(allowMalformed: true);
_process.stdout.transform(decoder).transform(const LineSplitter()).listen(_onLine);
_process.stderr.transform(decoder).transform(const LineSplitter()).listen(_onLine);
_process.exitCode.whenComplete(() {
if (_linesController.hasListener)
_linesController.close();
});
});
}
// 'W/ActivityManager(pid): '
static final RegExp _logFormat = new RegExp(r'^[VDIWEF]\/.*?\(\s*(\d+)\):\s');
static final List<RegExp> _whitelistedTags = <RegExp>[
new RegExp(r'^[VDIWEF]\/flutter[^:]*:\s+', caseSensitive: false),
new RegExp(r'^[IE]\/DartVM[^:]*:\s+'),
new RegExp(r'^[WEF]\/AndroidRuntime:\s+'),
new RegExp(r'^[WEF]\/ActivityManager:\s+.*(\bflutter\b|\bdomokit\b|\bsky\b)'),
new RegExp(r'^[WEF]\/System\.err:\s+'),
new RegExp(r'^[F]\/[\S^:]+:\s+')
];
// we default to true in case none of the log lines match
bool _acceptedLastLine = true;
// The format of the line is controlled by the '-v' parameter passed to
// adb logcat. We are currently passing 'time', which has the format:
// mm-dd hh:mm:ss.milliseconds Priority/Tag( PID): ....
void _onLine(String line) {
final Match timeMatch = AndroidDevice._timeRegExp.firstMatch(line);
if (timeMatch == null) {
return;
}
if (_timeOrigin != null) {
final String timestamp = timeMatch.group(0);
final DateTime time = _adbTimestampToDateTime(timestamp);
if (!time.isAfter(_timeOrigin)) {
// Ignore log messages before the origin.
return;
}
}
if (line.length == timeMatch.end) {
return;
}
// Chop off the time.
line = line.substring(timeMatch.end + 1);
final Match logMatch = _logFormat.firstMatch(line);
if (logMatch != null) {
bool acceptLine = false;
if (appPid != null && int.parse(logMatch.group(1)) == appPid) {
acceptLine = true;
} else {
// Filter on approved names and levels.
acceptLine = _whitelistedTags.any((RegExp re) => re.hasMatch(line));
}
if (acceptLine) {
_acceptedLastLine = true;
_linesController.add(line);
return;
}
_acceptedLastLine = false;
} else if (line == '--------- beginning of system' ||
line == '--------- beginning of main' ) {
// hide the ugly adb logcat log boundaries at the start
_acceptedLastLine = false;
} else {
// If it doesn't match the log pattern at all, then pass it through if we
// passed the last matching line through. It might be a multiline message.
if (_acceptedLastLine) {
_linesController.add(line);
return;
}
}
}
void _stop() {
// TODO(devoncarew): We should remove adb port forwarding here.
_process?.kill();
}
}
class _AndroidDevicePortForwarder extends DevicePortForwarder {
_AndroidDevicePortForwarder(this.device);
final AndroidDevice device;
static int _extractPort(String portString) {
return int.parse(portString.trim(), onError: (_) => null);
}
@override
List<ForwardedPort> get forwardedPorts {
final List<ForwardedPort> ports = <ForwardedPort>[];
final String stdout = runCheckedSync(device.adbCommandForDevice(
<String>['forward', '--list']
));
final List<String> lines = LineSplitter.split(stdout).toList();
for (String line in lines) {
if (line.startsWith(device.id)) {
final List<String> splitLine = line.split("tcp:");
// Sanity check splitLine.
if (splitLine.length != 3)
continue;
// Attempt to extract ports.
final int hostPort = _extractPort(splitLine[1]);
final int devicePort = _extractPort(splitLine[2]);
// Failed, skip.
if ((hostPort == null) || (devicePort == null))
continue;
ports.add(new ForwardedPort(hostPort, devicePort));
}
}
return ports;
}
@override
Future<int> forward(int devicePort, { int hostPort }) async {
if ((hostPort == null) || (hostPort == 0)) {
// Auto select host port.
hostPort = await portScanner.findAvailablePort();
}
await runCheckedAsync(device.adbCommandForDevice(
<String>['forward', 'tcp:$hostPort', 'tcp:$devicePort']
));
return hostPort;
}
@override
Future<Null> unforward(ForwardedPort forwardedPort) async {
await runCheckedAsync(device.adbCommandForDevice(
<String>['forward', '--remove', 'tcp:${forwardedPort.hostPort}']
));
}
}