// 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 'dart:typed_data'; import 'package:meta/meta.dart'; import '../application_package.dart'; import '../base/dds.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/logger.dart'; import '../build_info.dart'; import '../convert.dart'; import '../daemon.dart'; import '../device.dart'; import '../device_port_forwarder.dart'; import '../device_vm_service_discovery_for_attach.dart'; import '../project.dart'; import '../resident_runner.dart'; import 'debounce_data_stream.dart'; import 'file_transfer.dart'; bool _isNullable() => null is T; T _cast(Object? object) { if (!_isNullable() && object == null) { throw Exception('Expected $T, received null!'); } else { return object as T; } } /// A [DeviceDiscovery] that will connect to a flutter daemon and connects to /// the devices remotely. /// /// If [deltaFileTransfer] is true, the proxy will use an rsync-like algorithm that /// only transfers the changed part of the application package for deployment. class ProxiedDevices extends PollingDeviceDiscovery { ProxiedDevices(this.connection, { bool deltaFileTransfer = true, bool enableDdsProxy = false, required Logger logger, FileTransfer fileTransfer = const FileTransfer(), }) : _deltaFileTransfer = deltaFileTransfer, _enableDdsProxy = enableDdsProxy, _logger = logger, _fileTransfer = fileTransfer, super('Proxied devices'); /// [DaemonConnection] used to communicate with the daemon. final DaemonConnection connection; final Logger _logger; final bool _deltaFileTransfer; final bool _enableDdsProxy; final FileTransfer _fileTransfer; @override bool get supportsPlatform => true; @override bool get canListAnything => true; List? _devices; @override Future> devices({DeviceDiscoveryFilter? filter}) async => _filterDevices(_devices ?? await discoverDevices(), filter); @override Future> discoverDevices({ Duration? timeout, DeviceDiscoveryFilter? filter }) async { final List> discoveredDevices = _cast>(await connection.sendRequest('device.discoverDevices')).cast>(); final List devices = [ for (final Map device in discoveredDevices) deviceFromDaemonResult(device), ]; _devices = devices; return _filterDevices(devices, filter); } Future> _filterDevices(List devices, DeviceDiscoveryFilter? filter) async { if (filter == null) { return devices; } return filter.filterDevices(devices); } @override Future> pollingGetDevices({Duration? timeout}) => discoverDevices(timeout: timeout); @override List get wellKnownIds => const []; @visibleForTesting ProxiedDevice deviceFromDaemonResult(Map device) { final Map capabilities = _cast>(device['capabilities']); final String? connectionInterfaceName = _cast(device['connectionInterface']); final DeviceConnectionInterface? connectionInterface = connectionInterfaceName != null ? getDeviceConnectionInterfaceForName(connectionInterfaceName) : null; return ProxiedDevice( connection, _cast(device['id']), deltaFileTransfer: _deltaFileTransfer, enableDdsProxy: _enableDdsProxy, category: Category.fromString(_cast(device['category'])), platformType: PlatformType.fromString(_cast(device['platformType'])), targetPlatform: getTargetPlatformForName(_cast(device['platform'])), ephemeral: _cast(device['ephemeral']), isConnected: _cast(device['isConnected']) ?? true, connectionInterface: connectionInterface ?? DeviceConnectionInterface.attached, name: 'Proxied ${device['name']}', isLocalEmulator: _cast(device['emulator']), emulatorId: _cast(device['emulatorId']), sdkNameAndVersion: _cast(device['sdk']), supportsHotReload: _cast(capabilities['hotReload']), supportsHotRestart: _cast(capabilities['hotRestart']), supportsFlutterExit: _cast(capabilities['flutterExit']), supportsScreenshot: _cast(capabilities['screenshot']), supportsFastStart: _cast(capabilities['fastStart']), supportsHardwareRendering: _cast(capabilities['hardwareRendering']), logger: _logger, fileTransfer: _fileTransfer, ); } @override Future> getDiagnostics() async { try { final List diagnostics = _cast>(await connection.sendRequest('device.getDiagnostics')).cast(); return diagnostics; } on String catch (e) { // Daemon actually does throw string types. if (e.contains('command not understood')) { _logger.printTrace('The daemon is on an older version that does not support `device.getDiagnostics`.'); // Silently ignore. return []; } rethrow; } } } /// A [Device] that acts as a proxy to remotely connected device. /// /// The communication happens via a flutter daemon. /// /// If [deltaFileTransfer] is true, the proxy will use an rsync-like algorithm that /// only transfers the changed part of the application package for deployment. /// /// If [enableDdsProxy] is true, DDS will be started on the daemon instead of /// starting locally. class ProxiedDevice extends Device { ProxiedDevice(this.connection, String id, { bool deltaFileTransfer = true, bool enableDdsProxy = false, required Category? category, required PlatformType? platformType, required TargetPlatform targetPlatform, required bool ephemeral, required this.isConnected, required this.connectionInterface, required this.name, required bool isLocalEmulator, required String? emulatorId, required String sdkNameAndVersion, required this.supportsHotReload, required this.supportsHotRestart, required this.supportsFlutterExit, required this.supportsScreenshot, required this.supportsFastStart, required bool supportsHardwareRendering, required super.logger, FileTransfer fileTransfer = const FileTransfer(), }): _deltaFileTransfer = deltaFileTransfer, _enableDdsProxy = enableDdsProxy, _isLocalEmulator = isLocalEmulator, _emulatorId = emulatorId, _sdkNameAndVersion = sdkNameAndVersion, _supportsHardwareRendering = supportsHardwareRendering, _targetPlatform = targetPlatform, _logger = logger, _fileTransfer = fileTransfer, super(id, category: category, platformType: platformType, ephemeral: ephemeral); /// [DaemonConnection] used to communicate with the daemon. final DaemonConnection connection; final Logger _logger; final bool _deltaFileTransfer; final bool _enableDdsProxy; final FileTransfer _fileTransfer; @override final bool isConnected; @override final DeviceConnectionInterface connectionInterface; @override final String name; final bool _isLocalEmulator; @override Future get isLocalEmulator async => _isLocalEmulator; final String? _emulatorId; @override Future get emulatorId async => _emulatorId; @override Future supportsRuntimeMode(BuildMode buildMode) async => _cast(await connection.sendRequest('device.supportsRuntimeMode', { 'deviceId': id, 'buildMode': buildMode.toString(), })); final bool _supportsHardwareRendering; @override Future get supportsHardwareRendering async => _supportsHardwareRendering; // ProxiedDevice is intended to be used with prebuilt projects. No building // is required, so we returns true for all projects. @override bool isSupportedForProject(FlutterProject flutterProject) => true; @override Future isAppInstalled( ApplicationPackage app, { String? userIdentifier, }) => throw UnimplementedError(); @override Future isLatestBuildInstalled(ApplicationPackage app) => throw UnimplementedError(); @override Future installApp( ApplicationPackage app, { String? userIdentifier, }) => throw UnimplementedError(); @override Future uninstallApp( ApplicationPackage app, { String? userIdentifier, }) => throw UnimplementedError(); @override bool isSupported() => true; final TargetPlatform _targetPlatform; @override Future get targetPlatform async => _targetPlatform; TargetPlatform get targetPlatformSync => _targetPlatform; final String _sdkNameAndVersion; @override Future get sdkNameAndVersion async => _sdkNameAndVersion; @override FutureOr getLogReader({ covariant PrebuiltApplicationPackage? app, bool includePastLogs = false, }) => _ProxiedLogReader(connection, this, app); ProxiedPortForwarder? _proxiedPortForwarder; /// [proxiedPortForwarder] forwards a port from the remote host to local host. ProxiedPortForwarder get proxiedPortForwarder => _proxiedPortForwarder ??= ProxiedPortForwarder(connection, logger: _logger); ProxiedPortForwarder? _portForwarder; /// [portForwarder] forwards a port from the remote device to remote host, and /// then forward the port from remote host to local host. @override ProxiedPortForwarder get portForwarder => _portForwarder ??= ProxiedPortForwarder(connection, deviceId: id, logger: _logger); ProxiedDartDevelopmentService? _proxiedDds; @override DartDevelopmentService get dds { if (!_enableDdsProxy) { return super.dds; } return _proxiedDds ??= ProxiedDartDevelopmentService(connection, id, logger: _logger, proxiedPortForwarder: proxiedPortForwarder, devicePortForwarder: portForwarder); } @override void clearLogs() => throw UnimplementedError(); @override VMServiceDiscoveryForAttach getVMServiceDiscoveryForAttach({ String? appId, String? fuchsiaModule, int? filterDevicePort, int? expectedHostPort, required bool ipv6, required Logger logger, }) => ProxiedVMServiceDiscoveryForAttach( connection, id, proxiedPortForwarder: proxiedPortForwarder, appId: appId, fuchsiaModule: fuchsiaModule, filterDevicePort: filterDevicePort, expectedHostPort: expectedHostPort, ipv6: ipv6, logger: logger, fallbackDiscovery: () => super.getVMServiceDiscoveryForAttach( appId: appId, fuchsiaModule: fuchsiaModule, filterDevicePort: filterDevicePort, expectedHostPort: expectedHostPort, ipv6: ipv6, logger: logger, ), ); @override Future startApp( PrebuiltApplicationPackage package, { String? mainPath, String? route, required DebuggingOptions debuggingOptions, Map platformArgs = const {}, bool prebuiltApplication = false, String? userIdentifier, }) async { final Map result = _cast>(await connection.sendRequest('device.startApp', { 'deviceId': id, 'applicationPackageId': await applicationPackageId(package), 'mainPath': mainPath, 'route': route, 'debuggingOptions': debuggingOptions.toJson(), 'platformArgs': platformArgs, 'prebuiltApplication': prebuiltApplication, 'ipv6': debuggingOptions.ipv6, 'userIdentifier': userIdentifier, })); final bool started = _cast(result['started']); // TODO(bkonyi): remove once clients have migrated to relying on vmServiceUri. final String? vmServiceUriStr = _cast(result['vmServiceUri']) ?? _cast(result['observatoryUri']); final Uri? vmServiceUri = vmServiceUriStr == null ? null : Uri.parse(vmServiceUriStr); if (started) { if (vmServiceUri != null) { final int hostPort = await proxiedPortForwarder.forward(vmServiceUri.port); return LaunchResult.succeeded(vmServiceUri: vmServiceUri.replace(port: hostPort)); } else { return LaunchResult.succeeded(); } } else { return LaunchResult.failed(); } } @override final bool supportsHotReload; @override final bool supportsHotRestart; @override final bool supportsFlutterExit; @override final bool supportsScreenshot; @override final bool supportsFastStart; @override Future stopApp( covariant PrebuiltApplicationPackage? app, { String? userIdentifier, }) async { return _cast(await connection.sendRequest('device.stopApp', { 'deviceId': id, if (app != null) 'applicationPackageId': await applicationPackageId(app), 'userIdentifier': userIdentifier, })); } @override Future queryMemoryInfo() => throw UnimplementedError(); @override Future takeScreenshot(File outputFile) async { final String imageBase64 = _cast(await connection.sendRequest('device.takeScreenshot', { 'deviceId': id, })); await outputFile.writeAsBytes(base64.decode(imageBase64)); } @override Future dispose() async { await proxiedPortForwarder.dispose(); } final Map> _applicationPackageMap = >{}; Future applicationPackageId( PrebuiltApplicationPackage package) async { final File binary = package.applicationPackage as File; final String path = binary.absolute.path; if (_applicationPackageMap.containsKey(path)) { return _applicationPackageMap[path]!; } final String fileName = binary.basename; final Completer idCompleter = Completer(); _applicationPackageMap[path] = idCompleter.future; final Map args = {'path': fileName}; Map? rollingHashResultJson; if (_deltaFileTransfer) { rollingHashResultJson = _cast?>(await connection.sendRequest('proxy.calculateFileHashes', args)); } if (rollingHashResultJson == null) { // Either file not found on the remote end, or deltaFileTransfer is set to false, transfer the file directly. if (_deltaFileTransfer) { _logger.printTrace('Delta file transfer is enabled but file is not found on the remote end, do a full transfer.'); } await connection.sendRequest('proxy.writeTempFile', args, await binary.readAsBytes()); } else { final BlockHashes rollingHashResult = BlockHashes.fromJson(rollingHashResultJson); final List delta = await _fileTransfer.computeDelta(binary, rollingHashResult); // Delta is empty if the file does not need to be updated if (delta.isNotEmpty) { final List> deltaJson = delta.map((FileDeltaBlock block) => block.toJson()).toList(); final Uint8List buffer = await _fileTransfer.binaryForRebuilding(binary, delta); await connection.sendRequest('proxy.updateFile', { 'path': fileName, 'delta': deltaJson, }, buffer); } } if (_deltaFileTransfer) { // Ask the daemon to precache the hash content for subsequent runs. // Wait for several seconds for the app to be launched, to not interfere // with whatever the daemon is doing. unawaited(() async { await Future.delayed(const Duration(seconds: 60)); await connection.sendRequest('proxy.calculateFileHashes', { 'path': fileName, 'cacheResult': true, }); }()); } final String id = _cast(await connection.sendRequest('device.uploadApplicationPackage', { 'targetPlatform': getNameForTargetPlatform(_targetPlatform), 'applicationBinary': fileName, })); idCompleter.complete(id); return id; } } /// A [DeviceLogReader] for a proxied device. class _ProxiedLogReader extends DeviceLogReader { _ProxiedLogReader(this.connection, this.device, this.applicationPackage); final DaemonConnection connection; final ProxiedDevice device; final PrebuiltApplicationPackage? applicationPackage; @override String get name => device.name; final StreamController _logLinesStreamController = StreamController(); Stream? _logLines; String? _id; @override Stream get logLines => _logLines ??= _start(); Stream _start() { final PrebuiltApplicationPackage? package = applicationPackage; final Future applicationPackageId = package != null ? device.applicationPackageId(package) : Future.value(); final Future idFuture = applicationPackageId.then((String? applicationPackageId) async => _cast(await connection.sendRequest('device.logReader.start', { 'deviceId': device.id, if (applicationPackageId != null) 'applicationPackageId': applicationPackageId, }))); idFuture.then((String id) { _id = id; final Stream stream = connection.listenToEvent('device.logReader.logLines.$_id').map((DaemonEventData event) => event.data! as String); _logLinesStreamController.addStream(stream); }); return _logLinesStreamController.stream; } @override void dispose() { if (_id != null) { connection.sendRequest('device.logReader.stop', { 'id': _id, }); } } } /// A port forwarded by a [ProxiedPortForwarder]. class _ProxiedForwardedPort extends ForwardedPort { _ProxiedForwardedPort(this.connection, { required int hostPort, required int devicePort, required this.remoteDevicePort, required this.deviceId, required this.serverSocket }): super(hostPort, devicePort); /// [DaemonConnection] used to communicate with the daemon. final DaemonConnection connection; /// The forwarded port on the remote device. final int? remoteDevicePort; /// The device identifier of the remote device. final String? deviceId; /// The [ServerSocket] that is serving the local forwarded port. final ServerSocket serverSocket; @override void dispose() { unforward(); } /// Unforwards the remote port, and stops the local server. Future unforward() async { await serverSocket.close(); if (remoteDevicePort != null && deviceId != null) { await connection.sendRequest('device.unforward', { 'deviceId': deviceId!, 'devicePort': remoteDevicePort!, 'hostPort': devicePort, }); } } } typedef CreateSocketServer = Future Function(Logger logger, int? hostPort, bool? ipv6); /// A [DevicePortForwarder] for a proxied device. /// /// If [deviceId] is not null, the port forwarder forwards ports from the remote /// device, to the remote host, and then to the local host. /// /// If [deviceId] is null, then the port forwarder only forwards ports from the /// remote host to the local host. @visibleForTesting class ProxiedPortForwarder extends DevicePortForwarder { ProxiedPortForwarder(this.connection, { String? deviceId, required Logger logger, @visibleForTesting CreateSocketServer createSocketServer = _defaultCreateServerSocket, }) : _logger = logger, _deviceId = deviceId, _createSocketServer = createSocketServer; final String? _deviceId; DaemonConnection connection; final Logger _logger; final CreateSocketServer _createSocketServer; @override List get forwardedPorts => _hostPortToForwardedPorts.values.toList(); final Map _hostPortToForwardedPorts = {}; final List _connectedSockets = []; @override Future forward(int devicePort, {int? hostPort, bool? ipv6}) async { int? remoteDevicePort; final String? deviceId = _deviceId; // If deviceId is set, we need to forward the remote device port to remote host as well. // And then, forward the remote host port to a local host port. if (deviceId != null) { final Map result = _cast>( await connection.sendRequest('device.forward', { 'deviceId': deviceId, 'devicePort': devicePort, })); remoteDevicePort = devicePort; devicePort = result['hostPort']! as int; } final ServerSocket serverSocket = await _startProxyServer(devicePort, hostPort, ipv6); _hostPortToForwardedPorts[serverSocket.port] = _ProxiedForwardedPort( connection, hostPort: serverSocket.port, devicePort: devicePort, remoteDevicePort: remoteDevicePort, deviceId: deviceId, serverSocket: serverSocket, ); return serverSocket.port; } Future _startProxyServer(int devicePort, int? hostPort, bool? ipv6) async { final ServerSocket serverSocket = await _createSocketServer(_logger, hostPort, ipv6); serverSocket.listen((Socket socket) async { final String id = _cast(await connection.sendRequest('proxy.connect', { 'port': devicePort, })); final Stream> dataStream = connection.listenToEvent('proxy.data.$id').asyncExpand((DaemonEventData event) => event.binary); final StreamSubscription> subscription = dataStream.listen(socket.add); final Future disconnectFuture = connection.listenToEvent('proxy.disconnected.$id').first; bool socketDoneCalled = false; unawaited(disconnectFuture.then((_) async { try { if (socketDoneCalled) { await subscription.cancel(); } else { await (subscription.cancel(), socket.close()).wait; } } on Exception { // ignore } }, onError: (_) { // The event is not guaranteed to be sent if we initiated the disconnection. // Do nothing here. }, )); debounceDataStream(socket).listen((Uint8List data) { unawaited(connection.sendRequest('proxy.write', { 'id': id, }, data).then( (Object? obj) => obj, onError: (Object error, StackTrace stackTrace) { // Log the error, but proceed normally. Network failure should not // crash the tool. If this is critical, the place where the connection // is being used would crash. _logger.printWarning('Write to remote proxy error: $error'); _logger.printTrace('Write to remote proxy error: $error, stack trace: $stackTrace'); return null; }, )); }); _connectedSockets.add(socket); unawaited(socket.done.then( (Object? obj) => obj, onError: (Object error, StackTrace stackTrace) { // Do nothing here. Everything will be handled in the `then` block below. return false; }).whenComplete(() { socketDoneCalled = true; unawaited(subscription.cancel()); // Send a proxy disconnect event just in case. unawaited(connection.sendRequest('proxy.disconnect', { 'id': id, }).then( (Object? obj) => obj, onError: (Object error, StackTrace stackTrace) { // Ignore the error here. There might be a race condition when the // remote end also disconnects. In any case, this request is just to // notify the remote end to disconnect and we should not crash when // there is an error here. return null; }, )); _connectedSockets.remove(socket); })); }, onError: (Object error, StackTrace stackTrace) { _logger.printWarning('Server socket error: $error'); _logger.printTrace('Server socket error: $error, stack trace: $stackTrace'); }); return serverSocket; } @override Future unforward(ForwardedPort forwardedPort) async { // Look for the forwarded port entry in our own map. final _ProxiedForwardedPort? proxiedForwardedPort = _hostPortToForwardedPorts.remove(forwardedPort.hostPort); await proxiedForwardedPort?.unforward(); } @override Future dispose() async { for (final _ProxiedForwardedPort forwardedPort in _hostPortToForwardedPorts.values) { await forwardedPort.unforward(); } await Future.wait(>[ for (final Socket socket in _connectedSockets) socket.close(), ]); } /// Returns the original remote port given the local port. /// /// If this is not a port that is handled by this port forwarder, return null. int? originalRemotePort(int localForwardedPort) { return _hostPortToForwardedPorts[localForwardedPort]?.devicePort; } } Future _defaultCreateServerSocket(Logger logger, int? hostPort, bool? ipv6) async { if (ipv6 == null || !ipv6) { try { return await ServerSocket.bind(InternetAddress.loopbackIPv4, hostPort ?? 0); } on SocketException { logger.printTrace('Bind on $hostPort failed with IPv4, retrying on IPv6'); } } // If binding on ipv4 failed, try binding on ipv6. // Omit try catch here, let the failure fallthrough. return ServerSocket.bind(InternetAddress.loopbackIPv6, hostPort ?? 0); } /// A class that starts the [DartDevelopmentService] on the daemon. /// /// There are a lot of communications between DDS and the VM service on the /// device. When using proxied device, starting DDS remotely helps reduces the /// amount of data transferred with the remote daemon, hence improving latency. class ProxiedDartDevelopmentService with DartDevelopmentServiceLocalOperationsMixin implements DartDevelopmentService { ProxiedDartDevelopmentService( this.connection, this.deviceId, { required Logger logger, required ProxiedPortForwarder proxiedPortForwarder, required ProxiedPortForwarder devicePortForwarder, @visibleForTesting DartDevelopmentService? localDds, }) : _logger = logger, _proxiedPortForwarder = proxiedPortForwarder, _devicePortForwarder = devicePortForwarder, _localDds = localDds ?? DartDevelopmentService(logger: logger); final String deviceId; final Logger _logger; /// [DaemonConnection] used to communicate with the daemon. final DaemonConnection connection; /// [_proxiedPortForwarder] matches the [proxiedPortForwarder] of a ProxiedDevice. /// It forwards a port on the remote host to the local host. final ProxiedPortForwarder _proxiedPortForwarder; /// [_devicePortForwarder] matches the [portForwarder] of a ProxiedDevice. /// It forwards a port on the remotely connected device, to the remote host, then to the local host. final ProxiedPortForwarder _devicePortForwarder; @override Uri? get uri => _ddsStartedLocally ? _localDds.uri : _localUri; Uri? _localUri; @override Future get done => _completer.future; final Completer _completer = Completer(); final DartDevelopmentService _localDds; bool _ddsStartedLocally = false; @override Future startDartDevelopmentService( Uri vmServiceUri, { FlutterDevice? device, int? ddsPort, bool? ipv6, bool? disableServiceAuthCodes, bool enableDevTools = false, bool cacheStartupProfile = false, String? google3WorkspaceRoot, Uri? devToolsServerAddress, }) async { // Locate the original VM service port on the remote daemon. // A proxied device has two PortForwarder. Check both to determine which // one forwarded the VM service port. final int? remoteVMServicePort = _proxiedPortForwarder.originalRemotePort(vmServiceUri.port) ?? _devicePortForwarder.originalRemotePort(vmServiceUri.port); Future startLocalDds() async { _ddsStartedLocally = true; await _localDds.startDartDevelopmentService( vmServiceUri, ddsPort: ddsPort, ipv6: ipv6, disableServiceAuthCodes: disableServiceAuthCodes, cacheStartupProfile: cacheStartupProfile, enableDevTools: enableDevTools, google3WorkspaceRoot: google3WorkspaceRoot, devToolsServerAddress: devToolsServerAddress, ); unawaited(_localDds.done.then(_completer.complete)); } if (remoteVMServicePort == null) { _logger.printTrace('VM service port is not a forwarded port. Start DDS locally.'); await startLocalDds(); return; } final Uri remoteVMServiceUri = vmServiceUri.replace(port: remoteVMServicePort); String? remoteUriStr; const String method = 'device.startDartDevelopmentService'; try { // Proxies the `done` future. unawaited(connection .listenToEvent('device.dds.done.$deviceId') .first .then( (DaemonEventData event) => _completer.complete(), onError: (_) { // Ignore if we did not receive any event from the server. }, )); final Object? response = await connection.sendRequest(method, { 'deviceId': deviceId, 'vmServiceUri': remoteVMServiceUri.toString(), 'disableServiceAuthCodes': disableServiceAuthCodes, }); if (response is Map) { remoteUriStr = response['ddsUri'] as String?; } else { // For backwards compatibility in google3. // TODO(bkonyi): remove once a newer version of the flutter_tool is rolled out. remoteUriStr = _cast(response); } } on String catch (e) { if (!e.contains(method)) { rethrow; } // Remote daemon does not support the command, ignore. // We will try to start DDS locally below. } if (remoteUriStr == null) { _logger.printTrace('Remote daemon cannot start DDS. Start a local DDS instead.'); await startLocalDds(); return; } _logger.printTrace('Remote DDS started on $remoteUriStr.'); // Forward the port. final Uri remoteUri = Uri.parse(remoteUriStr); final int localPort = await _proxiedPortForwarder.forward( remoteUri.port, hostPort: ddsPort, ipv6: ipv6, ); _localUri = remoteUri.replace(port: localPort); _logger.printTrace('Local port forwarded DDS on $_localUri.'); _logger.sendEvent('device.proxied_dds_forwarded', { 'deviceId': deviceId, 'remoteUri': remoteUri.toString(), 'localUri': _localUri!.toString(), }); } @override Future shutdown() async { if (_ddsStartedLocally) { _localDds.shutdown(); _ddsStartedLocally = false; } else { await connection.sendRequest('device.shutdownDartDevelopmentService', { 'deviceId': deviceId, }); } } } class ProxiedVMServiceDiscoveryForAttach extends VMServiceDiscoveryForAttach { ProxiedVMServiceDiscoveryForAttach( this.connection, this.deviceId, { required this.proxiedPortForwarder, required this.fallbackDiscovery, this.appId, this.fuchsiaModule, this.filterDevicePort, this.expectedHostPort, required this.ipv6, required this.logger, }); /// [DaemonConnection] used to communicate with the daemon. final DaemonConnection connection; final String deviceId; final String? appId; final String? fuchsiaModule; final int? filterDevicePort; final int? expectedHostPort; final bool ipv6; final Logger logger; final ProxiedPortForwarder proxiedPortForwarder; VMServiceDiscoveryForAttach Function() fallbackDiscovery; Stream? _uris; @override Stream get uris { if (_uris == null) { String? requestId; final StreamController controller = StreamController(); controller.onListen = () { connection.sendRequest('device.startVMServiceDiscoveryForAttach', { 'deviceId': deviceId, 'appId': appId, 'fuchsiaModule': fuchsiaModule, 'filterDevicePort': filterDevicePort, 'ipv6': ipv6, }).then( (Object? response) async { requestId = _cast(response); final Stream vmService = connection .listenToEvent('device.VMServiceDiscoveryForAttach.$requestId') .asyncMap((DaemonEventData event) async { // Forward the port. final Uri remoteUri = Uri.parse(_cast(event.data)); final int port = remoteUri.port; final int localPort = await proxiedPortForwarder.forward(port, hostPort: expectedHostPort, ipv6: ipv6); return remoteUri.replace(port: localPort); }); await controller.addStream(vmService); }, onError: (Object e) { // Daemon throws string types. if (e is String && e.contains('command not understood')) { // Use a fallback if the daemon does not support VM service discovery. controller.addStream(fallbackDiscovery().uris); } else { controller.addError(e); } }, ); }; controller.onCancel = () { if (requestId != null) { connection.sendRequest('device.stopVMServiceDiscoveryForAttach', {'id': requestId}); } }; _uris = controller.stream; } return _uris!; } }