fuchsia_device.dart 14.3 KB
Newer Older
1 2 3 4 5 6
// Copyright 2017 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';

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

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

24 25 26
import 'fuchsia_sdk.dart';
import 'fuchsia_workflow.dart';

27 28 29
final String _ipv4Loopback = InternetAddress.loopbackIPv4.address;
final String _ipv6Loopback = InternetAddress.loopbackIPv6.address;

30 31 32 33 34
// Enables testing the fuchsia isolate discovery
Future<VMService> _kDefaultFuchsiaIsolateDiscoveryConnector(Uri uri) {
  return VMService.connect(uri);
}

35 36
/// Read the log for a particular device.
class _FuchsiaLogReader extends DeviceLogReader {
37
  _FuchsiaLogReader(this._device, [this._app]);
38

39 40
  // \S matches non-whitespace characters.
  static final RegExp _flutterLogOutput = RegExp(r'INFO: \S+\(flutter\): ');
41

42
  FuchsiaDevice _device;
43
  ApplicationPackage _app;
44

45 46
  @override
  String get name => _device.name;
47 48 49 50

  Stream<String> _logLines;
  @override
  Stream<String> get logLines {
51
    _logLines ??= _processLogs(fuchsiaSdk.syslogs(_device.id));
52 53 54
    return _logLines;
  }

55
  Stream<String> _processLogs(Stream<String> lines) {
56 57 58 59 60 61
    // 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
62 63
        ? _flutterLogOutput
        : RegExp('INFO: ${_app.name}\\(flutter\\): ');
64 65 66 67
    return Stream<String>.eventTransformed(
      lines,
      (Sink<String> outout) => _FuchsiaLogSink(outout, matchRegExp, startTime),
    );
68 69
  }

70 71 72 73
  @override
  String toString() => name;
}

74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
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;
    }
95 96
    _outputSink.add(
        '[${logTime.toLocal()}] Flutter: ${line.split(_matchRegExp).last}');
97 98 99
  }

  @override
100
  void addError(Object error, [StackTrace stackTrace]) {
101 102 103 104
    _outputSink.addError(error, stackTrace);
  }

  @override
105 106 107
  void close() {
    _outputSink.close();
  }
108 109
}

110 111 112 113 114 115 116 117 118 119 120 121 122 123
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>[];
    }
124
    final String text = await fuchsiaSdk.listDevices();
125
    if (text == null || text.isEmpty) {
126 127
      return <Device>[];
    }
128
    final List<FuchsiaDevice> devices = parseListDevices(text);
129
    return devices;
130 131 132 133 134 135 136
  }

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

@visibleForTesting
137 138
List<FuchsiaDevice> parseListDevices(String text) {
  final List<FuchsiaDevice> devices = <FuchsiaDevice>[];
139 140
  for (String rawLine in text.trim().split('\n')) {
    final String line = rawLine.trim();
141
    // ['ip', 'device name']
142
    final List<String> words = line.split(' ');
143 144 145
    if (words.length < 2) {
      continue;
    }
146
    final String name = words[1];
147 148
    final String id = words[0];
    devices.add(FuchsiaDevice(id, name: name));
149
  }
150
  return devices;
151 152
}

153
class FuchsiaDevice extends Device {
154
  FuchsiaDevice(String id, {this.name}) : super(id);
155 156

  @override
157 158 159 160
  bool get supportsHotReload => true;

  @override
  bool get supportsHotRestart => false;
161

162 163 164
  @override
  bool get supportsStopApp => false;

165 166 167 168
  @override
  final String name;

  @override
169
  Future<bool> get isLocalEmulator async => false;
170 171 172 173 174

  @override
  bool get supportsStartPaused => false;

  @override
175
  Future<bool> isAppInstalled(ApplicationPackage app) async => false;
176 177

  @override
178
  Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => false;
179 180

  @override
181
  Future<bool> installApp(ApplicationPackage app) => Future<bool>.value(false);
182 183

  @override
184
  Future<bool> uninstallApp(ApplicationPackage app) async => false;
185 186 187 188 189 190

  @override
  bool isSupported() => true;

  @override
  Future<LaunchResult> startApp(
191
    ApplicationPackage package, {
192 193 194 195
    String mainPath,
    String route,
    DebuggingOptions debuggingOptions,
    Map<String, dynamic> platformArgs,
196
    bool prebuiltApplication = false,
197
    bool usesTerminalUi = true,
198
    bool ipv6 = false,
199 200
  }) =>
      Future<void>.error('unimplemented');
201 202 203 204 205 206 207 208

  @override
  Future<bool> stopApp(ApplicationPackage app) async {
    // Currently we don't have a way to stop an app running on Fuchsia.
    return false;
  }

  @override
209
  Future<TargetPlatform> get targetPlatform async => TargetPlatform.fuchsia;
210 211

  @override
212
  Future<String> get sdkNameAndVersion async => 'Fuchsia';
213 214

  @override
215 216
  DeviceLogReader getLogReader({ApplicationPackage app}) =>
      _logReader ??= _FuchsiaLogReader(this, app);
217
  _FuchsiaLogReader _logReader;
218 219

  @override
220 221
  DevicePortForwarder get portForwarder =>
      _portForwarder ??= _FuchsiaPortForwarder(this);
222
  _FuchsiaPortForwarder _portForwarder;
223 224

