fuchsia_device.dart 23.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

7 8
import 'package:meta/meta.dart';

9
import '../application_package.dart';
10
import '../artifacts.dart';
11
import '../base/common.dart';
12 13
import '../base/context.dart';
import '../base/file_system.dart';
14
import '../base/io.dart';
15
import '../base/logger.dart';
16
import '../base/os.dart';
17
import '../base/platform.dart';
18 19
import '../base/process.dart';
import '../base/process_manager.dart';
20
import '../base/time.dart';
21 22
import '../build_info.dart';
import '../device.dart';
23
import '../globals.dart';
24
import '../project.dart';
25
import '../vmservice.dart';
26

27 28 29 30
import 'amber_ctl.dart';
import 'application_package.dart';
import 'fuchsia_build.dart';
import 'fuchsia_pm.dart';
31 32
import 'fuchsia_sdk.dart';
import 'fuchsia_workflow.dart';
33 34 35 36 37 38 39 40 41 42 43 44 45 46
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();
}

47 48 49
final String _ipv4Loopback = InternetAddress.loopbackIPv4.address;
final String _ipv6Loopback = InternetAddress.loopbackIPv6.address;

50 51 52 53 54
// Enables testing the fuchsia isolate discovery
Future<VMService> _kDefaultFuchsiaIsolateDiscoveryConnector(Uri uri) {
  return VMService.connect(uri);
}

55 56
/// Read the log for a particular device.
class _FuchsiaLogReader extends DeviceLogReader {
57
  _FuchsiaLogReader(this._device, [this._app]);
58

59 60
  // \S matches non-whitespace characters.
  static final RegExp _flutterLogOutput = RegExp(r'INFO: \S+\(flutter\): ');
61

62 63
  final FuchsiaDevice _device;
  final ApplicationPackage _app;
64

65 66
  @override
  String get name => _device.name;
67 68 69 70

  Stream<String> _logLines;
  @override
  Stream<String> get logLines {
71 72
    final Stream<String> logStream = fuchsiaSdk.syslogs(_device.id);
    _logLines ??= _processLogs(logStream);
73 74 75
    return _logLines;
  }

76
  Stream<String> _processLogs(Stream<String> lines) {
77 78 79
    if (lines == null) {
      return null;
    }
80 81 82 83 84 85
    // 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
86
        ? _flutterLogOutput
87
        : RegExp('INFO: ${_app.name}(\.cmx)?\\(flutter\\): ');
88 89
    return Stream<String>.eventTransformed(
      lines,
90
      (EventSink<String> output) => _FuchsiaLogSink(output, matchRegExp, startTime),
91
    );
92 93
  }

94 95
  @override
  String toString() => name;
96 97 98 99 100 101

  @override
  void dispose() {
    // The Fuchsia SDK syslog process is killed when the subscription to the
    // logLines Stream is canceled.
  }
102 103
}

104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
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;
    }
125 126
    _outputSink.add(
        '[${logTime.toLocal()}] Flutter: ${line.split(_matchRegExp).last}');
127 128 129
  }

  @override
130
  void addError(Object error, [StackTrace stackTrace]) {
131 132 133 134
    _outputSink.addError(error, stackTrace);
  }

  @override
135 136 137
  void close() {
    _outputSink.close();
  }
138 139
}

140 141 142 143 144 145 146 147 148 149 150 151 152 153
class FuchsiaDevices extends PollingDeviceDiscovery {
  FuchsiaDevices() : super('Fuchsia devices');

  @override
  bool get supportsPlatform => platform.isLinux || platform.isMacOS;

  @override
  bool get canListAnything => fuchsiaWorkflow.canListDevices;

  @override
  Future<List<Device>> pollingGetDevices() async {
    if (!fuchsiaWorkflow.canListDevices) {
      return <Device>[];
    }
154
    final String text = await fuchsiaSdk.listDevices();
155
    if (text == null || text.isEmpty) {
156 157
      return <Device>[];
    }
158
    final List<FuchsiaDevice> devices = parseListDevices(text);
159
    return devices;
160 161 162 163 164 165 166
  }

