refactor (Opus 4.5): modularize and split
This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
//! Command handlers for peer commands.
|
||||
|
||||
use std::{collections::HashSet, net::SocketAddr, sync::Arc};
|
||||
|
||||
use lanspread_db::db::{Game, GameDB, GameFileDescription};
|
||||
use tokio::sync::{RwLock, mpsc::UnboundedSender};
|
||||
|
||||
use crate::{
|
||||
PeerEvent,
|
||||
context::Ctx,
|
||||
download::download_game_files,
|
||||
local_games::{get_game_file_descriptions, load_local_game_db, local_download_available},
|
||||
network::{announce_games_to_peer, request_game_details_from_peer},
|
||||
peer_db::PeerGameDB,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Command handlers
|
||||
// =============================================================================
|
||||
|
||||
/// Handles the `ListGames` command.
|
||||
pub async fn handle_list_games_command(ctx: &Ctx, tx_notify_ui: &UnboundedSender<PeerEvent>) {
|
||||
log::info!("ListGames command received");
|
||||
emit_peer_game_list(&ctx.peer_game_db, tx_notify_ui).await;
|
||||
}
|
||||
|
||||
/// Emits the aggregated game list to the UI.
|
||||
pub async fn emit_peer_game_list(
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
) {
|
||||
let all_games = { peer_game_db.read().await.get_all_games() };
|
||||
if let Err(e) = tx_notify_ui.send(PeerEvent::ListGames(all_games)) {
|
||||
log::error!("Failed to send ListGames event: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to serve a game from local files.
|
||||
async fn try_serve_local_game(
|
||||
ctx: &Ctx,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
id: &str,
|
||||
) -> bool {
|
||||
let game_dir = { ctx.game_dir.read().await.clone() };
|
||||
let Some(game_dir) = game_dir else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let downloading = ctx.downloading_games.read().await;
|
||||
if !local_download_available(&game_dir, id, &downloading).await {
|
||||
return false;
|
||||
}
|
||||
drop(downloading);
|
||||
|
||||
match get_game_file_descriptions(id, &game_dir).await {
|
||||
Ok(file_descriptions) => {
|
||||
log::info!("Serving game {id} from local files");
|
||||
if let Err(e) = tx_notify_ui.send(PeerEvent::GotGameFiles {
|
||||
id: id.to_string(),
|
||||
file_descriptions,
|
||||
}) {
|
||||
log::error!("Failed to send GotGameFiles event: {e}");
|
||||
}
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to enumerate local file descriptions for {id}: {e}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles the `GetGame` command.
|
||||
pub async fn handle_get_game_command(
|
||||
ctx: &Ctx,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
id: String,
|
||||
) {
|
||||
if try_serve_local_game(ctx, tx_notify_ui, &id).await {
|
||||
return;
|
||||
}
|
||||
|
||||
log::info!("Requesting game from peers: {id}");
|
||||
let peers = { ctx.peer_game_db.read().await.peers_with_game(&id) };
|
||||
if peers.is_empty() {
|
||||
log::warn!("No peers have game {id}");
|
||||
if let Err(e) = tx_notify_ui.send(PeerEvent::NoPeersHaveGame { id: id.clone() }) {
|
||||
log::error!("Failed to send NoPeersHaveGame event: {e}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let peer_game_db = ctx.peer_game_db.clone();
|
||||
let tx_notify_ui = tx_notify_ui.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut fetched_any = false;
|
||||
for peer_addr in peers {
|
||||
match request_game_details_and_update(peer_addr, &id, peer_game_db.clone()).await {
|
||||
Ok(_) => {
|
||||
log::info!("Fetched game file list for {id} from peer {peer_addr}");
|
||||
fetched_any = true;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to fetch game files for {id} from {peer_addr}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fetched_any {
|
||||
let aggregated_files = { peer_game_db.read().await.aggregated_game_files(&id) };
|
||||
|
||||
if let Err(e) = tx_notify_ui.send(PeerEvent::GotGameFiles {
|
||||
id: id.clone(),
|
||||
file_descriptions: aggregated_files,
|
||||
}) {
|
||||
log::error!("Failed to send GotGameFiles event: {e}");
|
||||
}
|
||||
} else {
|
||||
log::warn!("Failed to retrieve game files for {id} from any peer");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Requests game details from a peer and updates the peer game database.
|
||||
async fn request_game_details_and_update(
|
||||
peer_addr: SocketAddr,
|
||||
game_id: &str,
|
||||
peer_game_db: Arc<RwLock<PeerGameDB>>,
|
||||
) -> eyre::Result<Vec<GameFileDescription>> {
|
||||
let (file_descriptions, _) = request_game_details_from_peer(peer_addr, game_id).await?;
|
||||
|
||||
{
|
||||
let mut db = peer_game_db.write().await;
|
||||
db.update_peer_game_files(peer_addr, game_id, file_descriptions.clone());
|
||||
}
|
||||
|
||||
Ok(file_descriptions)
|
||||
}
|
||||
|
||||
/// Handles the `DownloadGameFiles` command.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn handle_download_game_files_command(
|
||||
ctx: &Ctx,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
id: String,
|
||||
file_descriptions: Vec<GameFileDescription>,
|
||||
) {
|
||||
log::info!("Got PeerCommand::DownloadGameFiles");
|
||||
let games_folder = { ctx.game_dir.read().await.clone() };
|
||||
if games_folder.is_none() {
|
||||
log::error!("Cannot handle game file descriptions: games_folder is not set");
|
||||
return;
|
||||
}
|
||||
|
||||
let games_folder = games_folder.expect("checked above");
|
||||
|
||||
// Use majority validation to get trusted file descriptions and peer whitelist
|
||||
let (validated_descriptions, peer_whitelist, file_peer_map) = {
|
||||
match ctx
|
||||
.peer_game_db
|
||||
.read()
|
||||
.await
|
||||
.validate_file_sizes_majority(&id)
|
||||
{
|
||||
Ok((files, peers, file_peer_map)) => {
|
||||
log::info!(
|
||||
"Majority validation: {} validated files, {} trusted peers for game {id}",
|
||||
files.len(),
|
||||
peers.len()
|
||||
);
|
||||
(files, peers, file_peer_map)
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("File size majority validation failed for {id}: {e}");
|
||||
if let Err(send_err) =
|
||||
tx_notify_ui.send(PeerEvent::DownloadGameFilesFailed { id: id.clone() })
|
||||
{
|
||||
log::error!("Failed to send DownloadGameFilesFailed event: {send_err}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let resolved_descriptions = if file_descriptions.is_empty() {
|
||||
validated_descriptions
|
||||
} else {
|
||||
// If user provided specific descriptions, still validate them against majority
|
||||
// but keep user's selection (they might want specific files)
|
||||
file_descriptions
|
||||
};
|
||||
|
||||
if resolved_descriptions.is_empty() {
|
||||
log::error!(
|
||||
"No validated file descriptions available to download game {id}; request metadata first"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let downloading = ctx.downloading_games.read().await;
|
||||
if peer_whitelist.is_empty() {
|
||||
if local_download_available(&games_folder, &id, &downloading).await {
|
||||
drop(downloading);
|
||||
log::info!("Using locally downloaded files for game {id}; skipping peer transfer");
|
||||
if let Err(e) = tx_notify_ui.send(PeerEvent::DownloadGameFilesBegin { id: id.clone() })
|
||||
{
|
||||
log::error!("Failed to send DownloadGameFilesBegin event: {e}");
|
||||
}
|
||||
if let Err(e) =
|
||||
tx_notify_ui.send(PeerEvent::DownloadGameFilesFinished { id: id.clone() })
|
||||
{
|
||||
log::error!("Failed to send DownloadGameFilesFinished event: {e}");
|
||||
}
|
||||
} else {
|
||||
log::error!("No trusted peers available after majority validation for game {id}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
drop(downloading);
|
||||
|
||||
{
|
||||
let mut in_progress = ctx.downloading_games.write().await;
|
||||
if !in_progress.insert(id.clone()) {
|
||||
log::warn!("Download for {id} already in progress; ignoring new request");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let downloading_games = ctx.downloading_games.clone();
|
||||
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(
|
||||
&download_id,
|
||||
resolved_descriptions,
|
||||
games_folder,
|
||||
peer_whitelist,
|
||||
file_peer_map,
|
||||
tx_notify_ui_clone.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
{
|
||||
let mut guard = downloading_games.write().await;
|
||||
guard.remove(&download_id);
|
||||
}
|
||||
|
||||
if let Err(e) = result {
|
||||
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);
|
||||
}
|
||||
|
||||
/// Handles the `SetGameDir` command.
|
||||
pub async fn handle_set_game_dir_command(
|
||||
ctx: &Ctx,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
game_dir: String,
|
||||
) {
|
||||
*ctx.game_dir.write().await = Some(game_dir.clone());
|
||||
log::info!("Game directory set to: {game_dir}");
|
||||
|
||||
// Load local game database when game directory is set
|
||||
let game_dir = game_dir.clone();
|
||||
let tx_notify_ui = tx_notify_ui.clone();
|
||||
let ctx_clone = ctx.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
match load_local_game_db(&game_dir).await {
|
||||
Ok(db) => {
|
||||
update_and_announce_games(&ctx_clone, &tx_notify_ui, db).await;
|
||||
log::info!("Local game database loaded successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to load local game database: {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Handles the `GetPeerCount` command.
|
||||
pub async fn handle_get_peer_count_command(ctx: &Ctx, tx_notify_ui: &UnboundedSender<PeerEvent>) {
|
||||
log::info!("GetPeerCount command received");
|
||||
let peer_count = { ctx.peer_game_db.read().await.get_peer_addresses().len() };
|
||||
if let Err(e) = tx_notify_ui.send(PeerEvent::PeerCountUpdated(peer_count)) {
|
||||
log::error!("Failed to send PeerCountUpdated event: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Game announcement helpers
|
||||
// =============================================================================
|
||||
|
||||
/// Updates the local game database and announces changes to peers.
|
||||
pub async fn update_and_announce_games(
|
||||
ctx: &Ctx,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
new_db: GameDB,
|
||||
) {
|
||||
let local_game_db = ctx.local_game_db.clone();
|
||||
let mut db_guard = local_game_db.write().await;
|
||||
|
||||
let previous_games = db_guard
|
||||
.as_ref()
|
||||
.map(|db| db.games.keys().cloned().collect::<HashSet<_>>())
|
||||
.unwrap_or_default();
|
||||
|
||||
let current_game_ids = new_db.games.keys().cloned().collect::<HashSet<_>>();
|
||||
|
||||
// Check if any games were removed
|
||||
let removed_games: Vec<String> = previous_games
|
||||
.difference(¤t_game_ids)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
if removed_games.is_empty() {
|
||||
// Check if any games were added or updated
|
||||
if previous_games != current_game_ids {
|
||||
log::debug!("Local games directory structure changed, updating database");
|
||||
*db_guard = Some(new_db);
|
||||
|
||||
let all_games = db_guard
|
||||
.as_ref()
|
||||
.map(|db| db.all_games().into_iter().cloned().collect::<Vec<Game>>())
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Err(e) = tx_notify_ui.send(PeerEvent::LocalGamesUpdated(all_games.clone())) {
|
||||
log::error!("Failed to send LocalGamesUpdated event: {e}");
|
||||
}
|
||||
|
||||
// Broadcast update to all peers
|
||||
let peer_addresses = { ctx.peer_game_db.read().await.get_peer_addresses() };
|
||||
for peer_addr in peer_addresses {
|
||||
let games_clone = all_games.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = announce_games_to_peer(peer_addr, games_clone).await {
|
||||
log::warn!("Failed to announce games to {peer_addr}: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::info!("Detected removed games: {removed_games:?}");
|
||||
*db_guard = Some(new_db);
|
||||
|
||||
// Notify UI about the change
|
||||
let all_games = db_guard
|
||||
.as_ref()
|
||||
.map(|db| db.all_games().into_iter().cloned().collect::<Vec<Game>>())
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Err(e) = tx_notify_ui.send(PeerEvent::LocalGamesUpdated(all_games.clone())) {
|
||||
log::error!("Failed to send LocalGamesUpdated event: {e}");
|
||||
}
|
||||
|
||||
// Broadcast update to all peers
|
||||
let peer_addresses = { ctx.peer_game_db.read().await.get_peer_addresses() };
|
||||
for peer_addr in peer_addresses {
|
||||
let games_clone = all_games.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = announce_games_to_peer(peer_addr, games_clone).await {
|
||||
log::warn!("Failed to announce games to {peer_addr}: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user