From 5d587911927b24785bc91af262ed2656113abe3e Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sun, 17 May 2026 09:34:20 +0200 Subject: [PATCH] fix(peer-cli): fail missing downloads from peer event The peer core already emits NoPeersHaveGame when a requested game cannot be served by any known peer. The JSONL harness still waited for the generic file detail timeout before returning the download command error, which made the manual scenario slower and less precise. Correlate the existing no-peers event with the pending CLI download command so the harness returns a deterministic error immediately. This is harness bookkeeping only; game availability and peer behavior remain owned by lanspread-peer. Test Plan: - just fmt - just test - just clippy - just peer-cli-build - just peer-cli-image - just peer-cli-alpha, just peer-cli-bravo, just peer-cli-charlie - In charlie, send {"cmd":"download","game_id":"not-a-game"}; observe no-peers-have-game followed by error "no peers have game not-a-game" Refs: PEER_CLI_SCENARIOS.md --- crates/lanspread-peer-cli/src/main.rs | 30 +++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/crates/lanspread-peer-cli/src/main.rs b/crates/lanspread-peer-cli/src/main.rs index edb00da..01673b1 100644 --- a/crates/lanspread-peer-cli/src/main.rs +++ b/crates/lanspread-peer-cli/src/main.rs @@ -94,6 +94,7 @@ struct CliState { remote_games: Vec, active_operations: Vec, game_files: HashMap>, + unavailable_games: HashSet, } #[derive(Clone, serde::Serialize)] @@ -304,23 +305,32 @@ async fn game_files_for_download( shared: &SharedState, game_id: &str, ) -> eyre::Result> { - if let Some(files) = shared.state.read().await.game_files.get(game_id).cloned() { - return Ok(files); + { + let mut state = shared.state.write().await; + if let Some(files) = state.game_files.get(game_id).cloned() { + return Ok(files); + } + state.unavailable_games.remove(game_id); } sender.send(PeerCommand::GetGame(game_id.to_string()))?; let wait = async { loop { - if let Some(files) = shared.state.read().await.game_files.get(game_id).cloned() { - return files; + let state = shared.state.read().await; + if let Some(files) = state.game_files.get(game_id).cloned() { + return Ok(files); } + if state.unavailable_games.contains(game_id) { + eyre::bail!("no peers have game {game_id}"); + } + drop(state); shared.notify.notified().await; } }; tokio::time::timeout(Duration::from_secs(10), wait) .await - .wrap_err("timed out waiting for game file details") + .wrap_err("timed out waiting for game file details")? } async fn event_loop( @@ -396,7 +406,15 @@ async fn update_state_from_event(shared: &SharedState, event: PeerEvent) -> (&'s PeerEvent::UninstallGameBegin { id } => ("uninstall-begin", json!({"game_id": id})), PeerEvent::UninstallGameFinished { id } => ("uninstall-finished", json!({"game_id": id})), PeerEvent::UninstallGameFailed { id } => ("uninstall-failed", json!({"game_id": id})), - PeerEvent::NoPeersHaveGame { id } => ("no-peers-have-game", json!({"game_id": id})), + PeerEvent::NoPeersHaveGame { id } => { + shared + .state + .write() + .await + .unavailable_games + .insert(id.clone()); + ("no-peers-have-game", json!({"game_id": id})) + } PeerEvent::PeerConnected(addr) => ("peer-connected", peer_addr_json(addr)), PeerEvent::PeerDisconnected(addr) => ("peer-disconnected", peer_addr_json(addr)), PeerEvent::PeerDiscovered(addr) => ("peer-discovered", peer_addr_json(addr)),