fuchsia_device.dart 13.6 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 11
import '../base/common.dart';
import '../base/io.dart';
12
import '../base/logger.dart';
13
import '../base/platform.dart';
14 15
import '../base/process.dart';
import '../base/process_manager.dart';
16
import '../base/time.dart';
17 18
import '../build_info.dart';
import '../device.dart';
19 20
import '../globals.dart';
import '../vmservice.dart';
21

22 23 24
import 'fuchsia_sdk.dart';
import 'fuchsia_workflow.dart';

25 26 27
final String _ipv4Loopback = InternetAddress.loopbackIPv4.address;
final String _ipv6Loopback = InternetAddress.loopbackIPv6.address;

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

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

37
  static final RegExp _flutterLogOutput = RegExp(r'INFO: \w+\(flutter\): ');
38

39
  FuchsiaDevice _device;
40
  ApplicationPackage _app;
41

42 43 44 45 46
  @override String get name => _device.name;

  Stream<String> _logLines;
  @override
  Stream<String> get logLines {
47
    _logLines ??= _processLogs(fuchsiaSdk.syslogs());
48 49 50
    return _logLines;
  }

51
  Stream<String> _processLogs(Stream<String> lines) {
52 53 54 55 56 57 58 59
    // 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}\\(flutter\\): ');
60 61 62 63
    return Stream<String>.eventTransformed(
      lines,
      (Sink<String> outout) => _FuchsiaLogSink(outout, matchRegExp, startTime),
    );
64 65
  }

66 67 68 69
  @override
  String toString() => name;
}

70 71 72 73 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;
    }
    _outputSink.add('[${logTime.toLocal()}] Flutter: ${line.split(_matchRegExp).last}');
  }

  @override
95
  void addError(Object error, [ StackTrace stackTrace ]) {
96 97 98 99 100 101 102
    _outputSink.addError(error, stackTrace);
  }

  @override
  void close() { _outputSink.close(); }
}

103 104 105 106 107 108 109 110 111 112 113 114 115 116
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>[];
    }
117
    final String text = await fuchsiaSdk.listDevices();
118
    if (text == null || text.isEmpty) {
119 120
      return <Device>[];
    }
121
    final List<FuchsiaDevice> devices = parseListDevices(text);
122
    return devices;
123 124 125 126 127 128 129
  }

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

@visibleForTesting
130 131
List<FuchsiaDevice> parseListDevices(String text) {
  final List<FuchsiaDevice> devices = <FuchsiaDevice>[];
132 133
  for (String rawLine in text.trim().split('\n')) {
    final String line = rawLine.trim();
134
    // ['ip', 'device name']
135
    final List<String> words = line.split(' ');
136 137 138
    if (words.length < 2) {
      continue;
    }
139
    final String name = words[1];
140 141
    final String id = words[0];
    devices.add(FuchsiaDevice(id, name: name));
142
  }
143
  return devices;
144 145
}

146 147 148 149
class FuchsiaDevice extends Device {
  FuchsiaDevice(String id, { this.name }) : super(id);

  @override
150 151 152 153
  bool get supportsHotReload => true;

  @override
  bool get supportsHotRestart => false;
154

155 156 157
  @override
  bool get supportsStopApp => false;

158 159 160 161
  @override
  final String name;

  @override
162
  Future<bool> get isLocalEmulator async => false;
163 164 165 166 167

  @override
  bool get supportsStartPaused => false;

  @override
168
  Future<bool> isAppInstalled(ApplicationPackage app) async => false;
169 170

  @override
171
  Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => false;
172 173

  @override
174
  Future<bool> installApp(ApplicationPackage app) => Future<bool>.value(false);
175 176

  @override
177
  Future<bool> uninstallApp(ApplicationPackage app) async => false;
178 179 180 181 182 183

  @override
  bool isSupported() => true;

  @override
  Future<LaunchResult> startApp(
184
    ApplicationPackage package, {
185 186 187 188
    String mainPath,
    String route,
    DebuggingOptions debuggingOptions,
    Map<String, dynamic> platformArgs,
189 190
    bool prebuiltApplication = false,
    bool applicationNeedsRebuild = false,
191
    bool usesTerminalUi = true,
192
    bool ipv6 = false,
193
  }) => Future<void>.error('unimplemented');
194 195 196 197 198 199 200 201

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

