diff --git a/apps/desktop/src-tauri/src/peer_lan/discovery.rs b/apps/desktop/src-tauri/src/peer_lan/discovery.rs index 29050b22..e4b17f71 100644 --- a/apps/desktop/src-tauri/src/peer_lan/discovery.rs +++ b/apps/desktop/src-tauri/src/peer_lan/discovery.rs @@ -3,6 +3,8 @@ use std::collections::HashMap; use std::time::Duration; +use std::net::IpAddr; + use serde::Serialize; const SERVICE_TYPE: &str = "_savecloud._tcp.local."; @@ -31,7 +33,7 @@ pub async fn probe_lan_devices(target_ids: Vec) -> Result) -> Result String { .map(|bytes| String::from_utf8_lossy(bytes).into_owned()) .unwrap_or_default() } + +fn resolve_lan_host(info: &mdns_sd::ServiceInfo) -> String { + for ip in info.get_addresses() { + if let IpAddr::V4(v4) = ip { + return v4.to_string(); + } + } + for ip in info.get_addresses() { + if let IpAddr::V6(v6) = ip { + return format!("[{v6}]"); + } + } + info.get_hostname().trim_end_matches('.').to_string() +} diff --git a/apps/desktop/src-tauri/src/peer_lan/mdns_registry.rs b/apps/desktop/src-tauri/src/peer_lan/mdns_registry.rs new file mode 100644 index 00000000..ced99ec1 --- /dev/null +++ b/apps/desktop/src-tauri/src/peer_lan/mdns_registry.rs @@ -0,0 +1,55 @@ +//! Registro mDNS persistente (`_savecloud._tcp`). + +use std::sync::Mutex; + +use once_cell::sync::Lazy; + +const SERVICE_TYPE: &str = "_savecloud._tcp.local."; + +static MDNS_DAEMON: Lazy>> = + Lazy::new(|| Mutex::new(None)); + +fn daemon() -> Result>, String> { + MDNS_DAEMON + .lock() + .map_err(|e| format!("mDNS lock: {e}")) +} + +/// Publica (o actualiza) este dispositivo en la LAN. +pub fn publish_lan_service(device_id: &str, user_id: &str, port: u16) -> Result<(), String> { + let mut guard = daemon()?; + if guard.is_none() { + *guard = Some(mdns_sd::ServiceDaemon::new().map_err(|e| format!("mDNS daemon: {e}"))?); + } + let daemon = guard.as_ref().expect("daemon just set"); + + let host = gethostname::gethostname().to_string_lossy().into_owned(); + let instance = format!("savecloud-{device_id}"); + let mut properties = std::collections::HashMap::new(); + properties.insert("deviceId".to_string(), device_id.to_string()); + properties.insert("userId".to_string(), user_id.to_string()); + + let info = mdns_sd::ServiceInfo::new( + SERVICE_TYPE, + &instance, + &format!("{host}.local."), + "", + port, + properties, + ) + .map_err(|e| format!("mDNS ServiceInfo: {e}"))?; + + daemon + .register(info) + .map_err(|e| format!("mDNS register: {e}"))?; + Ok(()) +} + +/// Retira el anuncio mDNS de este dispositivo. +pub fn withdraw_lan_service() { + if let Ok(mut guard) = daemon() { + if let Some(daemon) = guard.take() { + let _ = daemon.shutdown(); + } + } +} diff --git a/apps/desktop/src-tauri/src/peer_lan/mod.rs b/apps/desktop/src-tauri/src/peer_lan/mod.rs index 746ffb8c..a05061d0 100644 --- a/apps/desktop/src-tauri/src/peer_lan/mod.rs +++ b/apps/desktop/src-tauri/src/peer_lan/mod.rs @@ -1,11 +1,14 @@ //! Transferencia LAN entre peers (mDNS + servidor HTTP + descarga streaming). pub mod discovery; +mod mdns_registry; mod poller; +mod presence; pub mod runner; pub mod server; pub mod session; pub use discovery::{probe_lan_devices, LanDeviceProbe}; pub use poller::{poll_and_serve_pending_sessions, spawn_pending_session_poller}; +pub use presence::spawn_lan_presence_advertiser; pub use runner::{run_peer_download, PeerDownloadParams}; diff --git a/apps/desktop/src-tauri/src/peer_lan/presence.rs b/apps/desktop/src-tauri/src/peer_lan/presence.rs new file mode 100644 index 00000000..af6a259d --- /dev/null +++ b/apps/desktop/src-tauri/src/peer_lan/presence.rs @@ -0,0 +1,105 @@ +//! Presencia LAN: anuncia el dispositivo en mDNS mientras comparte inventario. + +use std::sync::Mutex; +use std::time::Duration; + +use axum::{routing::get, Router}; +use once_cell::sync::Lazy; + +use crate::commands::sync::context::resolve_api_context; +use crate::config::load_settings; +use crate::peer_inventory::resolve_device_id; +use crate::peer_lan::mdns_registry::{publish_lan_service, withdraw_lan_service}; + +static PRESENCE: Lazy>> = Lazy::new(|| Mutex::new(None)); + +struct PresenceState { + port: u16, + handle: tokio::task::JoinHandle<()>, +} + +async fn start_presence_server() -> Result { + let app = Router::new().route("/health", get(|| async { "ok" })); + let listener = tokio::net::TcpListener::bind("0.0.0.0:0") + .await + .map_err(|e| format!("Presencia LAN: {e}"))?; + let port = listener.local_addr().map_err(|e| e.to_string())?.port(); + let handle = tokio::spawn(async move { + if let Err(e) = axum::serve(listener, app).await { + log::warn!("Servidor presencia LAN finalizado: {e}"); + } + }); + if let Ok(mut guard) = PRESENCE.lock() { + *guard = Some(PresenceState { port, handle }); + } + Ok(port) +} + +fn stop_presence_server() { + if let Ok(mut guard) = PRESENCE.lock() { + if let Some(state) = guard.take() { + state.handle.abort(); + } + } +} + +async fn refresh_presence_advertisement() { + let settings = load_settings(); + if !settings.share_game_inventory_with_cloud { + stop_presence_server(); + withdraw_lan_service(); + return; + } + + let Ok(device_id) = resolve_device_id() else { + return; + }; + let Ok(ctx) = resolve_api_context() else { + return; + }; + + let port = { + let needs_start = PRESENCE + .lock() + .ok() + .map(|g| g.is_none()) + .unwrap_or(true); + if needs_start { + match start_presence_server().await { + Ok(p) => p, + Err(e) => { + log::warn!("No se pudo iniciar presencia LAN: {e}"); + return; + } + } + } else { + PRESENCE + .lock() + .ok() + .and_then(|g| g.as_ref().map(|s| s.port)) + .unwrap_or(0) + } + }; + + if port == 0 { + return; + } + + if let Err(e) = publish_lan_service(&device_id, &ctx.user_id, port) { + log::warn!("mDNS presencia: {e}"); + } +} + +/// Vuelve a anunciar el puerto de presencia tras cerrar el servidor de transferencia. +pub async fn republish_presence_after_transfer() { + refresh_presence_advertisement().await; +} + +pub fn spawn_lan_presence_advertiser() { + tauri::async_runtime::spawn(async { + loop { + refresh_presence_advertisement().await; + tokio::time::sleep(Duration::from_secs(10)).await; + } + }); +} diff --git a/apps/desktop/src-tauri/src/peer_lan/server.rs b/apps/desktop/src-tauri/src/peer_lan/server.rs index efa3d0ad..2e1215e6 100644 --- a/apps/desktop/src-tauri/src/peer_lan/server.rs +++ b/apps/desktop/src-tauri/src/peer_lan/server.rs @@ -17,6 +17,8 @@ use tokio::io::AsyncSeekExt; use tokio_util::io::ReaderStream; use crate::peer_inventory::{load_local_manifest, resolve_install_root}; +use crate::peer_lan::mdns_registry::publish_lan_service; +use crate::peer_lan::presence::republish_presence_after_transfer; use crate::peer_lan::session::peek_valid_session; const CHUNK_SIZE: usize = 512 * 1024; @@ -31,10 +33,10 @@ static ACTIVE_SERVER: once_cell::sync::Lazy Result .map_err(|e| format!("No se pudo abrir puerto LAN: {e}"))?; let port = listener.local_addr().map_err(|e| e.to_string())?.port(); - register_mdns_service(&manifest.device_id, &manifest.user_id, port)?; + publish_lan_service(&manifest.device_id, &manifest.user_id, port)?; let handle = tokio::spawn(async move { if let Err(e) = axum::serve(listener, app).await { @@ -117,30 +119,6 @@ pub async fn start_lan_server_for_session(token: &str, game_key: &str) -> Result Ok(port) } -fn register_mdns_service(device_id: &str, user_id: &str, port: u16) -> Result<(), String> { - let service = mdns_sd::ServiceDaemon::new().map_err(|e| format!("mDNS: {e}"))?; - let host = gethostname::gethostname().to_string_lossy().into_owned(); - let instance = format!("savecloud-{device_id}"); - let mut properties = std::collections::HashMap::new(); - properties.insert("deviceId".to_string(), device_id.to_string()); - properties.insert("userId".to_string(), user_id.to_string()); - - let info = mdns_sd::ServiceInfo::new( - "_savecloud._tcp.local.", - &instance, - &format!("{host}.local."), - "", - port, - properties, - ) - .map_err(|e| format!("mDNS ServiceInfo: {e}"))?; - - service - .register(info) - .map_err(|e| format!("mDNS register: {e}"))?; - Ok(()) -} - async fn auth_middleware(request: Request, next: Next) -> Result { if request.uri().path() == "/health" { return Ok(next.run(request).await); diff --git a/apps/desktop/src-tauri/src/setup.rs b/apps/desktop/src-tauri/src/setup.rs index 6ab0d094..52e1cedf 100644 --- a/apps/desktop/src-tauri/src/setup.rs +++ b/apps/desktop/src-tauri/src/setup.rs @@ -20,6 +20,8 @@ use crate::system::process_check; use crate::torrent::{engine::TorrentEngine, state::TorrentState}; use crate::tray::tray_state::TrayState; use crate::voice::VoiceState; +use crate::peer_lan::spawn_lan_presence_advertiser; +use crate::peer_lan::spawn_pending_session_poller; use std::sync::Arc; use tauri::{App, Manager}; use tokio::sync::Mutex; @@ -224,7 +226,8 @@ pub fn init_states_and_background_tasks(app: &mut App) -> Result<(), Box