  @override
  Future<List<String>> getDiagnostics() async => const <String>[];
}

@visibleForTesting
167 168
List<FuchsiaDevice> parseListDevices(String text) {
  final List<FuchsiaDevice> devices = <FuchsiaDevice>[];
169 170
  for (String rawLine in text.trim().split('\n')) {
    final String line = rawLine.trim();
171
    // ['ip', 'device name']
172
    final List<String> words = line.split(' ');
173 174 175
    if (words.length < 2) {
      continue;
    }
176
    final String name = words[1];
177 178
    final String id = words[0];
    devices.add(FuchsiaDevice(id, name: name));
179
  }
180
  return devices;
181 182
}

183
class FuchsiaDevice extends Device {
184 185 186 187 188 189
  FuchsiaDevice(String id, {this.name}) : super(
      id,
      platformType: PlatformType.fuchsia,
      category: null,
      ephemeral: false,
  );
190 191

  @override
192 193 194 195
  bool get supportsHotReload => true;

  @override
  bool get supportsHotRestart => false;
196

197
  @override
198
  bool get supportsFlutterExit => false;
199

200 201 202 203
  @override
  final String name;

  @override
204
  Future<bool> get isLocalEmulator async => false;
205

206 207 208
  @override
  Future<String> get emulatorId async => null;

209 210 211 212
  @override
  bool get supportsStartPaused => false;

  @override
213
  Future<bool> isAppInstalled(ApplicationPackage app) async => false;
214 215

  @override
216
  Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => false;
217 218

  @override
219
  Future<bool> installApp(ApplicationPackage app) => Future<bool>.value(false);
220 221

  @override
222
  Future<bool> uninstallApp(ApplicationPackage app) async => false;
223 224 225 226 227 228

  @override
  bool isSupported() => true;

