refactor: reuse core discovery
CI / format (push) Has been cancelled
CI / test (push) Has been cancelled
CI / packaging (push) Has been cancelled

This commit is contained in:
Tien Do Nam
2026-07-31 18:32:52 +02:00
parent 6d547b6969
commit 7ec5d26294
28 changed files with 2468 additions and 1782 deletions
+2 -2
View File
@@ -216,9 +216,9 @@ Future<void> postInit(BuildContext context, Ref ref, bool appStart) async {
}
try {
ref.redux(nearbyDevicesProvider).dispatchAsync(StartMulticastListener()); // ignore: unawaited_futures
ref.redux(nearbyDevicesProvider).dispatchAsync(StartDiscoveryListener()); // ignore: unawaited_futures
} catch (e) {
_logger.warning('Starting multicast listener failed', e);
_logger.warning('Starting discovery listener failed', e);
}
// ignore: dead_code
@@ -135,7 +135,7 @@ class SettingsTabController extends ReduxNotifier<SettingsTabVm> {
state.portController.text = newServerState.port.toString();
await _settingsService.setAlias(newServerState.alias);
await _settingsService.setPort(newServerState.port);
external(_isolateController).dispatch(IsolateSendMulticastRestartListenerAction());
external(_isolateController).dispatch(IsolateDiscoveryRestartAction());
external(_localIpService).dispatchAsync(FetchLocalIpAction()); // ignore: unawaited_futures
}
} catch (e) {
@@ -44,14 +44,17 @@ class NearbyDevicesService extends ReduxNotifier<NearbyDevicesState> {
);
}
/// Binds the UDP port and listens for incoming announcements.
/// Starts the discovery (which binds the UDP port) and registers every
/// confirmed device: answered announcements, scan results and devices fed in
/// via [IsolateDiscoveryAddDeviceAction] all arrive on this one stream.
/// This should run forever as long as the app is running.
class StartMulticastListener extends AsyncReduxAction<NearbyDevicesService, NearbyDevicesState> {
class StartDiscoveryListener extends AsyncReduxAction<NearbyDevicesService, NearbyDevicesState> {
@override
Future<NearbyDevicesState> reduce() async {
await for (final device in notifier._isolateController.state.multicastDiscovery!.receiveFromIsolate) {
final stream = external(notifier._isolateController).dispatchTakeResult(IsolateDiscoveryListenAction());
await for (final device in stream) {
await dispatchAsync(RegisterDeviceAction(device));
notifier._discoveryLogger.addLog('[DISCOVER/UDP] ${device.alias} (${device.ip}, model: ${device.deviceModel})');
notifier._discoveryLogger.addLog('[DISCOVER] ${device.alias} (${device.ip}, model: ${device.deviceModel})');
}
return state;
}
@@ -138,7 +141,7 @@ class UnregisterSignalingDeviceAction extends ReduxAction<NearbyDevicesService,
class StartMulticastScan extends ReduxAction<NearbyDevicesService, NearbyDevicesState> {
@override
NearbyDevicesState reduce() {
external(notifier._isolateController).dispatch(IsolateSendMulticastAnnouncementAction());
external(notifier._isolateController).dispatch(IsolateDiscoveryAnnouncementAction());
return state;
}
}
@@ -166,18 +169,17 @@ class StartLegacyScan extends AsyncReduxAction<NearbyDevicesService, NearbyDevic
dispatch(_SetRunningIpsAction({...state.runningIps, localIp}));
final stream = external(notifier._isolateController).dispatchTakeResult(
IsolateInterfaceHttpDiscoveryAction(
networkInterface: localIp,
port: port,
https: https,
),
);
await for (final device in stream) {
notifier._discoveryLogger.addLog('[DISCOVER/TCP] ${device.alias} (${device.ip}, model: ${device.deviceModel})');
await dispatchAsync(RegisterDeviceAction(device));
}
// The found devices arrive on the [StartDiscoveryListener] stream;
// this stream only signals when the scan is finished.
await external(notifier._isolateController)
.dispatchTakeResult(
IsolateDiscoverySubnetScanAction(
networkInterface: localIp,
port: port,
https: https,
),
)
.drain<void>();
return state.copyWith(
runningIps: state.runningIps.where((ip) => ip != localIp).toSet(),
@@ -201,17 +203,16 @@ class StartFavoriteScan extends AsyncReduxAction<NearbyDevicesService, NearbyDev
}
dispatch(_SetRunningFavoriteScanAction(true));
final stream = external(notifier._isolateController).dispatchTakeResult(
IsolateFavoriteHttpDiscoveryAction(
favorites: devices.map((e) => (e.ip, e.port)).toList(),
https: https,
),
);
await for (final device in stream) {
notifier._discoveryLogger.addLog('[DISCOVER/TCP] ${device.alias} (${device.ip}, model: ${device.deviceModel})');
await dispatchAsync(RegisterDeviceAction(device));
}
// The found devices arrive on the [StartDiscoveryListener] stream;
// this stream only signals when every favorite has been probed.
await external(notifier._isolateController)
.dispatchTakeResult(
IsolateDiscoveryFavoriteScanAction(
favorites: devices.map((e) => (e.ip, e.port)).toList(),
https: https,
),
)
.drain<void>();
return state.copyWith(
runningFavoriteScan: false,
@@ -13,7 +13,6 @@ import 'package:localsend_app/provider/device_info_provider.dart';
import 'package:localsend_app/provider/favorites_provider.dart';
import 'package:localsend_app/provider/http_provider.dart';
import 'package:localsend_app/provider/logging/discovery_logs_provider.dart';
import 'package:localsend_app/provider/network/nearby_devices_provider.dart';
import 'package:localsend_app/provider/network/send_provider.dart';
import 'package:localsend_app/provider/network/server/server_provider.dart';
import 'package:localsend_app/provider/network/server/server_utils.dart';
@@ -59,7 +58,11 @@ class ReceiveController {
return;
}
await server.ref.redux(nearbyDevicesProvider).dispatchAsync(RegisterDeviceAction(event.info.toDevice(event.ip, HttpDiscovery(ip: event.ip))));
// Feed the device into the discovery store; it comes back (and is
// registered) via the [StartDiscoveryListener] stream.
server.ref
.redux(parentIsolateProvider)
.dispatch(IsolateDiscoveryAddDeviceAction(device: event.info.toDevice(event.ip, HttpDiscovery(ip: event.ip))));
server.ref.notifier(discoveryLoggerProvider).addLog('[DISCOVER/TCP] Received "/register" HTTP request: ${event.info.alias} (${event.ip})');
}
+19
View File
@@ -15,6 +15,7 @@ use futures_util::StreamExt;
use std::collections::HashSet;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, SystemTime};
use store::DeviceStore;
use tokio::sync::{mpsc, oneshot};
@@ -105,6 +106,10 @@ struct DiscoveryState {
store: DeviceStore,
event_tx: Option<mpsc::Sender<DiscoveryEvent>>,
/// Whether announcements of other devices are answered, see
/// [`DiscoveryHandle::set_answer_announcements`].
answering: AtomicBool,
/// The interface addresses a subnet scan is currently running for.
scanning: std::sync::Mutex<HashSet<Ipv4Addr>>,
}
@@ -279,6 +284,16 @@ impl DiscoveryHandle {
Ok(found)
}
/// Sets whether announcements of other devices are answered with a
/// register request (the answer is what makes the announcing device
/// enter the store). On by default.
///
/// An application whose server is not running turns this off: the answer
/// would advertise an HTTP port that nobody listens on.
pub fn set_answer_announcements(&self, answer: bool) {
self.state.answering.store(answer, Ordering::Relaxed);
}
/// Puts a device confirmed outside of discovery into the store, e.g. one
/// that answered an announcement by registering with this device's HTTP
/// server. The confirmation is emitted as `Discovered` or `Updated`;
@@ -353,6 +368,7 @@ pub async fn start(config: DiscoveryConfig, stop_rx: oneshot::Receiver<()>) -> D
timeout: config.timeout,
store: DeviceStore::new(),
event_tx: config.event_tx,
answering: AtomicBool::new(true),
scanning: std::sync::Mutex::new(HashSet::new()),
});
@@ -362,6 +378,9 @@ pub async fn start(config: DiscoveryConfig, stop_rx: oneshot::Receiver<()>) -> D
let state = state.clone();
async move {
while let Some(event) = multicast_rx.recv().await {
if !state.answering.load(Ordering::Relaxed) {
continue;
}
let MulticastEvent::Discovered {
ip,
scope_id,
@@ -10,6 +10,8 @@ import 'package:localsend_isolates/rust/frb_generated.dart';
part 'crypto.freezed.dart';
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`
Future<void> verifyCert({required String cert, required String publicKey}) =>
RustLib.instance.api.crateApiCryptoVerifyCert(cert: cert, publicKey: publicKey);
@@ -0,0 +1,188 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:localsend_isolates/rust/api/model.dart';
import 'package:localsend_isolates/rust/api/server.dart';
import 'package:localsend_isolates/rust/frb_generated.dart';
// These functions are ignored because they are not marked as `pub`: `rs_device`
/// Starts the discovery: binds the UDP multicast sockets on all usable
/// network interfaces, answers announcements of other devices with an HTTP
/// register request, and keeps the store of confirmed devices.
///
/// Announcements are received from the IPv4 [group] and, as a LocalSend
/// extension, from the (currently hardcoded) IPv6 group `ff12::fd3a:e420`.
///
/// [port] is used both to bind the multicast sockets and as the HTTP server
/// port announced to other devices. [cert_pem] and [private_key_pem] are this
/// device's TLS identity, sent as client certificate with every register
/// request; [fingerprint] must be the certificate's SHA-256 fingerprint.
///
/// Nothing is announced until [RsDiscovery::announce] is called.
///
/// Cannot fail besides an invalid [group]: when no multicast socket could be
/// bound (e.g. the port is taken by another process), discovery still runs
/// without multicast — see [RsDiscovery::multicast_error] — and still learns
/// about devices through [RsDiscovery::discover], [RsDiscovery::scan_subnet]
/// and [RsDiscovery::add_device].
Future<RsDiscovery> startDiscovery({
required String group,
required int port,
List<String>? networkWhitelist,
List<String>? networkBlacklist,
required String alias,
required String version,
String? deviceModel,
DeviceType? deviceType,
required String fingerprint,
required ProtocolTypeV2 protocol,
required bool download,
required String certPem,
required String privateKeyPem,
required BigInt timeoutMs,
}) => RustLib.instance.api.crateApiDiscoveryStartDiscovery(
group: group,
port: port,
networkWhitelist: networkWhitelist,
networkBlacklist: networkBlacklist,
alias: alias,
version: version,
deviceModel: deviceModel,
deviceType: deviceType,
fingerprint: fingerprint,
protocol: protocol,
download: download,
certPem: certPem,
privateKeyPem: privateKeyPem,
timeoutMs: timeoutMs,
);
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsDiscovery>>
abstract class RsDiscovery implements RustOpaqueInterface {
/// Puts a device confirmed outside of the discovery into the store, e.g.
/// one that answered an announcement by registering with this device's
/// HTTP server. The device is emitted on [RsDiscovery::listen].
Future<void> addDevice({required RsDiscoveredDevice device});
/// Announces this device to the network, which makes every other LocalSend
/// device on it register with this device over HTTP.
///
/// Devices registering in response arrive at the application as server
/// events, not here: feed them back via [RsDiscovery::add_device].
///
/// Returns once the whole announcement burst has been sent, which takes a
/// few seconds, or immediately once the discovery has been stopped or
/// multicast is unavailable.
Future<void> announce();
/// Discovers a device at a known address, e.g. a favorite or a peer that
/// multicast does not reach, by sending it a register request.
///
/// The confirmed device is also emitted on [RsDiscovery::listen].
/// Returns `None` when the device did not answer or answered with this
/// device's own fingerprint (i.e. the device discovered itself).
Future<RsDiscoveredDevice?> discover({required String host, required int port, required ProtocolTypeV2 protocol});
/// Emits a [RsDiscoveredDevice] for every device confirmation until the
/// discovery is stopped. Can only be listened to once.
Stream<RsDiscoveredDevice> listen();
/// The reason the multicast sockets could not be bound, when they could
/// not. Discovery then neither hears nor sends announcements.
Future<String?> multicastError();
/// Scans the `/24` subnet of the local interface address [interface_ip]
/// by sending every other host a register request, for networks that do
/// not carry multicast.
///
/// The found devices are emitted on [RsDiscovery::listen] as they answer;
/// this method returns once the whole scan has finished. At most one scan
/// runs per interface: a call for an address that is still being scanned
/// returns immediately.
Future<void> scanSubnet({required String interfaceIp, required int port, required ProtocolTypeV2 protocol});
/// Sets whether announcements of other devices are answered with a
/// register request (the answer is what makes the announcing device enter
/// the store). On by default.
///
/// Turned off while the HTTP server is not running: the answer would
/// advertise a port that nobody listens on.
Future<void> setAnswerAnnouncements({required bool answer});
/// Stops the discovery, which also ends the [RsDiscovery::listen] stream.
/// Returns after all sockets are closed, so the port can be bound again.
Future<void> stop();
}
/// A device that was confirmed over HTTP by the discovery: it answered a
/// register request, or its register request was accepted by our server and
/// was fed back via [RsDiscovery::add_device].
///
/// Emitted on [RsDiscovery::listen] for every confirmation, so a device
/// re-appears whenever it re-announces itself or is re-discovered.
class RsDiscoveredDevice {
final String alias;
/// Protocol version (major.minor) implemented by the device.
final String version;
final String? deviceModel;
final DeviceType? deviceType;
/// Fingerprint identifying the device; devices are deduplicated by it.
final String fingerprint;
/// The host the device was confirmed on: an IP address, or the scoped
/// form `fe80::1%3` for link-local IPv6 (the Rust HTTP client accepts
/// both back as a host).
final String host;
/// The port of the device's HTTP server.
final int port;
final ProtocolTypeV2 protocol;
/// Whether the device's download API is active.
final bool download;
const RsDiscoveredDevice({
required this.alias,
required this.version,
this.deviceModel,
this.deviceType,
required this.fingerprint,
required this.host,
required this.port,
required this.protocol,
required this.download,
});
@override
int get hashCode =>
alias.hashCode ^
version.hashCode ^
deviceModel.hashCode ^
deviceType.hashCode ^
fingerprint.hashCode ^
host.hashCode ^
port.hashCode ^
protocol.hashCode ^
download.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RsDiscoveredDevice &&
runtimeType == other.runtimeType &&
alias == other.alias &&
version == other.version &&
deviceModel == other.deviceModel &&
deviceType == other.deviceType &&
fingerprint == other.fingerprint &&
host == other.host &&
port == other.port &&
protocol == other.protocol &&
download == other.download;
}
@@ -1,141 +0,0 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:localsend_isolates/rust/api/model.dart';
import 'package:localsend_isolates/rust/api/server.dart';
import 'package:localsend_isolates/rust/frb_generated.dart';
/// Starts UDP multicast discovery: binds the multicast sockets on all usable
/// network interfaces and listens for announcements of other devices.
///
/// Announcements are sent to the IPv4 [group] and, as a LocalSend extension,
/// to the (currently hardcoded) IPv6 group `ff12::fd3a:e420`.
///
/// [port] is used both to bind the multicast sockets and as the HTTP server
/// port announced to other devices.
///
/// Nothing is announced until [RsMulticast::announce] is called.
///
/// Fails when [group] is not a valid IPv4 address or when no network
/// interface could be used at all.
Future<RsMulticast> startMulticast({
required String group,
required int port,
List<String>? networkWhitelist,
List<String>? networkBlacklist,
required String alias,
required String version,
String? deviceModel,
DeviceType? deviceType,
required String fingerprint,
required ProtocolTypeV2 protocol,
required bool download,
}) => RustLib.instance.api.crateApiMulticastStartMulticast(
group: group,
port: port,
networkWhitelist: networkWhitelist,
networkBlacklist: networkBlacklist,
alias: alias,
version: version,
deviceModel: deviceModel,
deviceType: deviceType,
fingerprint: fingerprint,
protocol: protocol,
download: download,
);
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<RsMulticast>>
abstract class RsMulticast implements RustOpaqueInterface {
/// Announces this device to the network, which makes every other LocalSend
/// device on it register with this device over HTTP.
///
/// Returns once the whole announcement burst has been sent, which takes a
/// few seconds, or immediately once discovery has been stopped.
Future<void> announce();
/// Emits a [RsMulticastDiscovered] for every announcement received from
/// another device until discovery is stopped.
/// Can only be listened to once.
Stream<RsMulticastDiscovered> listen();
/// Stops discovery, which also ends the [RsMulticast::listen] stream.
/// Returns after all sockets are closed, so the port can be bound again.
Future<void> stop();
}
class MulticastMessageV2 {
final String alias;
final String version;
final String? deviceModel;
final DeviceType? deviceType;
final String fingerprint;
final int port;
final ProtocolTypeV2 protocol;
final bool download;
const MulticastMessageV2({
required this.alias,
required this.version,
this.deviceModel,
this.deviceType,
required this.fingerprint,
required this.port,
required this.protocol,
required this.download,
});
@override
int get hashCode =>
alias.hashCode ^
version.hashCode ^
deviceModel.hashCode ^
deviceType.hashCode ^
fingerprint.hashCode ^
port.hashCode ^
protocol.hashCode ^
download.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MulticastMessageV2 &&
runtimeType == other.runtimeType &&
alias == other.alias &&
version == other.version &&
deviceModel == other.deviceModel &&
deviceType == other.deviceType &&
fingerprint == other.fingerprint &&
port == other.port &&
protocol == other.protocol &&
download == other.download;
}
/// Another device announced itself via UDP multicast.
///
/// The peer expects to be answered with an HTTP register request.
class RsMulticastDiscovered {
/// The address the announcement was sent from. The peer's HTTP server is
/// reachable at this address on `message.port`.
///
/// A link-local IPv6 source carries its interface scope as `fe80::1%3`,
/// which the Rust HTTP client accepts back as a host.
final String ip;
/// The announcement as it was received.
final MulticastMessageV2 message;
const RsMulticastDiscovered({
required this.ip,
required this.message,
});
@override
int get hashCode => ip.hashCode ^ message.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) || other is RsMulticastDiscovered && runtimeType == other.runtimeType && ip == other.ip && message == other.message;
}
File diff suppressed because it is too large Load Diff
@@ -10,10 +10,10 @@ import 'dart:ffi' as ffi;
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
import 'package:localsend_isolates/rust/api/cancel.dart';
import 'package:localsend_isolates/rust/api/crypto.dart';
import 'package:localsend_isolates/rust/api/discovery.dart';
import 'package:localsend_isolates/rust/api/http.dart';
import 'package:localsend_isolates/rust/api/logging.dart';
import 'package:localsend_isolates/rust/api/model.dart';
import 'package:localsend_isolates/rust/api/multicast.dart';
import 'package:localsend_isolates/rust/api/server.dart';
import 'package:localsend_isolates/rust/api/stream.dart';
import 'package:localsend_isolates/rust/api/webrtc.dart';
@@ -52,15 +52,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RsCancellationTokenPtr =>
wire._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationTokenPtr;
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RsDiscoveryPtr =>
wire._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscoveryPtr;
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RsHttpClientPtr =>
wire._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClientPtr;
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RsHttpServerPtr =>
wire._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServerPtr;
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RsMulticastPtr =>
wire._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticastPtr;
@protected
AnyhowException dco_decode_AnyhowException(dynamic raw);
@@ -88,15 +88,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsCancellationToken dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationToken(dynamic raw);
@protected
RsDiscovery dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(dynamic raw);
@protected
RsHttpClient dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(dynamic raw);
@protected
RsHttpServer dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(dynamic raw);
@protected
RsMulticast dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(dynamic raw);
@protected
Dart2RustStreamSink dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerDart2RustStreamSink(dynamic raw);
@@ -121,15 +121,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsCancellationToken dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationToken(dynamic raw);
@protected
RsDiscovery dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(dynamic raw);
@protected
RsHttpClient dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(dynamic raw);
@protected
RsHttpServer dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(dynamic raw);
@protected
RsMulticast dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(dynamic raw);
@protected
FutureOr<void> Function(LsSignalingConnection)
dco_decode_DartFn_Inputs_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerLsSignalingConnection_Output_unit_AnyhowException(
@@ -169,15 +169,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsCancellationToken dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationToken(dynamic raw);
@protected
RsDiscovery dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(dynamic raw);
@protected
RsHttpClient dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(dynamic raw);
@protected
RsHttpServer dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(dynamic raw);
@protected
RsMulticast dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(dynamic raw);
@protected
Set<String> dco_decode_Set_String_None(dynamic raw);
@@ -193,10 +193,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustStreamSink<Uint8List> dco_decode_StreamSink_list_prim_u_8_strict_Sse(dynamic raw);
@protected
RustStreamSink<RsHashFileEvent> dco_decode_StreamSink_rs_hash_file_event_Sse(dynamic raw);
RustStreamSink<RsDiscoveredDevice> dco_decode_StreamSink_rs_discovered_device_Sse(dynamic raw);
@protected
RustStreamSink<RsMulticastDiscovered> dco_decode_StreamSink_rs_multicast_discovered_Sse(dynamic raw);
RustStreamSink<RsHashFileEvent> dco_decode_StreamSink_rs_hash_file_event_Sse(dynamic raw);
@protected
RustStreamSink<RsServerEvent> dco_decode_StreamSink_rs_server_event_Sse(dynamic raw);
@@ -263,6 +263,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RegisterDtoV2 dco_decode_box_autoadd_register_dto_v_2(dynamic raw);
@protected
RsDiscoveredDevice dco_decode_box_autoadd_rs_discovered_device(dynamic raw);
@protected
RTCSendFileResponse dco_decode_box_autoadd_rtc_send_file_response(dynamic raw);
@@ -332,9 +335,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
LsHttpClientVersion dco_decode_ls_http_client_version(dynamic raw);
@protected
MulticastMessageV2 dco_decode_multicast_message_v_2(dynamic raw);
@protected
String? dco_decode_opt_String(dynamic raw);
@@ -360,6 +360,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
PrepareUploadResponseDto? dco_decode_opt_box_autoadd_prepare_upload_response_dto(dynamic raw);
@protected
RsDiscoveredDevice? dco_decode_opt_box_autoadd_rs_discovered_device(dynamic raw);
@protected
TlsConfig? dco_decode_opt_box_autoadd_tls_config(dynamic raw);
@@ -420,15 +423,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
ResultWithPublicKeyRegisterResponseDto dco_decode_result_with_public_key_register_response_dto(dynamic raw);
@protected
RsDiscoveredDevice dco_decode_rs_discovered_device(dynamic raw);
@protected
RsHashFileEvent dco_decode_rs_hash_file_event(dynamic raw);
@protected
RsHttpClientError dco_decode_rs_http_client_error(dynamic raw);
@protected
RsMulticastDiscovered dco_decode_rs_multicast_discovered(dynamic raw);
@protected
RsServerEvent dco_decode_rs_server_event(dynamic raw);
@@ -519,15 +522,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
RsDiscovery sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(SseDeserializer deserializer);
@protected
RsHttpClient sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(SseDeserializer deserializer);
@protected
RsHttpServer sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(SseDeserializer deserializer);
@protected
RsMulticast sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(SseDeserializer deserializer);
@protected
Dart2RustStreamSink sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerDart2RustStreamSink(
SseDeserializer deserializer,
@@ -562,15 +565,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
RsDiscovery sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(SseDeserializer deserializer);
@protected
RsHttpClient sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(SseDeserializer deserializer);
@protected
RsHttpServer sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(SseDeserializer deserializer);
@protected
RsMulticast sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(SseDeserializer deserializer);
@protected
Object sse_decode_DartOpaque(SseDeserializer deserializer);
@@ -606,15 +609,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsCancellationToken sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationToken(SseDeserializer deserializer);
@protected
RsDiscovery sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(SseDeserializer deserializer);
@protected
RsHttpClient sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(SseDeserializer deserializer);
@protected
RsHttpServer sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(SseDeserializer deserializer);
@protected
RsMulticast sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(SseDeserializer deserializer);
@protected
Set<String> sse_decode_Set_String_None(SseDeserializer deserializer);
@@ -630,10 +633,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustStreamSink<Uint8List> sse_decode_StreamSink_list_prim_u_8_strict_Sse(SseDeserializer deserializer);
@protected
RustStreamSink<RsHashFileEvent> sse_decode_StreamSink_rs_hash_file_event_Sse(SseDeserializer deserializer);
RustStreamSink<RsDiscoveredDevice> sse_decode_StreamSink_rs_discovered_device_Sse(SseDeserializer deserializer);
@protected
RustStreamSink<RsMulticastDiscovered> sse_decode_StreamSink_rs_multicast_discovered_Sse(SseDeserializer deserializer);
RustStreamSink<RsHashFileEvent> sse_decode_StreamSink_rs_hash_file_event_Sse(SseDeserializer deserializer);
@protected
RustStreamSink<RsServerEvent> sse_decode_StreamSink_rs_server_event_Sse(SseDeserializer deserializer);
@@ -700,6 +703,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RegisterDtoV2 sse_decode_box_autoadd_register_dto_v_2(SseDeserializer deserializer);
@protected
RsDiscoveredDevice sse_decode_box_autoadd_rs_discovered_device(SseDeserializer deserializer);
@protected
RTCSendFileResponse sse_decode_box_autoadd_rtc_send_file_response(SseDeserializer deserializer);
@@ -769,9 +775,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
LsHttpClientVersion sse_decode_ls_http_client_version(SseDeserializer deserializer);
@protected
MulticastMessageV2 sse_decode_multicast_message_v_2(SseDeserializer deserializer);
@protected
String? sse_decode_opt_String(SseDeserializer deserializer);
@@ -799,6 +802,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
PrepareUploadResponseDto? sse_decode_opt_box_autoadd_prepare_upload_response_dto(SseDeserializer deserializer);
@protected
RsDiscoveredDevice? sse_decode_opt_box_autoadd_rs_discovered_device(SseDeserializer deserializer);
@protected
TlsConfig? sse_decode_opt_box_autoadd_tls_config(SseDeserializer deserializer);
@@ -859,15 +865,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
ResultWithPublicKeyRegisterResponseDto sse_decode_result_with_public_key_register_response_dto(SseDeserializer deserializer);
@protected
RsDiscoveredDevice sse_decode_rs_discovered_device(SseDeserializer deserializer);
@protected
RsHashFileEvent sse_decode_rs_hash_file_event(SseDeserializer deserializer);
@protected
RsHttpClientError sse_decode_rs_http_client_error(SseDeserializer deserializer);
@protected
RsMulticastDiscovered sse_decode_rs_multicast_discovered(SseDeserializer deserializer);
@protected
RsServerEvent sse_decode_rs_server_event(SseDeserializer deserializer);
@@ -970,15 +976,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(RsDiscovery self, SseSerializer serializer);
@protected
void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(RsHttpClient self, SseSerializer serializer);
@protected
void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(RsHttpServer self, SseSerializer serializer);
@protected
void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(RsMulticast self, SseSerializer serializer);
@protected
void sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerDart2RustStreamSink(
Dart2RustStreamSink self,
@@ -1024,15 +1030,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(RsDiscovery self, SseSerializer serializer);
@protected
void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(RsHttpClient self, SseSerializer serializer);
@protected
void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(RsHttpServer self, SseSerializer serializer);
@protected
void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(RsMulticast self, SseSerializer serializer);
@protected
void
sse_encode_DartFn_Inputs_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerLsSignalingConnection_Output_unit_AnyhowException(
@@ -1088,15 +1094,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(RsDiscovery self, SseSerializer serializer);
@protected
void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(RsHttpClient self, SseSerializer serializer);
@protected
void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(RsHttpServer self, SseSerializer serializer);
@protected
void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(RsMulticast self, SseSerializer serializer);
@protected
void sse_encode_Set_String_None(Set<String> self, SseSerializer serializer);
@@ -1113,10 +1119,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
void sse_encode_StreamSink_list_prim_u_8_strict_Sse(RustStreamSink<Uint8List> self, SseSerializer serializer);
@protected
void sse_encode_StreamSink_rs_hash_file_event_Sse(RustStreamSink<RsHashFileEvent> self, SseSerializer serializer);
void sse_encode_StreamSink_rs_discovered_device_Sse(RustStreamSink<RsDiscoveredDevice> self, SseSerializer serializer);
@protected
void sse_encode_StreamSink_rs_multicast_discovered_Sse(RustStreamSink<RsMulticastDiscovered> self, SseSerializer serializer);
void sse_encode_StreamSink_rs_hash_file_event_Sse(RustStreamSink<RsHashFileEvent> self, SseSerializer serializer);
@protected
void sse_encode_StreamSink_rs_server_event_Sse(RustStreamSink<RsServerEvent> self, SseSerializer serializer);
@@ -1184,6 +1190,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_box_autoadd_register_dto_v_2(RegisterDtoV2 self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_rs_discovered_device(RsDiscoveredDevice self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_rtc_send_file_response(RTCSendFileResponse self, SseSerializer serializer);
@@ -1253,9 +1262,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_ls_http_client_version(LsHttpClientVersion self, SseSerializer serializer);
@protected
void sse_encode_multicast_message_v_2(MulticastMessageV2 self, SseSerializer serializer);
@protected
void sse_encode_opt_String(String? self, SseSerializer serializer);
@@ -1283,6 +1289,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_opt_box_autoadd_prepare_upload_response_dto(PrepareUploadResponseDto? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_rs_discovered_device(RsDiscoveredDevice? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_tls_config(TlsConfig? self, SseSerializer serializer);
@@ -1344,15 +1353,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_result_with_public_key_register_response_dto(ResultWithPublicKeyRegisterResponseDto self, SseSerializer serializer);
@protected
void sse_encode_rs_discovered_device(RsDiscoveredDevice self, SseSerializer serializer);
@protected
void sse_encode_rs_hash_file_event(RsHashFileEvent self, SseSerializer serializer);
@protected
void sse_encode_rs_http_client_error(RsHttpClientError self, SseSerializer serializer);
@protected
void sse_encode_rs_multicast_discovered(RsMulticastDiscovered self, SseSerializer serializer);
@protected
void sse_encode_rs_server_event(RsServerEvent self, SseSerializer serializer);
@@ -1672,6 +1681,38 @@ class RustLibWire implements BaseWire {
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationTokenPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(
ptr,
);
}
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscoveryPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_localsend_isolates_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery',
);
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery =
_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscoveryPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(
ptr,
);
}
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscoveryPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_localsend_isolates_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery',
);
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery =
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscoveryPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(
ffi.Pointer<ffi.Void> ptr,
) {
@@ -1735,36 +1776,4 @@ class RustLibWire implements BaseWire {
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer =
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServerPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(
ptr,
);
}
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticastPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_localsend_isolates_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast',
);
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast =
_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticastPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(
ptr,
);
}
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticastPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_localsend_isolates_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast',
);
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast =
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticastPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
}
@@ -12,10 +12,10 @@ import 'dart:convert';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
import 'package:localsend_isolates/rust/api/cancel.dart';
import 'package:localsend_isolates/rust/api/crypto.dart';
import 'package:localsend_isolates/rust/api/discovery.dart';
import 'package:localsend_isolates/rust/api/http.dart';
import 'package:localsend_isolates/rust/api/logging.dart';
import 'package:localsend_isolates/rust/api/model.dart';
import 'package:localsend_isolates/rust/api/multicast.dart';
import 'package:localsend_isolates/rust/api/server.dart';
import 'package:localsend_isolates/rust/api/stream.dart';
import 'package:localsend_isolates/rust/api/webrtc.dart';
@@ -54,15 +54,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RsCancellationTokenPtr =>
wire.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationToken;
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RsDiscoveryPtr =>
wire.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery;
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RsHttpClientPtr =>
wire.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient;
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RsHttpServerPtr =>
wire.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer;
CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RsMulticastPtr =>
wire.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast;
@protected
AnyhowException dco_decode_AnyhowException(dynamic raw);
@@ -90,15 +90,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsCancellationToken dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationToken(dynamic raw);
@protected
RsDiscovery dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(dynamic raw);
@protected
RsHttpClient dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(dynamic raw);
@protected
RsHttpServer dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(dynamic raw);
@protected
RsMulticast dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(dynamic raw);
@protected
Dart2RustStreamSink dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerDart2RustStreamSink(dynamic raw);
@@ -123,15 +123,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsCancellationToken dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationToken(dynamic raw);
@protected
RsDiscovery dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(dynamic raw);
@protected
RsHttpClient dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(dynamic raw);
@protected
RsHttpServer dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(dynamic raw);
@protected
RsMulticast dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(dynamic raw);
@protected
FutureOr<void> Function(LsSignalingConnection)
dco_decode_DartFn_Inputs_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerLsSignalingConnection_Output_unit_AnyhowException(
@@ -171,15 +171,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsCancellationToken dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationToken(dynamic raw);
@protected
RsDiscovery dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(dynamic raw);
@protected
RsHttpClient dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(dynamic raw);
@protected
RsHttpServer dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(dynamic raw);
@protected
RsMulticast dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(dynamic raw);
@protected
Set<String> dco_decode_Set_String_None(dynamic raw);
@@ -195,10 +195,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustStreamSink<Uint8List> dco_decode_StreamSink_list_prim_u_8_strict_Sse(dynamic raw);
@protected
RustStreamSink<RsHashFileEvent> dco_decode_StreamSink_rs_hash_file_event_Sse(dynamic raw);
RustStreamSink<RsDiscoveredDevice> dco_decode_StreamSink_rs_discovered_device_Sse(dynamic raw);
@protected
RustStreamSink<RsMulticastDiscovered> dco_decode_StreamSink_rs_multicast_discovered_Sse(dynamic raw);
RustStreamSink<RsHashFileEvent> dco_decode_StreamSink_rs_hash_file_event_Sse(dynamic raw);
@protected
RustStreamSink<RsServerEvent> dco_decode_StreamSink_rs_server_event_Sse(dynamic raw);
@@ -265,6 +265,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RegisterDtoV2 dco_decode_box_autoadd_register_dto_v_2(dynamic raw);
@protected
RsDiscoveredDevice dco_decode_box_autoadd_rs_discovered_device(dynamic raw);
@protected
RTCSendFileResponse dco_decode_box_autoadd_rtc_send_file_response(dynamic raw);
@@ -334,9 +337,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
LsHttpClientVersion dco_decode_ls_http_client_version(dynamic raw);
@protected
MulticastMessageV2 dco_decode_multicast_message_v_2(dynamic raw);
@protected
String? dco_decode_opt_String(dynamic raw);
@@ -362,6 +362,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
PrepareUploadResponseDto? dco_decode_opt_box_autoadd_prepare_upload_response_dto(dynamic raw);
@protected
RsDiscoveredDevice? dco_decode_opt_box_autoadd_rs_discovered_device(dynamic raw);
@protected
TlsConfig? dco_decode_opt_box_autoadd_tls_config(dynamic raw);
@@ -422,15 +425,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
ResultWithPublicKeyRegisterResponseDto dco_decode_result_with_public_key_register_response_dto(dynamic raw);
@protected
RsDiscoveredDevice dco_decode_rs_discovered_device(dynamic raw);
@protected
RsHashFileEvent dco_decode_rs_hash_file_event(dynamic raw);
@protected
RsHttpClientError dco_decode_rs_http_client_error(dynamic raw);
@protected
RsMulticastDiscovered dco_decode_rs_multicast_discovered(dynamic raw);
@protected
RsServerEvent dco_decode_rs_server_event(dynamic raw);
@@ -521,15 +524,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
RsDiscovery sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(SseDeserializer deserializer);
@protected
RsHttpClient sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(SseDeserializer deserializer);
@protected
RsHttpServer sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(SseDeserializer deserializer);
@protected
RsMulticast sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(SseDeserializer deserializer);
@protected
Dart2RustStreamSink sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerDart2RustStreamSink(
SseDeserializer deserializer,
@@ -564,15 +567,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
RsDiscovery sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(SseDeserializer deserializer);
@protected
RsHttpClient sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(SseDeserializer deserializer);
@protected
RsHttpServer sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(SseDeserializer deserializer);
@protected
RsMulticast sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(SseDeserializer deserializer);
@protected
Object sse_decode_DartOpaque(SseDeserializer deserializer);
@@ -608,15 +611,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RsCancellationToken sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationToken(SseDeserializer deserializer);
@protected
RsDiscovery sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(SseDeserializer deserializer);
@protected
RsHttpClient sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(SseDeserializer deserializer);
@protected
RsHttpServer sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(SseDeserializer deserializer);
@protected
RsMulticast sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(SseDeserializer deserializer);
@protected
Set<String> sse_decode_Set_String_None(SseDeserializer deserializer);
@@ -632,10 +635,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustStreamSink<Uint8List> sse_decode_StreamSink_list_prim_u_8_strict_Sse(SseDeserializer deserializer);
@protected
RustStreamSink<RsHashFileEvent> sse_decode_StreamSink_rs_hash_file_event_Sse(SseDeserializer deserializer);
RustStreamSink<RsDiscoveredDevice> sse_decode_StreamSink_rs_discovered_device_Sse(SseDeserializer deserializer);
@protected
RustStreamSink<RsMulticastDiscovered> sse_decode_StreamSink_rs_multicast_discovered_Sse(SseDeserializer deserializer);
RustStreamSink<RsHashFileEvent> sse_decode_StreamSink_rs_hash_file_event_Sse(SseDeserializer deserializer);
@protected
RustStreamSink<RsServerEvent> sse_decode_StreamSink_rs_server_event_Sse(SseDeserializer deserializer);
@@ -702,6 +705,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RegisterDtoV2 sse_decode_box_autoadd_register_dto_v_2(SseDeserializer deserializer);
@protected
RsDiscoveredDevice sse_decode_box_autoadd_rs_discovered_device(SseDeserializer deserializer);
@protected
RTCSendFileResponse sse_decode_box_autoadd_rtc_send_file_response(SseDeserializer deserializer);
@@ -771,9 +777,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
LsHttpClientVersion sse_decode_ls_http_client_version(SseDeserializer deserializer);
@protected
MulticastMessageV2 sse_decode_multicast_message_v_2(SseDeserializer deserializer);
@protected
String? sse_decode_opt_String(SseDeserializer deserializer);
@@ -801,6 +804,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
PrepareUploadResponseDto? sse_decode_opt_box_autoadd_prepare_upload_response_dto(SseDeserializer deserializer);
@protected
RsDiscoveredDevice? sse_decode_opt_box_autoadd_rs_discovered_device(SseDeserializer deserializer);
@protected
TlsConfig? sse_decode_opt_box_autoadd_tls_config(SseDeserializer deserializer);
@@ -861,15 +867,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
ResultWithPublicKeyRegisterResponseDto sse_decode_result_with_public_key_register_response_dto(SseDeserializer deserializer);
@protected
RsDiscoveredDevice sse_decode_rs_discovered_device(SseDeserializer deserializer);
@protected
RsHashFileEvent sse_decode_rs_hash_file_event(SseDeserializer deserializer);
@protected
RsHttpClientError sse_decode_rs_http_client_error(SseDeserializer deserializer);
@protected
RsMulticastDiscovered sse_decode_rs_multicast_discovered(SseDeserializer deserializer);
@protected
RsServerEvent sse_decode_rs_server_event(SseDeserializer deserializer);
@@ -972,15 +978,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(RsDiscovery self, SseSerializer serializer);
@protected
void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(RsHttpClient self, SseSerializer serializer);
@protected
void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(RsHttpServer self, SseSerializer serializer);
@protected
void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(RsMulticast self, SseSerializer serializer);
@protected
void sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerDart2RustStreamSink(
Dart2RustStreamSink self,
@@ -1026,15 +1032,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(RsDiscovery self, SseSerializer serializer);
@protected
void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(RsHttpClient self, SseSerializer serializer);
@protected
void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(RsHttpServer self, SseSerializer serializer);
@protected
void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(RsMulticast self, SseSerializer serializer);
@protected
void
sse_encode_DartFn_Inputs_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerLsSignalingConnection_Output_unit_AnyhowException(
@@ -1090,15 +1096,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(RsDiscovery self, SseSerializer serializer);
@protected
void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(RsHttpClient self, SseSerializer serializer);
@protected
void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(RsHttpServer self, SseSerializer serializer);
@protected
void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(RsMulticast self, SseSerializer serializer);
@protected
void sse_encode_Set_String_None(Set<String> self, SseSerializer serializer);
@@ -1115,10 +1121,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
void sse_encode_StreamSink_list_prim_u_8_strict_Sse(RustStreamSink<Uint8List> self, SseSerializer serializer);
@protected
void sse_encode_StreamSink_rs_hash_file_event_Sse(RustStreamSink<RsHashFileEvent> self, SseSerializer serializer);
void sse_encode_StreamSink_rs_discovered_device_Sse(RustStreamSink<RsDiscoveredDevice> self, SseSerializer serializer);
@protected
void sse_encode_StreamSink_rs_multicast_discovered_Sse(RustStreamSink<RsMulticastDiscovered> self, SseSerializer serializer);
void sse_encode_StreamSink_rs_hash_file_event_Sse(RustStreamSink<RsHashFileEvent> self, SseSerializer serializer);
@protected
void sse_encode_StreamSink_rs_server_event_Sse(RustStreamSink<RsServerEvent> self, SseSerializer serializer);
@@ -1186,6 +1192,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_box_autoadd_register_dto_v_2(RegisterDtoV2 self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_rs_discovered_device(RsDiscoveredDevice self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_rtc_send_file_response(RTCSendFileResponse self, SseSerializer serializer);
@@ -1255,9 +1264,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_ls_http_client_version(LsHttpClientVersion self, SseSerializer serializer);
@protected
void sse_encode_multicast_message_v_2(MulticastMessageV2 self, SseSerializer serializer);
@protected
void sse_encode_opt_String(String? self, SseSerializer serializer);
@@ -1285,6 +1291,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_opt_box_autoadd_prepare_upload_response_dto(PrepareUploadResponseDto? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_rs_discovered_device(RsDiscoveredDevice? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_tls_config(TlsConfig? self, SseSerializer serializer);
@@ -1346,15 +1355,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_result_with_public_key_register_response_dto(ResultWithPublicKeyRegisterResponseDto self, SseSerializer serializer);
@protected
void sse_encode_rs_discovered_device(RsDiscoveredDevice self, SseSerializer serializer);
@protected
void sse_encode_rs_hash_file_event(RsHashFileEvent self, SseSerializer serializer);
@protected
void sse_encode_rs_http_client_error(RsHttpClientError self, SseSerializer serializer);
@protected
void sse_encode_rs_multicast_discovered(RsMulticastDiscovered self, SseSerializer serializer);
@protected
void sse_encode_rs_server_event(RsServerEvent self, SseSerializer serializer);
@@ -1460,6 +1469,12 @@ class RustLibWire implements BaseWire {
void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationToken(int ptr) =>
wasmModule.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationToken(ptr);
void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(int ptr) =>
wasmModule.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(ptr);
void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(int ptr) =>
wasmModule.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(ptr);
void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(int ptr) =>
wasmModule.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(ptr);
@@ -1471,12 +1486,6 @@ class RustLibWire implements BaseWire {
void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(int ptr) =>
wasmModule.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(ptr);
void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(int ptr) =>
wasmModule.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(ptr);
void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(int ptr) =>
wasmModule.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(ptr);
}
@JS('wasm_bindgen')
@@ -1517,6 +1526,10 @@ extension type RustLibWasmModule._(JSObject _) implements JSObject {
external void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsCancellationToken(int ptr);
external void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(int ptr);
external void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsDiscovery(int ptr);
external void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(int ptr);
external void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpClient(int ptr);
@@ -1524,8 +1537,4 @@ extension type RustLibWasmModule._(JSObject _) implements JSObject {
external void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(int ptr);
external void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsHttpServer(int ptr);
external void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(int ptr);
external void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRsMulticast(int ptr);
}
@@ -0,0 +1,115 @@
import 'package:localsend_isolates/model/device.dart';
import 'package:localsend_isolates/src/isolate/child/main.dart';
import 'package:localsend_isolates/src/isolate/dto/send_to_isolate_data.dart';
import 'package:localsend_isolates/src/task/discovery/discovery.dart';
import 'package:typed_isolates/typed_isolates.dart';
sealed class DiscoveryTask {}
/// Starts the discovery and streams every confirmed device.
/// The stream never completes; it survives [DiscoveryRestartTask]s.
class DiscoveryListenTask implements DiscoveryTask {}
/// Sends an announcement to all devices on all network interfaces.
/// They will respond by registering with this device's HTTP server.
class DiscoveryAnnouncementTask implements DiscoveryTask {}
/// Restarts the discovery, e.g. after the port or the network settings changed.
class DiscoveryRestartTask implements DiscoveryTask {}
/// Scans the subnet of one network interface over HTTP.
/// Completes (without events) when the scan is finished; the found devices
/// arrive on the [DiscoveryListenTask] stream.
class DiscoverySubnetScanTask implements DiscoveryTask {
final String networkInterface;
final int port;
final bool https;
DiscoverySubnetScanTask({
required this.networkInterface,
required this.port,
required this.https,
});
}
/// Probes the known addresses of the favorites over HTTP.
/// Completes (without events) when every favorite has been probed; the found
/// devices arrive on the [DiscoveryListenTask] stream.
class DiscoveryFavoriteScanTask implements DiscoveryTask {
final List<(String, int)> favorites;
final bool https;
DiscoveryFavoriteScanTask({
required this.favorites,
required this.https,
});
}
/// Feeds a device confirmed outside of the discovery into the store, e.g. one
/// that registered with this device's HTTP server. The device comes back on
/// the [DiscoveryListenTask] stream.
class DiscoveryAddDeviceTask implements DiscoveryTask {
final Device device;
DiscoveryAddDeviceTask({
required this.device,
});
}
Future<void> setupDiscoveryIsolate(
Stream<SendToIsolateData<IsolateTask<DiscoveryTask>>> receiveFromMain,
void Function(IsolateTaskStreamResult<Device>) sendToMain,
InitialData initialData,
) async {
await setupChildIsolateHelper(
debugLabel: 'DiscoveryIsolate',
receiveFromMain: receiveFromMain,
sendToMain: sendToMain,
initialData: initialData,
handler: (ref, task) async {
switch (task.data) {
case DiscoveryListenTask():
await for (final device in ref.read(discoveryProvider).startListener()) {
sendToMain(
IsolateTaskStreamResult.event(
id: task.id,
data: device,
),
);
}
return;
case DiscoveryAnnouncementTask():
await ref.read(discoveryProvider).sendAnnouncement();
break;
case DiscoveryRestartTask():
ref.read(discoveryProvider).restartListener();
break;
case DiscoverySubnetScanTask data:
await ref
.read(discoveryProvider)
.scanSubnet(
networkInterface: data.networkInterface,
port: data.port,
https: data.https,
);
break;
case DiscoveryFavoriteScanTask data:
await ref
.read(discoveryProvider)
.discoverFavorites(
devices: data.favorites,
https: data.https,
);
break;
case DiscoveryAddDeviceTask data:
await ref.read(discoveryProvider).addDevice(data.device);
break;
}
sendToMain(
IsolateTaskStreamResult.done(
id: task.id,
),
);
},
);
}
@@ -6,23 +6,9 @@ class HttpClientCollection {
final String _privateKey;
final String _certificate;
/// A client that accepts any valid peer certificate.
///
/// Only for discovery, where the certificate of the peer is not known yet
/// and is learned from the response. Never use this to transfer files:
/// it cannot tell the discovered device apart from anyone else answering
/// on that address.
final RsHttpClient discovery;
/// The request timeout of the [discovery] client.
/// Also apply it to short-lived pinned requests made during discovery.
final int discoveryTimeout;
HttpClientCollection({
required String privateKey,
required String certificate,
required this.discovery,
required this.discoveryTimeout,
}) : _privateKey = privateKey,
_certificate = certificate;
@@ -48,19 +34,9 @@ class HttpClientCollection {
}
final httpProvider = ViewProvider((ref) {
final (securityContext, discoveryTimeout) = ref.watch(
syncProvider.select((state) => (state.securityContext, state.discoveryTimeout)),
);
final securityContext = ref.watch(syncProvider.select((state) => state.securityContext));
return HttpClientCollection(
privateKey: securityContext.privateKey,
certificate: securityContext.certificate,
discovery: createClient(
privateKey: securityContext.privateKey,
cert: securityContext.certificate,
version: LsHttpClientVersion.v2,
expectedFingerprint: null,
timeoutMs: discoveryTimeout,
),
discoveryTimeout: discoveryTimeout,
);
});
@@ -1,74 +0,0 @@
import 'package:localsend_isolates/model/device.dart';
import 'package:localsend_isolates/src/isolate/child/main.dart';
import 'package:localsend_isolates/src/isolate/dto/send_to_isolate_data.dart';
import 'package:localsend_isolates/src/task/discovery/http_scan_discovery.dart';
import 'package:typed_isolates/typed_isolates.dart';
sealed class HttpScanTask {}
class HttpInterfaceScanTask implements HttpScanTask {
final String networkInterface;
final int port;
final bool https;
HttpInterfaceScanTask({
required this.networkInterface,
required this.port,
required this.https,
});
}
class HttpFavoriteScanTask implements HttpScanTask {
final List<(String, int)> favorites;
final bool https;
HttpFavoriteScanTask({
required this.favorites,
required this.https,
});
}
Future<void> setupHttpScanDiscoveryIsolate(
Stream<SendToIsolateData<IsolateTask<HttpScanTask>>> receiveFromMain,
void Function(IsolateTaskStreamResult<Device>) sendToMain,
InitialData initialData,
) async {
await setupChildIsolateHelper(
debugLabel: 'HttpScanDiscoveryIsolate',
receiveFromMain: receiveFromMain,
sendToMain: sendToMain,
initialData: initialData,
handler: (ref, task) async {
final stream = switch (task.data) {
HttpInterfaceScanTask data =>
ref
.read(httpScanDiscoveryProvider)
.getStream(
networkInterface: data.networkInterface,
port: data.port,
https: data.https,
),
HttpFavoriteScanTask data =>
ref
.read(httpScanDiscoveryProvider)
.getFavoriteStream(
devices: data.favorites,
https: data.https,
),
};
await for (final device in stream) {
sendToMain(
IsolateTaskStreamResult.event(
id: task.id,
data: device,
),
);
}
sendToMain(
IsolateTaskStreamResult.done(
id: task.id,
),
);
},
);
}
@@ -1,53 +0,0 @@
import 'package:localsend_isolates/model/device.dart';
import 'package:localsend_isolates/src/isolate/child/main.dart';
import 'package:localsend_isolates/src/isolate/dto/send_to_isolate_data.dart';
import 'package:localsend_isolates/src/task/discovery/multicast_discovery.dart';
sealed class MulticastTask {}
/// Sends an announcement to all devices to all network interfaces.
/// They will respond with their device information.
///
/// This is not wrapped in an [IsolateTask] because
/// - (1) it is not important if the device was found by this specific announcement or another one
/// - (2) it is not 100% accurate to know if a device was found by this announcement or another one
class MulticastAnnouncementTask implements MulticastTask {
static const instance = MulticastAnnouncementTask._();
const MulticastAnnouncementTask._();
}
/// Restarts the listener.
class MulticastRestartListenerTask implements MulticastTask {
static const instance = MulticastRestartListenerTask._();
const MulticastRestartListenerTask._();
}
Future<void> setupMulticastDiscoveryIsolate(
Stream<SendToIsolateData<MulticastTask>> receiveFromMain,
void Function(Device) sendToMain,
InitialData initialData,
) async {
await setupChildIsolateHelper(
debugLabel: 'MulticastDiscoveryIsolate',
receiveFromMain: receiveFromMain,
sendToMain: sendToMain,
initialData: initialData,
init: (ref) async {
ref.read(multicastDiscoveryProvider).startListener().listen((event) {
sendToMain(event);
});
},
handler: (ref, task) async {
switch (task) {
case MulticastAnnouncementTask():
await ref.read(multicastDiscoveryProvider).sendAnnouncement();
break;
case MulticastRestartListenerTask():
ref.read(multicastDiscoveryProvider).restartListener();
break;
}
},
);
}
@@ -2,8 +2,7 @@ import 'dart:async';
import 'package:localsend_isolates/model/device.dart';
import 'package:localsend_isolates/rust/api/server.dart' show WebSendParams;
import 'package:localsend_isolates/src/isolate/child/http_scan_discovery_isolate.dart';
import 'package:localsend_isolates/src/isolate/child/multicast_discovery_isolate.dart';
import 'package:localsend_isolates/src/isolate/child/discovery_isolate.dart';
import 'package:localsend_isolates/src/isolate/child/server_isolate.dart';
import 'package:localsend_isolates/src/isolate/child/upload_isolate.dart';
import 'package:localsend_isolates/src/isolate/dto/send_to_isolate_data.dart';
@@ -12,12 +11,37 @@ import 'package:refena_flutter/refena_flutter.dart';
import 'package:typed_isolates/id.dart';
import 'package:typed_isolates/typed_isolates.dart';
class IsolateInterfaceHttpDiscoveryAction extends ReduxActionWithResult<IsolateController, ParentIsolateState, Stream<Device>> {
/// Starts the discovery and returns the stream of confirmed devices:
/// answered announcements, scan results and devices fed in via
/// [IsolateDiscoveryAddDeviceAction] all arrive on this one stream.
/// The stream never completes; it survives [IsolateDiscoveryRestartAction]s.
class IsolateDiscoveryListenAction extends ReduxActionWithResult<IsolateController, ParentIsolateState, Stream<Device>> {
@override
(ParentIsolateState, Stream<Device>) reduce() {
final connection = state.discovery;
if (connection == null) {
throw StateError('discovery is not initialized');
}
return (
state,
connection.sendWrappedTaskAndListenStream(
task: DiscoveryListenTask(),
),
);
}
}
/// Scans the subnet of one network interface over HTTP,
/// for networks that do not carry multicast.
/// The returned stream completes (without events) when the scan is finished;
/// the found devices arrive on the [IsolateDiscoveryListenAction] stream.
class IsolateDiscoverySubnetScanAction extends ReduxActionWithResult<IsolateController, ParentIsolateState, Stream<Device>> {
final String networkInterface;
final int port;
final bool https;
IsolateInterfaceHttpDiscoveryAction({
IsolateDiscoverySubnetScanAction({
required this.networkInterface,
required this.port,
required this.https,
@@ -25,68 +49,71 @@ class IsolateInterfaceHttpDiscoveryAction extends ReduxActionWithResult<IsolateC
@override
(ParentIsolateState, Stream<Device>) reduce() {
final connection = state.httpScanDiscovery;
final connection = state.discovery;
if (connection == null) {
throw StateError('httpScanDiscovery is not initialized');
throw StateError('discovery is not initialized');
}
final task = HttpInterfaceScanTask(
networkInterface: networkInterface,
port: port,
https: https,
);
return (
state,
connection.sendWrappedTaskAndListenStream(
task: task,
task: DiscoverySubnetScanTask(
networkInterface: networkInterface,
port: port,
https: https,
),
),
);
}
}
class IsolateFavoriteHttpDiscoveryAction extends ReduxActionWithResult<IsolateController, ParentIsolateState, Stream<Device>> {
/// Probes the known addresses of the favorites over HTTP.
/// The returned stream completes (without events) when every favorite has been
/// probed; the found devices arrive on the [IsolateDiscoveryListenAction] stream.
class IsolateDiscoveryFavoriteScanAction extends ReduxActionWithResult<IsolateController, ParentIsolateState, Stream<Device>> {
final List<(String, int)> favorites;
final bool https;
IsolateFavoriteHttpDiscoveryAction({
IsolateDiscoveryFavoriteScanAction({
required this.favorites,
required this.https,
});
@override
(ParentIsolateState, Stream<Device>) reduce() {
final connection = state.httpScanDiscovery;
final connection = state.discovery;
if (connection == null) {
throw StateError('httpScanDiscovery is not initialized');
throw StateError('discovery is not initialized');
}
final task = HttpFavoriteScanTask(
favorites: favorites,
https: https,
);
return (
state,
connection.sendWrappedTaskAndListenStream(
task: task,
task: DiscoveryFavoriteScanTask(
favorites: favorites,
https: https,
),
),
);
}
}
class IsolateSendMulticastAnnouncementAction extends ReduxAction<IsolateController, ParentIsolateState> {
/// Sends an announcement which makes every other LocalSend device on the
/// network register with this device's HTTP server.
class IsolateDiscoveryAnnouncementAction extends ReduxAction<IsolateController, ParentIsolateState> {
@override
ParentIsolateState reduce() {
final connection = state.multicastDiscovery;
final connection = state.discovery;
if (connection == null) {
throw StateError('multicastDiscovery is not initialized');
throw StateError('discovery is not initialized');
}
connection.sendToIsolate(
SendToIsolateData(
syncState: null,
data: MulticastAnnouncementTask.instance,
data: IsolateTask(
data: DiscoveryAnnouncementTask(),
),
),
);
@@ -94,18 +121,53 @@ class IsolateSendMulticastAnnouncementAction extends ReduxAction<IsolateControll
}
}
class IsolateSendMulticastRestartListenerAction extends ReduxAction<IsolateController, ParentIsolateState> {
/// Restarts the discovery, e.g. after the port or the network settings changed.
class IsolateDiscoveryRestartAction extends ReduxAction<IsolateController, ParentIsolateState> {
@override
ParentIsolateState reduce() {
final connection = state.multicastDiscovery;
final connection = state.discovery;
if (connection == null) {
throw StateError('multicastDiscovery is not initialized');
throw StateError('discovery is not initialized');
}
connection.sendToIsolate(
SendToIsolateData(
syncState: null,
data: MulticastRestartListenerTask.instance,
data: IsolateTask(
data: DiscoveryRestartTask(),
),
),
);
return state;
}
}
/// Feeds a device confirmed outside of the discovery into the discovery store,
/// e.g. one that registered with this device's HTTP server. The device comes
/// back on the [IsolateDiscoveryListenAction] stream.
class IsolateDiscoveryAddDeviceAction extends ReduxAction<IsolateController, ParentIsolateState> {
final Device device;
IsolateDiscoveryAddDeviceAction({
required this.device,
});
@override
ParentIsolateState reduce() {
final connection = state.discovery;
if (connection == null) {
throw StateError('discovery is not initialized');
}
connection.sendToIsolate(
SendToIsolateData(
syncState: null,
data: IsolateTask(
data: DiscoveryAddDeviceTask(
device: device,
),
),
),
);
@@ -119,13 +119,7 @@ class _PublishSyncStateAction extends ReduxAction<IsolateController, ParentIsola
@override
ParentIsolateState reduce() {
state.httpScanDiscovery?.sendToIsolate(
SendToIsolateData(
syncState: syncState,
data: null,
),
);
state.multicastDiscovery?.sendToIsolate(
state.discovery?.sendToIsolate(
SendToIsolateData(
syncState: syncState,
data: null,
@@ -1,8 +1,7 @@
import 'package:dart_mappable/dart_mappable.dart';
import 'package:localsend_isolates/model/device.dart';
import 'package:localsend_isolates/src/isolate/child/http_scan_discovery_isolate.dart';
import 'package:localsend_isolates/src/isolate/child/discovery_isolate.dart';
import 'package:localsend_isolates/src/isolate/child/main.dart';
import 'package:localsend_isolates/src/isolate/child/multicast_discovery_isolate.dart';
import 'package:localsend_isolates/src/isolate/child/server_isolate.dart';
import 'package:localsend_isolates/src/isolate/child/sync_provider.dart';
import 'package:localsend_isolates/src/isolate/child/upload_isolate.dart';
@@ -19,23 +18,20 @@ part 'parent_isolate_provider.mapper.dart';
@MappableClass()
class ParentIsolateState with ParentIsolateStateMappable {
final SyncState syncState;
final IsolateConnector<IsolateTaskStreamResult<Device>, SendToIsolateData<IsolateTask<HttpScanTask>>>? httpScanDiscovery;
final IsolateConnector<Device, SendToIsolateData<MulticastTask>>? multicastDiscovery;
final IsolateConnector<IsolateTaskStreamResult<Device>, SendToIsolateData<IsolateTask<DiscoveryTask>>>? discovery;
final IsolateConnector<IsolateTaskStreamResult<HttpUploadEvent>, SendToIsolateData<IsolateTask<BaseHttpUploadTask>>>? httpUpload;
final IsolateConnector<IsolateTaskStreamResult<HttpServerEvent>, SendToIsolateData<IsolateTask<BaseHttpServerTask>>>? httpServer;
ParentIsolateState({
required this.syncState,
required this.httpScanDiscovery,
required this.multicastDiscovery,
required this.discovery,
required this.httpUpload,
required this.httpServer,
});
static ParentIsolateState initial(SyncState syncState) => ParentIsolateState(
syncState: syncState,
httpScanDiscovery: null,
multicastDiscovery: null,
discovery: null,
httpUpload: null,
httpServer: null,
);
@@ -66,17 +62,8 @@ class IsolateController extends ReduxNotifier<ParentIsolateState> {
class IsolateSetupAction extends AsyncReduxAction<IsolateController, ParentIsolateState> {
@override
Future<ParentIsolateState> reduce() async {
final httpScanDiscovery =
await TypedIsolates.startIsolate<IsolateTaskStreamResult<Device>, SendToIsolateData<IsolateTask<HttpScanTask>>, InitialData>(
task: setupHttpScanDiscoveryIsolate,
param: InitialData(
syncState: state.syncState,
logLevel: Logger.root.level,
),
);
final multicastDiscovery = await TypedIsolates.startIsolate<Device, SendToIsolateData<MulticastTask>, InitialData>(
task: setupMulticastDiscoveryIsolate,
final discovery = await TypedIsolates.startIsolate<IsolateTaskStreamResult<Device>, SendToIsolateData<IsolateTask<DiscoveryTask>>, InitialData>(
task: setupDiscoveryIsolate,
param: InitialData(
syncState: state.syncState,
logLevel: Logger.root.level,
@@ -102,8 +89,7 @@ class IsolateSetupAction extends AsyncReduxAction<IsolateController, ParentIsola
);
return state.copyWith(
httpScanDiscovery: httpScanDiscovery,
multicastDiscovery: multicastDiscovery,
discovery: discovery,
httpUpload: httpUpload,
httpServer: httpServer,
);
@@ -113,8 +99,7 @@ class IsolateSetupAction extends AsyncReduxAction<IsolateController, ParentIsola
class IsolateDisposeAction extends ReduxAction<IsolateController, ParentIsolateState> {
@override
ParentIsolateState reduce() {
state.httpScanDiscovery?.isolate.kill();
state.multicastDiscovery?.isolate.kill();
state.discovery?.isolate.kill();
state.httpUpload?.isolate.kill();
state.httpServer?.isolate.kill();
return state;
@@ -31,24 +31,17 @@ class ParentIsolateStateMapper extends ClassMapperBase<ParentIsolateState> {
);
static IsolateConnector<
IsolateTaskStreamResult<Device>,
SendToIsolateData<IsolateTask<HttpScanTask>>
SendToIsolateData<IsolateTask<DiscoveryTask>>
>?
_$httpScanDiscovery(ParentIsolateState v) => v.httpScanDiscovery;
_$discovery(ParentIsolateState v) => v.discovery;
static const Field<
ParentIsolateState,
IsolateConnector<
IsolateTaskStreamResult<Device>,
SendToIsolateData<IsolateTask<HttpScanTask>>
SendToIsolateData<IsolateTask<DiscoveryTask>>
>
>
_f$httpScanDiscovery = Field('httpScanDiscovery', _$httpScanDiscovery);
static IsolateConnector<Device, SendToIsolateData<MulticastTask>>?
_$multicastDiscovery(ParentIsolateState v) => v.multicastDiscovery;
static const Field<
ParentIsolateState,
IsolateConnector<Device, SendToIsolateData<MulticastTask>>
>
_f$multicastDiscovery = Field('multicastDiscovery', _$multicastDiscovery);
_f$discovery = Field('discovery', _$discovery);
static IsolateConnector<
IsolateTaskStreamResult<HttpUploadEvent>,
SendToIsolateData<IsolateTask<BaseHttpUploadTask>>
@@ -79,8 +72,7 @@ class ParentIsolateStateMapper extends ClassMapperBase<ParentIsolateState> {
@override
final MappableFields<ParentIsolateState> fields = const {
#syncState: _f$syncState,
#httpScanDiscovery: _f$httpScanDiscovery,
#multicastDiscovery: _f$multicastDiscovery,
#discovery: _f$discovery,
#httpUpload: _f$httpUpload,
#httpServer: _f$httpServer,
};
@@ -88,8 +80,7 @@ class ParentIsolateStateMapper extends ClassMapperBase<ParentIsolateState> {
static ParentIsolateState _instantiate(DecodingData data) {
return ParentIsolateState(
syncState: data.dec(_f$syncState),
httpScanDiscovery: data.dec(_f$httpScanDiscovery),
multicastDiscovery: data.dec(_f$multicastDiscovery),
discovery: data.dec(_f$discovery),
httpUpload: data.dec(_f$httpUpload),
httpServer: data.dec(_f$httpServer),
);
@@ -171,11 +162,9 @@ abstract class ParentIsolateStateCopyWith<
SyncState? syncState,
IsolateConnector<
IsolateTaskStreamResult<Device>,
SendToIsolateData<IsolateTask<HttpScanTask>>
SendToIsolateData<IsolateTask<DiscoveryTask>>
>?
httpScanDiscovery,
IsolateConnector<Device, SendToIsolateData<MulticastTask>>?
multicastDiscovery,
discovery,
IsolateConnector<
IsolateTaskStreamResult<HttpUploadEvent>,
SendToIsolateData<IsolateTask<BaseHttpUploadTask>>
@@ -206,15 +195,13 @@ class _ParentIsolateStateCopyWithImpl<$R, $Out>
@override
$R call({
SyncState? syncState,
Object? httpScanDiscovery = $none,
Object? multicastDiscovery = $none,
Object? discovery = $none,
Object? httpUpload = $none,
Object? httpServer = $none,
}) => $apply(
FieldCopyWithData({
if (syncState != null) #syncState: syncState,
if (httpScanDiscovery != $none) #httpScanDiscovery: httpScanDiscovery,
if (multicastDiscovery != $none) #multicastDiscovery: multicastDiscovery,
if (discovery != $none) #discovery: discovery,
if (httpUpload != $none) #httpUpload: httpUpload,
if (httpServer != $none) #httpServer: httpServer,
}),
@@ -222,14 +209,7 @@ class _ParentIsolateStateCopyWithImpl<$R, $Out>
@override
ParentIsolateState $make(CopyWithData data) => ParentIsolateState(
syncState: data.get(#syncState, or: $value.syncState),
httpScanDiscovery: data.get(
#httpScanDiscovery,
or: $value.httpScanDiscovery,
),
multicastDiscovery: data.get(
#multicastDiscovery,
or: $value.multicastDiscovery,
),
discovery: data.get(#discovery, or: $value.discovery),
httpUpload: data.get(#httpUpload, or: $value.httpUpload),
httpServer: data.get(#httpServer, or: $value.httpServer),
);
@@ -0,0 +1,183 @@
import 'dart:async';
import 'package:localsend_isolates/constants.dart';
import 'package:localsend_isolates/model/device.dart';
import 'package:localsend_isolates/model/dto/multicast_dto.dart';
import 'package:localsend_isolates/rust/api/discovery.dart';
import 'package:localsend_isolates/rust/api/server.dart' show ProtocolTypeV2;
import 'package:localsend_isolates/src/isolate/child/sync_provider.dart';
import 'package:localsend_isolates/util/rust.dart';
import 'package:logging/logging.dart';
import 'package:refena_flutter/refena_flutter.dart';
final _logger = Logger('Discovery');
final discoveryProvider = Provider((ref) {
return DiscoveryService(ref);
});
/// Owns the Rust discovery (`core/discovery`): the multicast sockets, the
/// register requests answering announcements, the subnet scans and the store
/// of confirmed devices all live on the Rust side. This service configures it
/// from the [syncProvider] state and maps every confirmation to a [Device].
class DiscoveryService {
DiscoveryService(this._ref);
final Ref _ref;
RsDiscovery? _discovery;
Completer<void> _retryCompleter = Completer();
bool _listening = false;
/// Starts the discovery and emits every device confirmation:
/// answered announcements, scan results and devices fed in
/// via [addDevice] all arrive on this one stream.
Stream<Device> startListener() {
if (_listening) {
_logger.info('Already listening to discovery');
return const Stream.empty();
}
_listening = true;
final devices = StreamController<Device>();
unawaited(_runListener(devices));
return devices.stream;
}
Future<void> _runListener(StreamController<Device> devices) async {
// Announcements are only answered while the server runs: the answer would
// advertise an HTTP port that nobody listens on otherwise.
_ref.stream(syncProvider).listen((event) {
if (event.prev.serverRunning != event.next.serverRunning) {
unawaited(_discovery?.setAnswerAnnouncements(answer: event.next.serverRunning));
}
});
while (true) {
final syncState = _ref.read(syncProvider);
final RsDiscovery discovery;
try {
discovery = await startDiscovery(
group: syncState.multicastGroup,
port: syncState.port,
networkWhitelist: syncState.networkWhitelist,
networkBlacklist: syncState.networkBlacklist,
alias: syncState.alias,
version: protocolVersion,
deviceModel: syncState.deviceInfo.deviceModel,
deviceType: syncState.deviceInfo.deviceType.toRust(),
fingerprint: syncState.securityContext.certificateHash,
protocol: syncState.protocol == ProtocolType.https ? ProtocolTypeV2.https : ProtocolTypeV2.http,
download: syncState.download,
certPem: syncState.securityContext.certificate,
privateKeyPem: syncState.securityContext.privateKey,
timeoutMs: BigInt.from(syncState.discoveryTimeout),
);
} catch (e) {
_logger.warning('Could not start discovery (group: ${syncState.multicastGroup}, port: ${syncState.port})', e);
// Wait for the next restart request instead of hot-looping
_retryCompleter = Completer();
await _retryCompleter.future;
continue;
}
final multicastError = await discovery.multicastError();
if (multicastError != null) {
_logger.warning('Discovery runs without multicast (group: ${syncState.multicastGroup}, port: ${syncState.port}): $multicastError');
}
if (!_ref.read(syncProvider).serverRunning) {
await discovery.setAnswerAnnouncements(answer: false);
}
_discovery = discovery;
// Tell everyone in the network that I am online.
unawaited(discovery.announce());
await for (final device in discovery.listen()) {
if (!devices.isClosed) {
devices.add(device.toDevice());
}
}
// The stream ended because [restartListener] stopped the discovery.
_discovery = null;
}
}
/// Restarts the discovery, e.g. after the port or the network settings changed.
void restartListener() {
final discovery = _discovery;
if (discovery != null) {
// Ends the listen stream, which makes [startListener] rebind.
unawaited(discovery.stop());
} else if (!_retryCompleter.isCompleted) {
// Starting failed previously; let [startListener] try again.
_retryCompleter.complete();
}
}
/// Sends an announcement which triggers a response on every LocalSend member of the network.
Future<void> sendAnnouncement() async {
final discovery = _discovery;
if (discovery == null) {
_logger.info('Discovery is not running, skipping announcement');
return;
}
_logger.info('Announce via UDP');
await discovery.announce();
}
/// Scans the subnet of [networkInterface] by probing every host over HTTP,
/// for networks that do not carry multicast.
/// Found devices arrive on the [startListener] stream; this method returns
/// once the whole scan has finished.
Future<void> scanSubnet({required String networkInterface, required int port, required bool https}) async {
final discovery = _discovery;
if (discovery == null) {
_logger.info('Discovery is not running, skipping subnet scan');
return;
}
await discovery.scanSubnet(
interfaceIp: networkInterface,
port: port,
protocol: https ? ProtocolTypeV2.https : ProtocolTypeV2.http,
);
}
/// Probes the known addresses of the favorites.
/// Found devices arrive on the [startListener] stream; this method returns
/// once every favorite has been probed.
Future<void> discoverFavorites({required List<(String, int)> devices, required bool https}) async {
final discovery = _discovery;
if (discovery == null) {
_logger.info('Discovery is not running, skipping favorite scan');
return;
}
final protocol = https ? ProtocolTypeV2.https : ProtocolTypeV2.http;
await Future.wait([
for (final (host, port) in devices) discovery.discover(host: host, port: port, protocol: protocol),
]);
}
/// Feeds a device confirmed outside of the discovery into the store, e.g.
/// one that registered with this device's HTTP server.
/// The device comes back on the [startListener] stream.
Future<void> addDevice(Device device) async {
final discovery = _discovery;
final ip = device.ip;
if (discovery == null || ip == null) {
// A device lost here re-appears on its next register request, which the
// announcement sent when the discovery (re)starts triggers by itself.
_logger.info('Discovery is not running, skipping device ${device.alias} ($ip)');
return;
}
await discovery.addDevice(device: device.toRsDiscoveredDevice(ip));
}
}
@@ -1,61 +0,0 @@
import 'package:localsend_isolates/model/device.dart';
import 'package:localsend_isolates/src/task/discovery/http_target_discovery.dart';
import 'package:logging/logging.dart';
import 'package:pool/pool.dart';
import 'package:refena_flutter/refena_flutter.dart';
final _logger = Logger('HttpScanDiscovery');
const _concurrency = 50;
final httpScanDiscoveryProvider = ViewProvider((ref) {
return HttpScanDiscoveryService(
targetedDiscoveryService: ref.accessor(httpTargetDiscoveryProvider),
);
});
class _CancelToken {
bool cancelled = false;
}
/// The token of the currently running scan per network interface.
Map<String, _CancelToken> _cancelTokens = {};
class HttpScanDiscoveryService {
final StateAccessor<HttpTargetDiscoveryService> _targetedDiscoveryService;
HttpScanDiscoveryService({
required StateAccessor<HttpTargetDiscoveryService> targetedDiscoveryService,
}) : _targetedDiscoveryService = targetedDiscoveryService;
Stream<Device> getStream({required String networkInterface, required int port, required bool https}) {
final ipList = List.generate(256, (i) => '${networkInterface.split('.').take(3).join('.')}.$i').where((ip) => ip != networkInterface).toList();
// Let the previous scan of this interface skip its remaining requests, so its stream ends.
_cancelTokens[networkInterface]?.cancelled = true;
final token = _cancelTokens[networkInterface] = _CancelToken();
final stream = Pool(_concurrency).forEach<String, Device?>(ipList, (ip) async => token.cancelled ? null : _doRequest(ip, port, https));
return stream.where((device) => device != null).cast<Device>();
}
Stream<Device> getFavoriteStream({required List<(String, int)> devices, required bool https}) {
final stream = Pool(_concurrency).forEach<(String, int), Device?>(devices, (device) => _doRequest(device.$1, device.$2, https));
return stream.where((device) => device != null).cast<Device>();
}
Future<Device?> _doRequest(String currentIp, int port, bool https) async {
_logger.fine('Requesting $currentIp');
final device = await _targetedDiscoveryService.state.discover(
ip: currentIp,
port: port,
https: https,
onError: null,
);
if (device != null) {
_logger.info('[DISCOVER/TCP] ${device.alias} (${device.ip}, model: ${device.deviceModel})');
}
return device;
}
}
@@ -1,60 +0,0 @@
import 'package:localsend_isolates/api_route_builder.dart';
import 'package:localsend_isolates/constants.dart';
import 'package:localsend_isolates/model/device.dart';
import 'package:localsend_isolates/rust/api/http.dart';
import 'package:localsend_isolates/rust/api/model.dart' as rust_model;
import 'package:localsend_isolates/src/isolate/child/http_provider.dart';
import 'package:localsend_isolates/src/isolate/child/sync_provider.dart';
import 'package:localsend_isolates/util/rust.dart';
import 'package:logging/logging.dart';
import 'package:refena_flutter/refena_flutter.dart';
final _logger = Logger('TargetedDiscovery');
final httpTargetDiscoveryProvider = ViewProvider((ref) {
final client = ref.watch(httpProvider).discovery;
final syncState = ref.watch(syncProvider);
return HttpTargetDiscoveryService(
client,
syncState.toRegisterDto(),
syncState.securityContext.certificateHash,
);
});
/// Try to discover a single device using the given IP and port.
class HttpTargetDiscoveryService {
final RsHttpClient _client;
final rust_model.RegisterDto _registerDto;
final String _fingerprint;
HttpTargetDiscoveryService(this._client, this._registerDto, this._fingerprint);
Future<Device?> discover({
required String ip,
required int port,
required bool https,
void Function(String url, Object? error)? onError = defaultErrorPrinter,
}) async {
final url = ApiRoute.register.targetRaw(ip, port, https, protocolVersion);
try {
final response = await _client.register(
protocol: https ? rust_model.ProtocolType.https : rust_model.ProtocolType.http,
ip: ip,
port: port,
payload: _registerDto,
);
if (response.body.token == _fingerprint) {
// discovered itself
return null;
}
return response.body.toDevice(ip, port, https, HttpDiscovery(ip: ip));
} catch (e) {
onError?.call(url, e);
return null;
}
}
static void defaultErrorPrinter(String url, Object? error) {
_logger.warning('$url: $error');
}
}
@@ -1,143 +0,0 @@
import 'dart:async';
import 'package:localsend_isolates/constants.dart';
import 'package:localsend_isolates/model/device.dart';
import 'package:localsend_isolates/model/dto/multicast_dto.dart';
import 'package:localsend_isolates/rust/api/multicast.dart';
import 'package:localsend_isolates/rust/api/server.dart' show ProtocolTypeV2;
import 'package:localsend_isolates/src/isolate/child/http_provider.dart';
import 'package:localsend_isolates/src/isolate/child/sync_provider.dart';
import 'package:localsend_isolates/util/rust.dart';
import 'package:logging/logging.dart';
import 'package:refena_flutter/refena_flutter.dart';
final _logger = Logger('Multicast');
final multicastDiscoveryProvider = Provider((ref) {
return MulticastService(ref);
});
class MulticastService {
MulticastService(this._ref);
final Ref _ref;
RsMulticast? _multicast;
Completer<void> _retryCompleter = Completer();
bool _listening = false;
/// Binds the UDP sockets and listens to multicast announcements.
Stream<Device> startListener() {
if (_listening) {
_logger.info('Already listening to multicast');
return const Stream.empty();
}
_listening = true;
// Verified devices are pushed in as [_answerAnnouncement] resolves, which
// runs concurrently: one unreachable peer must not hold up the others.
final devices = StreamController<Device>();
unawaited(_runListener(devices));
return devices.stream;
}
Future<void> _runListener(StreamController<Device> devices) async {
while (true) {
final syncState = _ref.read(syncProvider);
final RsMulticast multicast;
try {
multicast = await startMulticast(
group: syncState.multicastGroup,
port: syncState.port,
networkWhitelist: syncState.networkWhitelist,
networkBlacklist: syncState.networkBlacklist,
alias: syncState.alias,
version: protocolVersion,
deviceModel: syncState.deviceInfo.deviceModel,
deviceType: syncState.deviceInfo.deviceType.toRust(),
fingerprint: syncState.securityContext.certificateHash,
protocol: syncState.protocol == ProtocolType.https ? ProtocolTypeV2.https : ProtocolTypeV2.http,
download: syncState.download,
);
} catch (e) {
_logger.warning('Could not start multicast discovery (group: ${syncState.multicastGroup}, port: ${syncState.port})', e);
// Wait for the next restart request instead of hot-looping
_retryCompleter = Completer();
await _retryCompleter.future;
continue;
}
_multicast = multicast;
// Tell everyone in the network that I am online.
unawaited(multicast.announce());
await for (final event in multicast.listen()) {
if (!_ref.read(syncProvider).serverRunning) {
// only respond when server is running
continue;
}
unawaited(() async {
final device = await _answerAnnouncement(event.message.toDevice(event.ip));
if (device != null && !devices.isClosed) {
devices.add(device);
}
}());
}
// The stream ended because [restartListener] stopped the discovery.
_multicast = null;
}
}
/// Restarts the listener, e.g. after the port or the network settings changed.
void restartListener() {
final multicast = _multicast;
if (multicast != null) {
// Ends the listen stream, which makes [startListener] rebind.
unawaited(multicast.stop());
} else if (!_retryCompleter.isCompleted) {
// Starting failed previously; let [startListener] try again.
_retryCompleter.complete();
}
}
/// Sends an announcement which triggers a response on every LocalSend member of the network.
Future<void> sendAnnouncement() async {
final multicast = _multicast;
if (multicast == null) {
_logger.info('Multicast discovery is not running, skipping announcement');
return;
}
_logger.info('Announce via UDP');
await multicast.announce();
}
/// Responds to an announcement and returns the peer.
/// Data from the multicast announcement is ignored.
///
/// Returns null if the peer could not be reached or could not be verified.
Future<Device?> _answerAnnouncement(Device peer) async {
final clients = _ref.read(httpProvider);
try {
final response = await clients
.pinnedTo(peer.fingerprint, timeoutMs: clients.discoveryTimeout)
.register(
protocol: peer.getProtocolType(),
ip: peer.ip!,
port: peer.port,
payload: _ref.read(syncProvider).toRegisterDto(),
);
_logger.info('Respond to announcement of ${peer.alias} (${peer.ip}, model: ${peer.deviceModel}) via TCP');
return response.body.toDevice(peer.ip!, peer.port, peer.https, const MulticastDiscovery());
} catch (e) {
_logger.warning('Could not respond to announcement of ${peer.alias} (${peer.ip})', e);
return null;
}
}
}
+21 -5
View File
@@ -2,9 +2,9 @@ import 'package:localsend_isolates/constants.dart';
import 'package:localsend_isolates/model/device.dart';
import 'package:localsend_isolates/model/dto/file_dto.dart';
import 'package:localsend_isolates/model/dto/multicast_dto.dart';
import 'package:localsend_isolates/rust/api/discovery.dart' as rust_discovery;
import 'package:localsend_isolates/rust/api/http.dart' as rust_http;
import 'package:localsend_isolates/rust/api/model.dart' as rust_model;
import 'package:localsend_isolates/rust/api/multicast.dart' as rust_multicast;
import 'package:localsend_isolates/rust/api/server.dart' as rust_server;
import 'package:localsend_isolates/src/isolate/child/sync_provider.dart';
import 'package:mime/mime.dart';
@@ -129,11 +129,11 @@ extension RustFileDtoExt on rust_model.FileDto {
}
}
extension MulticastMessageV2Ext on rust_multicast.MulticastMessageV2 {
Device toDevice(String ip) {
extension RsDiscoveredDeviceExt on rust_discovery.RsDiscoveredDevice {
Device toDevice() {
return Device(
signalingId: null,
ip: ip,
ip: host,
version: version,
port: port,
https: protocol == rust_server.ProtocolTypeV2.https,
@@ -142,7 +142,23 @@ extension MulticastMessageV2Ext on rust_multicast.MulticastMessageV2 {
deviceModel: deviceModel,
deviceType: deviceType?.toDart() ?? DeviceType.desktop,
download: download,
discoveryMethods: {MulticastDiscovery()},
discoveryMethods: {HttpDiscovery(ip: host)},
);
}
}
extension DeviceToRsDiscoveredDeviceExt on Device {
rust_discovery.RsDiscoveredDevice toRsDiscoveredDevice(String ip) {
return rust_discovery.RsDiscoveredDevice(
alias: alias,
version: version,
deviceModel: deviceModel,
deviceType: deviceType.toRust(),
fingerprint: fingerprint,
host: ip,
port: port,
protocol: https ? rust_server.ProtocolTypeV2.https : rust_server.ProtocolTypeV2.http,
download: download,
);
}
}
@@ -0,0 +1,264 @@
use crate::frb_generated::StreamSink;
use localsend::discovery::{
DeviceChannel, DeviceIdentity, DiscoveredDevice, DiscoveryConfig, DiscoveryEvent,
DiscoveryHandle, HttpChannel,
};
use localsend::http::dto_v2::ProtocolTypeV2;
use localsend::model::discovery::DeviceType;
use localsend::multicast::{DEFAULT_MULTICAST_GROUP_V6, InterfaceFilter, MulticastDevice};
use std::time::Duration;
use tokio::sync::{Mutex, mpsc, oneshot};
/// A device that was confirmed over HTTP by the discovery: it answered a
/// register request, or its register request was accepted by our server and
/// was fed back via [RsDiscovery::add_device].
///
/// Emitted on [RsDiscovery::listen] for every confirmation, so a device
/// re-appears whenever it re-announces itself or is re-discovered.
pub struct RsDiscoveredDevice {
pub alias: String,
/// Protocol version (major.minor) implemented by the device.
pub version: String,
pub device_model: Option<String>,
pub device_type: Option<DeviceType>,
/// Fingerprint identifying the device; devices are deduplicated by it.
pub fingerprint: String,
/// The host the device was confirmed on: an IP address, or the scoped
/// form `fe80::1%3` for link-local IPv6 (the Rust HTTP client accepts
/// both back as a host).
pub host: String,
/// The port of the device's HTTP server.
pub port: u16,
pub protocol: ProtocolTypeV2,
/// Whether the device's download API is active.
pub download: bool,
}
pub struct RsDiscovery {
handle: DiscoveryHandle,
event_rx: Mutex<Option<mpsc::Receiver<DiscoveryEvent>>>,
stop_tx: Mutex<Option<oneshot::Sender<()>>>,
}
/// Starts the discovery: binds the UDP multicast sockets on all usable
/// network interfaces, answers announcements of other devices with an HTTP
/// register request, and keeps the store of confirmed devices.
///
/// Announcements are received from the IPv4 [group] and, as a LocalSend
/// extension, from the (currently hardcoded) IPv6 group `ff12::fd3a:e420`.
///
/// [port] is used both to bind the multicast sockets and as the HTTP server
/// port announced to other devices. [cert_pem] and [private_key_pem] are this
/// device's TLS identity, sent as client certificate with every register
/// request; [fingerprint] must be the certificate's SHA-256 fingerprint.
///
/// Nothing is announced until [RsDiscovery::announce] is called.
///
/// Cannot fail besides an invalid [group]: when no multicast socket could be
/// bound (e.g. the port is taken by another process), discovery still runs
/// without multicast — see [RsDiscovery::multicast_error] — and still learns
/// about devices through [RsDiscovery::discover], [RsDiscovery::scan_subnet]
/// and [RsDiscovery::add_device].
pub async fn start_discovery(
group: String,
port: u16,
network_whitelist: Option<Vec<String>>,
network_blacklist: Option<Vec<String>>,
alias: String,
version: String,
device_model: Option<String>,
device_type: Option<DeviceType>,
fingerprint: String,
protocol: ProtocolTypeV2,
download: bool,
cert_pem: String,
private_key_pem: String,
timeout_ms: u64,
) -> anyhow::Result<RsDiscovery> {
let group = group
.parse()
.map_err(|_| anyhow::anyhow!("Invalid multicast group: {group}"))?;
let (event_tx, event_rx) = mpsc::channel::<DiscoveryEvent>(16);
let (stop_tx, stop_rx) = oneshot::channel::<()>();
let handle = localsend::discovery::start(
DiscoveryConfig {
group,
// Hardcoded for now; becomes a parameter once the app exposes it.
group_v6: Some(DEFAULT_MULTICAST_GROUP_V6),
port,
interface_filter: InterfaceFilter {
whitelist: network_whitelist,
blacklist: network_blacklist,
},
device: MulticastDevice {
alias,
version,
device_model,
device_type,
fingerprint,
port,
protocol,
download,
},
identity: DeviceIdentity {
cert_pem,
private_key_pem,
},
timeout: Duration::from_millis(timeout_ms),
event_tx: Some(event_tx),
},
stop_rx,
)
.await;
Ok(RsDiscovery {
handle,
event_rx: Mutex::new(Some(event_rx)),
stop_tx: Mutex::new(Some(stop_tx)),
})
}
impl RsDiscovery {
/// Emits a [RsDiscoveredDevice] for every device confirmation until the
/// discovery is stopped. Can only be listened to once.
pub async fn listen(&self, sink: StreamSink<RsDiscoveredDevice>) {
let Some(mut event_rx) = self.event_rx.lock().await.take() else {
let _ = sink.add_error(anyhow::anyhow!("Discovery events already listened to"));
return;
};
while let Some(event) = event_rx.recv().await {
let (DiscoveryEvent::Discovered { device } | DiscoveryEvent::Updated { device }) =
event;
let _ = sink.add(rs_device(device));
}
}
/// The reason the multicast sockets could not be bound, when they could
/// not. Discovery then neither hears nor sends announcements.
pub fn multicast_error(&self) -> Option<String> {
self.handle.multicast_error().map(|err| format!("{err:#}"))
}
/// Announces this device to the network, which makes every other LocalSend
/// device on it register with this device over HTTP.
///
/// Devices registering in response arrive at the application as server
/// events, not here: feed them back via [RsDiscovery::add_device].
///
/// Returns once the whole announcement burst has been sent, which takes a
/// few seconds, or immediately once the discovery has been stopped or
/// multicast is unavailable.
pub async fn announce(&self) {
self.handle.announce().await;
}
/// Sets whether announcements of other devices are answered with a
/// register request (the answer is what makes the announcing device enter
/// the store). On by default.
///
/// Turned off while the HTTP server is not running: the answer would
/// advertise a port that nobody listens on.
pub fn set_answer_announcements(&self, answer: bool) {
self.handle.set_answer_announcements(answer);
}
/// Discovers a device at a known address, e.g. a favorite or a peer that
/// multicast does not reach, by sending it a register request.
///
/// The confirmed device is also emitted on [RsDiscovery::listen].
/// Returns `None` when the device did not answer or answered with this
/// device's own fingerprint (i.e. the device discovered itself).
pub async fn discover(
&self,
host: String,
port: u16,
protocol: ProtocolTypeV2,
) -> Option<RsDiscoveredDevice> {
match self.handle.discover(&host, port, protocol).await {
Ok(found) => found.map(|found| rs_device(found.device)),
Err(err) => {
tracing::debug!("Could not discover {host}:{port}: {err:#}");
None
}
}
}
/// Scans the `/24` subnet of the local interface address [interface_ip]
/// by sending every other host a register request, for networks that do
/// not carry multicast.
///
/// The found devices are emitted on [RsDiscovery::listen] as they answer;
/// this method returns once the whole scan has finished. At most one scan
/// runs per interface: a call for an address that is still being scanned
/// returns immediately.
pub async fn scan_subnet(
&self,
interface_ip: String,
port: u16,
protocol: ProtocolTypeV2,
) -> anyhow::Result<()> {
let interface_ip = interface_ip
.parse()
.map_err(|_| anyhow::anyhow!("Invalid interface address: {interface_ip}"))?;
self.handle.scan_subnet(interface_ip, port, protocol).await?;
Ok(())
}
/// Puts a device confirmed outside of the discovery into the store, e.g.
/// one that answered an announcement by registering with this device's
/// HTTP server. The device is emitted on [RsDiscovery::listen].
pub async fn add_device(&self, device: RsDiscoveredDevice) {
self.handle
.add_device(DiscoveredDevice {
alias: device.alias,
version: device.version,
device_model: device.device_model,
device_type: device.device_type,
fingerprint: device.fingerprint,
channel: DeviceChannel::Http(HttpChannel {
host: device.host,
port: device.port,
protocol: device.protocol,
}),
download: device.download,
})
.await;
}
/// Stops the discovery, which also ends the [RsDiscovery::listen] stream.
/// Returns after all sockets are closed, so the port can be bound again.
pub async fn stop(&self) {
if let Some(stop_tx) = self.stop_tx.lock().await.take() {
let _ = stop_tx.send(());
self.handle.wait_stopped().await;
}
}
}
/// The stored device flattened for the Dart side; only the channel of the
/// current confirmation is carried.
fn rs_device(device: DiscoveredDevice) -> RsDiscoveredDevice {
let DeviceChannel::Http(http) = device.channel;
RsDiscoveredDevice {
alias: device.alias,
version: device.version,
device_model: device.device_model,
device_type: device.device_type,
fingerprint: device.fingerprint,
host: http.host,
port: http.port,
protocol: http.protocol,
download: device.download,
}
}
@@ -1,9 +1,9 @@
pub mod cancel;
pub mod crypto;
pub mod discovery;
pub mod http;
pub mod logging;
pub mod model;
pub mod multicast;
pub mod server;
pub mod stream;
pub mod webrtc;
@@ -1,155 +0,0 @@
use crate::frb_generated::StreamSink;
use flutter_rust_bridge::frb;
use localsend::model::discovery::DeviceType;
pub use localsend::model::discovery::{MulticastMessageV2, ProtocolTypeV2};
use localsend::multicast::{
DEFAULT_MULTICAST_GROUP_V6, InterfaceFilter, MulticastConfig, MulticastDevice, MulticastEvent,
MulticastHandle,
};
use tokio::sync::{Mutex, mpsc, oneshot};
/// Another device announced itself via UDP multicast.
///
/// The peer expects to be answered with an HTTP register request.
pub struct RsMulticastDiscovered {
/// The address the announcement was sent from. The peer's HTTP server is
/// reachable at this address on `message.port`.
///
/// A link-local IPv6 source carries its interface scope as `fe80::1%3`,
/// which the Rust HTTP client accepts back as a host.
pub ip: String,
/// The announcement as it was received.
pub message: MulticastMessageV2,
}
pub struct RsMulticast {
handle: MulticastHandle,
event_rx: Mutex<Option<mpsc::Receiver<MulticastEvent>>>,
stop_tx: Mutex<Option<oneshot::Sender<()>>>,
}
/// Starts UDP multicast discovery: binds the multicast sockets on all usable
/// network interfaces and listens for announcements of other devices.
///
/// Announcements are sent to the IPv4 [group] and, as a LocalSend extension,
/// to the (currently hardcoded) IPv6 group `ff12::fd3a:e420`.
///
/// [port] is used both to bind the multicast sockets and as the HTTP server
/// port announced to other devices.
///
/// Nothing is announced until [RsMulticast::announce] is called.
///
/// Fails when [group] is not a valid IPv4 address or when no network
/// interface could be used at all.
pub async fn start_multicast(
group: String,
port: u16,
network_whitelist: Option<Vec<String>>,
network_blacklist: Option<Vec<String>>,
alias: String,
version: String,
device_model: Option<String>,
device_type: Option<DeviceType>,
fingerprint: String,
protocol: ProtocolTypeV2,
download: bool,
) -> anyhow::Result<RsMulticast> {
let group = group
.parse()
.map_err(|_| anyhow::anyhow!("Invalid multicast group: {group}"))?;
let (event_tx, event_rx) = mpsc::channel::<MulticastEvent>(16);
let (stop_tx, stop_rx) = oneshot::channel::<()>();
let handle = localsend::multicast::start(
MulticastConfig {
group,
// Hardcoded for now; becomes a parameter once the app exposes it.
group_v6: Some(DEFAULT_MULTICAST_GROUP_V6),
port,
interface_filter: InterfaceFilter {
whitelist: network_whitelist,
blacklist: network_blacklist,
},
device: MulticastDevice {
alias,
version,
device_model,
device_type,
fingerprint,
port,
protocol,
download,
},
event_tx,
},
stop_rx,
)
.await?;
Ok(RsMulticast {
handle,
event_rx: Mutex::new(Some(event_rx)),
stop_tx: Mutex::new(Some(stop_tx)),
})
}
impl RsMulticast {
/// Emits a [RsMulticastDiscovered] for every announcement received from
/// another device until discovery is stopped.
/// Can only be listened to once.
pub async fn listen(&self, sink: StreamSink<RsMulticastDiscovered>) {
let Some(mut event_rx) = self.event_rx.lock().await.take() else {
let _ = sink.add_error(anyhow::anyhow!("Multicast events already listened to"));
return;
};
while let Some(event) = event_rx.recv().await {
let MulticastEvent::Discovered {
ip,
scope_id,
message,
} = event;
let _ = sink.add(RsMulticastDiscovered {
ip: match scope_id {
// A link-local source is unreachable without its scope.
Some(scope_id) => format!("{ip}%{scope_id}"),
None => ip.to_string(),
},
message,
});
}
}
/// Announces this device to the network, which makes every other LocalSend
/// device on it register with this device over HTTP.
///
/// Returns once the whole announcement burst has been sent, which takes a
/// few seconds, or immediately once discovery has been stopped.
pub async fn announce(&self) {
self.handle.announce().await;
}
/// Stops discovery, which also ends the [RsMulticast::listen] stream.
/// Returns after all sockets are closed, so the port can be bound again.
pub async fn stop(&self) {
if let Some(stop_tx) = self.stop_tx.lock().await.take() {
let _ = stop_tx.send(());
self.handle.wait_stopped().await;
}
}
}
#[frb(mirror(MulticastMessageV2))]
pub struct _MulticastMessageV2 {
pub alias: String,
pub version: String,
pub device_model: Option<String>,
pub device_type: Option<DeviceType>,
pub fingerprint: String,
pub port: u16,
pub protocol: ProtocolTypeV2,
pub download: bool,
}
File diff suppressed because it is too large Load Diff