  @override
225
  void clearLogs() {}
226

227 228 229 230 231 232 233 234 235 236
  @override
  OverrideArtifacts get artifactOverrides {
    return _artifactOverrides ??= OverrideArtifacts(
      parent: Artifacts.instance,
      platformKernelDill: fuchsiaArtifacts.platformKernelDill,
      flutterPatchedSdk: fuchsiaArtifacts.flutterPatchedSdk,
    );
  }
  OverrideArtifacts _artifactOverrides;

237 238
  @override
  bool get supportsScreenshot => false;
239

240 241 242 243 244 245 246 247 248 249 250
  bool get ipv6 {
    // Workaround for https://github.com/dart-lang/sdk/issues/29456
    final String fragment = id.split('%').first;
    try {
      Uri.parseIPv6Address(fragment);
      return true;
    } on FormatException {
      return false;
    }
  }

251 252
  /// List the ports currently running a dart observatory.
  Future<List<int>> servicePorts() async {
253 254
    final String findOutput = await shell('find /hub -name vmservice-port');
    if (findOutput.trim() == '') {
255 256
      throwToolExit(
          'No Dart Observatories found. Are you running a debug build?');
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
      return null;
    }
    final List<int> ports = <int>[];
    for (String path in findOutput.split('\n')) {
      if (path == '') {
        continue;
      }
      final String lsOutput = await shell('ls $path');
      for (String line in lsOutput.split('\n')) {
        if (line == '') {
          continue;
        }
        final int port = int.tryParse(line);
        if (port != null) {
          ports.add(port);
        }
      }
    }
    return ports;
276 277 278 279 280
  }

  /// Run `command` on the Fuchsia device shell.
  Future<String> shell(String command) async {
    final RunResult result = await runAsync(<String>[
281 282 283 284 285 286
      'ssh',
      '-F',
      fuchsiaArtifacts.sshConfig.absolute.path,
      id,
      command
    ]);
287
    if (result.exitCode != 0) {
288 289
      throwToolExit(
          'Command failed: $command\nstdout: ${result.stdout}\nstderr: ${result.stderr}');
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
      return null;
    }
    return result.stdout;
  }

  /// 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;
  }
329

330 331 332
  FuchsiaIsolateDiscoveryProtocol getIsolateDiscoveryProtocol(
          String isolateName) =>
      FuchsiaIsolateDiscoveryProtocol(this, isolateName);
333 334 335

  @override
  bool isSupportedForProject(FlutterProject flutterProject) => true;
336 337 338
}

class FuchsiaIsolateDiscoveryProtocol {
339 340 341
  FuchsiaIsolateDiscoveryProtocol(
    this._device,
    this._isolateName, [
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362
    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}...',
363
      timeout: null, // could take an arbitrary amount of time
364 365 366 367 368 369 370
    );
    _pollingTimer ??= Timer(_pollDuration, _findIsolate);
    return _foundUri.future.then((Uri uri) {
      _uri = uri;
      return uri;
    });
  }
371

372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
  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)) {
          _foundUri.complete(_device.ipv6
410 411
              ? Uri.parse('http://[$_ipv6Loopback]:${address.port}/')
              : Uri.parse('http://$_ipv4Loopback:${address.port}/'));
412 413 414 415 416 417 418 419 420 421 422 423
          _status.stop();
          return;
        }
      }
    }
    if (_pollOnce) {
      _foundUri.completeError(Exception('Max iterations exceeded'));
      _status.stop();
      return;
    }
    _pollingTimer = Timer(_pollDuration, _findIsolate);
  }
424 425 426 427 428 429 430 431 432
}

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

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

  @override
433
  Future<int> forward(int devicePort, {int hostPort}) async {
434 435 436 437
    hostPort ??= await _findPort();
    // Note: the provided command works around a bug in -N, see US-515
    // for more explanation.
    final List<String> command = <String>[
438 439 440 441 442 443 444 445 446 447 448
      'ssh',
      '-6',
      '-F',
      fuchsiaArtifacts.sshConfig.absolute.path,
      '-nNT',
      '-vvv',
      '-f',
      '-L',
      '$hostPort:$_ipv4Loopback:$devicePort',
      device.id,
      'true',
449 450
    ];
    final Process process = await processManager.start(command);
451
    unawaited(process.exitCode.then((int exitCode) {
452 453 454
      if (exitCode != 0) {
        throwToolExit('Failed to forward port:$devicePort');
      }
455
    }));
456 457 458 459 460 461 462 463 464 465 466 467 468 469 470
    _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>[
471 472 473 474 475 476 477 478 479 480
      'ssh',
      '-F',
      fuchsiaArtifacts.sshConfig.absolute.path,
      '-O',
      'cancel',
      '-vvv',
      '-L',
      '${forwardedPort.hostPort}:$_ipv4Loopback:${forwardedPort.devicePort}',
      device.id
    ];
481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496
    final ProcessResult result = await processManager.run(command);
    if (result.exitCode != 0) {
      throwToolExit(result.stderr);
    }
  }

  static Future<int> _findPort() async {
    int port = 0;
    ServerSocket serverSocket;
    try {
      serverSocket = await ServerSocket.bind(_ipv4Loopback, 0);
      port = serverSocket.port;
    } catch (e) {
      // Failures are signaled by a return value of 0 from this function.
      printTrace('_findPort failed: $e');
    }
497
    if (serverSocket != null) {
498
      await serverSocket.close();
499
    }
500 501 502 503
    return port;
  }
}

504 505 506 507 508 509
class FuchsiaModulePackage extends ApplicationPackage {
  FuchsiaModulePackage({@required this.name}) : super(id: name);

  @override
  final String name;
}