Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions apps/desktop/src-tauri/src/peer_lan/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
Expand Down Expand Up @@ -31,7 +33,7 @@ pub async fn probe_lan_devices(target_ids: Vec<String>) -> Result<Vec<LanDeviceP
.browse(SERVICE_TYPE)
.map_err(|e| format!("mDNS browse: {e}"))?;

let deadline = std::time::Instant::now() + Duration::from_secs(3);
let deadline = std::time::Instant::now() + Duration::from_secs(5);

while std::time::Instant::now() < deadline {
match receiver.recv_timeout(Duration::from_millis(250)) {
Expand All @@ -41,7 +43,7 @@ pub async fn probe_lan_devices(target_ids: Vec<String>) -> Result<Vec<LanDeviceP
if device_id.is_empty() || !wanted.contains(&device_id) {
continue;
}
let host = info.get_hostname().trim_end_matches('.').to_string();
let host = resolve_lan_host(&info);
let port = info.get_port();
found.insert(
device_id.clone(),
Expand Down Expand Up @@ -84,3 +86,17 @@ fn txt_get(info: &mdns_sd::ServiceInfo, key: &str) -> 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()
}
55 changes: 55 additions & 0 deletions apps/desktop/src-tauri/src/peer_lan/mdns_registry.rs
Original file line number Diff line number Diff line change
@@ -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<Mutex<Option<mdns_sd::ServiceDaemon>>> =
Lazy::new(|| Mutex::new(None));

fn daemon() -> Result<std::sync::MutexGuard<'static, Option<mdns_sd::ServiceDaemon>>, 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();
}
}
}
3 changes: 3 additions & 0 deletions apps/desktop/src-tauri/src/peer_lan/mod.rs
Original file line number Diff line number Diff line change
@@ -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};
105 changes: 105 additions & 0 deletions apps/desktop/src-tauri/src/peer_lan/presence.rs
Original file line number Diff line number Diff line change
@@ -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<Mutex<Option<PresenceState>>> = Lazy::new(|| Mutex::new(None));

struct PresenceState {
port: u16,
handle: tokio::task::JoinHandle<()>,
}

async fn start_presence_server() -> Result<u16, String> {
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;
}
});
}
36 changes: 7 additions & 29 deletions apps/desktop/src-tauri/src/peer_lan/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,10 +33,10 @@ static ACTIVE_SERVER: once_cell::sync::Lazy<std::sync::Mutex<Option<tokio::task:
once_cell::sync::Lazy::new(|| std::sync::Mutex::new(None));

pub async fn stop_lan_server() {
if let Ok(mut guard) = ACTIVE_SERVER.lock() {
if let Some(handle) = guard.take() {
handle.abort();
}
let handle = ACTIVE_SERVER.lock().ok().and_then(|mut guard| guard.take());
if let Some(handle) = handle {
handle.abort();
republish_presence_after_transfer().await;
}
}

Expand Down Expand Up @@ -102,7 +104,7 @@ pub async fn start_lan_server_for_session(token: &str, game_key: &str) -> 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 {
Expand All @@ -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<Response, StatusCode> {
if request.uri().path() == "/health" {
return Ok(next.run(request).await);
Expand Down
5 changes: 4 additions & 1 deletion apps/desktop/src-tauri/src/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -224,7 +226,8 @@ pub fn init_states_and_background_tasks(app: &mut App) -> Result<(), Box<dyn std
// 8. Controlador de gamepads (no necesita guard: es stateless)
start_gamepad_loop(app.handle().clone());

crate::peer_lan::spawn_pending_session_poller();
spawn_pending_session_poller();
spawn_lan_presence_advertiser();

Ok(())
}