diff --git a/crates/lanspread-peer/src/handlers.rs b/crates/lanspread-peer/src/handlers.rs index 7da859b..055ced3 100644 --- a/crates/lanspread-peer/src/handlers.rs +++ b/crates/lanspread-peer/src/handlers.rs @@ -508,28 +508,33 @@ async fn run_started_install_operation( } } }; - end_operation(ctx, tx_notify_ui, &id).await; - operation_guard.disarm(); match result { Ok(()) => { + if let Err(err) = refresh_local_game_for_ending_operation(ctx, tx_notify_ui, &id).await + { + log::error!("Failed to refresh local library after install: {err}"); + } + end_operation(ctx, tx_notify_ui, &id).await; + operation_guard.disarm(); events::send( tx_notify_ui, PeerEvent::InstallGameFinished { id: id.clone() }, ); - if let Err(err) = refresh_local_game(ctx, tx_notify_ui, &id).await { - log::error!("Failed to refresh local library after install: {err}"); - } } Err(err) => { log::error!("Install operation failed for {id}: {err}"); + if let Err(refresh_err) = + refresh_local_game_for_ending_operation(ctx, tx_notify_ui, &id).await + { + log::error!("Failed to refresh local library after install failure: {refresh_err}"); + } + end_operation(ctx, tx_notify_ui, &id).await; + operation_guard.disarm(); events::send( tx_notify_ui, PeerEvent::InstallGameFailed { id: id.clone() }, ); - if let Err(refresh_err) = refresh_local_game(ctx, tx_notify_ui, &id).await { - log::error!("Failed to refresh local library after install failure: {refresh_err}"); - } } } } @@ -559,11 +564,15 @@ async fn run_uninstall_operation(ctx: &Ctx, tx_notify_ui: &UnboundedSender { + if let Err(err) = refresh_local_game_for_ending_operation(ctx, tx_notify_ui, &id).await + { + log::error!("Failed to refresh local library after uninstall: {err}"); + } + end_operation(ctx, tx_notify_ui, &id).await; + operation_guard.disarm(); events::send( tx_notify_ui, PeerEvent::UninstallGameFinished { id: id.clone() }, @@ -571,16 +580,21 @@ async fn run_uninstall_operation(ctx: &Ctx, tx_notify_ui: &UnboundedSender { log::error!("Uninstall operation failed for {id}: {err}"); + if let Err(refresh_err) = + refresh_local_game_for_ending_operation(ctx, tx_notify_ui, &id).await + { + log::error!( + "Failed to refresh local library after uninstall failure: {refresh_err}" + ); + } + end_operation(ctx, tx_notify_ui, &id).await; + operation_guard.disarm(); events::send( tx_notify_ui, PeerEvent::UninstallGameFailed { id: id.clone() }, ); } } - - if let Err(err) = refresh_local_game(ctx, tx_notify_ui, &id).await { - log::error!("Failed to refresh local library after uninstall: {err}"); - } } async fn run_remove_downloaded_operation( @@ -612,11 +626,15 @@ async fn run_remove_downloaded_operation( install::remove_downloaded(&game_dir, &id).await }; - end_operation(ctx, tx_notify_ui, &id).await; - operation_guard.disarm(); match result { Ok(()) => { + if let Err(err) = refresh_local_game_for_ending_operation(ctx, tx_notify_ui, &id).await + { + log::error!("Failed to refresh local library after downloaded-file removal: {err}"); + } + end_operation(ctx, tx_notify_ui, &id).await; + operation_guard.disarm(); events::send( tx_notify_ui, PeerEvent::RemoveDownloadedGameFinished { id: id.clone() }, @@ -624,16 +642,21 @@ async fn run_remove_downloaded_operation( } Err(err) => { log::error!("Downloaded-file removal failed for {id}: {err}"); + if let Err(refresh_err) = + refresh_local_game_for_ending_operation(ctx, tx_notify_ui, &id).await + { + log::error!( + "Failed to refresh local library after downloaded-file removal failure: {refresh_err}" + ); + } + end_operation(ctx, tx_notify_ui, &id).await; + operation_guard.disarm(); events::send( tx_notify_ui, PeerEvent::RemoveDownloadedGameFailed { id: id.clone() }, ); } } - - if let Err(err) = refresh_local_game(ctx, tx_notify_ui, &id).await { - log::error!("Failed to refresh local library after downloaded-file removal: {err}"); - } } async fn begin_operation( @@ -809,7 +832,7 @@ async fn scan_and_announce_local_library( ) -> eyre::Result<()> { let catalog = ctx.catalog.read().await.clone(); let scan = scan_local_library(game_dir, &catalog).await?; - update_and_announce_games_with_policy(ctx, tx_notify_ui, scan, event_policy).await; + update_and_announce_games_with_policy(ctx, tx_notify_ui, scan, event_policy, None).await; Ok(()) } @@ -826,6 +849,28 @@ async fn refresh_local_game( tx_notify_ui, scan, LocalLibraryEventPolicy::OnChange, + None, + ) + .await; + Ok(()) +} + +/// Refreshes the game whose operation has completed before clearing its +/// active-operation snapshot, while preserving freeze behavior for other games. +async fn refresh_local_game_for_ending_operation( + ctx: &Ctx, + tx_notify_ui: &UnboundedSender, + id: &str, +) -> eyre::Result<()> { + let game_dir = { ctx.game_dir.read().await.clone() }; + let catalog = ctx.catalog.read().await.clone(); + let scan = rescan_local_game(&game_dir, &catalog, id).await?; + update_and_announce_games_with_policy( + ctx, + tx_notify_ui, + scan, + LocalLibraryEventPolicy::OnChange, + Some(id), ) .await; Ok(()) @@ -878,6 +923,7 @@ pub async fn update_and_announce_games( tx_notify_ui, scan, LocalLibraryEventPolicy::OnChange, + None, ) .await; } @@ -887,6 +933,7 @@ async fn update_and_announce_games_with_policy( tx_notify_ui: &UnboundedSender, scan: LocalLibraryScan, event_policy: LocalLibraryEventPolicy, + ending_operation_id: Option<&str>, ) { let LocalLibraryScan { mut game_db, @@ -894,7 +941,10 @@ async fn update_and_announce_games_with_policy( revision, } = scan; - let active_operation_ids = active_operation_ids(ctx).await; + let mut active_operation_ids = active_operation_ids(ctx).await; + if let Some(id) = ending_operation_id { + active_operation_ids.remove(id); + } if !active_operation_ids.is_empty() { let previous = ctx.local_library.read().await.games.clone(); for id in &active_operation_ids { @@ -1361,7 +1411,7 @@ mod tests { } #[tokio::test] - async fn install_refreshes_settled_state_after_guard_release() { + async fn install_refreshes_settled_state_before_operation_clear() { let temp = TempDir::new("lanspread-handler-install"); let root = temp.game_root(); write_file(&root.join("version.ini"), b"20250101"); @@ -1383,13 +1433,13 @@ mod tests { } _ => panic!("expected InstallGameBegin"), } + assert_local_update(recv_event(&mut rx).await, true, true); assert_active_update(recv_event(&mut rx).await, Vec::new()); assert!(matches!( recv_event(&mut rx).await, PeerEvent::InstallGameFinished { id } if id == "game" )); assert!(ctx.active_operations.read().await.is_empty()); - assert_local_update(recv_event(&mut rx).await, true, true); } #[tokio::test] @@ -1444,6 +1494,7 @@ mod tests { } _ => panic!("expected InstallGameBegin"), } + assert_local_update(recv_event(&mut rx).await, true, true); assert_active_update(recv_event(&mut rx).await, Vec::new()); assert!(matches!( recv_event(&mut rx).await, @@ -1451,11 +1502,10 @@ mod tests { )); assert!(ctx.active_operations.read().await.is_empty()); assert!(ctx.active_downloads.read().await.is_empty()); - assert_local_update(recv_event(&mut rx).await, true, true); } #[tokio::test] - async fn update_refreshes_settled_state_after_guard_release() { + async fn update_refreshes_settled_state_before_operation_clear() { let temp = TempDir::new("lanspread-handler-update"); let root = temp.game_root(); write_file(&root.join("version.ini"), b"20250101"); @@ -1478,13 +1528,13 @@ mod tests { } _ => panic!("expected InstallGameBegin"), } + assert_local_update(recv_event(&mut rx).await, true, true); assert_active_update(recv_event(&mut rx).await, Vec::new()); assert!(matches!( recv_event(&mut rx).await, PeerEvent::InstallGameFinished { id } if id == "game" )); assert!(ctx.active_operations.read().await.is_empty()); - assert_local_update(recv_event(&mut rx).await, true, true); } #[tokio::test] @@ -1509,13 +1559,13 @@ mod tests { operation: InstallOperation::Installing } if id == "game" )); + let game = local_update_game(recv_event(&mut rx).await, true, true); + assert_eq!(game.local_version.as_deref(), Some("20240101")); assert_active_update(recv_event(&mut rx).await, Vec::new()); assert!(matches!( recv_event(&mut rx).await, PeerEvent::InstallGameFinished { id } if id == "game" )); - let game = local_update_game(recv_event(&mut rx).await, true, true); - assert_eq!(game.local_version.as_deref(), Some("20240101")); write_file(&root.join("version.ini"), b"20250101"); write_file(&root.join("game.eti"), b"new archive"); @@ -1532,13 +1582,13 @@ mod tests { operation: InstallOperation::Updating } if id == "game" )); + let game = local_update_game(recv_event(&mut rx).await, true, true); + assert_eq!(game.local_version.as_deref(), Some("20250101")); assert_active_update(recv_event(&mut rx).await, Vec::new()); assert!(matches!( recv_event(&mut rx).await, PeerEvent::InstallGameFinished { id } if id == "game" )); - let game = local_update_game(recv_event(&mut rx).await, true, true); - assert_eq!(game.local_version.as_deref(), Some("20250101")); run_uninstall_operation(&ctx, &tx, "game".to_string()).await; assert_active_update( @@ -1549,18 +1599,18 @@ mod tests { recv_event(&mut rx).await, PeerEvent::UninstallGameBegin { id } if id == "game" )); + let game = local_update_game(recv_event(&mut rx).await, false, true); + assert_eq!(game.local_version.as_deref(), Some("20250101")); assert_active_update(recv_event(&mut rx).await, Vec::new()); assert!(matches!( recv_event(&mut rx).await, PeerEvent::UninstallGameFinished { id } if id == "game" )); - let game = local_update_game(recv_event(&mut rx).await, false, true); - assert_eq!(game.local_version.as_deref(), Some("20250101")); assert!(ctx.active_operations.read().await.is_empty()); } #[tokio::test] - async fn uninstall_refreshes_settled_state_after_guard_release() { + async fn uninstall_refreshes_settled_state_before_operation_clear() { let temp = TempDir::new("lanspread-handler-uninstall"); let root = temp.game_root(); write_file(&root.join("version.ini"), b"20250101"); @@ -1580,17 +1630,17 @@ mod tests { recv_event(&mut rx).await, PeerEvent::UninstallGameBegin { id } if id == "game" )); + assert_local_update(recv_event(&mut rx).await, false, true); assert_active_update(recv_event(&mut rx).await, Vec::new()); assert!(matches!( recv_event(&mut rx).await, PeerEvent::UninstallGameFinished { id } if id == "game" )); assert!(ctx.active_operations.read().await.is_empty()); - assert_local_update(recv_event(&mut rx).await, false, true); } #[tokio::test] - async fn remove_downloaded_refreshes_settled_state_after_guard_release() { + async fn remove_downloaded_refreshes_settled_state_before_operation_clear() { let temp = TempDir::new("lanspread-handler-remove-downloaded"); let root = temp.game_root(); write_file(&root.join("version.ini"), b"20250101"); @@ -1615,15 +1665,15 @@ mod tests { recv_event(&mut rx).await, PeerEvent::RemoveDownloadedGameBegin { id } if id == "game" )); + let PeerEvent::LocalLibraryChanged { games } = recv_event(&mut rx).await else { + panic!("expected LocalLibraryChanged"); + }; + assert!(games.is_empty()); assert_active_update(recv_event(&mut rx).await, Vec::new()); assert!(matches!( recv_event(&mut rx).await, PeerEvent::RemoveDownloadedGameFinished { id } if id == "game" )); - let PeerEvent::LocalLibraryChanged { games } = recv_event(&mut rx).await else { - panic!("expected LocalLibraryChanged"); - }; - assert!(games.is_empty()); assert!(!root.exists()); assert!(ctx.active_operations.read().await.is_empty()); }