From 2952b596e2549a7cc67ecd70ebdf5163596be140 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Fri, 14 Nov 2025 02:16:53 +0100 Subject: [PATCH] peers gone... --- crates/lanspread-peer/src/lib.rs | 109 ++++++++++++++++-- .../src-tauri/src/lib.rs | 91 +++++++++------ crates/lanspread-tauri-deno-ts/src/App.tsx | 26 +++++ 3 files changed, 181 insertions(+), 45 deletions(-) diff --git a/crates/lanspread-peer/src/lib.rs b/crates/lanspread-peer/src/lib.rs index 25d298e..ab660b8 100644 --- a/crates/lanspread-peer/src/lib.rs +++ b/crates/lanspread-peer/src/lib.rs @@ -33,6 +33,7 @@ use tokio::{ RwLock, mpsc::{UnboundedReceiver, UnboundedSender}, }, + task::JoinHandle, }; use uuid::Uuid; @@ -126,6 +127,9 @@ pub enum PeerEvent { DownloadGameFilesFailed { id: String, }, + DownloadGameFilesAllPeersGone { + id: String, + }, NoPeersHaveGame { id: String, }, @@ -1309,6 +1313,7 @@ struct Ctx { peer_game_db: Arc>, local_peer_addr: Arc>>, downloading_games: Arc>>, + active_downloads: Arc>>>, } #[derive(Clone)] @@ -1347,6 +1352,7 @@ pub async fn run_peer( peer_game_db: Arc::new(RwLock::new(PeerGameDB::new())), local_peer_addr: Arc::new(RwLock::new(None)), downloading_games: Arc::new(RwLock::new(HashSet::new())), + active_downloads: Arc::new(RwLock::new(HashMap::new())), }; let peer_ctx = PeerCtx { @@ -1384,8 +1390,16 @@ pub async fn run_peer( // Start ping service task let tx_notify_ui_ping = tx_notify_ui.clone(); let peer_game_db_ping = ctx.peer_game_db.clone(); + let downloading_games_ping = ctx.downloading_games.clone(); + let active_downloads_ping = ctx.active_downloads.clone(); tokio::spawn(async move { - run_ping_service(tx_notify_ui_ping, peer_game_db_ping).await; + run_ping_service( + tx_notify_ui_ping, + peer_game_db_ping, + downloading_games_ping, + active_downloads_ping, + ) + .await; }); // Start local game directory monitoring task @@ -1686,32 +1700,39 @@ async fn handle_download_game_files_command( } let downloading_games = ctx.downloading_games.clone(); - let tx_notify_ui = tx_notify_ui.clone(); - tokio::spawn(async move { + let active_downloads = ctx.active_downloads.clone(); + let tx_notify_ui_clone = tx_notify_ui.clone(); + let download_id = id.clone(); + + let handle = tokio::spawn(async move { let result = download_game_files( - &id, + &download_id, resolved_descriptions, games_folder, peer_whitelist, file_peer_map, - tx_notify_ui.clone(), + tx_notify_ui_clone.clone(), ) .await; { let mut guard = downloading_games.write().await; - guard.remove(&id); + guard.remove(&download_id); } if let Err(e) = result { - log::error!("Download failed for {id}: {e}"); - if let Err(send_err) = - tx_notify_ui.send(PeerEvent::DownloadGameFilesFailed { id: id.clone() }) - { + log::error!("Download failed for {download_id}: {e}"); + if let Err(send_err) = tx_notify_ui_clone.send(PeerEvent::DownloadGameFilesFailed { + id: download_id.clone(), + }) { log::error!("Failed to send DownloadGameFilesFailed event: {send_err}"); } } + + let _ = active_downloads.write().await.remove(&download_id); }); + + ctx.active_downloads.write().await.insert(id, handle); } async fn handle_set_game_dir_command(ctx: &Ctx, game_dir: String) { @@ -2155,9 +2176,54 @@ async fn request_game_details_from_peer( } } +async fn handle_active_downloads_without_peers( + peer_game_db: &Arc>, + downloading_games: &Arc>>, + active_downloads: &Arc>>>, + tx_notify_ui: &UnboundedSender, +) { + let active_ids: Vec = { downloading_games.read().await.iter().cloned().collect() }; + if active_ids.is_empty() { + return; + } + + for id in active_ids { + let has_peers = { + let guard = peer_game_db.read().await; + !guard.peers_with_game(&id).is_empty() + }; + + if has_peers { + continue; + } + + let removed_from_tracking = { + let mut guard = downloading_games.write().await; + guard.remove(&id) + }; + + if !removed_from_tracking { + continue; + } + + if let Some(handle) = { active_downloads.write().await.remove(&id) } { + handle.abort(); + } + + if let Err(e) = + tx_notify_ui.send(PeerEvent::DownloadGameFilesAllPeersGone { id: id.clone() }) + { + log::error!("Failed to send DownloadGameFilesAllPeersGone event: {e}"); + } + } +} + +#[allow(clippy::too_many_lines)] async fn run_ping_service( tx_notify_ui: UnboundedSender, peer_game_db: Arc>, + downloading_games: Arc>>, + active_downloads: Arc>>>, ) { log::info!( "Starting ping service ({PEER_PING_INTERVAL_SECS}s interval, \ @@ -2174,6 +2240,8 @@ async fn run_ping_service( for peer_addr in peer_addresses { let tx_notify_ui_clone = tx_notify_ui.clone(); let peer_game_db_clone = peer_game_db.clone(); + let downloading_games_clone = downloading_games.clone(); + let active_downloads_clone = active_downloads.clone(); tokio::spawn(async move { match ping_peer(peer_addr).await { @@ -2208,6 +2276,13 @@ async fn run_ping_service( } emit_peer_game_list(&peer_game_db_clone, &tx_notify_ui_clone).await; + handle_active_downloads_without_peers( + &peer_game_db_clone, + &downloading_games_clone, + &active_downloads_clone, + &tx_notify_ui_clone, + ) + .await; } } } @@ -2233,6 +2308,13 @@ async fn run_ping_service( } emit_peer_game_list(&peer_game_db_clone, &tx_notify_ui_clone).await; + handle_active_downloads_without_peers( + &peer_game_db_clone, + &downloading_games_clone, + &active_downloads_clone, + &tx_notify_ui_clone, + ) + .await; } } } @@ -2266,6 +2348,13 @@ async fn run_ping_service( if removed_any { emit_peer_game_list(&peer_game_db, &tx_notify_ui).await; + handle_active_downloads_without_peers( + &peer_game_db, + &downloading_games, + &active_downloads, + &tx_notify_ui, + ) + .await; } } } diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs b/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs index 5b94374..d2ec1c6 100644 --- a/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs +++ b/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs @@ -148,6 +148,47 @@ fn cleanup_backup_folder(backup_path: &Path) -> eyre::Result<()> { Ok(()) } +async fn cleanup_failed_download(app_handle: &AppHandle, id: &str) { + app_handle + .state::() + .inner() + .games_in_download + .write() + .await + .remove(id); + + let games_folder = app_handle + .state::() + .inner() + .games_folder + .read() + .await + .clone(); + + if games_folder.is_empty() { + return; + } + + let backup_name = format!("___TO_BE_DELETE___{id}"); + let backup_path = PathBuf::from(&games_folder).join(&backup_name); + let game_path = PathBuf::from(&games_folder).join(id); + + if game_path.exists() { + if let Err(e) = std::fs::remove_dir_all(&game_path) { + log::error!("Failed to delete half-downloaded game folder: {e}"); + } else { + log::info!( + "Deleted half-downloaded game folder: {}", + game_path.display() + ); + } + } + + if let Err(e) = restore_game_folder(&game_path, &backup_path) { + log::error!("Failed to restore backup after download failure: {e}"); + } +} + #[tauri::command] async fn update_game(id: String, state: tauri::State<'_, LanSpreadState>) -> tauri::Result { let games_in_download = state.inner().games_in_download.clone(); @@ -850,43 +891,23 @@ pub fn run() { log::error!("Failed to emit game-download-failed event: {e}"); } - app_handle - .state::() - .inner() - .games_in_download - .write() - .await - .remove(&id.clone()); + cleanup_failed_download(&app_handle, &id).await; + } + PeerEvent::DownloadGameFilesAllPeersGone { id } => { + log::warn!( + "PeerEvent::DownloadGameFilesAllPeersGone received for {id}" + ); - // Check if a backup exists and restore from backup if failed - // The backup folder name follows the pattern: ___TO_BE_DELETE___{game_name} - let games_folder = app_handle - .state::() - .inner() - .games_folder - .read() - .await - .clone(); - - if !games_folder.is_empty() { - let backup_name = format!("___TO_BE_DELETE___{id}"); - let backup_path = PathBuf::from(&games_folder).join(backup_name); - let game_path = PathBuf::from(&games_folder).join(&id); - - // Delete the half-downloaded game folder if it exists - if game_path.exists() { - if let Err(e) = std::fs::remove_dir_all(&game_path) { - log::error!("Failed to delete half-downloaded game folder: {e}"); - } else { - log::info!("Deleted half-downloaded game folder: {}", game_path.display()); - } - } - - // Restore the backup - if let Err(e) = restore_game_folder(&game_path, &backup_path) { - log::error!("Failed to restore backup after download failure: {e}"); - } + if let Err(e) = app_handle.emit( + "game-download-peers-gone", + Some(id.clone()), + ) { + log::error!( + "Failed to emit game-download-peers-gone event: {e}" + ); } + + cleanup_failed_download(&app_handle, &id).await; } PeerEvent::PeerConnected(addr) => { log::info!("Peer connected: {addr}"); diff --git a/crates/lanspread-tauri-deno-ts/src/App.tsx b/crates/lanspread-tauri-deno-ts/src/App.tsx index cc3f5c2..0d846d3 100644 --- a/crates/lanspread-tauri-deno-ts/src/App.tsx +++ b/crates/lanspread-tauri-deno-ts/src/App.tsx @@ -136,6 +136,31 @@ const App = () => { return unlisten; }; + const setupPeersGoneListener = async () => { + const unlisten = await listen('game-download-peers-gone', (event) => { + const game_id = event.payload as string; + console.log(`❌ game-download-peers-gone ${game_id} event received`); + clearCheckingPeersTimeout(game_id); + setGameItems(prev => prev.map(item => item.id === game_id + ? { + ...item, + install_status: item.installed ? InstallStatus.Installed : InstallStatus.NotInstalled, + status_message: 'Failed: All Peers gone', + status_level: 'error', + } + : item)); + + const pathString = String(gameDir); + if (!pathString) { + console.error('gameDir is empty before invoke!'); + return; + } + invoke('update_game_directory', { path: pathString }) + .catch(error => console.error('❌ Error updating game directory:', error)); + }); + return unlisten; + }; + const setupNoPeersListener = async () => { const unlisten = await listen('game-no-peers', (event) => { const game_id = event.payload as string; @@ -154,6 +179,7 @@ const App = () => { }; setupDownloadFailedListener(); + setupPeersGoneListener(); setupNoPeersListener(); const setupPeerCountListener = async () => {