  @override
  Future<LaunchResult> startApp(
229
    covariant FuchsiaApp package, {
230 231 232 233
    String mainPath,
    String route,
    DebuggingOptions debuggingOptions,
    Map<String, dynamic> platformArgs,
234 235
    bool prebuiltApplication = false,
    bool ipv6 = false,
236 237 238
  }) async {
    if (!prebuiltApplication) {
      await buildFuchsia(fuchsiaProject: FlutterProject.current().fuchsia,
239
                         targetPlatform: await targetPlatform,
240 241 242 243 244 245
                         target: mainPath,
                         buildInfo: debuggingOptions.buildInfo);
    }
    // Stop the app if it's currently running.
    await stopApp(package);
    // Find out who the device thinks we are.
246 247 248 249
    final String host = await fuchsiaSdk.fuchsiaDevFinder.resolve(
      name,
      local: true,
    );
250 251 252 253
    if (host == null) {
      printError('Failed to resolve host for Fuchsia device');
      return LaunchResult.failed();
    }
254 255 256 257 258
    final int port = await os.findFreePort();
    if (port == 0) {
      printError('Failed to find a free port');
      return LaunchResult.failed();
    }
259 260 261

    // Try Start with a fresh package repo in case one was left over from a
    // previous run.
262 263
    final Directory packageRepo =
        fs.directory(fs.path.join(getFuchsiaBuildDirectory(), '.pkg-repo'));
264 265 266 267 268 269 270 271 272 273
    try {
      if (packageRepo.existsSync()) {
        packageRepo.deleteSync(recursive: true);
      }
      packageRepo.createSync(recursive: true);
    } catch (e) {
      printError('Failed to create Fuchisa package repo directory '
                 'at ${packageRepo.path}: $e');
      return LaunchResult.failed();
    }
274 275 276

    final String appName = FlutterProject.current().manifest.appName;
    final Status status = logger.startProgress(
277
      'Starting Fuchsia application $appName...',
278 279 280 281 282
      timeout: null,
    );
    FuchsiaPackageServer fuchsiaPackageServer;
    bool serverRegistered = false;
    try {
283 284 285 286 287 288 289 290 291 292 293 294
      // 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')) {
        printError('Failed to get amber to prefetch tiles');
        return LaunchResult.failed();
      }
      if (!await fuchsiaDeviceTools.amberCtl.getUp(this, 'tiles_ctl')) {
        printError('Failed to get amber to prefetch tiles_ctl');
        return LaunchResult.failed();
      }

295
      // Start up a package server.
296
      const String packageServerName = FuchsiaPackageServer.toolHost;
297 298
      fuchsiaPackageServer = FuchsiaPackageServer(
          packageRepo.path, packageServerName, host, port);
299 300 301 302
      if (!await fuchsiaPackageServer.start()) {
        printError('Failed to start the Fuchsia package server');
        return LaunchResult.failed();
      }
303 304

      // Serve the application's package.
305 306 307 308 309 310 311
      final File farArchive = package.farArchive(
          debuggingOptions.buildInfo.mode);
      if (!await fuchsiaPackageServer.addPackage(farArchive)) {
        printError('Failed to add package to the package server');
        return LaunchResult.failed();
      }

312 313
      // Serve the flutter_runner.
      final File flutterRunnerArchive = fs.file(artifacts.getArtifactPath(
314
        Artifact.fuchsiaFlutterRunner,
315 316 317 318 319 320 321 322
        platform: await targetPlatform,
        mode: debuggingOptions.buildInfo.mode,
      ));
      if (!await fuchsiaPackageServer.addPackage(flutterRunnerArchive)) {
        printError('Failed to add flutter_runner package to the package server');
        return LaunchResult.failed();
      }

323 324
      // Teach the package controller about the package server.
      if (!await fuchsiaDeviceTools.amberCtl.addRepoCfg(this, fuchsiaPackageServer)) {
325 326 327 328 329
        printError('Failed to teach amber about the package server');
        return LaunchResult.failed();
      }
      serverRegistered = true;

330
      // Tell the package controller to prefetch the flutter_runner.
331 332 333 334 335 336 337 338 339 340 341 342 343
      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';
        }
344 345 346 347 348 349 350
      }
      if (!await fuchsiaDeviceTools.amberCtl.pkgCtlResolve(
          this, fuchsiaPackageServer, flutterRunnerName)) {
        printError('Failed to get pkgctl to prefetch the flutter_runner');
        return LaunchResult.failed();
      }

351 352 353 354
      // Tell the package controller to prefetch the app.
      if (!await fuchsiaDeviceTools.amberCtl.pkgCtlResolve(
          this, fuchsiaPackageServer, appName)) {
        printError('Failed to get pkgctl to prefetch the package');
355 356 357 358 359 360 361 362 363 364
        return LaunchResult.failed();
      }

      // Ensure tiles_ctl is started, and start the app.
      if (!await FuchsiaTilesCtl.ensureStarted(this)) {
        printError('Failed to ensure that tiles is started on the device');
        return LaunchResult.failed();
      }

      // Instruct tiles_ctl to start the app.
365
      final String fuchsiaUrl = 'fuchsia-pkg://$packageServerName/$appName#meta/$appName.cmx';
366
      if (!await fuchsiaDeviceTools.tilesCtl.add(this, fuchsiaUrl, <String>[])) {
367 368 369 370
        printError('Failed to add the app to tiles');
        return LaunchResult.failed();
      }
    } finally {
371 372
      // Try to un-teach the package controller about the package server if
      // needed.
373
      if (serverRegistered) {
374
        await fuchsiaDeviceTools.amberCtl.pkgCtlRepoRemove(this, fuchsiaPackageServer);
375 376
      }
      // Shutdown the package server and delete the package repo;
377
      printTrace('Shutting down the tool\'s package server.');
378
      fuchsiaPackageServer?.stop();
379 380 381 382 383 384 385
      printTrace('Removing the tool\'s package repo: at ${packageRepo.path}');
      try {
        packageRepo.deleteSync(recursive: true);
      } catch (e) {
        printError('Failed to remove Fuchsia package repo directory '
                   'at ${packageRepo.path}: $e.');
      }
386 387 388
      status.cancel();
    }

