refactor (Opus 4.5): modularize and split

This commit is contained in:
2025-11-28 21:10:42 +01:00
parent df01131f8d
commit 53c7fe10ba
11 changed files with 3301 additions and 2729 deletions
+378
View File
@@ -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(&current_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}");
}
});
}
}
}