feat: bind cli to core/discovery

This commit is contained in:
Tien Do Nam
2026-07-31 16:04:30 +02:00
parent a331c21c56
commit 25a0b8a85e
15 changed files with 762 additions and 531 deletions
+25 -13
View File
@@ -5,6 +5,7 @@ use super::App;
use crate::device_list::{DeviceList, DeviceListOutcome, DeviceRow, Row};
use crate::ui::Category;
use crossterm::event::KeyEvent;
use localsend::discovery::StatefulDevice;
impl App {
pub(super) fn open_device_list(&mut self) {
@@ -28,14 +29,15 @@ impl App {
let mut empty = true;
for (fingerprint, paired) in self.storage.paired.iter() {
empty = false;
let discovered = self.registry.by_fingerprint(fingerprint);
let discovered = self.discovery.device_by_fingerprint(fingerprint);
rows.push(Row::Device(DeviceRow {
fingerprint: fingerprint.clone(),
alias: discovered
.map(|device| device.alias.clone())
.as_ref()
.map(|stored| stored.device.alias.clone())
.unwrap_or_else(|| paired.alias.clone()),
slot: discovered.and_then(|device| device.slot),
host: discovered.map(|device| device.host.clone()),
slot: self.slots.get(fingerprint),
hosts: discovered.as_ref().map(channel_hosts).unwrap_or_default(),
paired: true,
}));
}
@@ -46,16 +48,16 @@ impl App {
rows.push(Row::Spacer);
rows.push(Row::Header("Discovered"));
let mut empty = true;
for device in self.registry.devices() {
if self.storage.paired.contains(&device.fingerprint) {
for stored in self.discovery.devices() {
if self.storage.paired.contains(&stored.device.fingerprint) {
continue;
}
empty = false;
rows.push(Row::Device(DeviceRow {
fingerprint: device.fingerprint.clone(),
alias: device.alias.clone(),
slot: device.slot,
host: Some(device.host.clone()),
fingerprint: stored.device.fingerprint.clone(),
alias: stored.device.alias.clone(),
slot: self.slots.get(&stored.device.fingerprint),
hosts: channel_hosts(&stored),
paired: false,
}));
}
@@ -81,7 +83,7 @@ impl App {
self.close_device_list();
if !self.preselected.is_empty() {
self.start_send(&fingerprint, self.preselected.clone());
} else if let Some(device) = self.registry.by_fingerprint(&fingerprint).cloned() {
} else if let Some(device) = self.discovery.device_by_fingerprint(&fingerprint) {
self.open_picker(device);
}
}
@@ -102,10 +104,10 @@ impl App {
/// device moves up into "Paired"). The log line shows up once the list
/// is closed.
fn pair(&mut self, fingerprint: &str) {
let Some(device) = self.registry.by_fingerprint(fingerprint) else {
let Some(stored) = self.discovery.device_by_fingerprint(fingerprint) else {
return;
};
let alias = device.alias.clone();
let alias = stored.device.alias;
match self
.storage
.paired
@@ -151,3 +153,13 @@ impl App {
}
}
}
/// The addresses a stored device can be dialed at, best first.
fn channel_hosts(stored: &StatefulDevice) -> Vec<String> {
stored
.get_ranked_channels()
.into_iter()
.filter_map(|channel| channel.http())
.map(|http| http.host.clone())
.collect()
}
+43 -80
View File
@@ -1,91 +1,54 @@
//! Device discovery: multicast announcements are answered with an HTTP
//! register request, and confirmed devices get a slot in the registry.
//! Device discovery: the core discovery answers multicast announcements with
//! an HTTP register request and stores confirmed devices; the CLI assigns a
//! hotkey slot to every device entering the store.
use super::{App, AppEvent};
use super::App;
use crate::slots::slot_label;
use crate::ui::Category;
use localsend::http::client::v2::LsHttpClientV2;
use localsend::http::dto::ProtocolType;
use localsend::http::dto_v2::ProtocolTypeV2;
use localsend::multicast::MulticastEvent;
use std::time::Duration;
use localsend::discovery::{DeviceChannel, DiscoveredDevice, DiscoveryEvent, HttpChannel};
use localsend::http::dto_v2::RegisterDtoV2;
impl App {
pub(super) fn handle_multicast(&mut self, event: MulticastEvent) {
let MulticastEvent::Discovered {
ip,
scope_id,
message,
} = event;
if message.fingerprint == self.storage.identity.fingerprint {
pub(super) fn handle_discovery(&mut self, event: DiscoveryEvent) {
let DiscoveryEvent::Discovered { device } = event else {
// Re-confirmations update the store silently; multi-homed peers
// re-announce with a different address all the time.
return;
};
let slot = self.slots.assign(&device.fingerprint);
let host = device
.http()
.map(|http| http.host.as_str())
.unwrap_or("unknown address");
self.ui.log(
Category::Discovery,
&format!("[{}] {} ({host})", slot_label(slot), device.alias),
);
}
/// Feeds a device confirmed outside of discovery — it registered with our
/// HTTP server, or it sent a transfer request — into the discovery store;
/// a device new to the store comes back as a `Discovered` event.
pub(super) fn device_confirmed(&self, host: String, info: RegisterDtoV2) {
if info.fingerprint == self.storage.identity.fingerprint {
return;
}
let host = match scope_id {
Some(scope_id) => format!("{ip}%{scope_id}"),
None => ip.to_string(),
let device = DiscoveredDevice {
alias: info.alias,
version: info.version,
device_model: info.device_model,
device_type: info.device_type,
fingerprint: info.fingerprint,
channel: DeviceChannel::Http(HttpChannel {
host,
port: info.port,
protocol: info.protocol,
}),
download: info.download,
};
// Answer the announcement with an HTTP register request; the device
// is only shown once that request succeeds.
let identity = self.storage.identity.clone();
let events_tx = self.events_tx.clone();
let discovery = self.discovery.clone();
tokio::spawn(async move {
let expected_fingerprint = match message.protocol {
ProtocolTypeV2::Https => Some(message.fingerprint.clone()),
ProtocolTypeV2::Http => None,
};
let Ok(client) = LsHttpClientV2::try_new(
&identity.key_pem,
&identity.cert_pem,
expected_fingerprint,
Some(Duration::from_secs(5)),
) else {
return;
};
let protocol = match message.protocol {
ProtocolTypeV2::Http => ProtocolType::Http,
ProtocolTypeV2::Https => ProtocolType::Https,
};
let result = client
.register(protocol, &host, message.port, identity.register_dto())
.await;
if let Ok(response) = result {
let _ = events_tx
.send(AppEvent::DeviceUp {
alias: response.body.alias,
host,
port: message.port,
protocol: message.protocol,
fingerprint: message.fingerprint,
})
.await;
}
discovery.add_device(device).await;
});
}
pub(super) fn device_up(
&mut self,
alias: String,
host: String,
port: u16,
protocol: ProtocolTypeV2,
fingerprint: String,
) {
if fingerprint == self.storage.identity.fingerprint {
return;
}
if let Some(device) = self
.registry
.upsert(alias, host, port, protocol, fingerprint)
{
self.ui.log(
Category::Discovery,
&format!(
"[{}] {} ({})",
device.slot_label(),
device.alias,
device.host
),
);
}
}
}
+48 -64
View File
@@ -6,17 +6,18 @@ mod status;
use crate::Args;
use crate::device_list::DeviceList;
use crate::devices::DeviceRegistry;
use crate::picker::Picker;
use crate::slots::Slots;
use crate::storage;
use crate::ui::{Category, Ui};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use localsend::http::dto_v2::ProtocolTypeV2;
use localsend::discovery::{
DEFAULT_DISCOVERY_TIMEOUT, DeviceIdentity, DiscoveryConfig, DiscoveryEvent, DiscoveryHandle,
};
use localsend::http::server::v2::ServerEventV2;
use localsend::http::server::{ServerConfigV2, ServerHandle, start_with_port};
use localsend::multicast::{
self, DEFAULT_MULTICAST_GROUP, DEFAULT_MULTICAST_GROUP_V6, DEFAULT_PORT, InterfaceFilter,
MulticastConfig, MulticastEvent,
DEFAULT_MULTICAST_GROUP, DEFAULT_MULTICAST_GROUP_V6, DEFAULT_PORT, InterfaceFilter,
};
use receive::{Answer, PendingReceive, ReceiveSession};
use sending::SendState;
@@ -30,16 +31,6 @@ pub enum AppEvent {
/// A key was pressed.
Key(KeyEvent),
/// A device was confirmed reachable (it registered with us, or it
/// announced itself and answered our register request).
DeviceUp {
alias: String,
host: String,
port: u16,
protocol: ProtocolTypeV2,
fingerprint: String,
},
/// A file of the active receive session finished (or failed).
ReceiveFileResult {
session_id: String,
@@ -63,7 +54,13 @@ pub enum AppEvent {
struct App {
ui: Ui,
server: Arc<ServerHandle>,
registry: DeviceRegistry,
/// The core discovery: the store of confirmed devices, and the multicast
/// side when it could be started.
discovery: Arc<DiscoveryHandle>,
/// The hotkeys (1-9) of the devices in the discovery store.
slots: Slots,
/// Config, identity and paired devices, see [`storage::Repository`].
storage: storage::Repository,
@@ -106,34 +103,37 @@ pub async fn run(args: Args) -> anyhow::Result<()> {
.await?;
let server = Arc::new(server);
// Multicast discovery. Failure is not fatal: transfers to this device
// still work for peers that know its address.
let (multicast_tx, multicast_rx) = mpsc::channel::<MulticastEvent>(16);
let (multicast_stop_tx, multicast_stop_rx) = oneshot::channel::<()>();
let multicast = multicast::start(
MulticastConfig {
group: DEFAULT_MULTICAST_GROUP,
group_v6: Some(DEFAULT_MULTICAST_GROUP_V6),
port: DEFAULT_PORT,
interface_filter: InterfaceFilter::default(),
device: identity.multicast_device(),
event_tx: multicast_tx,
},
multicast_stop_rx,
)
.await;
let multicast = match multicast {
Ok(handle) => Some(Arc::new(handle)),
Err(err) => {
eprintln!("Multicast discovery unavailable: {err:#}");
None
}
};
let mut multicast_rx = multicast.is_some().then_some(multicast_rx);
if let Some(handle) = &multicast {
// Discovery: multicast, plus the register requests answering other
// devices' announcements. Multicast failure is not fatal: the store keeps
// collecting the devices that contact this device over HTTP.
let (discovery_tx, mut discovery_rx) = mpsc::channel::<DiscoveryEvent>(16);
let (discovery_stop_tx, discovery_stop_rx) = oneshot::channel::<()>();
let discovery = Arc::new(
localsend::discovery::start(
DiscoveryConfig {
group: DEFAULT_MULTICAST_GROUP,
group_v6: Some(DEFAULT_MULTICAST_GROUP_V6),
port: DEFAULT_PORT,
interface_filter: InterfaceFilter::default(),
device: identity.multicast_device(),
identity: DeviceIdentity {
cert_pem: identity.cert_pem.clone(),
private_key_pem: identity.key_pem.clone(),
},
timeout: DEFAULT_DISCOVERY_TIMEOUT,
event_tx: Some(discovery_tx),
},
discovery_stop_rx,
)
.await,
);
if let Some(err) = discovery.multicast_error() {
eprintln!("Multicast unavailable: {err:#}");
}
{
// Announce this device; peers answer with an HTTP register request.
let handle = handle.clone();
tokio::spawn(async move { handle.announce().await });
let discovery = discovery.clone();
tokio::spawn(async move { discovery.announce().await });
}
crossterm::terminal::enable_raw_mode()?;
@@ -159,7 +159,8 @@ pub async fn run(args: Args) -> anyhow::Result<()> {
let mut app = App {
ui: Ui::new(),
server: server.clone(),
registry: DeviceRegistry::new(),
discovery: discovery.clone(),
slots: Slots::new(),
storage,
pending: None,
receive: None,
@@ -187,8 +188,8 @@ pub async fn run(args: Args) -> anyhow::Result<()> {
Some(event) = server_rx.recv() => {
app.handle_server_event(event);
}
Some(event) = recv_opt(&mut multicast_rx) => {
app.handle_multicast(event);
Some(event) = discovery_rx.recv() => {
app.handle_discovery(event);
}
_ = tick.tick() => {
app.tick();
@@ -206,34 +207,17 @@ pub async fn run(args: Args) -> anyhow::Result<()> {
app.ui.set_status(None);
let _ = crossterm::terminal::disable_raw_mode();
let _ = server_stop_tx.send(());
let _ = multicast_stop_tx.send(());
let _ = discovery_stop_tx.send(());
let _ = tokio::time::timeout(Duration::from_secs(1), server.wait_stopped()).await;
if let Some(multicast) = &multicast {
let _ = tokio::time::timeout(Duration::from_secs(1), multicast.wait_stopped()).await;
}
let _ = tokio::time::timeout(Duration::from_secs(1), discovery.wait_stopped()).await;
Ok(())
}
/// Receives from an optional channel, pending forever when there is none.
async fn recv_opt<T>(rx: &mut Option<mpsc::Receiver<T>>) -> Option<T> {
match rx {
Some(rx) => rx.recv().await,
None => std::future::pending().await,
}
}
impl App {
/// Handles an event; returns `true` when the application should quit.
async fn handle_event(&mut self, event: AppEvent) -> bool {
match event {
AppEvent::Key(key) => return self.handle_key(key),
AppEvent::DeviceUp {
alias,
host,
port,
protocol,
fingerprint,
} => self.device_up(alias, host, port, protocol, fingerprint),
AppEvent::ReceiveFileResult {
session_id,
file_id,
+2 -14
View File
@@ -99,13 +99,7 @@ impl App {
pub(super) fn handle_server_event(&mut self, event: ServerEventV2) {
match event {
ServerEventV2::Register { ip, info } => {
self.device_up(
info.alias,
ip.to_string(),
info.port,
info.protocol,
info.fingerprint,
);
self.device_confirmed(ip.to_string(), info);
}
ServerEventV2::PrepareUpload {
session_id,
@@ -116,13 +110,7 @@ impl App {
decision_tx,
} => {
// The sender is clearly reachable; make sure it has a slot.
self.device_up(
info.alias.clone(),
ip.to_string(),
info.port,
info.protocol,
info.fingerprint.clone(),
);
self.device_confirmed(ip.to_string(), info.clone());
let sender = SenderTarget {
host: ip.to_string(),
+30 -9
View File
@@ -2,12 +2,12 @@
//! transfer driven by the [`crate::send_task`].
use super::App;
use crate::devices::Device;
use crate::picker::{Picker, PickerOutcome};
use crate::send_task;
use crate::ui::Category;
use crate::util::SpeedMeter;
use crossterm::event::KeyEvent;
use localsend::discovery::StatefulDevice;
use localsend::model::transfer::FileDto;
use std::collections::HashMap;
use std::path::PathBuf;
@@ -28,24 +28,31 @@ pub(super) struct SendState {
impl App {
pub(super) fn start_picking(&mut self, slot: u8) {
let Some(device) = self.registry.by_slot(slot).cloned() else {
let device = self
.slots
.fingerprint_by_slot(slot)
.and_then(|fingerprint| self.discovery.device_by_fingerprint(fingerprint));
let Some(device) = device else {
self.ui
.log(Category::Send, &format!("No device on [{slot}]"));
return;
};
if !self.preselected.is_empty() {
self.start_send(&device.fingerprint, self.preselected.clone());
self.start_send(&device.device.fingerprint, self.preselected.clone());
return;
}
self.open_picker(device);
}
pub(super) fn open_picker(&mut self, device: Device) {
pub(super) fn open_picker(&mut self, device: StatefulDevice) {
if self.send.is_some() {
self.ui.log(Category::Send, "A send is already in progress");
return;
}
match Picker::open(device.fingerprint, device.alias.clone()) {
match Picker::open(
device.device.fingerprint.clone(),
device.device.alias.clone(),
) {
Ok(picker) => {
self.ui.suspend();
self.picker = Some(picker);
@@ -53,7 +60,10 @@ impl App {
Err(err) => {
self.ui.log(
Category::Send,
&format!("{}: could not open the file picker: {err}", device.alias),
&format!(
"{}: could not open the file picker: {err}",
device.device.alias
),
);
}
}
@@ -85,7 +95,18 @@ impl App {
self.ui.log(Category::Send, "A send is already in progress");
return;
}
let Some(device) = self.registry.by_fingerprint(fingerprint).cloned() else {
let Some(device) = self.discovery.device_by_fingerprint(fingerprint) else {
return;
};
let Some(host) = device
.get_best_channel()
.and_then(|channel| channel.http())
.map(|http| http.host.clone())
else {
self.ui.log(
Category::Send,
&format!("{}: No dialable address", device.device.alias),
);
return;
};
@@ -134,8 +155,8 @@ impl App {
let cancel = send_task::SendCancel::new();
self.send = Some(SendState {
session_id: None,
alias: device.alias.clone(),
host: device.host.clone(),
alias: device.device.alias.clone(),
host,
total_bytes,
sent: progress.clone(),
cancel: cancel.clone(),
+51 -8
View File
@@ -30,9 +30,10 @@ pub struct DeviceRow {
/// The send hotkey (1-9) of the discovered device, if one was free.
pub slot: Option<u8>,
/// Where the device was last seen; `None` for a paired device that has
/// not been discovered in this run and therefore cannot be sent to.
pub host: Option<String>,
/// The addresses the device was seen on, the best first; empty for a paired
/// device that has not been discovered in this run and therefore cannot
/// be sent to.
pub hosts: Vec<String>,
/// Whether the device is paired; the pair hotkey toggles it.
pub paired: bool,
@@ -112,7 +113,7 @@ impl DeviceList {
KeyCode::Enter => {
// A paired device that was never discovered has no address.
if let Some(device) = self.selected_device()
&& device.host.is_some()
&& !device.hosts.is_empty()
{
return DeviceListOutcome::Send {
fingerprint: device.fingerprint.clone(),
@@ -221,11 +222,17 @@ fn row_item(row: &Row) -> ListItem<'_> {
Some(slot) => slot.to_string(),
None => "-".to_string(),
};
match &device.host {
Some(host) => {
ListItem::new(Span::raw(format!(" [{slot}] {} ({host})", device.alias)))
match device.hosts.is_empty() {
false => {
let hosts: Vec<String> =
device.hosts.iter().map(|host| shorten_host(host)).collect();
ListItem::new(Span::raw(format!(
" [{slot}] {} ({})",
device.alias,
hosts.join(", ")
)))
}
None => ListItem::new(Span::styled(
true => ListItem::new(Span::styled(
format!(" [{slot}] {} (offline)", device.alias),
Style::default().fg(Color::DarkGray),
)),
@@ -233,3 +240,39 @@ fn row_item(row: &Row) -> ListItem<'_> {
}
}
}
/// Shortens a host for the row: an IPv6 address is reduced to its last group
/// (keeping the scope), so `fe80::6888:8aff:febd:8578%15` renders as
/// `::8578%15`. Display only; sending always uses the full address.
fn shorten_host(host: &str) -> String {
let (address, scope) = match host.split_once('%') {
Some((address, scope)) => (address, Some(scope)),
None => (host, None),
};
if !address.contains(':') {
return host.to_string();
}
let last_group = address.rsplit(':').next().unwrap_or_default();
match scope {
Some(scope) => format!("::{last_group}%{scope}"),
None => format!("::{last_group}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_shorten_host() {
assert_eq!(shorten_host("fe80::6888:8aff:febd:8578%15"), "::8578%15");
assert_eq!(shorten_host("fe80::6888:8aff:febd:8578%7"), "::8578%7");
assert_eq!(shorten_host("fe80::1%3"), "::1%3");
assert_eq!(shorten_host("2a00:1450:4001:829::200e"), "::200e");
assert_eq!(
shorten_host("192.168.178.183"),
"192.168.178.183",
"IPv4 must stay untouched"
);
}
}
-90
View File
@@ -1,90 +0,0 @@
use localsend::http::dto_v2::ProtocolTypeV2;
/// A discovered LocalSend device.
#[derive(Clone)]
pub struct Device {
/// The hotkey (1-9) assigned to this device, if one was free.
pub slot: Option<u8>,
pub alias: String,
/// The host to dial the device at: an IP address, or the scoped form
/// `fe80::1%3` for link-local IPv6 (the HTTP client accepts both).
pub host: String,
pub port: u16,
pub protocol: ProtocolTypeV2,
pub fingerprint: String,
}
impl Device {
pub fn slot_label(&self) -> String {
match self.slot {
Some(slot) => slot.to_string(),
None => "-".to_string(),
}
}
}
/// All devices seen in this run, identified by fingerprint.
pub struct DeviceRegistry {
devices: Vec<Device>,
}
impl DeviceRegistry {
pub fn new() -> Self {
Self {
devices: Vec::new(),
}
}
/// Adds or updates a device. Returns the device only when it is new
/// (i.e. should be logged): a known device is updated silently, because
/// multi-homed peers re-announce with a different address all the time.
pub fn upsert(
&mut self,
alias: String,
host: String,
port: u16,
protocol: ProtocolTypeV2,
fingerprint: String,
) -> Option<Device> {
if let Some(device) = self
.devices
.iter_mut()
.find(|device| device.fingerprint == fingerprint)
{
device.alias = alias;
device.host = host;
device.port = port;
device.protocol = protocol;
return None;
}
let slot =
(1..=9u8).find(|slot| !self.devices.iter().any(|device| device.slot == Some(*slot)));
let device = Device {
slot,
alias,
host,
port,
protocol,
fingerprint,
};
self.devices.push(device.clone());
Some(device)
}
pub fn by_slot(&self, slot: u8) -> Option<&Device> {
self.devices.iter().find(|device| device.slot == Some(slot))
}
pub fn by_fingerprint(&self, fingerprint: &str) -> Option<&Device> {
self.devices
.iter()
.find(|device| device.fingerprint == fingerprint)
}
/// All devices in discovery order.
pub fn devices(&self) -> &[Device] {
&self.devices
}
}
+1 -1
View File
@@ -1,9 +1,9 @@
mod app;
mod banner;
mod device_list;
mod devices;
mod picker;
mod send_task;
mod slots;
mod storage;
mod ui;
mod util;
+19 -15
View File
@@ -1,10 +1,10 @@
use crate::app::AppEvent;
use crate::devices::Device;
use crate::storage::Identity;
use crate::ui::Category;
use crate::util;
use bytes::Bytes;
use futures_util::StreamExt;
use localsend::discovery::StatefulDevice;
use localsend::http::client::ClientError;
use localsend::http::client::v2::LsHttpClientV2;
use localsend::http::dto::ProtocolType;
@@ -47,7 +47,7 @@ impl SendCancel {
/// Always ends by emitting [AppEvent::SendEnded].
pub async fn run_send(
identity: Arc<Identity>,
device: Device,
device: StatefulDevice,
files: HashMap<String, FileDto>,
paths: HashMap<String, PathBuf>,
progress: Arc<AtomicU64>,
@@ -60,14 +60,14 @@ pub async fn run_send(
async fn send_inner(
identity: Arc<Identity>,
device: Device,
device: StatefulDevice,
files: HashMap<String, FileDto>,
paths: HashMap<String, PathBuf>,
progress: Arc<AtomicU64>,
cancel: SendCancel,
events: &mpsc::Sender<AppEvent>,
) {
let alias = device.alias.clone();
let alias = device.device.alias.clone();
let log = |text: String| {
let events = events.clone();
async move {
@@ -80,12 +80,16 @@ async fn send_inner(
}
};
let protocol = match device.protocol {
let Some(http) = device.get_best_channel().and_then(|channel| channel.http()) else {
log(format!("{alias}: No dialable address")).await;
return;
};
let protocol = match http.protocol {
ProtocolTypeV2::Http => ProtocolType::Http,
ProtocolTypeV2::Https => ProtocolType::Https,
};
let expected_fingerprint = match device.protocol {
ProtocolTypeV2::Https => Some(device.fingerprint.clone()),
let expected_fingerprint = match http.protocol {
ProtocolTypeV2::Https => Some(device.device.fingerprint.clone()),
ProtocolTypeV2::Http => None,
};
let client = match LsHttpClientV2::try_new(
@@ -109,8 +113,8 @@ async fn send_inner(
let prepared = match client
.prepare_upload(
protocol.clone(),
&device.host,
device.port,
&http.host,
http.port,
None,
payload,
None,
@@ -193,8 +197,8 @@ async fn send_inner(
match client
.upload(
protocol.clone(),
&device.host,
device.port,
&http.host,
http.port,
None,
&response.session_id,
file_id,
@@ -220,8 +224,8 @@ async fn send_inner(
let _ = client
.cancel(
protocol.clone(),
&device.host,
device.port,
&http.host,
http.port,
&response.session_id,
)
.await;
@@ -238,8 +242,8 @@ async fn send_inner(
let _ = client
.cancel(
protocol.clone(),
&device.host,
device.port,
&http.host,
http.port,
&response.session_id,
)
.await;
+52
View File
@@ -0,0 +1,52 @@
/// The hotkeys (1-9) assigned to discovered devices, in discovery order.
pub struct Slots {
/// One entry per discovered fingerprint. A device beyond the ninth gets
/// `None` and keeps it: slots are never released, so a hotkey means the
/// same device for the whole run.
assigned: Vec<(String, Option<u8>)>,
}
impl Slots {
pub fn new() -> Self {
Self {
assigned: Vec::new(),
}
}
/// Assigns the next free slot to a fingerprint; returns the assigned
/// slot, also when the fingerprint already has one.
pub fn assign(&mut self, fingerprint: &str) -> Option<u8> {
if let Some((_, slot)) = self.assigned.iter().find(|(known, _)| known == fingerprint) {
return *slot;
}
let slot = (1..=9u8).find(|slot| {
!self
.assigned
.iter()
.any(|(_, assigned)| *assigned == Some(*slot))
});
self.assigned.push((fingerprint.to_string(), slot));
slot
}
pub fn get(&self, fingerprint: &str) -> Option<u8> {
self.assigned
.iter()
.find(|(known, _)| known == fingerprint)
.and_then(|(_, slot)| *slot)
}
pub fn fingerprint_by_slot(&self, slot: u8) -> Option<&str> {
self.assigned
.iter()
.find(|(_, assigned)| *assigned == Some(slot))
.map(|(fingerprint, _)| fingerprint.as_str())
}
}
pub fn slot_label(slot: Option<u8>) -> String {
match slot {
Some(slot) => slot.to_string(),
None => "-".to_string(),
}
}
+39 -22
View File
@@ -1,7 +1,7 @@
mod store;
pub use store::{
DeviceChannel, DeviceLog, DiscoveredDevice, DiscoveredDeviceWithLogs, HttpChannel,
ChannelStatus, DeviceChannel, DeviceLog, DiscoveredDevice, HttpChannel, StatefulDevice,
};
use crate::http::client::{ClientError, LsHttpClientV2};
@@ -77,7 +77,7 @@ pub struct DiscoveryConfig {
}
/// An event emitted by the discovery. Every event is also logged in
/// [`DiscoveredDeviceWithLogs::logs`]; the accumulated state is read from
/// [`StatefulDevice::logs`]; the accumulated state is read from
/// [`DiscoveryHandle::devices`].
#[derive(Clone, Debug)]
pub enum DiscoveryEvent {
@@ -144,7 +144,7 @@ impl DiscoveryState {
host: &str,
port: u16,
protocol: ProtocolTypeV2,
) -> Result<Option<DiscoveredDeviceWithLogs>, ClientError> {
) -> Result<Option<StatefulDevice>, ClientError> {
let response = client
.register(client_protocol(protocol), host, port, self.register_dto())
.await?;
@@ -170,7 +170,7 @@ impl DiscoveryState {
/// Puts a device into the store, logging the confirmation on it, and
/// emits the resulting event. Returns whether the device is new, and
/// its stored state after the merge.
async fn found(&self, device: DiscoveredDevice) -> (bool, DiscoveredDeviceWithLogs) {
async fn found(&self, device: DiscoveredDevice) -> (bool, StatefulDevice) {
let (event, merged) = self.store.upsert(device, SystemTime::now());
let is_new = matches!(event, DiscoveryEvent::Discovered { .. });
if let Some(event_tx) = &self.event_tx {
@@ -183,7 +183,9 @@ impl DiscoveryState {
/// A handle to a running discovery: the store of discovered devices and the
/// application-initiated operations.
pub struct DiscoveryHandle {
multicast: MulticastHandle,
/// The multicast side, or the reason it could not be started; discovery
/// keeps working without it, see [`DiscoveryHandle::multicast_error`].
multicast: Result<MulticastHandle, anyhow::Error>,
state: Arc<DiscoveryState>,
}
@@ -196,9 +198,22 @@ impl DiscoveryHandle {
/// themselves. Feed them back via [`DiscoveryHandle::add_device`].
///
/// Returns once the whole announcement burst has been sent, which takes a
/// few seconds, or immediately once discovery has been stopped.
/// few seconds, or immediately once discovery has been stopped or
/// multicast is unavailable.
pub async fn announce(&self) {
self.multicast.announce().await;
if let Ok(multicast) = &self.multicast {
multicast.announce().await;
}
}
/// The reason the multicast sockets could not be bound (e.g. the port is
/// taken, or there is no usable network interface), when they could not.
///
/// Discovery then neither hears nor sends announcements; it still learns
/// about devices through [`DiscoveryHandle::discover`],
/// [`DiscoveryHandle::scan_subnet`] and [`DiscoveryHandle::add_device`].
pub fn multicast_error(&self) -> Option<&anyhow::Error> {
self.multicast.as_ref().err()
}
/// Discovers a device at a known address, e.g. a favorite or a peer that
@@ -214,7 +229,7 @@ impl DiscoveryHandle {
host: &str,
port: u16,
protocol: ProtocolTypeV2,
) -> Result<Option<DiscoveredDeviceWithLogs>, ClientError> {
) -> Result<Option<StatefulDevice>, ClientError> {
let client = self.state.unpinned_client()?;
self.state.probe(&client, host, port, protocol).await
}
@@ -230,7 +245,7 @@ impl DiscoveryHandle {
interface_ip: Ipv4Addr,
port: u16,
protocol: ProtocolTypeV2,
) -> Result<Vec<DiscoveredDeviceWithLogs>, ClientError> {
) -> Result<Vec<StatefulDevice>, ClientError> {
if !self.state.scanning.lock().unwrap().insert(interface_ip) {
return Ok(Vec::new());
}
@@ -273,19 +288,22 @@ impl DiscoveryHandle {
}
/// All discovered devices in discovery order.
pub fn devices(&self) -> Vec<DiscoveredDeviceWithLogs> {
pub fn devices(&self) -> Vec<StatefulDevice> {
self.state.store.devices()
}
pub fn device_by_fingerprint(&self, fingerprint: &str) -> Option<DiscoveredDeviceWithLogs> {
pub fn device_by_fingerprint(&self, fingerprint: &str) -> Option<StatefulDevice> {
self.state.store.by_fingerprint(fingerprint)
}
/// Waits until discovery has terminated and the multicast sockets have
/// been closed, so that the port can be bound again.
/// Must be called after requesting a stop via the stop channel.
/// Returns immediately when multicast is unavailable.
pub async fn wait_stopped(&self) {
self.multicast.wait_stopped().await;
if let Ok(multicast) = &self.multicast {
multicast.wait_stopped().await;
}
}
}
@@ -309,14 +327,13 @@ impl Drop for ScanGuard<'_> {
/// Binds the multicast sockets and starts answering announcements of other
/// devices. Nothing is announced until [`DiscoveryHandle::announce`] is called.
///
/// Fails when no network interface could be used, e.g. because the port is
/// already bound by another process or because there is no network at all.
pub async fn start(
config: DiscoveryConfig,
stop_rx: oneshot::Receiver<()>,
) -> anyhow::Result<DiscoveryHandle> {
/// Starting cannot fail: when no multicast socket could be bound, e.g. because
/// the port is already bound by another process or because there is no network
/// at all, discovery runs without multicast.
pub async fn start(config: DiscoveryConfig, stop_rx: oneshot::Receiver<()>) -> DiscoveryHandle {
let (multicast_tx, mut multicast_rx) = mpsc::channel(MULTICAST_CHANNEL_SIZE);
// On failure `multicast_tx` is dropped, which ends the answering task.
let multicast = multicast::start(
MulticastConfig {
group: config.group,
@@ -328,7 +345,7 @@ pub async fn start(
},
stop_rx,
)
.await?;
.await;
let state = Arc::new(DiscoveryState {
device: config.device,
@@ -358,7 +375,7 @@ pub async fn start(
}
});
Ok(DiscoveryHandle { multicast, state })
DiscoveryHandle { multicast, state }
}
/// Answers an announcement with a register request, as the protocol requires.
@@ -437,11 +454,11 @@ fn confirmed_device(
device_model: response.device_model,
device_type: response.device_type,
fingerprint,
channels: vec![DeviceChannel::Http(HttpChannel {
channel: DeviceChannel::Http(HttpChannel {
host,
port,
protocol,
})],
}),
download: response.download,
}
}
+191 -60
View File
@@ -2,6 +2,8 @@
use super::DiscoveryEvent;
use crate::model::discovery::{DeviceType, ProtocolTypeV2};
use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::Mutex;
use std::time::SystemTime;
@@ -29,26 +31,41 @@ pub struct DiscoveredDevice {
/// In HTTPS mode the SHA-256 hash of the certificate, otherwise a random string.
pub fingerprint: String,
/// The channels the device is reachable on, in discovery order.
/// A transfer can jump to another channel when one fails.
pub channels: Vec<DeviceChannel>,
/// The channel the device was confirmed over.
pub channel: DeviceChannel,
/// Whether the device's download API is active.
pub download: bool,
}
/// A [`DiscoveredDevice`] as kept in the store, together with the history of
/// events that affected it.
/// A [`DiscoveredDevice`] as kept in the store.
#[derive(Clone, Debug)]
pub struct DiscoveredDeviceWithLogs {
pub struct StatefulDevice {
/// The device as it was most recently confirmed.
pub device: DiscoveredDevice,
/// Every channel the device was confirmed on, with its current status.
/// Starts with `Available`, the application is responsible
/// to set it to `NotReachable` on error.
pub channels: HashMap<DeviceChannel, ChannelStatus>,
/// The events that affected this device, oldest first, at most
/// [`MAX_LOGS`]. Every confirmation is logged, so the last entry is when
/// the device was last seen.
pub logs: Vec<DeviceLog>,
}
/// Whether a [`DeviceChannel`] of a [`StatefulDevice`] is believed to work.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ChannelStatus {
/// The channel worked when it was last used.
Available,
/// The channel failed when it was last used. A re-confirmation over the
/// channel makes it available again.
NotReachable,
}
/// A [`DiscoveryEvent`] that affected a device, with the time it happened.
#[derive(Clone, Debug)]
pub struct DeviceLog {
@@ -56,20 +73,38 @@ pub struct DeviceLog {
pub event: DiscoveryEvent,
}
impl DiscoveredDevice {
/// The device's HTTP channels — the addresses it is reachable on — in
/// discovery order. A multi-homed device has one per address it was
/// discovered on.
pub fn http_channels(&self) -> impl Iterator<Item = &HttpChannel> {
// With more channel kinds this becomes a `filter_map`.
self.channels.iter().map(|channel| match channel {
DeviceChannel::Http(http) => http,
})
impl StatefulDevice {
pub fn get_best_channel(&self) -> Option<&DeviceChannel> {
self.get_ranked_channels().into_iter().next()
}
/// The device's first HTTP channel, when it has one.
pub fn get_ranked_channels(&self) -> Vec<&DeviceChannel> {
let mut channels: Vec<_> = self.channels.iter().collect();
channels.sort_by_key(|(channel, status)| {
std::cmp::Reverse((
**status == ChannelStatus::Available,
channel.is_ipv6(),
self.last_confirmed(channel),
))
});
channels.into_iter().map(|(channel, _)| channel).collect()
}
/// The position of the channel's most recent confirmation in the logs;
/// `None` when its confirmations have been dropped from the log cap.
fn last_confirmed(&self, channel: &DeviceChannel) -> Option<usize> {
self.logs.iter().rposition(|log| {
let (DiscoveryEvent::Discovered { device } | DiscoveryEvent::Updated { device }) =
&log.event;
device.channel == *channel
})
}
}
impl DiscoveredDevice {
/// The device's HTTP channel, when it was confirmed over HTTP.
pub fn http(&self) -> Option<&HttpChannel> {
self.http_channels().next()
self.channel.http()
}
}
@@ -77,13 +112,20 @@ impl DiscoveredDevice {
///
/// Only HTTP exists so far; other transports (e.g. WebRTC, Bluetooth) will
/// become further variants.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum DeviceChannel {
/// The device's HTTP server (protocol v2), reachable at one address.
Http(HttpChannel),
}
impl DeviceChannel {
/// The HTTP address, when this is an HTTP channel.
pub fn http(&self) -> Option<&HttpChannel> {
match self {
DeviceChannel::Http(http) => Some(http),
}
}
/// Whether two channels address the same endpoint, so that a
/// re-confirmation updates the known channel instead of adding one.
fn same_endpoint(&self, other: &DeviceChannel) -> bool {
@@ -91,10 +133,21 @@ impl DeviceChannel {
(DeviceChannel::Http(own), DeviceChannel::Http(other)) => own.host == other.host,
}
}
/// Whether the channel dials an IPv6 address.
fn is_ipv6(&self) -> bool {
match self {
DeviceChannel::Http(http) => {
// Strip the scope of a link-local address like `fe80::1%3`.
let host = http.host.split('%').next().unwrap_or(&http.host);
matches!(host.parse::<IpAddr>(), Ok(IpAddr::V6(_)))
}
}
}
}
/// The address of a device's HTTP server.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct HttpChannel {
/// The host to dial: an IP address, or the scoped form `fe80::1%3` for
/// link-local IPv6 (the HTTP client accepts both).
@@ -110,7 +163,7 @@ pub struct HttpChannel {
/// All devices discovered in this run, identified by fingerprint, in
/// discovery order.
pub(super) struct DeviceStore {
devices: Mutex<Vec<DiscoveredDeviceWithLogs>>,
devices: Mutex<Vec<StatefulDevice>>,
}
impl DeviceStore {
@@ -126,12 +179,13 @@ impl DeviceStore {
/// stored state after the merge.
///
/// Channels are merged by endpoint: a rediscovery over a known address
/// updates its channel in place, an unknown address adds one.
/// replaces its channel (and makes it available again), an unknown
/// address adds one.
pub(super) fn upsert(
&self,
device: DiscoveredDevice,
timestamp: SystemTime,
) -> (DiscoveryEvent, DiscoveredDeviceWithLogs) {
) -> (DiscoveryEvent, StatefulDevice) {
let mut devices = self.devices.lock().unwrap();
match devices
.iter_mut()
@@ -142,13 +196,12 @@ impl DeviceStore {
device: device.clone(),
};
let mut channels = std::mem::take(&mut known.device.channels);
for channel in device.channels {
match channels.iter_mut().find(|c| c.same_endpoint(&channel)) {
Some(known) => *known = channel,
None => channels.push(channel),
}
}
known
.channels
.retain(|channel, _| !channel.same_endpoint(&device.channel));
known
.channels
.insert(device.channel.clone(), ChannelStatus::Available);
known.logs.push(DeviceLog {
timestamp,
@@ -159,14 +212,15 @@ impl DeviceStore {
known.logs.drain(..excess);
}
known.device = DiscoveredDevice { channels, ..device };
known.device = device;
(event, known.clone())
}
None => {
let event = DiscoveryEvent::Discovered {
device: device.clone(),
};
let known = DiscoveredDeviceWithLogs {
let known = StatefulDevice {
channels: HashMap::from([(device.channel.clone(), ChannelStatus::Available)]),
device,
logs: vec![DeviceLog {
timestamp,
@@ -180,11 +234,11 @@ impl DeviceStore {
}
/// All discovered devices in discovery order.
pub(super) fn devices(&self) -> Vec<DiscoveredDeviceWithLogs> {
pub(super) fn devices(&self) -> Vec<StatefulDevice> {
self.devices.lock().unwrap().clone()
}
pub(super) fn by_fingerprint(&self, fingerprint: &str) -> Option<DiscoveredDeviceWithLogs> {
pub(super) fn by_fingerprint(&self, fingerprint: &str) -> Option<StatefulDevice> {
self.devices
.lock()
.unwrap()
@@ -205,15 +259,19 @@ mod tests {
device_model: None,
device_type: Some(DeviceType::Desktop),
fingerprint: fingerprint.to_string(),
channels: vec![DeviceChannel::Http(HttpChannel {
channel: DeviceChannel::Http(HttpChannel {
host: host.to_string(),
port: 53317,
protocol: ProtocolTypeV2::Https,
})],
}),
download: false,
}
}
fn channel_host(channel: &DeviceChannel) -> &str {
channel.http().unwrap().host.as_str()
}
/// The marker telling log entries apart: the host the logged snapshot
/// was confirmed on.
fn log_marker(log: &DeviceLog) -> &str {
@@ -282,64 +340,137 @@ mod tests {
let devices = store.devices();
assert_eq!(devices.len(), 1);
let hosts: Vec<&str> = devices[0]
.device
.http_channels()
.map(|http| http.host.as_str())
.collect();
let known = &devices[0];
assert!(
known
.channels
.values()
.all(|status| *status == ChannelStatus::Available),
"a confirmed channel must be available"
);
let mut hosts: Vec<&str> = known.channels.keys().map(channel_host).collect();
hosts.sort();
assert_eq!(
hosts,
["192.168.0.10", "fe80::1%3"],
"every address the device was confirmed on must be kept"
);
assert_eq!(
channel_host(&known.device.channel),
"fe80::1%3",
"the device must carry the channel of the last confirmation"
);
}
#[test]
fn test_upsert_updates_channel_of_known_endpoint_in_place() {
fn test_upsert_replaces_channel_of_known_endpoint() {
let store = DeviceStore::new();
store.upsert(device("a", "192.168.0.10"), SystemTime::now());
store.upsert(device("a", "fe80::1%3"), SystemTime::now());
let mut update = device("a", "192.168.0.10");
match &mut update.channels[0] {
match &mut update.channel {
DeviceChannel::Http(http) => http.port = 54000,
}
store.upsert(update, SystemTime::now());
let known = store.by_fingerprint("a").unwrap();
let channels: Vec<(&str, u16)> = known
.device
.http_channels()
.map(|http| (http.host.as_str(), http.port))
let mut channels: Vec<(&str, u16)> = known
.channels
.keys()
.map(|channel| {
let http = channel.http().unwrap();
(http.host.as_str(), http.port)
})
.collect();
channels.sort();
assert_eq!(
channels,
[("192.168.0.10", 54000), ("fe80::1%3", 53317)],
"a known address must be updated in place, not duplicated"
"a known address must be replaced, not duplicated"
);
}
#[test]
fn test_upsert_keeps_channels_missing_from_the_update() {
fn test_get_channel_prefers_the_most_recent_confirmation() {
let store = DeviceStore::new();
store.upsert(device("a", "192.168.0.10"), SystemTime::now());
store.upsert(device("a", "10.0.0.10"), SystemTime::now());
let known = store.by_fingerprint("a").unwrap();
assert_eq!(channel_host(known.get_best_channel().unwrap()), "10.0.0.10");
let mut update = device("a", "10.0.0.10");
update.channels.clear();
store.upsert(update, SystemTime::now());
store.upsert(device("a", "192.168.0.10"), SystemTime::now());
let known = store.by_fingerprint("a").unwrap();
assert_eq!(
store
.by_fingerprint("a")
.unwrap()
.device
.http()
.unwrap()
.host,
channel_host(known.get_best_channel().unwrap()),
"192.168.0.10",
"an update without an HTTP channel must not drop the known one"
"the latest confirmation must win among equals"
);
}
#[test]
fn test_get_channel_prefers_ipv6_over_ipv4() {
let store = DeviceStore::new();
store.upsert(device("a", "fe80::1%3"), SystemTime::now());
store.upsert(device("a", "192.168.0.10"), SystemTime::now());
let known = store.by_fingerprint("a").unwrap();
assert_eq!(
channel_host(known.get_best_channel().unwrap()),
"fe80::1%3",
"IPv6 must beat a more recent IPv4 confirmation"
);
}
#[test]
fn test_get_channel_prefers_available_channels() {
let store = DeviceStore::new();
store.upsert(device("a", "192.168.0.10"), SystemTime::now());
store.upsert(device("a", "fe80::1%3"), SystemTime::now());
let mut known = store.by_fingerprint("a").unwrap();
for (channel, status) in known.channels.iter_mut() {
if channel_host(channel) == "fe80::1%3" {
*status = ChannelStatus::NotReachable;
}
}
assert_eq!(
channel_host(known.get_best_channel().unwrap()),
"192.168.0.10",
"an available IPv4 channel must beat a not-reachable IPv6 one"
);
}
#[test]
fn test_get_channels_sorts_best_first() {
let store = DeviceStore::new();
store.upsert(device("a", "192.168.0.10"), SystemTime::now());
store.upsert(device("a", "10.0.0.10"), SystemTime::now());
store.upsert(device("a", "fe80::1%3"), SystemTime::now());
let mut known = store.by_fingerprint("a").unwrap();
let hosts: Vec<&str> = known.get_ranked_channels().into_iter().map(channel_host).collect();
assert_eq!(
hosts,
["fe80::1%3", "10.0.0.10", "192.168.0.10"],
"IPv6 must come first, then the more recent IPv4 confirmation"
);
for (channel, status) in known.channels.iter_mut() {
if channel_host(channel) == "fe80::1%3" {
*status = ChannelStatus::NotReachable;
}
}
let hosts: Vec<&str> = known.get_ranked_channels().into_iter().map(channel_host).collect();
assert_eq!(
hosts,
["10.0.0.10", "192.168.0.10", "fe80::1%3"],
"a not-reachable channel must sort last"
);
}
+1 -1
View File
@@ -14,7 +14,7 @@ pub enum DeviceType {
}
/// Protocol type for HTTP or HTTPS connections.
#[derive(Clone, Copy, Debug, Deserialize, Eq, Serialize, PartialEq)]
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Serialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ProtocolTypeV2 {
Http,
+68 -5
View File
@@ -15,7 +15,7 @@ use localsend::discovery::{
use localsend::http::server::{start_with_port, ServerConfigV2, TlsConfig};
use localsend::http::state::ClientInfo;
use localsend::model::discovery::{DeviceType, ProtocolTypeV2, PROTOCOL_VERSION_V2};
use localsend::multicast::MulticastDevice;
use localsend::multicast::{InterfaceFilter, MulticastDevice};
use std::net::{Ipv4Addr, Ipv6Addr};
use std::sync::atomic::{AtomicU16, Ordering};
use std::time::Duration;
@@ -161,8 +161,10 @@ async fn start_instance(
},
stop_rx,
)
.await
.ok()?;
.await;
if handle.multicast_error().is_some() {
return None;
}
Some(TestInstance {
fingerprint: cert.fingerprint,
@@ -296,8 +298,9 @@ async fn test_subnet_scan_finds_device_on_loopback() {
assert_eq!(stored.device.alias, "ScanTarget");
assert!(
stored
.device
.http_channels()
.channels
.keys()
.filter_map(|channel| channel.http())
.all(|http| http.host != "127.0.0.99"),
"the interface address itself must not be probed"
);
@@ -353,6 +356,66 @@ async fn test_targeted_discovery_reads_fingerprint_from_certificate_on_https() {
.is_none());
}
#[tokio::test]
async fn test_discovery_works_without_multicast() {
let server_port = free_port();
let _server_stop =
start_register_server(server_port, "Target", "target-fingerprint", None).await;
// An interface whitelist matching no interface makes the multicast side
// fail deterministically.
let cert = generate_self_signed().expect("Failed to generate an identity");
let (stop_tx, stop_rx) = oneshot::channel();
let handle = discovery::start(
DiscoveryConfig {
group: TEST_GROUP,
group_v6: Some(TEST_GROUP_V6),
port: NEXT_MULTICAST_PORT.fetch_add(1, Ordering::Relaxed),
interface_filter: InterfaceFilter {
whitelist: Some(vec!["203.0.113.1".to_string()]),
blacklist: None,
},
device: MulticastDevice {
alias: "NoMulticast".to_string(),
version: PROTOCOL_VERSION_V2.to_string(),
device_model: Some("Rust".to_string()),
device_type: Some(DeviceType::Headless),
fingerprint: cert.fingerprint.clone(),
port: free_port(),
protocol: ProtocolTypeV2::Http,
download: false,
},
identity: DeviceIdentity {
cert_pem: cert.certificate_pem,
private_key_pem: cert.private_key_pem,
},
timeout: discovery::DEFAULT_DISCOVERY_TIMEOUT,
event_tx: None,
},
stop_rx,
)
.await;
assert!(
handle.multicast_error().is_some(),
"no interface matches the whitelist, so multicast must be unavailable"
);
// Announcing is a no-op, the active operations still work.
handle.announce().await;
let device = handle
.discover("127.0.0.1", server_port, ProtocolTypeV2::Http)
.await
.expect("Targeted discovery failed")
.expect("The target must not be mistaken for the device itself");
assert_eq!(device.device.alias, "Target");
assert!(handle.device_by_fingerprint("target-fingerprint").is_some());
// Stopping must not hang without multicast sockets to close.
drop(stop_tx);
handle.wait_stopped().await;
}
#[tokio::test]
async fn test_announcement_is_answered_and_device_stored() {
let multicast_port = NEXT_MULTICAST_PORT.fetch_add(1, Ordering::Relaxed);
+192 -149
View File
@@ -25,9 +25,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.100"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "autocfg"
@@ -42,7 +42,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
dependencies = [
"axum-core",
"base64",
"base64 0.22.1",
"bytes",
"form_urlencoded",
"futures-util",
@@ -62,7 +62,7 @@ dependencies = [
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sha1",
"sha1 0.10.6",
"sync_wrapper",
"tokio",
"tokio-tungstenite",
@@ -98,6 +98,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9"
[[package]]
name = "bitflags"
version = "2.6.0"
@@ -113,6 +119,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block-buffer"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
dependencies = [
"hybrid-array",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
@@ -127,9 +142,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.11.0"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cc"
@@ -146,6 +161,17 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chacha20"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"rand_core 0.10.1",
]
[[package]]
name = "chrono"
version = "0.4.39"
@@ -160,6 +186,12 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -175,6 +207,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "croner"
version = "2.1.0"
@@ -194,6 +235,15 @@ dependencies = [
"typenum",
]
[[package]]
name = "crypto-common"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
dependencies = [
"hybrid-array",
]
[[package]]
name = "data-encoding"
version = "2.6.0"
@@ -206,8 +256,19 @@ version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"block-buffer 0.10.4",
"crypto-common 0.1.6",
]
[[package]]
name = "digest"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [
"block-buffer 0.12.1",
"const-oid",
"crypto-common 0.2.2",
]
[[package]]
@@ -248,45 +309,44 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.31"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
[[package]]
name = "futures-macro"
version = "0.3.31"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.95",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307"
[[package]]
name = "futures-task"
version = "0.3.31"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
[[package]]
name = "futures-util"
version = "0.3.31"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
dependencies = [
"futures-core",
"futures-macro",
"futures-sink",
"futures-task",
"pin-project-lite",
"pin-utils",
"slab",
]
@@ -308,26 +368,26 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasi",
]
[[package]]
name = "getrandom"
version = "0.3.3"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
"rand_core 0.10.1",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
dependencies = [
"allocator-api2",
"equivalent",
@@ -380,6 +440,15 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hybrid-array"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b"
dependencies = [
"typenum",
]
[[package]]
name = "hyper"
version = "1.5.2"
@@ -462,20 +531,20 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.175"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "localsend"
version = "0.1.0"
dependencies = [
"anyhow",
"base64",
"base64 0.23.0",
"bytes",
"futures-util",
"lru",
"rand 0.9.2",
"rand 0.10.2",
"serde",
"serde_json",
"thiserror",
@@ -483,7 +552,7 @@ dependencies = [
"tokio-stream",
"tracing",
"tracing-subscriber",
"tungstenite 0.28.0",
"tungstenite 0.30.0",
"uuid",
]
@@ -505,9 +574,9 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "lru"
version = "0.16.3"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6"
dependencies = [
"hashbrown",
]
@@ -532,13 +601,13 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mio"
version = "1.0.3"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
"wasi",
"windows-sys",
]
[[package]]
@@ -547,7 +616,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.59.0",
"windows-sys",
]
[[package]]
@@ -558,7 +627,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.95",
]
[[package]]
@@ -611,12 +680,6 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
@@ -646,9 +709,9 @@ dependencies = [
[[package]]
name = "r-efi"
version = "5.3.0"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
@@ -657,18 +720,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_chacha",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.2"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
"chacha20",
"getrandom 0.4.3",
"rand_core 0.10.1",
]
[[package]]
@@ -681,16 +745,6 @@ dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.3",
]
[[package]]
name = "rand_core"
version = "0.6.4"
@@ -702,12 +756,9 @@ dependencies = [
[[package]]
name = "rand_core"
version = "0.9.3"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.3",
]
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "redox_syscall"
@@ -738,9 +789,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.228"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
@@ -748,34 +799,35 @@ dependencies = [
[[package]]
name = "serde_core"
version = "1.0.228"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 3.0.3",
]
[[package]]
name = "serde_json"
version = "1.0.143"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
"zmij",
]
[[package]]
@@ -806,7 +858,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"axum",
"base64",
"base64 0.22.1",
"futures-util",
"localsend",
"serde",
@@ -825,8 +877,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
"cpufeatures 0.2.16",
"digest 0.10.7",
]
[[package]]
name = "sha1"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"digest 0.11.3",
]
[[package]]
@@ -870,12 +933,12 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "socket2"
version = "0.6.0"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys",
]
[[package]]
@@ -889,6 +952,17 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "1.0.2"
@@ -897,22 +971,22 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "thiserror"
version = "2.0.18"
version = "2.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
version = "2.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 3.0.3",
]
[[package]]
@@ -927,9 +1001,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.49.0"
version = "1.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
dependencies = [
"bytes",
"libc",
@@ -939,7 +1013,7 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
"windows-sys",
]
[[package]]
@@ -959,20 +1033,20 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.6.0"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 3.0.3",
]
[[package]]
name = "tokio-stream"
version = "0.1.18"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"
dependencies = [
"futures-core",
"pin-project-lite",
@@ -1039,7 +1113,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.95",
]
[[package]]
@@ -1065,9 +1139,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.22"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"nu-ansi-term",
"sharded-slab",
@@ -1090,33 +1164,32 @@ dependencies = [
"httparse",
"log",
"rand 0.8.5",
"sha1",
"sha1 0.10.6",
"thiserror",
"utf-8",
]
[[package]]
name = "tungstenite"
version = "0.28.0"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442"
checksum = "e48ac77174b19c110a50ab2128b24215ac9cb40e0e12e093fb602d175c569d22"
dependencies = [
"bytes",
"data-encoding",
"http",
"httparse",
"log",
"rand 0.9.2",
"sha1",
"rand 0.10.2",
"sha1 0.11.0",
"thiserror",
"utf-8",
]
[[package]]
name = "typenum"
version = "1.17.0"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unicode-ident"
@@ -1132,11 +1205,11 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "uuid"
version = "1.20.0"
version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
dependencies = [
"getrandom 0.3.3",
"getrandom 0.4.3",
"js-sys",
"serde_core",
"wasm-bindgen",
@@ -1160,15 +1233,6 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
@@ -1191,7 +1255,7 @@ dependencies = [
"log",
"proc-macro2",
"quote",
"syn",
"syn 2.0.95",
"wasm-bindgen-shared",
]
@@ -1213,7 +1277,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.95",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -1242,24 +1306,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -1333,15 +1379,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]
[[package]]
name = "zerocopy"
version = "0.7.35"
@@ -1360,5 +1397,11 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.95",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"