| // 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 '../device_port_forwarder.dart'; | |
| import '../globals.dart' as globals; | |
| import '../project.dart'; | |
| import '../runner/flutter_command.dart'; | |
| import '../vmservice.dart'; | |
| import 'application_package.dart'; | |
| import 'fuchsia_ffx.dart'; | |
| import 'fuchsia_pm.dart'; | |
| import 'fuchsia_sdk.dart'; | |
| import 'fuchsia_workflow.dart'; | |
| import 'pkgctl.dart'; | |
| /// The [FuchsiaDeviceTools] instance. | |
| FuchsiaDeviceTools get fuchsiaDeviceTools => context.get<FuchsiaDeviceTools>()!; | |
| /// Fuchsia device-side tools. | |
| class FuchsiaDeviceTools { | |
| late final FuchsiaPkgctl pkgctl = FuchsiaPkgctl(); | |
| late final FuchsiaFfx ffx = FuchsiaFfx(); | |
| } | |
| final String _ipv4Loopback = InternetAddress.loopbackIPv4.address; | |
| final String _ipv6Loopback = InternetAddress.loopbackIPv6.address; | |
| // Enables testing the fuchsia isolate discovery | |
| Future<FlutterVmService> _kDefaultFuchsiaIsolateDiscoveryConnector(Uri uri) { | |
| return connectToVmService(uri, logger: globals.logger); | |
| } | |
| Future<void> _kDefaultDartDevelopmentServiceStarter( | |
| Device device, | |
| Uri vmServiceUri, | |
| bool disableServiceAuthCodes, | |
| ) async { | |
| await device.dds.startDartDevelopmentService( | |
| vmServiceUri, | |
| hostPort: 0, | |
| ipv6: true, | |
| disableServiceAuthCodes: disableServiceAuthCodes, | |
| logger: globals.logger, | |
| ); | |
| } | |
| /// Read the log for a particular device. | |
| class _FuchsiaLogReader extends DeviceLogReader { | |
| _FuchsiaLogReader(this._device, this._systemClock, [this._app]); | |
| // \S matches non-whitespace characters. | |
| static final RegExp _flutterLogOutput = RegExp(r'INFO: \S+\(flutter\): '); | |
| final FuchsiaDevice _device; | |
| final ApplicationPackage? _app; | |
| final SystemClock _systemClock; | |
| @override | |
| String get name => _device.name; | |
| Stream<String>? _logLines; | |
| @override | |
| Stream<String> get logLines { | |
| final Stream<String>? logStream = globals.fuchsiaSdk?.syslogs(_device.id); | |
| _logLines ??= _processLogs(logStream); | |
| return _logLines ?? const Stream<String>.empty(); | |
| } | |
| 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 ApplicationPackage? app = _app; | |
| final RegExp matchRegExp = app == null | |
| ? _flutterLogOutput | |
| : RegExp('INFO: ${app.name}(\\.cm)?\\(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>[]; | |
| } | |
| // TODO(omerlevran): Remove once soft transition is complete fxb/67602. | |
| 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]; | |
| // TODO(omerlevran): Add support for resolve on the FuchsiaSdk Object. | |
| final String? resolvedHost = await _fuchsiaSdk.fuchsiaFfx.resolve(name); | |
| if (resolvedHost == null) { | |
| _logger.printError('Failed to resolve host for Fuchsia device `$name`'); | |
| return null; | |
| } | |
| return FuchsiaDevice(resolvedHost, name: name); | |
| } | |
| @override | |
| List<String> get wellKnownIds => const <String>[]; | |
| } | |
| class FuchsiaDevice extends Device { | |
| FuchsiaDevice(super.id, {required this.name}) | |
| : super( | |
| 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; | |
| late final Future<bool> isSession = _initIsSession(); | |
| /// Determine if the Fuchsia device is running a session based build. | |
| /// | |
| /// If the device is running a session based build, `ffx session` should be | |
| /// used to launch apps. Fuchsia flutter apps cannot currently be launched | |
| /// without a session. | |
| Future<bool> _initIsSession() async { | |
| return await globals.fuchsiaSdk?.fuchsiaFfx.sessionShow() != null; | |
| } | |
| @override | |
| Future<bool> isAppInstalled( | |
| ApplicationPackage app, { | |
| String? userIdentifier, | |
| }) async => false; | |
| @override | |
| Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => false; | |
| @override | |
| Future<bool> installApp( | |
| ApplicationPackage app, { | |
| String? userIdentifier, | |
| }) => Future<bool>.value(false); | |
| @override | |
| Future<bool> uninstallApp( | |
| ApplicationPackage app, { | |
| String? userIdentifier, | |
| }) async => false; | |
| @override | |
| bool isSupported() => true; | |
| @override | |
| bool supportsRuntimeMode(BuildMode buildMode) => | |
| buildMode != BuildMode.jitRelease; | |
| @override | |
| Future<LaunchResult> startApp( | |
| FuchsiaApp package, { | |
| String? mainPath, | |
| String? route, | |
| required DebuggingOptions debuggingOptions, | |
| Map<String, Object?> platformArgs = const <String, Object?>{}, | |
| bool prebuiltApplication = false, | |
| bool ipv6 = false, | |
| String? userIdentifier, | |
| }) async { | |
| if (await isSession) { | |
| globals.printTrace('Running on a session framework based build.'); | |
| } else { | |
| globals.printTrace('Running on a non session framework based build.'); | |
| } | |
| if (!prebuiltApplication) { | |
| throwToolExit( | |
| 'This tool does not currently build apps for fuchsia.\n' | |
| 'Build the app using a supported Fuchsia workflow.\n' | |
| 'Then use the --${FlutterOptions.kUseApplicationBinary} flag.' | |
| ); | |
| } | |
| // Stop the app if it's currently running. | |
| await stopApp(package); | |
| // 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 Fuchsia 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...', | |
| ); | |
| FuchsiaPackageServer? fuchsiaPackageServer; | |
| bool serverRegistered = false; | |
| String fuchsiaUrl; | |
| try { | |
| // Start up a package server. | |
| const String packageServerName = FuchsiaPackageServer.toolHost; | |
| fuchsiaPackageServer = | |
| FuchsiaPackageServer(packageRepo.path, packageServerName, '', 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.pkgctl | |
| .addRepo(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.pkgctl | |
| .resolve(this, fuchsiaPackageServer.name, 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.pkgctl | |
| .resolve(this, fuchsiaPackageServer.name, appName)) { | |
| globals.printError('Failed to get pkgctl to prefetch the package'); | |
| return LaunchResult.failed(); | |
| } | |
| fuchsiaUrl = 'fuchsia-pkg://$packageServerName/$appName#meta/$appName.cm'; | |
| if (await isSession) { | |
| // Instruct ffx session to start the app | |
| final bool addedApp = | |
| await globals.fuchsiaSdk?.fuchsiaFfx.sessionAdd(fuchsiaUrl) ?? false; | |
| if (!addedApp) { | |
| globals.printError('Failed to add the app via `ffx session add`'); | |
| return LaunchResult.failed(); | |
| } | |
| } else { | |
| globals.printError( | |
| 'Fuchsia flutter apps can only be launched within a session'); | |
| return LaunchResult.failed(); | |
| } | |
| } finally { | |
| // Try to un-teach the package controller about the package server if | |
| // needed. | |
| if (serverRegistered && fuchsiaPackageServer != null) { | |
| await fuchsiaDeviceTools.pkgctl.rmRepo(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 successfully 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 vmService uri. | |
| final FuchsiaIsolateDiscoveryProtocol discovery = | |
| getIsolateDiscoveryProtocol(appName); | |
| try { | |
| final Uri vmServiceUri = await discovery.uri; | |
| return LaunchResult.succeeded(vmServiceUri: vmServiceUri); | |
| } finally { | |
| discovery.dispose(); | |
| } | |
| } | |
| @override | |
| Future<bool> stopApp( | |
| ApplicationPackage? app, { | |
| String? userIdentifier, | |
| }) async { | |
| if (await isSession) { | |
| // Currently there are no way to close a running app programmatically | |
| // using the session framework afaik. So this is a no-op. | |
| return true; | |
| } | |
| // Fuchsia flutter apps currently require a session, but if that changes, | |
| // add the relevant "stopApp" code here. | |
| return true; | |
| } | |
| 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 Exception('${outputFile.path} must be a .ppm file'); | |
| } | |
| final RunResult screencapResult = | |
| await shell('screencap > /tmp/screenshot.ppm'); | |
| if (screencapResult.exitCode != 0) { | |
| throw Exception( | |
| '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 Exception('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 | |
| late final Future<TargetPlatform> targetPlatform = _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, globals.systemClock, app); | |
| } | |
| _FuchsiaLogReader? _logReader; | |
| @override | |
| DevicePortForwarder get portForwarder => | |
| _portForwarder ??= _FuchsiaPortForwarder(this); | |
| DevicePortForwarder? _portForwarder; | |
| @visibleForTesting | |
| set portForwarder(DevicePortForwarder forwarder) { | |
| _portForwarder = forwarder; | |
| } | |
| @override | |
| void clearLogs() {} | |
| /// [true] if the current host address is IPv6. | |
| late final bool ipv6 = isIPv6Address(id); | |
| /// Return the address that the device should use to communicate with the | |
| /// host. | |
| late final Future<String> hostAddress = () async { | |
| final RunResult result = await shell(r'echo $SSH_CONNECTION'); | |
| void fail() { | |
| throwToolExit('Failed to get local address, aborting.\n$result'); | |
| } | |
| if (result.exitCode != 0) { | |
| fail(); | |
| } | |
| final List<String> splitResult = result.stdout.split(' '); | |
| if (splitResult.isEmpty) { | |
| fail(); | |
| } | |
| final String addr = splitResult[0].replaceAll('%', '%25'); | |
| if (addr.isEmpty) { | |
| fail(); | |
| } | |
| return addr; | |
| }(); | |
| /// List the ports currently running a dart vmService. | |
| 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}'"); | |
| } | |
| final String findOutput = findResult.stdout; | |
| if (findOutput.trim() == '') { | |
| throwToolExit( | |
| 'No Dart Observatories found. Are you running a debug build?'); | |
| } | |
| 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"); | |
| } | |
| 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 { | |
| final File? sshConfig = globals.fuchsiaArtifacts?.sshConfig; | |
| if (sshConfig == null) { | |
| throwToolExit('Cannot interact with device. No ssh config.\n' | |
| 'Try setting FUCHSIA_SSH_CONFIG or FUCHSIA_BUILD_DIR.'); | |
| } | |
| return globals.processUtils.run(<String>[ | |
| 'ssh', | |
| '-F', | |
| 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 { | |
| final File? sshConfig = globals.fuchsiaArtifacts!.sshConfig; | |
| if (sshConfig == null) { | |
| throwToolExit('Cannot interact with device. No ssh config.\n' | |
| 'Try setting FUCHSIA_SSH_CONFIG or FUCHSIA_BUILD_DIR.'); | |
| } | |
| return globals.processUtils.run(<String>[ | |
| 'scp', | |
| '-F', | |
| 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. | |
| Future<int> findIsolatePort(String isolateName, List<int> ports) async { | |
| for (final int port in ports) { | |
| try { | |
| // 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 FlutterVmService vmService = | |
| await connectToVmService(uri, logger: globals.logger); | |
| final List<FlutterView> flutterViews = | |
| await vmService.getFlutterViews(); | |
| for (final FlutterView flutterView in flutterViews) { | |
| final vm_service.IsolateRef? uiIsolate = flutterView.uiIsolate; | |
| if (uiIsolate == null) { | |
| continue; | |
| } | |
| final int? port = vmService.httpAddress?.port; | |
| if (port != null && | |
| (uiIsolate.name?.contains(isolateName) ?? false)) { | |
| return port; | |
| } | |
| } | |
| } on SocketException catch (err) { | |
| globals.printTrace('Failed to connect to $port: $err'); | |
| } | |
| } | |
| throwToolExit('No ports found running $isolateName'); | |
| } | |
| 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._ddsStarter = _kDefaultDartDevelopmentServiceStarter, | |
| this._pollOnce = false, | |
| ]); | |
| static const Duration _pollDuration = Duration(seconds: 10); | |
| final Map<int, FlutterVmService> _ports = <int, FlutterVmService>{}; | |
| final FuchsiaDevice _device; | |
| final String _isolateName; | |
| final Completer<Uri> _foundUri = Completer<Uri>(); | |
| final Future<FlutterVmService> Function(Uri) _vmServiceConnector; | |
| final Future<void> Function(Device, Uri, bool) _ddsStarter; | |
| // 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}...', | |
| ); | |
| 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) { | |
| FlutterVmService? 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'); | |
| await _ddsStarter(_device, uri, true); | |
| service = await _vmServiceConnector(_device.dds.uri!); | |
| _ports[port] = service; | |
| } on SocketException catch (err) { | |
| globals.printTrace('Failed to connect to $localPort: $err'); | |
| continue; | |
| } | |
| } | |
| final List<FlutterView> flutterViews = | |
| await service?.getFlutterViews() ?? <FlutterView>[]; | |
| for (final FlutterView flutterView in flutterViews) { | |
| final vm_service.IsolateRef? uiIsolate = flutterView.uiIsolate; | |
| if (uiIsolate == null) { | |
| continue; | |
| } | |
| final int? port = service?.httpAddress?.port; | |
| if (port != null && (uiIsolate.name?.contains(_isolateName) ?? false)) { | |
| _foundUri.complete(_device.ipv6 | |
| ? Uri.parse('http://[$_ipv6Loopback]:$port/') | |
| : Uri.parse('http://$_ipv4Loopback:$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'); | |
| } | |
| final File? sshConfig = globals.fuchsiaArtifacts?.sshConfig; | |
| if (sshConfig == null) { | |
| throwToolExit('Cannot interact with device. No ssh config.\n' | |
| 'Try setting FUCHSIA_SSH_CONFIG or FUCHSIA_BUILD_DIR.'); | |
| } | |
| // The provided command works around a bug in -N, see US-515 | |
| // for more explanation. | |
| final List<String> command = <String>[ | |
| 'ssh', | |
| '-6', | |
| '-F', | |
| 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 File? sshConfig = globals.fuchsiaArtifacts?.sshConfig; | |
| if (sshConfig == null) { | |
| // Nothing to cancel. | |
| return; | |
| } | |
| final List<String> command = <String>[ | |
| 'ssh', | |
| '-F', | |
| 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); | |
| } | |
| } | |
| } |