389 390
    if (debuggingOptions.buildInfo.mode.isRelease) {
      printTrace('App succesfully started in a release mode.');
391 392
      return LaunchResult.succeeded();
    }
393
    printTrace('App started in a non-release mode. Setting up vmservice connection.');
394 395 396

    // In a debug or profile build, try to find the observatory uri.
    final FuchsiaIsolateDiscoveryProtocol discovery =
397
      getIsolateDiscoveryProtocol(appName);
398 399 400 401 402 403 404
    try {
      final Uri observatoryUri = await discovery.uri;
      return LaunchResult.succeeded(observatoryUri: observatoryUri);
    } finally {
      discovery.dispose();
    }
  }
405 406

  @override
407 408 409
  Future<bool> stopApp(covariant FuchsiaApp app) async {
    final int appKey = await FuchsiaTilesCtl.findAppKey(this, app.id);
    if (appKey != -1) {
410
      if (!await fuchsiaDeviceTools.tilesCtl.remove(this, appKey)) {
411 412 413 414 415
        printError('tiles_ctl remove on ${app.id} failed.');
        return false;
      }
    }
    return true;
416 417
  }

418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
  TargetPlatform _targetPlatform;

  Future<TargetPlatform> _queryTargetPlatform() async {
    final RunResult result = await shell('uname -m');
    if (result.exitCode != 0) {
      printError('Could not determine Fuchsia target platform type:\n$result\n'
                 'Defaulting to arm64.');
      return TargetPlatform.fuchsia_arm64;
    }
    final String machine = result.stdout.trim();
    switch (machine) {
      case 'aarch64':
        return TargetPlatform.fuchsia_arm64;
      case 'x86_64':
        return TargetPlatform.fuchsia_x64;
      default:
        printError('Unknown Fuchsia target platform "$machine". '
                   'Defaulting to arm64.');
        return TargetPlatform.fuchsia_arm64;
    }
  }

440
  @override
441
  Future<TargetPlatform> get targetPlatform async => _targetPlatform ??= await _queryTargetPlatform();
442 443

  @override
444 445 446 447 448 449 450 451 452 453 454 455 456 457
  Future<String> get sdkNameAndVersion async {
    const String versionPath = '/pkgfs/packages/build-info/0/data/version';
    final RunResult catResult = await shell('cat $versionPath');
    if (catResult.exitCode != 0) {
      printTrace('Failed to cat $versionPath: ${catResult.stderr}');
      return 'Fuchsia';
    }
    final String version = catResult.stdout.trim();
    if (version.isEmpty) {
      printTrace('$versionPath was empty');
      return 'Fuchsia';
    }
    return 'Fuchsia $version';
  }
458 459

  @override
460 461
  DeviceLogReader getLogReader({ApplicationPackage app}) =>
      _logReader ??= _FuchsiaLogReader(this, app);
462
  _FuchsiaLogReader _logReader;
463 464

  @override
465 466
  DevicePortForwarder get portForwarder =>
      _portForwarder ??= _FuchsiaPortForwarder(this);
467 468 469 470 471 472
  DevicePortForwarder _portForwarder;

  @visibleForTesting
  set portForwarder(DevicePortForwarder forwarder) {
    _portForwarder = forwarder;
  }
473 474

  @override
475
  void clearLogs() {}
476 477 478

  @override
  bool get supportsScreenshot => false;
479

