blob: 79948430f9ea2012a19f53e6ddfb25ffa826009c [file] [log] [blame]
// Copyright 2014 The Flutter 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 'package:meta/meta.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
import '../application_package.dart';
import '../artifacts.dart';
import '../base/common.dart';
import '../base/context.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/net.dart';
import '../base/platform.dart';
import '../base/process.dart';
import '../base/time.dart';
import '../build_info.dart';
import '../device.dart';
import '../globals.dart' as globals;
import '../project.dart';
import '../vmservice.dart';
import 'amber_ctl.dart';
import 'application_package.dart';
import 'fuchsia_build.dart';
import 'fuchsia_pm.dart';
import 'fuchsia_sdk.dart';
import 'fuchsia_workflow.dart';
import 'tiles_ctl.dart';
/// The [FuchsiaDeviceTools] instance.
FuchsiaDeviceTools get fuchsiaDeviceTools => context.get<FuchsiaDeviceTools>();
/// Fuchsia device-side tools.
class FuchsiaDeviceTools {
FuchsiaAmberCtl _amberCtl;
FuchsiaAmberCtl get amberCtl => _amberCtl ??= FuchsiaAmberCtl();
FuchsiaTilesCtl _tilesCtl;
FuchsiaTilesCtl get tilesCtl => _tilesCtl ??= FuchsiaTilesCtl();
}
final String _ipv4Loopback = InternetAddress.loopbackIPv4.address;
final String _ipv6Loopback = InternetAddress.loopbackIPv6.address;
// Enables testing the fuchsia isolate discovery
Future<vm_service.VmService> _kDefaultFuchsiaIsolateDiscoveryConnector(Uri uri) {
return connectToVmService(uri);
}
/// Read the log for a particular device.
class _FuchsiaLogReader extends DeviceLogReader {
_FuchsiaLogReader(this._device, [this._app]);
// \S matches non-whitespace characters.
static final RegExp _flutterLogOutput = RegExp(r'INFO: \S+\(flutter\): ');
final FuchsiaDevice _device;
final ApplicationPackage _app;
@override
String get name => _device.name;
Stream<String> _logLines;
@override
Stream<String> get logLines {
final Stream<String> logStream = fuchsiaSdk.syslogs(_device.id);
_logLines ??= _processLogs(logStream);
return _logLines;
}
Stream<String> _processLogs(Stream<String> lines) {
if (lines == null) {
return null;
}
// Get the starting time of the log processor to filter logs from before
// the process attached.
final DateTime startTime = systemClock.now();
// Determine if line comes from flutter, and optionally whether it matches
// the correct fuchsia module.
final RegExp matchRegExp = _app == null
? _flutterLogOutput
: RegExp('INFO: ${_app.name}(\\.cmx)?\\(flutter\\): ');
return Stream<String>.eventTransformed(
lines,
(EventSink<String> output) => _FuchsiaLogSink(output, matchRegExp, startTime),
);
}
@override
String toString() => name;
@override
void dispose() {
// The Fuchsia SDK syslog process is killed when the subscription to the
// logLines Stream is canceled.
}
}
class _FuchsiaLogSink implements EventSink<String> {
_FuchsiaLogSink(this._outputSink, this._matchRegExp, this._startTime);
static final RegExp _utcDateOutput = RegExp(r'\d+\-\d+\-\d+ \d+:\d+:\d+');
final EventSink<String> _outputSink;
final RegExp _matchRegExp;
final DateTime _startTime;
@override
void add(String line) {
if (!_matchRegExp.hasMatch(line)) {
return;
}
final String rawDate = _utcDateOutput.firstMatch(line)?.group(0);
if (rawDate == null) {
return;
}
final DateTime logTime = DateTime.parse(rawDate);
if (logTime.millisecondsSinceEpoch < _startTime.millisecondsSinceEpoch) {
return;
}
_outputSink.add(
'[${logTime.toLocal()}] Flutter: ${line.split(_matchRegExp).last}');
}
@override
void addError(Object error, [StackTrace stackTrace]) {
_outputSink.addError(error, stackTrace);
}
@override
void close() {
_outputSink.close();
}
}
/// Device discovery for Fuchsia devices.
class FuchsiaDevices extends PollingDeviceDiscovery {
FuchsiaDevices({
@required Platform platform,
@required FuchsiaWorkflow fuchsiaWorkflow,
@required FuchsiaSdk fuchsiaSdk,
@required Logger logger,
}) : _platform = platform,
_fuchsiaWorkflow = fuchsiaWorkflow,
_fuchsiaSdk = fuchsiaSdk,
_logger = logger,
super('Fuchsia devices');
final Platform _platform;
final FuchsiaWorkflow _fuchsiaWorkflow;
final FuchsiaSdk _fuchsiaSdk;
final Logger _logger;
@override
bool get supportsPlatform => isFuchsiaSupportedPlatform(_platform);
@override
bool get canListAnything => _fuchsiaWorkflow.canListDevices;
@override
Future<List<Device>> pollingGetDevices({ Duration timeout }) async {
if (!_fuchsiaWorkflow.canListDevices) {
return <Device>[];
}
final List<String> text = (await _fuchsiaSdk.listDevices(timeout: timeout))
?.split('\n');
if (text == null || text.isEmpty) {
return <Device>[];
}
final List<FuchsiaDevice> devices = <FuchsiaDevice>[];
for (final String line in text) {
final FuchsiaDevice device = await _parseDevice(line);
if (device == null) {
continue;
}
devices.add(device);
}
return devices;
}
@override
Future<List<String>> getDiagnostics() async => const <String>[];
Future<FuchsiaDevice> _parseDevice(String text) async {
final String line = text.trim();
// ['ip', 'device name']
final List<String> words = line.split(' ');
if (words.length < 2) {
return null;
}
final String name = words[1];
final String resolvedHost = await _fuchsiaSdk.fuchsiaDevFinder.resolve(
name,
local: false,
);
if (resolvedHost == null) {
_logger.printError('Failed to resolve host for Fuchsia device `$name`');
return null;
}
return FuchsiaDevice(resolvedHost, name: name);
}
}
class FuchsiaDevice extends Device {
FuchsiaDevice(String id, {this.name}) : super(
id,
platformType: PlatformType.fuchsia,
category: null,
ephemeral: true,
);
@override
bool get supportsHotReload => true;
@override
bool get supportsHotRestart => false;
@override
bool get supportsFlutterExit => false;
@override
final String name;
@override
Future<bool> get isLocalEmulator async => false;
@override
Future<String> get emulatorId async => null;
@override
bool get supportsStartPaused => false;
@override
Future<bool> isAppInstalled(ApplicationPackage app) async => false;
@override
Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => false;
@override
Future<bool> installApp(ApplicationPackage app) => Future<bool>.value(false);
@override
Future<bool> uninstallApp(ApplicationPackage app) async => false;
@override
bool isSupported() => true;
@override
Future<LaunchResult> startApp(
covariant FuchsiaApp package, {
String mainPath,
String route,
DebuggingOptions debuggingOptions,
Map<String, dynamic> platformArgs,
bool prebuiltApplication = false,
bool ipv6 = false,
}) async {
if (!prebuiltApplication) {
await buildFuchsia(fuchsiaProject: FlutterProject.current().fuchsia,
targetPlatform: await targetPlatform,
target: mainPath,
buildInfo: debuggingOptions.buildInfo);
}
// Stop the app if it's currently running.
await stopApp(package);
final String host = await fuchsiaSdk.fuchsiaDevFinder.resolve(
name,
local: true,
);
if (host == null) {
globals.printError('Failed to resolve host for Fuchsia device');
return LaunchResult.failed();
}
// Find out who the device thinks we are.
final int port = await globals.os.findFreePort();
if (port == 0) {
globals.printError('Failed to find a free port');
return LaunchResult.failed();
}
// Try Start with a fresh package repo in case one was left over from a
// previous run.
final Directory packageRepo =
globals.fs.directory(globals.fs.path.join(getFuchsiaBuildDirectory(), '.pkg-repo'));
try {
if (packageRepo.existsSync()) {
packageRepo.deleteSync(recursive: true);
}
packageRepo.createSync(recursive: true);
} on Exception catch (e) {
globals.printError('Failed to create Fuchisa package repo directory '
'at ${packageRepo.path}: $e');
return LaunchResult.failed();
}
final String appName = FlutterProject.current().manifest.appName;
final Status status = globals.logger.startProgress(
'Starting Fuchsia application $appName...',
timeout: null,
);
FuchsiaPackageServer fuchsiaPackageServer;
bool serverRegistered = false;
try {
// Ask amber to pre-fetch some things we'll need before setting up our own
// package server. This is to avoid relying on amber correctly using
// multiple package servers, support for which is in flux.
if (!await fuchsiaDeviceTools.amberCtl.getUp(this, 'tiles')) {
globals.printError('Failed to get amber to prefetch tiles');
return LaunchResult.failed();
}
if (!await fuchsiaDeviceTools.amberCtl.getUp(this, 'tiles_ctl')) {
globals.printError('Failed to get amber to prefetch tiles_ctl');
return LaunchResult.failed();
}
// Start up a package server.
const String packageServerName = FuchsiaPackageServer.toolHost;
fuchsiaPackageServer = FuchsiaPackageServer(
packageRepo.path, packageServerName, host, port);
if (!await fuchsiaPackageServer.start()) {
globals.printError('Failed to start the Fuchsia package server');
return LaunchResult.failed();
}
// Serve the application's package.
final File farArchive = package.farArchive(
debuggingOptions.buildInfo.mode);
if (!await fuchsiaPackageServer.addPackage(farArchive)) {
globals.printError('Failed to add package to the package server');
return LaunchResult.failed();
}
// Serve the flutter_runner.
final File flutterRunnerArchive = globals.fs.file(globals.artifacts.getArtifactPath(
Artifact.fuchsiaFlutterRunner,
platform: await targetPlatform,
mode: debuggingOptions.buildInfo.mode,
));
if (!await fuchsiaPackageServer.addPackage(flutterRunnerArchive)) {
globals.printError('Failed to add flutter_runner package to the package server');
return LaunchResult.failed();
}
// Teach the package controller about the package server.
if (!await fuchsiaDeviceTools.amberCtl.addRepoCfg(this, fuchsiaPackageServer)) {
globals.printError('Failed to teach amber about the package server');
return LaunchResult.failed();
}
serverRegistered = true;
// Tell the package controller to prefetch the flutter_runner.
String flutterRunnerName;
if (debuggingOptions.buildInfo.usesAot) {
if (debuggingOptions.buildInfo.mode.isRelease) {
flutterRunnerName = 'flutter_aot_product_runner';
} else {
flutterRunnerName = 'flutter_aot_runner';
}
} else {
if (debuggingOptions.buildInfo.mode.isRelease) {
flutterRunnerName = 'flutter_jit_product_runner';
} else {
flutterRunnerName = 'flutter_jit_runner';
}
}
if (!await fuchsiaDeviceTools.amberCtl.pkgCtlResolve(
this, fuchsiaPackageServer, flutterRunnerName)) {
globals.printError('Failed to get pkgctl to prefetch the flutter_runner');
return LaunchResult.failed();
}
// Tell the package controller to prefetch the app.
if (!await fuchsiaDeviceTools.amberCtl.pkgCtlResolve(
this, fuchsiaPackageServer, appName)) {
globals.printError('Failed to get pkgctl to prefetch the package');
return LaunchResult.failed();
}
// Ensure tiles_ctl is started, and start the app.
if (!await FuchsiaTilesCtl.ensureStarted(this)) {
globals.printError('Failed to ensure that tiles is started on the device');
return LaunchResult.failed();
}
// Instruct tiles_ctl to start the app.
final String fuchsiaUrl = 'fuchsia-pkg://$packageServerName/$appName#meta/$appName.cmx';
if (!await fuchsiaDeviceTools.tilesCtl.add(this, fuchsiaUrl, <String>[])) {
globals.printError('Failed to add the app to tiles');
return LaunchResult.failed();
}
} finally {
// Try to un-teach the package controller about the package server if
// needed.
if (serverRegistered) {
await fuchsiaDeviceTools.amberCtl.pkgCtlRepoRemove(this, fuchsiaPackageServer);
}
// Shutdown the package server and delete the package repo;
globals.printTrace("Shutting down the tool's package server.");
fuchsiaPackageServer?.stop();
globals.printTrace("Removing the tool's package repo: at ${packageRepo.path}");
try {
packageRepo.deleteSync(recursive: true);
} on Exception catch (e) {
globals.printError('Failed to remove Fuchsia package repo directory '
'at ${packageRepo.path}: $e.');
}
status.cancel();
}
if (debuggingOptions.buildInfo.mode.isRelease) {
globals.printTrace('App succesfully started in a release mode.');
return LaunchResult.succeeded();
}
globals.printTrace('App started in a non-release mode. Setting up vmservice connection.');
// In a debug or profile build, try to find the observatory uri.
final FuchsiaIsolateDiscoveryProtocol discovery =
getIsolateDiscoveryProtocol(appName);
try {
final Uri observatoryUri = await discovery.uri;
return LaunchResult.succeeded(observatoryUri: observatoryUri);
} finally {
discovery.dispose();
}
}
@override
Future<bool> stopApp(covariant FuchsiaApp app) async {
final int appKey = await FuchsiaTilesCtl.findAppKey(this, app.id);
if (appKey != -1) {
if (!await fuchsiaDeviceTools.tilesCtl.remove(this, appKey)) {
globals.printError('tiles_ctl remove on ${app.id} failed.');
return false;
}
}
return true;
}
TargetPlatform _targetPlatform;
Future<TargetPlatform> _queryTargetPlatform() async {
const TargetPlatform defaultTargetPlatform = TargetPlatform.fuchsia_arm64;
if (!globals.fuchsiaArtifacts.hasSshConfig) {
globals.printTrace('Could not determine Fuchsia target platform because '
'Fuchsia ssh configuration is missing.\n'
'Defaulting to arm64.');
return defaultTargetPlatform;
}
final RunResult result = await shell('uname -m');
if (result.exitCode != 0) {
globals.printError('Could not determine Fuchsia target platform type:\n$result\n'
'Defaulting to arm64.');
return defaultTargetPlatform;
}
final String machine = result.stdout.trim();
switch (machine) {
case 'aarch64':
return TargetPlatform.fuchsia_arm64;
case 'x86_64':
return TargetPlatform.fuchsia_x64;
default:
globals.printError('Unknown Fuchsia target platform "$machine". '
'Defaulting to arm64.');
return defaultTargetPlatform;
}
}
@override
bool get supportsScreenshot => isFuchsiaSupportedPlatform(globals.platform);
@override
Future<void> takeScreenshot(File outputFile) async {
if (outputFile.basename.split('.').last != 'ppm') {
throw '${outputFile.path} must be a .ppm file';
}
final RunResult screencapResult = await shell('screencap > /tmp/screenshot.ppm');
if (screencapResult.exitCode != 0) {
throw 'Could not take a screenshot on device $name:\n$screencapResult';
}
try {
final RunResult scpResult = await scp('/tmp/screenshot.ppm', outputFile.path);
if (scpResult.exitCode != 0) {
throw 'Failed to copy screenshot from device:\n$scpResult';
}
} finally {
try {
final RunResult deleteResult = await shell('rm /tmp/screenshot.ppm');
if (deleteResult.exitCode != 0) {
globals.printError(
'Failed to delete screenshot.ppm from the device:\n$deleteResult'
);
}
} on Exception catch (e) {
globals.printError(
'Failed to delete screenshot.ppm from the device: $e'
);
}
}
}
@override
Future<TargetPlatform> get targetPlatform async => _targetPlatform ??= await _queryTargetPlatform();
@override
Future<String> get sdkNameAndVersion async {
const String defaultName = 'Fuchsia';
if (!globals.fuchsiaArtifacts.hasSshConfig) {
globals.printTrace('Could not determine Fuchsia sdk name or version '
'because Fuchsia ssh configuration is missing.');
return defaultName;
}
const String versionPath = '/pkgfs/packages/build-info/0/data/version';
final RunResult catResult = await shell('cat $versionPath');
if (catResult.exitCode != 0) {
globals.printTrace('Failed to cat $versionPath: ${catResult.stderr}');
return defaultName;
}
final String version = catResult.stdout.trim();
if (version.isEmpty) {
globals.printTrace('$versionPath was empty');
return defaultName;
}
return 'Fuchsia $version';
}
@override
DeviceLogReader getLogReader({
ApplicationPackage app,
bool includePastLogs = false,
}) {
assert(!includePastLogs, 'Past log reading not supported on Fuchsia.');
return _logReader ??= _FuchsiaLogReader(this, app);
}
_FuchsiaLogReader _logReader;
@override
DevicePortForwarder get portForwarder =>
_portForwarder ??= _FuchsiaPortForwarder(this);
DevicePortForwarder _portForwarder;
@visibleForTesting
set portForwarder(DevicePortForwarder forwarder) {
_portForwarder = forwarder;
}
@override
void clearLogs() {}
bool _ipv6;
/// [true] if the current host address is IPv6.
bool get ipv6 => _ipv6 ??= isIPv6Address(id);
/// List the ports currently running a dart observatory.
Future<List<int>> servicePorts() async {
const String findCommand = 'find /hub -name vmservice-port';
final RunResult findResult = await shell(findCommand);
if (findResult.exitCode != 0) {
throwToolExit("'$findCommand' on device $name failed. stderr: '${findResult.stderr}'");
return null;
}
final String findOutput = findResult.stdout;
if (findOutput.trim() == '') {
throwToolExit(
'No Dart Observatories found. Are you running a debug build?');
return null;
}
final List<int> ports = <int>[];
for (final String path in findOutput.split('\n')) {
if (path == '') {
continue;
}
final String lsCommand = 'ls $path';
final RunResult lsResult = await shell(lsCommand);
if (lsResult.exitCode != 0) {
throwToolExit("'$lsCommand' on device $name failed");
return null;
}
final String lsOutput = lsResult.stdout;
for (final String line in lsOutput.split('\n')) {
if (line == '') {
continue;
}
final int port = int.tryParse(line);
if (port != null) {
ports.add(port);
}
}
}
return ports;
}
/// Run `command` on the Fuchsia device shell.
Future<RunResult> shell(String command) async {
if (globals.fuchsiaArtifacts.sshConfig == null) {
throwToolExit('Cannot interact with device. No ssh config.\n'
'Try setting FUCHSIA_SSH_CONFIG or FUCHSIA_BUILD_DIR.');
}
return await processUtils.run(<String>[
'ssh',
'-F',
globals.fuchsiaArtifacts.sshConfig.absolute.path,
id, // Device's IP address.
command,
]);
}
/// Transfer the file [origin] from the device to [destination].
Future<RunResult> scp(String origin, String destination) async {
if (globals.fuchsiaArtifacts.sshConfig == null) {
throwToolExit('Cannot interact with device. No ssh config.\n'
'Try setting FUCHSIA_SSH_CONFIG or FUCHSIA_BUILD_DIR.');
}
return await processUtils.run(<String>[
'scp',
'-F',
globals.fuchsiaArtifacts.sshConfig.absolute.path,
'$id:$origin',
destination,
]);
}
/// Finds the first port running a VM matching `isolateName` from the
/// provided set of `ports`.
///
/// Returns null if no isolate port can be found.
///
// TODO(jonahwilliams): replacing this with the hub will require an update
// to the flutter_runner.
Future<int> findIsolatePort(String isolateName, List<int> ports) async {
for (final int port in ports) {
try {
// Note: The square-bracket enclosure for using the IPv6 loopback
// didn't appear to work, but when assigning to the IPv4 loopback device,
// netstat shows that the local port is actually being used on the IPv6
// loopback (::1).
final Uri uri = Uri.parse('http://[$_ipv6Loopback]:$port');
final vm_service.VmService vmService = await connectToVmService(uri);
final List<FlutterView> flutterViews = await vmService.getFlutterViews();
for (final FlutterView flutterView in flutterViews) {
if (flutterView.uiIsolate == null) {
continue;
}
if (flutterView.uiIsolate.name.contains(isolateName)) {
return vmService.httpAddress.port;
}
}
} on SocketException catch (err) {
globals.printTrace('Failed to connect to $port: $err');
}
}
throwToolExit('No ports found running $isolateName');
return null;
}
FuchsiaIsolateDiscoveryProtocol getIsolateDiscoveryProtocol(String isolateName) {
return FuchsiaIsolateDiscoveryProtocol(this, isolateName);
}
@override
bool isSupportedForProject(FlutterProject flutterProject) {
return flutterProject.fuchsia.existsSync();
}
@override
Future<void> dispose() async {
await _portForwarder?.dispose();
}
}
class FuchsiaIsolateDiscoveryProtocol {
FuchsiaIsolateDiscoveryProtocol(
this._device,
this._isolateName, [
this._vmServiceConnector = _kDefaultFuchsiaIsolateDiscoveryConnector,
this._pollOnce = false,
]);
static const Duration _pollDuration = Duration(seconds: 10);
final Map<int, vm_service.VmService> _ports = <int, vm_service.VmService>{};
final FuchsiaDevice _device;
final String _isolateName;
final Completer<Uri> _foundUri = Completer<Uri>();
final Future<vm_service.VmService> Function(Uri) _vmServiceConnector;
// whether to only poll once.
final bool _pollOnce;
Timer _pollingTimer;
Status _status;
FutureOr<Uri> get uri {
if (_uri != null) {
return _uri;
}
_status ??= globals.logger.startProgress(
'Waiting for a connection from $_isolateName on ${_device.name}...',
timeout: null, // could take an arbitrary amount of time
);
unawaited(_findIsolate()); // Completes the _foundUri Future.
return _foundUri.future.then((Uri uri) {
_uri = uri;
return uri;
});
}
Uri _uri;
void dispose() {
if (!_foundUri.isCompleted) {
_status?.cancel();
_status = null;
_pollingTimer?.cancel();
_pollingTimer = null;
_foundUri.completeError(Exception('Did not complete'));
}
}
Future<void> _findIsolate() async {
final List<int> ports = await _device.servicePorts();
for (final int port in ports) {
vm_service.VmService service;
if (_ports.containsKey(port)) {
service = _ports[port];
} else {
final int localPort = await _device.portForwarder.forward(port);
try {
final Uri uri = Uri.parse('http://[$_ipv6Loopback]:$localPort');
service = await _vmServiceConnector(uri);
_ports[port] = service;
} on SocketException catch (err) {
globals.printTrace('Failed to connect to $localPort: $err');
continue;
}
}
final List<FlutterView> flutterViews = await service.getFlutterViews();
for (final FlutterView flutterView in flutterViews) {
if (flutterView.uiIsolate == null) {
continue;
}
if (flutterView.uiIsolate.name.contains(_isolateName)) {
_foundUri.complete(_device.ipv6
? Uri.parse('http://[$_ipv6Loopback]:${service.httpAddress.port}/')
: Uri.parse('http://$_ipv4Loopback:${service.httpAddress.port}/'));
_status.stop();
return;
}
}
}
if (_pollOnce) {
_foundUri.completeError(Exception('Max iterations exceeded'));
_status.stop();
return;
}
_pollingTimer = Timer(_pollDuration, _findIsolate);
}
}
class _FuchsiaPortForwarder extends DevicePortForwarder {
_FuchsiaPortForwarder(this.device);
final FuchsiaDevice device;
final Map<int, Process> _processes = <int, Process>{};
@override
Future<int> forward(int devicePort, {int hostPort}) async {
hostPort ??= await globals.os.findFreePort();
if (hostPort == 0) {
throwToolExit('Failed to forward port $devicePort. No free host-side ports');
}
// Note: the provided command works around a bug in -N, see US-515
// for more explanation.
final List<String> command = <String>[
'ssh',
'-6',
'-F',
globals.fuchsiaArtifacts.sshConfig.absolute.path,
'-nNT',
'-vvv',
'-f',
'-L',
'$hostPort:$_ipv4Loopback:$devicePort',
device.id, // Device's IP address.
'true',
];
final Process process = await globals.processManager.start(command);
unawaited(process.exitCode.then((int exitCode) {
if (exitCode != 0) {
throwToolExit('Failed to forward port:$devicePort');
}
}));
_processes[hostPort] = process;
_forwardedPorts.add(ForwardedPort(hostPort, devicePort));
return hostPort;
}
@override
List<ForwardedPort> get forwardedPorts => _forwardedPorts;
final List<ForwardedPort> _forwardedPorts = <ForwardedPort>[];
@override
Future<void> unforward(ForwardedPort forwardedPort) async {
_forwardedPorts.remove(forwardedPort);
final Process process = _processes.remove(forwardedPort.hostPort);
process?.kill();
final List<String> command = <String>[
'ssh',
'-F',
globals.fuchsiaArtifacts.sshConfig.absolute.path,
'-O',
'cancel',
'-vvv',
'-L',
'${forwardedPort.hostPort}:$_ipv4Loopback:${forwardedPort.devicePort}',
device.id, // Device's IP address.
];
final ProcessResult result = await globals.processManager.run(command);
if (result.exitCode != 0) {
throwToolExit(
'Unforward command failed:\n'
'stdout: ${result.stdout}\n'
'stderr: ${result.stderr}'
);
}
}
@override
Future<void> dispose() async {
final List<ForwardedPort> forwardedPortsCopy =
List<ForwardedPort>.of(forwardedPorts);
for (final ForwardedPort port in forwardedPortsCopy) {
await unforward(port);
}
}
}