  @override
202
  Future<TargetPlatform> get targetPlatform async => TargetPlatform.fuchsia;
203 204

  @override
205
  Future<String> get sdkNameAndVersion async => 'Fuchsia';
206 207

  @override
208
  DeviceLogReader getLogReader({ ApplicationPackage app }) => _logReader ??= _FuchsiaLogReader(this, app);
209
  _FuchsiaLogReader _logReader;
210 211

  @override
212 213
  DevicePortForwarder get portForwarder => _portForwarder ??= _FuchsiaPortForwarder(this);
  _FuchsiaPortForwarder _portForwarder;
214 215 216 217 218 219 220

  @override
  void clearLogs() {
  }

  @override
  bool get supportsScreenshot => false;
221

222 223 224 225 226 227 228 229 230 231 232
  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;
    }
  }

233 234
  /// List the ports currently running a dart observatory.
  Future<List<int>> servicePorts() async {
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
    final String findOutput = await shell('find /hub -name vmservice-port');
    if (findOutput.trim() == '') {
      throwToolExit('No Dart Observatories found. Are you running a debug build?');
      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;
257 258 259 260 261
  }

  /// Run `command` on the Fuchsia device shell.
  Future<String> shell(String command) async {
    final RunResult result = await runAsync(<String>[
262
      'ssh', '-F', fuchsiaArtifacts.sshConfig.absolute.path, id, command]);
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
    if (result.exitCode != 0) {
      throwToolExit('Command failed: $command\nstdout: ${result.stdout}\nstderr: ${result.stderr}');
      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;
  }
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 329 330

  FuchsiaIsolateDiscoveryProtocol  getIsolateDiscoveryProtocol(String isolateName) => FuchsiaIsolateDiscoveryProtocol(this, isolateName);
}

class FuchsiaIsolateDiscoveryProtocol {
  FuchsiaIsolateDiscoveryProtocol(this._device, this._isolateName, [
    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}...',
331
      timeout: null, // could take an arbitrary amount of time
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390
    );
    _pollingTimer ??= Timer(_pollDuration, _findIsolate);
    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 (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
            ? Uri.parse('http://[$_ipv6Loopback]:${address.port}/')
            : Uri.parse('http://$_ipv4Loopback:${address.port}/'));
          _status.stop();
          return;
        }
      }
    }
    if (_pollOnce) {
      _foundUri.completeError(Exception('Max iterations exceeded'));
      _status.stop();
      return;
    }
    _pollingTimer = Timer(_pollDuration, _findIsolate);
  }
391 392 393 394 395 396 397 398 399
}

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

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

  @override
400
  Future<int> forward(int devicePort, { int hostPort }) async {
401 402 403 404
    hostPort ??= await _findPort();
    // Note: the provided command works around a bug in -N, see US-515
    // for more explanation.
    final List<String> command = <String>[
405
      'ssh', '-6', '-F', fuchsiaArtifacts.sshConfig.absolute.path, '-nNT', '-vvv', '-f',
406 407 408
      '-L', '$hostPort:$_ipv4Loopback:$devicePort', device.id, 'true'
    ];
    final Process process = await processManager.start(command);
409
    unawaited(process.exitCode.then((int exitCode) {
410 411 412
      if (exitCode != 0) {
        throwToolExit('Failed to forward port:$devicePort');
      }
413
    }));
414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
    _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>[
429
        'ssh', '-F', fuchsiaArtifacts.sshConfig.absolute.path, '-O', 'cancel', '-vvv',
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452
        '-L', '${forwardedPort.hostPort}:$_ipv4Loopback:${forwardedPort.devicePort}', device.id];
    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');
    }
    if (serverSocket != null)
      await serverSocket.close();
    return port;
  }
}

453 454 455 456 457 458
class FuchsiaModulePackage extends ApplicationPackage {
  FuchsiaModulePackage({@required this.name}) : super(id: name);

  @override
  final String name;
}