480
  Future<bool> get ipv6 async {
481
    // Workaround for https://github.com/dart-lang/sdk/issues/29456
482
    final String fragment = (await _resolvedIp).split('%').first;
483 484 485 486 487 488 489 490
    try {
      Uri.parseIPv6Address(fragment);
      return true;
    } on FormatException {
      return false;
    }
  }

491 492
  /// List the ports currently running a dart observatory.
  Future<List<int>> servicePorts() async {
493 494 495
    const String findCommand = 'find /hub -name vmservice-port';
    final RunResult findResult = await shell(findCommand);
    if (findResult.exitCode != 0) {
496
      throwToolExit("'$findCommand' on device $name failed. stderr: '${findResult.stderr}'");
497 498 499
      return null;
    }
    final String findOutput = findResult.stdout;
500
    if (findOutput.trim() == '') {
501 502
      throwToolExit(
          'No Dart Observatories found. Are you running a debug build?');
503 504 505 506 507 508 509
      return null;
    }
    final List<int> ports = <int>[];
    for (String path in findOutput.split('\n')) {
      if (path == '') {
        continue;
      }
510 511 512
      final String lsCommand = 'ls $path';
      final RunResult lsResult = await shell(lsCommand);
      if (lsResult.exitCode != 0) {
513
        throwToolExit("'$lsCommand' on device $name failed");
514 515 516
        return null;
      }
      final String lsOutput = lsResult.stdout;
517 518 519 520 521 522 523 524 525 526 527
      for (String line in lsOutput.split('\n')) {
        if (line == '') {
          continue;
        }
        final int port = int.tryParse(line);
        if (port != null) {
          ports.add(port);
        }
      }
    }
    return ports;
528 529
  }

530 531 532 533 534 535 536 537 538
  String _cachedResolvedIp;

  Future<String> get _resolvedIp async {
    return _cachedResolvedIp ??= await fuchsiaSdk.fuchsiaDevFinder.resolve(
      name,
      local: false,
    );
  }

539
  /// Run `command` on the Fuchsia device shell.
540 541 542 543 544
  Future<RunResult> shell(String command) async {
    if (fuchsiaArtifacts.sshConfig == null) {
      throwToolExit('Cannot interact with device. No ssh config.\n'
                    'Try setting FUCHSIA_SSH_CONFIG or FUCHSIA_BUILD_DIR.');
    }
545
    return await processUtils.run(<String>[
546 547 548
      'ssh',
      '-F',
      fuchsiaArtifacts.sshConfig.absolute.path,
549
      await _resolvedIp,
550
      command,
551
    ]);
552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587
  }

  /// 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 (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 VMService vmService = await VMService.connect(uri);
        await vmService.getVM();
        await vmService.refreshViews();
        for (FlutterView flutterView in vmService.vm.views) {
          if (flutterView.uiIsolate == null) {
            continue;
          }
          final Uri address = flutterView.owner.vmService.httpAddress;
          if (flutterView.uiIsolate.name.contains(isolateName)) {
            return address.port;
          }
        }
      } on SocketException catch (err) {
        printTrace('Failed to connect to $port: $err');
      }
    }
    throwToolExit('No ports found running $isolateName');
    return null;
  }
588

589 590 591
  FuchsiaIsolateDiscoveryProtocol getIsolateDiscoveryProtocol(String isolateName) {
    return FuchsiaIsolateDiscoveryProtocol(this, isolateName);
  }
592 593

  @override
594 595 596
  bool isSupportedForProject(FlutterProject flutterProject) {
    return flutterProject.fuchsia.existsSync();
  }
597 598 599 600 601

  @override
  Future<void> dispose() async {
    await _portForwarder?.dispose();
  }
602 603 604
}

class FuchsiaIsolateDiscoveryProtocol {
605 606 607
  FuchsiaIsolateDiscoveryProtocol(
    this._device,
    this._isolateName, [
608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628
    this._vmServiceConnector = _kDefaultFuchsiaIsolateDiscoveryConnector,
    this._pollOnce = false,
  ]);

