
The daemon server is expecting the client to pass `deviceId` in `device.shutdownDartDevelopmentService` method.
24a792dae1/packages/flutter_tools/lib/src/commands/daemon.dart (L1239)
827 lines
28 KiB
Dart
827 lines
28 KiB
Dart
// 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 '../project.dart';
|
|
import 'debounce_data_stream.dart';
|
|
import 'file_transfer.dart';
|
|
|
|
bool _isNullable<T>() => null is T;
|
|
|
|
T _cast<T>(Object? object) {
|
|
if (!_isNullable<T>() && 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<Device>? _devices;
|
|
|
|
@override
|
|
Future<List<Device>> devices({DeviceDiscoveryFilter? filter}) async =>
|
|
_filterDevices(_devices ?? await discoverDevices(), filter);
|
|
|
|
@override
|
|
Future<List<Device>> discoverDevices({
|
|
Duration? timeout,
|
|
DeviceDiscoveryFilter? filter
|
|
}) async {
|
|
final List<Map<String, Object?>> discoveredDevices = _cast<List<dynamic>>(await connection.sendRequest('device.discoverDevices')).cast<Map<String, Object?>>();
|
|
final List<ProxiedDevice> devices = <ProxiedDevice>[
|
|
for (final Map<String, Object?> device in discoveredDevices)
|
|
deviceFromDaemonResult(device),
|
|
];
|
|
|
|
_devices = devices;
|
|
return _filterDevices(devices, filter);
|
|
}
|
|
|
|
Future<List<Device>> _filterDevices(List<Device> devices, DeviceDiscoveryFilter? filter) async {
|
|
if (filter == null) {
|
|
return devices;
|
|
}
|
|
return filter.filterDevices(devices);
|
|
}
|
|
|
|
@override
|
|
Future<List<Device>> pollingGetDevices({Duration? timeout}) => discoverDevices(timeout: timeout);
|
|
|
|
@override
|
|
List<String> get wellKnownIds => const <String>[];
|
|
|
|
@visibleForTesting
|
|
ProxiedDevice deviceFromDaemonResult(Map<String, Object?> device) {
|
|
final Map<String, Object?> capabilities = _cast<Map<String, Object?>>(device['capabilities']);
|
|
return ProxiedDevice(
|
|
connection, _cast<String>(device['id']),
|
|
deltaFileTransfer: _deltaFileTransfer,
|
|
enableDdsProxy: _enableDdsProxy,
|
|
category: Category.fromString(_cast<String>(device['category'])),
|
|
platformType: PlatformType.fromString(_cast<String>(device['platformType'])),
|
|
targetPlatform: getTargetPlatformForName(_cast<String>(device['platform'])),
|
|
ephemeral: _cast<bool>(device['ephemeral']),
|
|
name: 'Proxied ${device['name']}',
|
|
isLocalEmulator: _cast<bool>(device['emulator']),
|
|
emulatorId: _cast<String?>(device['emulatorId']),
|
|
sdkNameAndVersion: _cast<String>(device['sdk']),
|
|
supportsHotReload: _cast<bool>(capabilities['hotReload']),
|
|
supportsHotRestart: _cast<bool>(capabilities['hotRestart']),
|
|
supportsFlutterExit: _cast<bool>(capabilities['flutterExit']),
|
|
supportsScreenshot: _cast<bool>(capabilities['screenshot']),
|
|
supportsFastStart: _cast<bool>(capabilities['fastStart']),
|
|
supportsHardwareRendering: _cast<bool>(capabilities['hardwareRendering']),
|
|
logger: _logger,
|
|
fileTransfer: _fileTransfer,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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.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 Logger 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 String name;
|
|
|
|
final bool _isLocalEmulator;
|
|
@override
|
|
Future<bool> get isLocalEmulator async => _isLocalEmulator;
|
|
|
|
final String? _emulatorId;
|
|
@override
|
|
Future<String?> get emulatorId async => _emulatorId;
|
|
|
|
@override
|
|
Future<bool> supportsRuntimeMode(BuildMode buildMode) async =>
|
|
_cast<bool>(await connection.sendRequest('device.supportsRuntimeMode', <String, Object>{
|
|
'deviceId': id,
|
|
'buildMode': buildMode.toString(),
|
|
}));
|
|
|
|
final bool _supportsHardwareRendering;
|
|
@override
|
|
Future<bool> 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<bool> isAppInstalled(
|
|
ApplicationPackage app, {
|
|
String? userIdentifier,
|
|
}) => throw UnimplementedError();
|
|
|
|
@override
|
|
Future<bool> isLatestBuildInstalled(ApplicationPackage app) => throw UnimplementedError();
|
|
|
|
@override
|
|
Future<bool> installApp(
|
|
ApplicationPackage app, {
|
|
String? userIdentifier,
|
|
}) => throw UnimplementedError();
|
|
|
|
@override
|
|
Future<bool> uninstallApp(
|
|
ApplicationPackage app, {
|
|
String? userIdentifier,
|
|
}) => throw UnimplementedError();
|
|
|
|
@override
|
|
bool isSupported() => true;
|
|
|
|
final TargetPlatform _targetPlatform;
|
|
@override
|
|
Future<TargetPlatform> get targetPlatform async => _targetPlatform;
|
|
TargetPlatform get targetPlatformSync => _targetPlatform;
|
|
|
|
final String _sdkNameAndVersion;
|
|
@override
|
|
Future<String> get sdkNameAndVersion async => _sdkNameAndVersion;
|
|
|
|
@override
|
|
FutureOr<DeviceLogReader> 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);
|
|
|
|
DevicePortForwarder? _portForwarder;
|
|
/// [portForwarder] forwards a port from the remote device to remote host, and
|
|
/// then forward the port from remote host to local host.
|
|
@override
|
|
DevicePortForwarder 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);
|
|
}
|
|
|
|
@override
|
|
void clearLogs() => throw UnimplementedError();
|
|
|
|
@override
|
|
Future<LaunchResult> startApp(
|
|
PrebuiltApplicationPackage package, {
|
|
String? mainPath,
|
|
String? route,
|
|
required DebuggingOptions debuggingOptions,
|
|
Map<String, Object?> platformArgs = const <String, Object?>{},
|
|
bool prebuiltApplication = false,
|
|
bool ipv6 = false,
|
|
String? userIdentifier,
|
|
}) async {
|
|
final Map<String, Object?> result = _cast<Map<String, Object?>>(await connection.sendRequest('device.startApp', <String, Object?>{
|
|
'deviceId': id,
|
|
'applicationPackageId': await applicationPackageId(package),
|
|
'mainPath': mainPath,
|
|
'route': route,
|
|
'debuggingOptions': debuggingOptions.toJson(),
|
|
'platformArgs': platformArgs,
|
|
'prebuiltApplication': prebuiltApplication,
|
|
'ipv6': ipv6,
|
|
'userIdentifier': userIdentifier,
|
|
}));
|
|
final bool started = _cast<bool>(result['started']);
|
|
// TODO(bkonyi): remove once clients have migrated to relying on vmServiceUri.
|
|
final String? vmServiceUriStr = _cast<String?>(result['vmServiceUri']) ?? _cast<String?>(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<bool> stopApp(
|
|
covariant PrebuiltApplicationPackage? app, {
|
|
String? userIdentifier,
|
|
}) async {
|
|
return _cast<bool>(await connection.sendRequest('device.stopApp', <String, Object?>{
|
|
'deviceId': id,
|
|
if (app != null)
|
|
'applicationPackageId': await applicationPackageId(app),
|
|
'userIdentifier': userIdentifier,
|
|
}));
|
|
}
|
|
|
|
@override
|
|
Future<MemoryInfo> queryMemoryInfo() => throw UnimplementedError();
|
|
|
|
@override
|
|
Future<void> takeScreenshot(File outputFile) async {
|
|
final String imageBase64 = _cast<String>(await connection.sendRequest('device.takeScreenshot', <String, Object?>{
|
|
'deviceId': id,
|
|
}));
|
|
await outputFile.writeAsBytes(base64.decode(imageBase64));
|
|
}
|
|
|
|
@override
|
|
Future<void> dispose() async {
|
|
await proxiedPortForwarder.dispose();
|
|
}
|
|
|
|
final Map<String, Future<String>> _applicationPackageMap =
|
|
<String, Future<String>>{};
|
|
Future<String> 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<String> idCompleter = Completer<String>();
|
|
_applicationPackageMap[path] = idCompleter.future;
|
|
|
|
final Map<String, Object> args = <String, Object>{'path': fileName};
|
|
|
|
Map<String, Object?>? rollingHashResultJson;
|
|
if (_deltaFileTransfer) {
|
|
rollingHashResultJson = _cast<Map<String, Object?>?>(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<FileDeltaBlock> delta = await _fileTransfer.computeDelta(binary, rollingHashResult);
|
|
|
|
// Delta is empty if the file does not need to be updated
|
|
if (delta.isNotEmpty) {
|
|
final List<Map<String, Object>> deltaJson = delta.map((FileDeltaBlock block) => block.toJson()).toList();
|
|
final Uint8List buffer = await _fileTransfer.binaryForRebuilding(binary, delta);
|
|
|
|
await connection.sendRequest('proxy.updateFile', <String, Object>{
|
|
'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<void>.delayed(const Duration(seconds: 60));
|
|
await connection.sendRequest('proxy.calculateFileHashes', <String, Object>{
|
|
'path': fileName,
|
|
'cacheResult': true,
|
|
});
|
|
}());
|
|
}
|
|
|
|
final String id = _cast<String>(await connection.sendRequest('device.uploadApplicationPackage', <String, Object>{
|
|
'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<String> _logLinesStreamController = StreamController<String>();
|
|
Stream<String>? _logLines;
|
|
|
|
String? _id;
|
|
|
|
@override
|
|
Stream<String> get logLines => _logLines ??= _start();
|
|
|
|
Stream<String> _start() {
|
|
final PrebuiltApplicationPackage? package = applicationPackage;
|
|
final Future<String?> applicationPackageId = package != null ? device.applicationPackageId(package) : Future<String?>.value();
|
|
final Future<String> idFuture = applicationPackageId.then((String? applicationPackageId) async =>
|
|
_cast<String>(await connection.sendRequest('device.logReader.start', <String, Object>{
|
|
'deviceId': device.id,
|
|
if (applicationPackageId != null)
|
|
'applicationPackageId': applicationPackageId,
|
|
})));
|
|
idFuture.then((String id) {
|
|
_id = id;
|
|
final Stream<String> 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', <String, Object?>{
|
|
'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<void> unforward() async {
|
|
await serverSocket.close();
|
|
|
|
if (remoteDevicePort != null && deviceId != null) {
|
|
await connection.sendRequest('device.unforward', <String, Object>{
|
|
'deviceId': deviceId!,
|
|
'devicePort': remoteDevicePort!,
|
|
'hostPort': devicePort,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
typedef CreateSocketServer = Future<ServerSocket> 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<ForwardedPort> get forwardedPorts => _hostPortToForwardedPorts.values.toList();
|
|
|
|
final Map<int, _ProxiedForwardedPort> _hostPortToForwardedPorts = <int, _ProxiedForwardedPort>{};
|
|
|
|
final List<Socket> _connectedSockets = <Socket>[];
|
|
|
|
@override
|
|
Future<int> 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<String, Object?> result = _cast<Map<String, Object?>>(
|
|
await connection.sendRequest('device.forward', <String, Object>{
|
|
'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<ServerSocket> _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<String>(await connection.sendRequest('proxy.connect', <String, Object>{
|
|
'port': devicePort,
|
|
}));
|
|
final Stream<List<int>> dataStream = connection.listenToEvent('proxy.data.$id').asyncExpand((DaemonEventData event) => event.binary);
|
|
dataStream.listen(socket.add);
|
|
final Future<DaemonEventData> disconnectFuture = connection.listenToEvent('proxy.disconnected.$id').first;
|
|
unawaited(disconnectFuture.then<void>((_) async {
|
|
try {
|
|
await socket.close();
|
|
} 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', <String, Object>{
|
|
'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(() {
|
|
// Send a proxy disconnect event just in case.
|
|
unawaited(connection.sendRequest('proxy.disconnect', <String, Object>{
|
|
'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<void> 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<void> dispose() async {
|
|
for (final _ProxiedForwardedPort forwardedPort in _hostPortToForwardedPorts.values) {
|
|
await forwardedPort.unforward();
|
|
}
|
|
|
|
await Future.wait(<Future<void>>[
|
|
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<ServerSocket> _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 implements DartDevelopmentService {
|
|
ProxiedDartDevelopmentService(
|
|
this.connection,
|
|
this.deviceId, {
|
|
required Logger logger,
|
|
required ProxiedPortForwarder proxiedPortForwarder,
|
|
@visibleForTesting DartDevelopmentService? localDds,
|
|
}) : _logger = logger,
|
|
_proxiedPortForwarder = proxiedPortForwarder,
|
|
_localDds = localDds ?? DartDevelopmentService();
|
|
|
|
final String deviceId;
|
|
|
|
final Logger _logger;
|
|
|
|
/// [DaemonConnection] used to communicate with the daemon.
|
|
final DaemonConnection connection;
|
|
|
|
final ProxiedPortForwarder _proxiedPortForwarder;
|
|
|
|
Uri? _localUri;
|
|
|
|
@override
|
|
Uri? get uri => _ddsStartedLocally ? _localDds.uri : _localUri;
|
|
|
|
@override
|
|
Future<void> get done => _completer.future;
|
|
final Completer<void> _completer = Completer<void>();
|
|
|
|
final DartDevelopmentService _localDds;
|
|
|
|
bool _ddsStartedLocally = false;
|
|
|
|
@override
|
|
Future<void> startDartDevelopmentService(
|
|
Uri vmServiceUri, {
|
|
required Logger logger,
|
|
int? hostPort,
|
|
bool? ipv6,
|
|
bool? disableServiceAuthCodes,
|
|
bool cacheStartupProfile = false,
|
|
}) async {
|
|
// Locate the original VM service port on the remote daemon.
|
|
final int? remoteVMServicePort = _proxiedPortForwarder.originalRemotePort(vmServiceUri.port);
|
|
|
|
if (remoteVMServicePort == null) {
|
|
_logger.printTrace('VM service port is not a forwarded port. Start DDS locally.');
|
|
_ddsStartedLocally = true;
|
|
await _localDds.startDartDevelopmentService(
|
|
vmServiceUri,
|
|
logger: logger,
|
|
hostPort: hostPort,
|
|
ipv6: ipv6,
|
|
disableServiceAuthCodes: disableServiceAuthCodes,
|
|
cacheStartupProfile: cacheStartupProfile,
|
|
);
|
|
unawaited(_localDds.done.then(_completer.complete));
|
|
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.
|
|
},
|
|
));
|
|
remoteUriStr = _cast<String?>(await connection.sendRequest(method, <String, Object?>{
|
|
'deviceId': deviceId,
|
|
'vmServiceUri': remoteVMServiceUri.toString(),
|
|
'disableServiceAuthCodes': disableServiceAuthCodes,
|
|
}));
|
|
} 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.');
|
|
_ddsStartedLocally = true;
|
|
await _localDds.startDartDevelopmentService(
|
|
vmServiceUri,
|
|
logger: logger,
|
|
hostPort: hostPort,
|
|
ipv6: ipv6,
|
|
disableServiceAuthCodes: disableServiceAuthCodes,
|
|
cacheStartupProfile: cacheStartupProfile,
|
|
);
|
|
unawaited(_localDds.done.then(_completer.complete));
|
|
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: hostPort,
|
|
ipv6: ipv6,
|
|
);
|
|
|
|
_localUri = remoteUri.replace(port: localPort);
|
|
_logger.printTrace('Local port forwarded DDS on $_localUri.');
|
|
_logger.sendEvent('device.proxied_dds_forwarded', <String, String>{
|
|
'deviceId': deviceId,
|
|
'remoteUri': remoteUri.toString(),
|
|
'localUri': _localUri!.toString(),
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<void> shutdown() async {
|
|
if (_ddsStartedLocally) {
|
|
await _localDds.shutdown();
|
|
_ddsStartedLocally = false;
|
|
} else {
|
|
await connection.sendRequest('device.shutdownDartDevelopmentService', <String, Object?>{
|
|
'deviceId': deviceId,
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
void setExternalDevToolsUri(Uri uri) {
|
|
connection.sendRequest('device.setExternalDevToolsUriForDartDevelopmentService', <String, Object?>{
|
|
'deviceId': deviceId,
|
|
'uri': uri.toString(),
|
|
});
|
|
}
|
|
}
|