refactor(peer): make startup directory-driven

Peer startup used to bootstrap itself by spawning the runtime and immediately
sending a SetGameDir command back through its own control channel. The Tauri
integration then polled shared state until a directory appeared and waited two
seconds before asking peers for games. That made startup ordering implicit and
left a race-prone sleep in the UI bridge.

Install the initial game directory directly into the peer context instead. The
runtime now attempts the initial local-library scan before starting discovery,
then launches the server, discovery, liveness, and local monitor services from
that initialized context. Later directory changes still use SetGameDir, so the
existing UI command surface stays intact.

Use PathBuf and Path references across peer filesystem boundaries so directory
state is represented as a path rather than an optional string. The Tauri layer
now validates a selected game directory before storing it, loads the bundled
catalog on first use, and starts or updates the peer runtime from one helper.
Peer event fan-out is split into named handlers so the Tauri setup closure only
wires state and starts the event loop.

Shutdown goodbye notifications are still best-effort, but they are now awaited
with a short timeout instead of being spawned and forgotten. The tradeoff is a
small bounded wait during peer runtime shutdown in exchange for clearer task
ownership.

Test Plan:
- cargo test -p lanspread-peer
- cargo clippy
- cargo clippy --benches
- cargo clippy --tests
- cargo +nightly fmt
- git diff --check

Refs: none
This commit is contained in:
2026-05-02 17:09:00 +02:00
parent 5480d1bdd4
commit 87d00e7df6
10 changed files with 355 additions and 393 deletions
+19 -18
View File
@@ -33,7 +33,7 @@ mod startup;
// Public re-exports
// =============================================================================
use std::{net::SocketAddr, sync::Arc};
use std::{net::SocketAddr, path::PathBuf, sync::Arc};
pub use config::{CHUNK_SIZE, MAX_RETRY_COUNT};
pub use error::PeerError;
@@ -52,6 +52,7 @@ use crate::{
handle_get_peer_count_command,
handle_list_games_command,
handle_set_game_dir_command,
load_local_library,
},
};
@@ -106,7 +107,7 @@ pub enum PeerCommand {
file_descriptions: Vec<GameFileDescription>,
},
/// Set the local game directory.
SetGameDir(String),
SetGameDir(PathBuf),
/// Request the current peer count.
GetPeerCount,
}
@@ -131,22 +132,22 @@ pub enum PeerCommand {
///
/// A channel sender for sending commands to the peer system.
pub fn start_peer(
game_dir: String,
game_dir: impl Into<PathBuf>,
tx_notify_ui: UnboundedSender<PeerEvent>,
peer_game_db: Arc<RwLock<PeerGameDB>>,
) -> eyre::Result<UnboundedSender<PeerCommand>> {
log::info!("Starting peer system with game directory: {game_dir}");
let game_dir = game_dir.into();
log::info!(
"Starting peer system with game directory: {}",
game_dir.display()
);
let peer_id = identity::load_or_create_peer_id()?;
let (tx_control, rx_control) = tokio::sync::mpsc::unbounded_channel();
let tx_control_clone = tx_control.clone();
startup::spawn_peer_runtime(rx_control, tx_notify_ui, peer_game_db, peer_id);
startup::spawn_peer_runtime(rx_control, tx_notify_ui, peer_game_db, peer_id, game_dir);
// Set the game directory
tx_control.send(PeerCommand::SetGameDir(game_dir))?;
Ok(tx_control_clone)
Ok(tx_control)
}
/// Main peer execution loop that handles peer commands and manages the peer system.
@@ -155,11 +156,15 @@ async fn run_peer(
tx_notify_ui: UnboundedSender<PeerEvent>,
peer_game_db: Arc<RwLock<PeerGameDB>>,
peer_id: String,
game_dir: PathBuf,
) -> eyre::Result<()> {
let ctx = Ctx::new(peer_game_db.clone(), peer_id);
startup::spawn_startup_services(&ctx, &tx_notify_ui)?;
let ctx = Ctx::new(peer_game_db, peer_id, game_dir);
if let Err(err) = load_local_library(&ctx, &tx_notify_ui).await {
log::error!("Failed to load initial local game database: {err}");
}
startup::spawn_startup_services(&ctx, &tx_notify_ui);
handle_peer_commands(&ctx, &tx_notify_ui, &mut rx_control).await;
startup::spawn_goodbye_notifications(&ctx).await;
startup::send_goodbye_notifications(&ctx).await;
Ok(())
}
@@ -169,11 +174,7 @@ async fn handle_peer_commands(
tx_notify_ui: &UnboundedSender<PeerEvent>,
rx_control: &mut UnboundedReceiver<PeerCommand>,
) {
loop {
let Some(cmd) = rx_control.recv().await else {
break;
};
while let Some(cmd) = rx_control.recv().await {
match cmd {
PeerCommand::ListGames => {
handle_list_games_command(ctx, tx_notify_ui).await;