  static const Duration _pollDuration = Duration(seconds: 10);
  final Map<int, VMService> _ports = <int, VMService>{};
  final FuchsiaDevice _device;
  final String _isolateName;
  final Completer<Uri> _foundUri = Completer<Uri>();
  final Future<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 ??= logger.startProgress(
      'Waiting for a connection from $_isolateName on ${_device.name}...',
629
      timeout: null, // could take an arbitrary amount of time
630
    );
631
    unawaited(_findIsolate());  // Completes the _foundUri Future.
632 633 634 635 636
    return _foundUri.future.then((Uri uri) {
      _uri = uri;
      return uri;
    });
  }
637

638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674
  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 (int port in ports) {
      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) {
          printTrace('Failed to connect to $localPort: $err');
          continue;
        }
      }
      await service.getVM();
      await service.refreshViews();
      for (FlutterView flutterView in service.vm.views) {
        if (flutterView.uiIsolate == null) {
          continue;
        }
        final Uri address = flutterView.owner.vmService.httpAddress;
        if (flutterView.uiIsolate.name.contains(_isolateName)) {
675
          _foundUri.complete(await _device.ipv6
676 677
              ? Uri.parse('http://[$_ipv6Loopback]:${address.port}/')
              : Uri.parse('http://$_ipv4Loopback:${address.port}/'));
678 679 680 681 682 683 684 685 686 687 688 689
          _status.stop();
          return;
        }
      }
    }
    if (_pollOnce) {
      _foundUri.completeError(Exception('Max iterations exceeded'));
      _status.stop();
      return;
    }
    _pollingTimer = Timer(_pollDuration, _findIsolate);
  }
690 691 692 693 694 695 696 697 698
}

class _FuchsiaPortForwarder extends DevicePortForwarder {
  _FuchsiaPortForwarder(this.device);

  final FuchsiaDevice device;
  final Map<int, Process> _processes = <int, Process>{};

  @override
699
  Future<int> forward(int devicePort, {int hostPort}) async {
700 701 702 703
    hostPort ??= await os.findFreePort();
    if (hostPort == 0) {
      throwToolExit('Failed to forward port $devicePort. No free host-side ports');
    }
704 705 706
    // Note: the provided command works around a bug in -N, see US-515
    // for more explanation.
    final List<String> command = <String>[
707 708 709 710 711 712 713 714 715
      'ssh',
      '-6',
      '-F',
      fuchsiaArtifacts.sshConfig.absolute.path,
      '-nNT',
      '-vvv',
      '-f',
      '-L',
      '$hostPort:$_ipv4Loopback:$devicePort',
716
      await device._resolvedIp,
717
      'true',
718 719
    ];
    final Process process = await processManager.start(command);
720
    unawaited(process.exitCode.then((int exitCode) {
721 722 723
      if (exitCode != 0) {
        throwToolExit('Failed to forward port:$devicePort');
      }
724
    }));
725 726 727 728 729 730 731 732 733 734 735 736 737 738 739
    _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>[
740 741 742 743 744 745 746 747
      'ssh',
      '-F',
      fuchsiaArtifacts.sshConfig.absolute.path,
      '-O',
      'cancel',
      '-vvv',
      '-L',
      '${forwardedPort.hostPort}:$_ipv4Loopback:${forwardedPort.devicePort}',
748
      await device._resolvedIp,
749
    ];
750 751
    final ProcessResult result = await processManager.run(command);
    if (result.exitCode != 0) {
752
      throwToolExit('Unforward command failed: $result');
753 754
    }
  }
755 756 757 758 759 760 761

  @override
  Future<void> dispose() async {
    for (ForwardedPort port in forwardedPorts) {
      await unforward(port);
    }
  }
762
}