// 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'; import 'package:meta/meta.dart'; import '../application_package.dart'; import '../base/common.dart'; import '../base/io.dart'; import '../base/logger.dart'; import '../base/platform.dart'; import '../base/process.dart'; import '../base/process_manager.dart'; import '../base/time.dart'; import '../build_info.dart'; import '../device.dart'; import '../globals.dart'; import '../vmservice.dart'; import 'fuchsia_sdk.dart'; import 'fuchsia_workflow.dart'; final String _ipv4Loopback = InternetAddress.loopbackIPv4.address; final String _ipv6Loopback = InternetAddress.loopbackIPv6.address; // Enables testing the fuchsia isolate discovery Future _kDefaultFuchsiaIsolateDiscoveryConnector(Uri uri) { return VMService.connect(uri); } /// Read the log for a particular device. class _FuchsiaLogReader extends DeviceLogReader { _FuchsiaLogReader(this._device, [this._app]); static final RegExp _flutterLogOutput = RegExp(r'INFO: \w+\(flutter\): '); FuchsiaDevice _device; ApplicationPackage _app; @override String get name => _device.name; Stream _logLines; @override Stream get logLines { _logLines ??= _processLogs(fuchsiaSdk.syslogs()); return _logLines; } Stream _processLogs(Stream lines) { // 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\\): '); return Stream.eventTransformed( lines, (Sink outout) => _FuchsiaLogSink(outout, matchRegExp, startTime), ); } @override String toString() => name; } class _FuchsiaLogSink implements EventSink { _FuchsiaLogSink(this._outputSink, this._matchRegExp, this._startTime); static final RegExp _utcDateOutput = RegExp(r'\d+\-\d+\-\d+ \d+:\d+:\d+'); final EventSink _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(); } } class FuchsiaDevices extends PollingDeviceDiscovery { FuchsiaDevices() : super('Fuchsia devices'); @override bool get supportsPlatform => platform.isLinux || platform.isMacOS; @override bool get canListAnything => fuchsiaWorkflow.canListDevices; @override Future> pollingGetDevices() async { if (!fuchsiaWorkflow.canListDevices) { return []; } final String text = await fuchsiaSdk.listDevices(); if (text == null || text.isEmpty) { return []; } final List devices = parseListDevices(text); return devices; } @override Future> getDiagnostics() async => const []; } @visibleForTesting List parseListDevices(String text) { final List devices = []; for (String rawLine in text.trim().split('\n')) { final String line = rawLine.trim(); // ['ip', 'device name'] final List words = line.split(' '); if (words.length < 2) { continue; } final String name = words[1]; final String id = words[0]; devices.add(FuchsiaDevice(id, name: name)); } return devices; } class FuchsiaDevice extends Device { FuchsiaDevice(String id, { this.name }) : super(id); @override bool get supportsHotReload => true; @override bool get supportsHotRestart => false; @override bool get supportsStopApp => false; @override final String name; @override Future get isLocalEmulator async => false; @override bool get supportsStartPaused => false; @override Future isAppInstalled(ApplicationPackage app) async => false; @override Future isLatestBuildInstalled(ApplicationPackage app) async => false; @override Future installApp(ApplicationPackage app) => Future.value(false); @override Future uninstallApp(ApplicationPackage app) async => false; @override bool isSupported() => true; @override Future startApp( ApplicationPackage package, { String mainPath, String route, DebuggingOptions debuggingOptions, Map platformArgs, bool prebuiltApplication = false, bool applicationNeedsRebuild = false, bool usesTerminalUi = true, bool ipv6 = false, }) => Future.error('unimplemented'); @override Future stopApp(ApplicationPackage app) async { // Currently we don't have a way to stop an app running on Fuchsia. return false; } @override Future get targetPlatform async => TargetPlatform.fuchsia; @override Future get sdkNameAndVersion async => 'Fuchsia'; @override DeviceLogReader getLogReader({ ApplicationPackage app }) => _logReader ??= _FuchsiaLogReader(this, app); _FuchsiaLogReader _logReader; @override DevicePortForwarder get portForwarder => _portForwarder ??= _FuchsiaPortForwarder(this); _FuchsiaPortForwarder _portForwarder; @override void clearLogs() { } @override bool get supportsScreenshot => false; 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; } } /// List the ports currently running a dart observatory. Future> servicePorts() async { 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 ports = []; 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; } /// Run `command` on the Fuchsia device shell. Future shell(String command) async { final RunResult result = await runAsync([ 'ssh', '-F', fuchsiaArtifacts.sshConfig.absolute.path, id, command]); 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 findIsolatePort(String isolateName, List 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; } 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 _ports = {}; final FuchsiaDevice _device; final String _isolateName; final Completer _foundUri = Completer(); final Future Function(Uri) _vmServiceConnector; // whether to only poll once. final bool _pollOnce; Timer _pollingTimer; Status _status; FutureOr get uri { if (_uri != null) { return _uri; } _status ??= logger.startProgress( 'Waiting for a connection from $_isolateName on ${_device.name}...', timeout: null, // could take an arbitrary amount of time ); _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 _findIsolate() async { final List 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); } } class _FuchsiaPortForwarder extends DevicePortForwarder { _FuchsiaPortForwarder(this.device); final FuchsiaDevice device; final Map _processes = {}; @override Future forward(int devicePort, { int hostPort }) async { hostPort ??= await _findPort(); // Note: the provided command works around a bug in -N, see US-515 // for more explanation. final List command = [ 'ssh', '-6', '-F', fuchsiaArtifacts.sshConfig.absolute.path, '-nNT', '-vvv', '-f', '-L', '$hostPort:$_ipv4Loopback:$devicePort', device.id, 'true', ]; final Process process = await 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 get forwardedPorts => _forwardedPorts; final List _forwardedPorts = []; @override Future unforward(ForwardedPort forwardedPort) async { _forwardedPorts.remove(forwardedPort); final Process process = _processes.remove(forwardedPort.hostPort); process?.kill(); final List command = [ 'ssh', '-F', fuchsiaArtifacts.sshConfig.absolute.path, '-O', 'cancel', '-vvv', '-L', '${forwardedPort.hostPort}:$_ipv4Loopback:${forwardedPort.devicePort}', device.id]; final ProcessResult result = await processManager.run(command); if (result.exitCode != 0) { throwToolExit(result.stderr); } } static Future _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; } } class FuchsiaModulePackage extends ApplicationPackage { FuchsiaModulePackage({@required this.name}) : super(id: name); @override final String name; }