ChatGPT Codex 5.5 xhigh refactored even more
This commit is contained in:
@@ -19,3 +19,7 @@ eyre = { workspace = true }
|
||||
sqlx = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[lib]
|
||||
test = false
|
||||
doctest = false
|
||||
|
||||
@@ -12,10 +12,10 @@ todo = "warn"
|
||||
unwrap_used = "warn"
|
||||
|
||||
[dependencies]
|
||||
# external
|
||||
bytes = { workspace = true }
|
||||
eyre = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
@@ -15,5 +15,7 @@ unwrap_used = "warn"
|
||||
eyre = { workspace = true }
|
||||
log = { workspace = true }
|
||||
mdns-sd = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[lib]
|
||||
test = false
|
||||
doctest = false
|
||||
|
||||
@@ -9,8 +9,6 @@ todo = "warn"
|
||||
unwrap_used = "warn"
|
||||
|
||||
[dependencies]
|
||||
# local
|
||||
lanspread-compat = { path = "../lanspread-compat" }
|
||||
lanspread-db = { path = "../lanspread-db" }
|
||||
lanspread-mdns = { path = "../lanspread-mdns" }
|
||||
lanspread-proto = { path = "../lanspread-proto" }
|
||||
@@ -28,6 +26,8 @@ serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-util = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
//! UI event helpers used by peer command and service code.
|
||||
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use tokio::sync::{RwLock, mpsc::UnboundedSender};
|
||||
|
||||
use crate::{PeerEvent, peer_db::PeerGameDB};
|
||||
|
||||
pub fn send(tx_notify_ui: &UnboundedSender<PeerEvent>, event: PeerEvent, label: &str) {
|
||||
if let Err(err) = tx_notify_ui.send(event) {
|
||||
log::error!("Failed to send {label} event: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn emit_peer_game_list(
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
) {
|
||||
let games = { peer_game_db.read().await.get_all_games() };
|
||||
send(tx_notify_ui, PeerEvent::ListGames(games), "ListGames");
|
||||
}
|
||||
|
||||
pub async fn emit_peer_count(
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
) {
|
||||
let peer_count = { peer_game_db.read().await.get_peer_addresses().len() };
|
||||
send(
|
||||
tx_notify_ui,
|
||||
PeerEvent::PeerCountUpdated(peer_count),
|
||||
"PeerCountUpdated",
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn emit_peer_discovered(
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
peer_addr: SocketAddr,
|
||||
) {
|
||||
send(
|
||||
tx_notify_ui,
|
||||
PeerEvent::PeerDiscovered(peer_addr),
|
||||
"PeerDiscovered",
|
||||
);
|
||||
emit_peer_count(peer_game_db, tx_notify_ui).await;
|
||||
}
|
||||
|
||||
pub async fn emit_peer_lost(
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
peer_addr: SocketAddr,
|
||||
) {
|
||||
send(tx_notify_ui, PeerEvent::PeerLost(peer_addr), "PeerLost");
|
||||
emit_peer_count(peer_game_db, tx_notify_ui).await;
|
||||
}
|
||||
@@ -2,19 +2,24 @@
|
||||
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use lanspread_db::db::{Game, GameFileDescription};
|
||||
use lanspread_db::db::GameFileDescription;
|
||||
use tokio::sync::{RwLock, mpsc::UnboundedSender};
|
||||
|
||||
use crate::{
|
||||
PeerEvent,
|
||||
context::Ctx,
|
||||
download::download_game_files,
|
||||
events,
|
||||
identity::FEATURE_LIBRARY_DELTA,
|
||||
local_games::{
|
||||
LocalLibraryScan, get_game_file_descriptions, local_download_available, scan_local_library,
|
||||
LocalLibraryScan,
|
||||
get_game_file_descriptions,
|
||||
local_download_available,
|
||||
scan_local_library,
|
||||
},
|
||||
network::{announce_games_to_peer, request_game_details_from_peer, send_library_delta},
|
||||
peer_db::{PeerGameDB, PeerId},
|
||||
peer_db::PeerGameDB,
|
||||
remote_peer::ensure_peer_id_for_addr,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
@@ -24,32 +29,7 @@ use crate::{
|
||||
/// 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}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_peer_id_for_addr(
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
peer_addr: SocketAddr,
|
||||
) -> PeerId {
|
||||
let mut db = peer_game_db.write().await;
|
||||
if let Some(peer_id) = db.peer_id_for_addr(&peer_addr).cloned() {
|
||||
return peer_id;
|
||||
}
|
||||
|
||||
let legacy_id = format!("legacy-{peer_addr}");
|
||||
db.upsert_peer(legacy_id.clone(), peer_addr);
|
||||
legacy_id
|
||||
events::emit_peer_game_list(&ctx.peer_game_db, tx_notify_ui).await;
|
||||
}
|
||||
|
||||
/// Tries to serve a game from local files.
|
||||
@@ -165,12 +145,10 @@ pub async fn handle_download_game_files_command(
|
||||
) {
|
||||
log::info!("Got PeerCommand::DownloadGameFiles");
|
||||
let games_folder = { ctx.game_dir.read().await.clone() };
|
||||
if games_folder.is_none() {
|
||||
let Some(games_folder) = games_folder else {
|
||||
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) = {
|
||||
@@ -312,10 +290,7 @@ pub async fn handle_set_game_dir_command(
|
||||
/// 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}");
|
||||
}
|
||||
events::emit_peer_count(&ctx.peer_game_db, tx_notify_ui).await;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -348,11 +323,7 @@ pub async fn update_and_announce_games(
|
||||
*db_guard = Some(game_db.clone());
|
||||
}
|
||||
|
||||
let all_games = game_db
|
||||
.all_games()
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.collect::<Vec<Game>>();
|
||||
let all_games = game_db.all_games().into_iter().cloned().collect::<Vec<_>>();
|
||||
|
||||
if let Err(e) = tx_notify_ui.send(PeerEvent::LocalGamesUpdated(all_games.clone())) {
|
||||
log::error!("Failed to send LocalGamesUpdated event: {e}");
|
||||
|
||||
@@ -16,6 +16,7 @@ mod config;
|
||||
mod context;
|
||||
mod download;
|
||||
mod error;
|
||||
mod events;
|
||||
mod handlers;
|
||||
mod identity;
|
||||
mod library;
|
||||
@@ -24,6 +25,7 @@ mod network;
|
||||
mod path_validation;
|
||||
mod peer;
|
||||
mod peer_db;
|
||||
mod remote_peer;
|
||||
mod services;
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -409,8 +409,8 @@ impl PeerGameDB {
|
||||
#[must_use]
|
||||
pub fn game_files_for(&self, game_id: &str) -> Vec<(SocketAddr, Vec<GameFileDescription>)> {
|
||||
self.peers
|
||||
.iter()
|
||||
.filter_map(|(_, peer)| {
|
||||
.values()
|
||||
.filter_map(|peer| {
|
||||
peer.files
|
||||
.get(game_id)
|
||||
.cloned()
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
//! Shared helpers for remote peer identity and legacy game announcements.
|
||||
|
||||
use std::{collections::HashMap, net::SocketAddr, sync::Arc};
|
||||
|
||||
use lanspread_db::db::Game;
|
||||
use lanspread_proto::{Availability, GameSummary};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
library::compute_library_digest,
|
||||
peer_db::{PeerGameDB, PeerId},
|
||||
};
|
||||
|
||||
pub async fn ensure_peer_id_for_addr(
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
peer_addr: SocketAddr,
|
||||
) -> PeerId {
|
||||
let mut db = peer_game_db.write().await;
|
||||
if let Some(peer_id) = db.peer_id_for_addr(&peer_addr).cloned() {
|
||||
return peer_id;
|
||||
}
|
||||
|
||||
let legacy_id = format!("legacy-{peer_addr}");
|
||||
db.upsert_peer(legacy_id.clone(), peer_addr);
|
||||
legacy_id
|
||||
}
|
||||
|
||||
pub fn summary_from_game(game: &Game) -> GameSummary {
|
||||
GameSummary {
|
||||
id: game.id.clone(),
|
||||
name: game.name.clone(),
|
||||
size: game.size,
|
||||
downloaded: game.downloaded,
|
||||
installed: game.installed,
|
||||
eti_version: game.eti_game_version.clone(),
|
||||
manifest_hash: 0,
|
||||
availability: Availability::Ready,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_peer_from_game_list(
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
peer_addr: SocketAddr,
|
||||
games: &[Game],
|
||||
) -> Vec<Game> {
|
||||
let summaries = games.iter().map(summary_from_game).collect::<Vec<_>>();
|
||||
let mut by_id = HashMap::with_capacity(summaries.len());
|
||||
for summary in &summaries {
|
||||
by_id.insert(summary.id.clone(), summary.clone());
|
||||
}
|
||||
let digest = compute_library_digest(&by_id);
|
||||
let peer_id = ensure_peer_id_for_addr(peer_game_db, peer_addr).await;
|
||||
|
||||
let mut db = peer_game_db.write().await;
|
||||
db.update_peer_games(&peer_id, summaries);
|
||||
let features = db.peer_features(&peer_id);
|
||||
db.update_peer_library(&peer_id, 0, digest, features);
|
||||
db.get_all_games()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,91 @@
|
||||
//! mDNS advertisement for the local peer server.
|
||||
|
||||
use std::{collections::HashMap, net::SocketAddr, time::Duration};
|
||||
|
||||
use lanspread_mdns::{LANSPREAD_SERVICE_TYPE, MdnsAdvertiser};
|
||||
use lanspread_proto::PROTOCOL_VERSION;
|
||||
|
||||
use crate::{context::PeerCtx, network::select_advertise_ip};
|
||||
|
||||
pub(super) async fn start_mdns_advertiser(
|
||||
ctx: &PeerCtx,
|
||||
server_addr: SocketAddr,
|
||||
) -> eyre::Result<()> {
|
||||
let advertise_ip = select_advertise_ip()?;
|
||||
let advertise_addr = SocketAddr::new(advertise_ip, server_addr.port());
|
||||
log::info!("Advertising peer via mDNS from {advertise_addr}");
|
||||
|
||||
{
|
||||
let mut guard = ctx.local_peer_addr.write().await;
|
||||
*guard = Some(advertise_addr);
|
||||
}
|
||||
|
||||
let peer_id = ctx.peer_id.as_ref().clone();
|
||||
let hostname = gethostname::gethostname().to_string_lossy().into_owned();
|
||||
let advertised_name = advertised_service_name(&hostname, &peer_id);
|
||||
let monitor_name = advertised_name.clone();
|
||||
let properties = advertisement_properties(ctx, &hostname, &peer_id).await;
|
||||
|
||||
let mdns = tokio::task::spawn_blocking(move || {
|
||||
MdnsAdvertiser::new(
|
||||
LANSPREAD_SERVICE_TYPE,
|
||||
&advertised_name,
|
||||
advertise_addr,
|
||||
Some(properties),
|
||||
)
|
||||
})
|
||||
.await??;
|
||||
|
||||
tokio::spawn(async move {
|
||||
log::info!("Registered mDNS service with name: {monitor_name}");
|
||||
while let Ok(event) = mdns.monitor.recv() {
|
||||
match event {
|
||||
lanspread_mdns::DaemonEvent::Error(err) => {
|
||||
log::error!("mDNS error: {err}");
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
_ => {
|
||||
log::trace!("mDNS event: {event:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn advertised_service_name(hostname: &str, peer_id: &str) -> String {
|
||||
let max_hostname_len = 63usize.saturating_sub(peer_id.len() + 1);
|
||||
let truncated_hostname = if hostname.len() > max_hostname_len {
|
||||
hostname.get(..max_hostname_len).unwrap_or(hostname)
|
||||
} else {
|
||||
hostname
|
||||
};
|
||||
|
||||
if truncated_hostname.is_empty() {
|
||||
peer_id.to_string()
|
||||
} else {
|
||||
format!("{truncated_hostname}-{peer_id}")
|
||||
}
|
||||
}
|
||||
|
||||
async fn advertisement_properties(
|
||||
ctx: &PeerCtx,
|
||||
hostname: &str,
|
||||
peer_id: &str,
|
||||
) -> HashMap<String, String> {
|
||||
let (library_rev, library_digest) = {
|
||||
let library_guard = ctx.local_library.read().await;
|
||||
(library_guard.revision, library_guard.digest)
|
||||
};
|
||||
|
||||
let mut properties = HashMap::new();
|
||||
properties.insert("peer_id".to_string(), peer_id.to_string());
|
||||
properties.insert("proto_ver".to_string(), PROTOCOL_VERSION.to_string());
|
||||
properties.insert("library_rev".to_string(), library_rev.to_string());
|
||||
properties.insert("library_digest".to_string(), library_digest.to_string());
|
||||
if !hostname.is_empty() {
|
||||
properties.insert("hostname".to_string(), hostname.to_string());
|
||||
}
|
||||
properties
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
//! mDNS peer discovery and discovery-time protocol negotiation.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use lanspread_mdns::{LANSPREAD_SERVICE_TYPE, MdnsBrowser, MdnsService};
|
||||
use lanspread_proto::PROTOCOL_VERSION;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use crate::{
|
||||
PeerEvent,
|
||||
context::Ctx,
|
||||
events,
|
||||
peer_db::PeerId,
|
||||
services::{handshake::perform_handshake_with_peer, legacy::request_games_from_peer},
|
||||
};
|
||||
|
||||
struct MdnsPeerInfo {
|
||||
addr: std::net::SocketAddr,
|
||||
peer_id: Option<PeerId>,
|
||||
proto_ver: Option<u32>,
|
||||
library_rev: u64,
|
||||
library_digest: u64,
|
||||
}
|
||||
|
||||
/// Runs the peer discovery service using mDNS.
|
||||
pub async fn run_peer_discovery(tx_notify_ui: UnboundedSender<PeerEvent>, ctx: Ctx) {
|
||||
log::info!("Starting peer discovery task");
|
||||
|
||||
let service_type = LANSPREAD_SERVICE_TYPE.to_string();
|
||||
|
||||
loop {
|
||||
let (service_tx, mut service_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let service_type_clone = service_type.clone();
|
||||
|
||||
let worker_handle = tokio::task::spawn_blocking(move || -> eyre::Result<()> {
|
||||
let browser = MdnsBrowser::new(&service_type_clone)?;
|
||||
loop {
|
||||
if let Some(service) = browser.next_service(None)? {
|
||||
if service_tx.send(service).is_err() {
|
||||
log::debug!("Peer discovery consumer dropped; stopping worker");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
log::warn!("mDNS browser closed; stopping peer discovery worker");
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
while let Some(service) = service_rx.recv().await {
|
||||
let info = parse_mdns_peer(&service);
|
||||
if is_self_advertisement(&info, &ctx).await {
|
||||
log::trace!("Ignoring self advertisement at {}", info.addr);
|
||||
continue;
|
||||
}
|
||||
|
||||
handle_discovered_peer(info, &ctx, &tx_notify_ui).await;
|
||||
}
|
||||
|
||||
match worker_handle.await {
|
||||
Ok(Ok(())) => {
|
||||
log::warn!("Peer discovery worker exited; restarting shortly");
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
log::error!("Peer discovery worker failed: {err}");
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Peer discovery worker join error: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_mdns_peer(service: &MdnsService) -> MdnsPeerInfo {
|
||||
MdnsPeerInfo {
|
||||
addr: service.addr,
|
||||
peer_id: service.properties.get("peer_id").cloned(),
|
||||
proto_ver: service
|
||||
.properties
|
||||
.get("proto_ver")
|
||||
.and_then(|value| value.parse::<u32>().ok()),
|
||||
library_rev: service
|
||||
.properties
|
||||
.get("library_rev")
|
||||
.and_then(|value| value.parse::<u64>().ok())
|
||||
.unwrap_or(0),
|
||||
library_digest: service
|
||||
.properties
|
||||
.get("library_digest")
|
||||
.and_then(|value| value.parse::<u64>().ok())
|
||||
.unwrap_or(0),
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_self_advertisement(info: &MdnsPeerInfo, ctx: &Ctx) -> bool {
|
||||
let guard = ctx.local_peer_addr.read().await;
|
||||
guard.as_ref().is_some_and(|addr| *addr == info.addr)
|
||||
|| info
|
||||
.peer_id
|
||||
.as_ref()
|
||||
.is_some_and(|peer_id| peer_id == ctx.peer_id.as_ref())
|
||||
}
|
||||
|
||||
async fn handle_discovered_peer(
|
||||
info: MdnsPeerInfo,
|
||||
ctx: &Ctx,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
) {
|
||||
let peer_id = info
|
||||
.peer_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("legacy-{}", info.addr));
|
||||
|
||||
let upsert = {
|
||||
let mut db = ctx.peer_game_db.write().await;
|
||||
let upsert = db.upsert_peer(peer_id.clone(), info.addr);
|
||||
let features = db.peer_features(&peer_id);
|
||||
if info.library_rev > 0 || info.library_digest > 0 {
|
||||
db.update_peer_library(&peer_id, info.library_rev, info.library_digest, features);
|
||||
}
|
||||
upsert
|
||||
};
|
||||
|
||||
if upsert.is_new {
|
||||
log::info!("Discovered peer at: {}", info.addr);
|
||||
events::emit_peer_discovered(&ctx.peer_game_db, tx_notify_ui, info.addr).await;
|
||||
}
|
||||
|
||||
if upsert.is_new || upsert.addr_changed {
|
||||
spawn_protocol_negotiation(&info, ctx, tx_notify_ui.clone(), peer_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_protocol_negotiation(
|
||||
info: &MdnsPeerInfo,
|
||||
ctx: &Ctx,
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
peer_id: PeerId,
|
||||
) {
|
||||
let peer_addr = info.addr;
|
||||
let proto_ver = info.proto_ver;
|
||||
let peer_id_arc = ctx.peer_id.clone();
|
||||
let local_library = ctx.local_library.clone();
|
||||
let peer_game_db = ctx.peer_game_db.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let handshake_result = if proto_ver.is_none() || proto_ver == Some(PROTOCOL_VERSION) {
|
||||
perform_handshake_with_peer(
|
||||
peer_id_arc,
|
||||
local_library,
|
||||
peer_game_db.clone(),
|
||||
tx_notify_ui.clone(),
|
||||
peer_addr,
|
||||
Some(peer_id),
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Err(eyre::eyre!("Skipping hello for legacy peer"))
|
||||
};
|
||||
|
||||
if handshake_result.is_err()
|
||||
&& let Err(err) = request_games_from_peer(peer_addr, tx_notify_ui, peer_game_db).await
|
||||
{
|
||||
log::error!("Failed to request games from peer {peer_addr}: {err}");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
//! Protocol handshakes and library synchronization between peers.
|
||||
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use lanspread_proto::{Hello, HelloAck, LibraryDelta, LibrarySnapshot, PROTOCOL_VERSION};
|
||||
use tokio::sync::{RwLock, mpsc::UnboundedSender};
|
||||
|
||||
use crate::{
|
||||
PeerEvent,
|
||||
context::PeerCtx,
|
||||
events,
|
||||
identity::default_features,
|
||||
library::{LocalLibraryState, build_library_snapshot, build_library_summary},
|
||||
network::{exchange_hello, send_library_delta, send_library_snapshot, send_library_summary},
|
||||
peer_db::{PeerGameDB, PeerId, PeerUpsert},
|
||||
};
|
||||
|
||||
enum LibraryUpdate {
|
||||
Delta(LibraryDelta),
|
||||
Snapshot(LibrarySnapshot),
|
||||
}
|
||||
|
||||
pub(super) async fn build_hello_ack(ctx: &PeerCtx) -> HelloAck {
|
||||
let library_guard = ctx.local_library.read().await;
|
||||
HelloAck {
|
||||
peer_id: ctx.peer_id.as_ref().clone(),
|
||||
proto_ver: PROTOCOL_VERSION,
|
||||
library_rev: library_guard.revision,
|
||||
library_digest: library_guard.digest,
|
||||
features: default_features(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_hello_from_state(
|
||||
peer_id: &str,
|
||||
local_library: &Arc<RwLock<LocalLibraryState>>,
|
||||
) -> Hello {
|
||||
let library_guard = local_library.read().await;
|
||||
Hello {
|
||||
peer_id: peer_id.to_string(),
|
||||
proto_ver: PROTOCOL_VERSION,
|
||||
library_rev: library_guard.revision,
|
||||
library_digest: library_guard.digest,
|
||||
features: default_features(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn perform_handshake_with_peer(
|
||||
peer_id: Arc<String>,
|
||||
local_library: Arc<RwLock<LocalLibraryState>>,
|
||||
peer_game_db: Arc<RwLock<PeerGameDB>>,
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
peer_addr: SocketAddr,
|
||||
peer_id_hint: Option<PeerId>,
|
||||
) -> eyre::Result<()> {
|
||||
let hello = build_hello_from_state(peer_id.as_ref(), &local_library).await;
|
||||
let ack = exchange_hello(peer_addr, hello).await?;
|
||||
|
||||
if ack.proto_ver != PROTOCOL_VERSION {
|
||||
log::warn!(
|
||||
"Peer {peer_addr} uses incompatible protocol {} (expected {PROTOCOL_VERSION})",
|
||||
ack.proto_ver
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if ack.peer_id == *peer_id {
|
||||
log::trace!("Ignoring handshake with self for {peer_addr}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(expected) = peer_id_hint.as_ref()
|
||||
&& expected != &ack.peer_id
|
||||
{
|
||||
log::warn!(
|
||||
"Peer {peer_addr} id mismatch: mDNS advertised {expected}, hello ack returned {}",
|
||||
ack.peer_id
|
||||
);
|
||||
let _ = peer_game_db.write().await.remove_peer(expected);
|
||||
}
|
||||
|
||||
let upsert = record_remote_library(
|
||||
&peer_game_db,
|
||||
ack.peer_id.clone(),
|
||||
peer_addr,
|
||||
ack.library_rev,
|
||||
ack.library_digest,
|
||||
ack.features.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
after_peer_library_recorded(
|
||||
upsert,
|
||||
peer_addr,
|
||||
ack.library_rev,
|
||||
ack.library_digest,
|
||||
&local_library,
|
||||
&peer_game_db,
|
||||
&tx_notify_ui,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) async fn accept_inbound_hello(
|
||||
ctx: &PeerCtx,
|
||||
remote_addr: Option<SocketAddr>,
|
||||
hello: Hello,
|
||||
) -> HelloAck {
|
||||
if hello.peer_id == *ctx.peer_id {
|
||||
log::trace!("Ignoring hello from self");
|
||||
return build_hello_ack(ctx).await;
|
||||
}
|
||||
|
||||
if hello.proto_ver != PROTOCOL_VERSION {
|
||||
log::warn!(
|
||||
"Incompatible protocol from {remote_addr:?}: {}",
|
||||
hello.proto_ver
|
||||
);
|
||||
return build_hello_ack(ctx).await;
|
||||
}
|
||||
|
||||
if let Some(addr) = remote_addr {
|
||||
let upsert = record_remote_library(
|
||||
&ctx.peer_game_db,
|
||||
hello.peer_id.clone(),
|
||||
addr,
|
||||
hello.library_rev,
|
||||
hello.library_digest,
|
||||
hello.features.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
after_peer_library_recorded(
|
||||
upsert,
|
||||
addr,
|
||||
hello.library_rev,
|
||||
hello.library_digest,
|
||||
&ctx.local_library,
|
||||
&ctx.peer_game_db,
|
||||
&ctx.tx_notify_ui,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
build_hello_ack(ctx).await
|
||||
}
|
||||
|
||||
pub(super) fn spawn_library_resync(
|
||||
peer_id: Arc<String>,
|
||||
local_library: Arc<RwLock<LocalLibraryState>>,
|
||||
peer_game_db: Arc<RwLock<PeerGameDB>>,
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
peer_addr: SocketAddr,
|
||||
peer_id_hint: PeerId,
|
||||
reason: &'static str,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = perform_handshake_with_peer(
|
||||
peer_id,
|
||||
local_library,
|
||||
peer_game_db,
|
||||
tx_notify_ui,
|
||||
peer_addr,
|
||||
Some(peer_id_hint),
|
||||
)
|
||||
.await
|
||||
{
|
||||
log::warn!("Failed to {reason} library from {peer_addr}: {err}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn record_remote_library(
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
peer_id: PeerId,
|
||||
peer_addr: SocketAddr,
|
||||
library_rev: u64,
|
||||
library_digest: u64,
|
||||
features: Vec<String>,
|
||||
) -> PeerUpsert {
|
||||
let mut db = peer_game_db.write().await;
|
||||
let upsert = db.upsert_peer(peer_id.clone(), peer_addr);
|
||||
db.update_peer_library(&peer_id, library_rev, library_digest, features);
|
||||
upsert
|
||||
}
|
||||
|
||||
async fn after_peer_library_recorded(
|
||||
upsert: PeerUpsert,
|
||||
peer_addr: SocketAddr,
|
||||
remote_library_rev: u64,
|
||||
remote_library_digest: u64,
|
||||
local_library: &Arc<RwLock<LocalLibraryState>>,
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
) {
|
||||
if upsert.is_new {
|
||||
events::emit_peer_discovered(peer_game_db, tx_notify_ui, peer_addr).await;
|
||||
send_local_library_summary(peer_addr, local_library).await;
|
||||
}
|
||||
|
||||
send_local_library_update_if_needed(
|
||||
peer_addr,
|
||||
local_library,
|
||||
remote_library_rev,
|
||||
remote_library_digest,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn send_local_library_summary(
|
||||
peer_addr: SocketAddr,
|
||||
local_library: &Arc<RwLock<LocalLibraryState>>,
|
||||
) {
|
||||
let summary = {
|
||||
let library_guard = local_library.read().await;
|
||||
build_library_summary(&library_guard)
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = send_library_summary(peer_addr, summary).await {
|
||||
log::warn!("Failed to send library summary to {peer_addr}: {err}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn send_local_library_update_if_needed(
|
||||
peer_addr: SocketAddr,
|
||||
local_library: &Arc<RwLock<LocalLibraryState>>,
|
||||
remote_rev: u64,
|
||||
remote_digest: u64,
|
||||
) {
|
||||
if let Some(update) = select_library_update(local_library, remote_rev, remote_digest).await {
|
||||
tokio::spawn(async move {
|
||||
let result = match update {
|
||||
LibraryUpdate::Delta(delta) => send_library_delta(peer_addr, delta).await,
|
||||
LibraryUpdate::Snapshot(snapshot) => {
|
||||
send_library_snapshot(peer_addr, snapshot).await
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = result {
|
||||
log::warn!("Failed to send library update to {peer_addr}: {err}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn select_library_update(
|
||||
local_library: &Arc<RwLock<LocalLibraryState>>,
|
||||
remote_rev: u64,
|
||||
remote_digest: u64,
|
||||
) -> Option<LibraryUpdate> {
|
||||
let library_guard = local_library.read().await;
|
||||
if library_guard.digest == remote_digest || remote_rev > library_guard.revision {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(delta) = library_guard.delta_since(remote_rev) {
|
||||
return Some(LibraryUpdate::Delta(delta));
|
||||
}
|
||||
|
||||
Some(LibraryUpdate::Snapshot(build_library_snapshot(
|
||||
&library_guard,
|
||||
)))
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//! Compatibility path for peers that only support the original game-list protocol.
|
||||
|
||||
use std::{net::SocketAddr, sync::Arc, time::Duration};
|
||||
|
||||
use tokio::sync::{RwLock, mpsc::UnboundedSender};
|
||||
|
||||
use crate::{
|
||||
PeerEvent,
|
||||
events,
|
||||
network::fetch_games_from_peer,
|
||||
peer_db::PeerGameDB,
|
||||
remote_peer::update_peer_from_game_list,
|
||||
};
|
||||
|
||||
pub(super) async fn request_games_from_peer(
|
||||
peer_addr: SocketAddr,
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
peer_game_db: Arc<RwLock<PeerGameDB>>,
|
||||
) -> eyre::Result<()> {
|
||||
let mut retry_count = 0;
|
||||
|
||||
loop {
|
||||
let games = fetch_games_from_peer(peer_addr).await?;
|
||||
log::info!("Received {} games from peer {peer_addr}", games.len());
|
||||
|
||||
if games.is_empty() && retry_count < 1 {
|
||||
log::info!("Received 0 games from peer {peer_addr}, scheduling retry in 5s");
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
retry_count += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let aggregated_games = update_peer_from_game_list(&peer_game_db, peer_addr, &games).await;
|
||||
events::send(
|
||||
&tx_notify_ui,
|
||||
PeerEvent::ListGames(aggregated_games),
|
||||
"ListGames",
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
//! Peer liveness checks and stale-peer cleanup.
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use tokio::{
|
||||
sync::{RwLock, mpsc::UnboundedSender},
|
||||
task::JoinHandle,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
PeerEvent,
|
||||
config::{PEER_PING_IDLE_SECS, PEER_PING_INTERVAL_SECS, peer_stale_timeout},
|
||||
events,
|
||||
network::ping_peer,
|
||||
peer_db::{PeerGameDB, PeerId},
|
||||
};
|
||||
|
||||
/// Runs the ping service to check peer liveness.
|
||||
pub async fn run_ping_service(
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
peer_game_db: Arc<RwLock<PeerGameDB>>,
|
||||
downloading_games: Arc<RwLock<HashSet<String>>>,
|
||||
active_downloads: Arc<RwLock<HashMap<String, JoinHandle<()>>>>,
|
||||
) {
|
||||
log::info!(
|
||||
"Starting ping service ({PEER_PING_INTERVAL_SECS}s interval, \
|
||||
{}s idle threshold, {}s timeout)",
|
||||
PEER_PING_IDLE_SECS,
|
||||
peer_stale_timeout().as_secs()
|
||||
);
|
||||
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(PEER_PING_INTERVAL_SECS));
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
ping_idle_peers(
|
||||
&peer_game_db,
|
||||
&downloading_games,
|
||||
&active_downloads,
|
||||
&tx_notify_ui,
|
||||
)
|
||||
.await;
|
||||
|
||||
prune_stale_peers(
|
||||
&peer_game_db,
|
||||
&downloading_games,
|
||||
&active_downloads,
|
||||
&tx_notify_ui,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn ping_idle_peers(
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
downloading_games: &Arc<RwLock<HashSet<String>>>,
|
||||
active_downloads: &Arc<RwLock<HashMap<String, JoinHandle<()>>>>,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
) {
|
||||
let peer_snapshots = { peer_game_db.read().await.peer_liveness_snapshot() };
|
||||
|
||||
for (peer_id, peer_addr, last_seen) in peer_snapshots {
|
||||
if last_seen.elapsed() < Duration::from_secs(PEER_PING_IDLE_SECS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let tx_notify_ui = tx_notify_ui.clone();
|
||||
let peer_game_db = peer_game_db.clone();
|
||||
let downloading_games = downloading_games.clone();
|
||||
let active_downloads = active_downloads.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
match ping_peer(peer_addr).await {
|
||||
Ok(true) => {
|
||||
peer_game_db.write().await.update_last_seen(&peer_id);
|
||||
}
|
||||
Ok(false) => {
|
||||
log::warn!("Peer {peer_addr} failed ping check");
|
||||
remove_peer_and_refresh(
|
||||
&peer_game_db,
|
||||
&downloading_games,
|
||||
&active_downloads,
|
||||
&tx_notify_ui,
|
||||
peer_id,
|
||||
"Removed stale peer",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to ping peer {peer_addr}: {err}");
|
||||
remove_peer_and_refresh(
|
||||
&peer_game_db,
|
||||
&downloading_games,
|
||||
&active_downloads,
|
||||
&tx_notify_ui,
|
||||
peer_id,
|
||||
"Removed peer due to ping error",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn prune_stale_peers(
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
downloading_games: &Arc<RwLock<HashSet<String>>>,
|
||||
active_downloads: &Arc<RwLock<HashMap<String, JoinHandle<()>>>>,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
) {
|
||||
let stale_peers = {
|
||||
peer_game_db
|
||||
.read()
|
||||
.await
|
||||
.get_stale_peer_ids(peer_stale_timeout())
|
||||
};
|
||||
|
||||
let mut removed_any = false;
|
||||
for peer_id in stale_peers {
|
||||
removed_any |= remove_peer(peer_game_db, tx_notify_ui, peer_id, "Removed stale peer").await;
|
||||
}
|
||||
|
||||
if removed_any {
|
||||
events::emit_peer_game_list(peer_game_db, tx_notify_ui).await;
|
||||
handle_active_downloads_without_peers(
|
||||
peer_game_db,
|
||||
downloading_games,
|
||||
active_downloads,
|
||||
tx_notify_ui,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_peer_and_refresh(
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
downloading_games: &Arc<RwLock<HashSet<String>>>,
|
||||
active_downloads: &Arc<RwLock<HashMap<String, JoinHandle<()>>>>,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
peer_id: PeerId,
|
||||
log_label: &str,
|
||||
) {
|
||||
if remove_peer(peer_game_db, tx_notify_ui, peer_id, log_label).await {
|
||||
events::emit_peer_game_list(peer_game_db, tx_notify_ui).await;
|
||||
handle_active_downloads_without_peers(
|
||||
peer_game_db,
|
||||
downloading_games,
|
||||
active_downloads,
|
||||
tx_notify_ui,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_peer(
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
peer_id: PeerId,
|
||||
log_label: &str,
|
||||
) -> bool {
|
||||
let removed_peer = { peer_game_db.write().await.remove_peer(&peer_id) };
|
||||
let Some(peer) = removed_peer else {
|
||||
return false;
|
||||
};
|
||||
|
||||
log::info!("{log_label}: {}", peer.addr);
|
||||
events::emit_peer_lost(peer_game_db, tx_notify_ui, peer.addr).await;
|
||||
true
|
||||
}
|
||||
|
||||
async fn handle_active_downloads_without_peers(
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
downloading_games: &Arc<RwLock<HashSet<String>>>,
|
||||
active_downloads: &Arc<RwLock<HashMap<String, JoinHandle<()>>>>,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
) {
|
||||
let active_ids = {
|
||||
downloading_games
|
||||
.read()
|
||||
.await
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
if active_ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for id in active_ids {
|
||||
if peers_still_have_game(peer_game_db, &id).await {
|
||||
continue;
|
||||
}
|
||||
|
||||
let removed_from_tracking = {
|
||||
let mut guard = downloading_games.write().await;
|
||||
guard.remove(&id)
|
||||
};
|
||||
|
||||
if !removed_from_tracking {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(handle) = { active_downloads.write().await.remove(&id) } {
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
events::send(
|
||||
tx_notify_ui,
|
||||
PeerEvent::DownloadGameFilesAllPeersGone { id },
|
||||
"DownloadGameFilesAllPeersGone",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn peers_still_have_game(peer_game_db: &Arc<RwLock<PeerGameDB>>, game_id: &str) -> bool {
|
||||
let guard = peer_game_db.read().await;
|
||||
!guard.peers_with_game(game_id).is_empty()
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//! Local game directory monitor.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use crate::{
|
||||
PeerEvent,
|
||||
config::LOCAL_GAME_MONITOR_INTERVAL_SECS,
|
||||
context::Ctx,
|
||||
handlers::update_and_announce_games,
|
||||
local_games::scan_local_library,
|
||||
};
|
||||
|
||||
/// Monitors the local game directory for changes.
|
||||
pub async fn run_local_game_monitor(tx_notify_ui: UnboundedSender<PeerEvent>, ctx: Ctx) {
|
||||
log::info!(
|
||||
"Starting local game directory monitor ({LOCAL_GAME_MONITOR_INTERVAL_SECS}s interval)"
|
||||
);
|
||||
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(LOCAL_GAME_MONITOR_INTERVAL_SECS));
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
let game_dir = { ctx.game_dir.read().await.clone() };
|
||||
if let Some(game_dir) = game_dir {
|
||||
match scan_local_library(&game_dir).await {
|
||||
Ok(scan) => {
|
||||
update_and_announce_games(&ctx, &tx_notify_ui, scan).await;
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to scan local games directory: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
//! QUIC server accept loop.
|
||||
|
||||
use std::{net::SocketAddr, time::Duration};
|
||||
|
||||
use s2n_quic::{Connection, Server, provider::limits::Limits};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use crate::{
|
||||
PeerEvent,
|
||||
config::{CERT_PEM, KEY_PEM},
|
||||
context::PeerCtx,
|
||||
events,
|
||||
services::{advertise::start_mdns_advertiser, stream::handle_peer_stream},
|
||||
};
|
||||
|
||||
/// Runs the QUIC server and mDNS advertiser.
|
||||
pub async fn run_server_component(
|
||||
addr: SocketAddr,
|
||||
ctx: PeerCtx,
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
) -> eyre::Result<()> {
|
||||
let limits = Limits::default()
|
||||
.with_max_handshake_duration(Duration::from_secs(3))?
|
||||
.with_max_idle_timeout(Duration::from_secs(3))?;
|
||||
|
||||
let mut server = Server::builder()
|
||||
.with_tls((CERT_PEM, KEY_PEM))?
|
||||
.with_io(addr)?
|
||||
.with_limits(limits)?
|
||||
.start()?;
|
||||
|
||||
let server_addr = server.local_addr()?;
|
||||
log::info!("Peer server listening on {server_addr}");
|
||||
|
||||
start_mdns_advertiser(&ctx, server_addr).await?;
|
||||
|
||||
while let Some(connection) = server.accept().await {
|
||||
let ctx = ctx.clone();
|
||||
let tx_notify_ui = tx_notify_ui.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = handle_peer_connection(connection, ctx, tx_notify_ui).await {
|
||||
log::error!("Peer connection error: {err}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_peer_connection(
|
||||
mut connection: Connection,
|
||||
ctx: PeerCtx,
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
) -> eyre::Result<()> {
|
||||
let remote_addr = connection.remote_addr()?;
|
||||
log::info!("{remote_addr} peer connected");
|
||||
events::send(
|
||||
&tx_notify_ui,
|
||||
PeerEvent::PeerConnected(remote_addr),
|
||||
"PeerConnected",
|
||||
);
|
||||
|
||||
while let Ok(Some(stream)) = connection.accept_bidirectional_stream().await {
|
||||
let ctx = ctx.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = handle_peer_stream(stream, ctx, Some(remote_addr)).await {
|
||||
log::error!("{remote_addr:?} peer stream error: {err}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
events::send(
|
||||
&tx_notify_ui,
|
||||
PeerEvent::PeerDisconnected(remote_addr),
|
||||
"PeerDisconnected",
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
//! Request dispatch for a single bidirectional QUIC stream.
|
||||
|
||||
use std::{net::SocketAddr, path::PathBuf};
|
||||
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use lanspread_db::db::{Game, GameFileDescription};
|
||||
use lanspread_proto::{LibraryDelta, LibrarySnapshot, LibrarySummary, Message, Request, Response};
|
||||
use s2n_quic::stream::{BidirectionalStream, SendStream};
|
||||
use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec};
|
||||
|
||||
use crate::{
|
||||
PeerEvent,
|
||||
context::PeerCtx,
|
||||
error::PeerError,
|
||||
events,
|
||||
local_games::get_game_file_descriptions,
|
||||
peer::{send_game_file_chunk, send_game_file_data},
|
||||
remote_peer::{ensure_peer_id_for_addr, update_peer_from_game_list},
|
||||
services::handshake::{
|
||||
accept_inbound_hello,
|
||||
perform_handshake_with_peer,
|
||||
spawn_library_resync,
|
||||
},
|
||||
};
|
||||
|
||||
type ResponseWriter = FramedWrite<SendStream, LengthDelimitedCodec>;
|
||||
|
||||
/// Handles a bidirectional stream from a peer.
|
||||
pub(super) async fn handle_peer_stream(
|
||||
stream: BidirectionalStream,
|
||||
ctx: PeerCtx,
|
||||
remote_addr: Option<SocketAddr>,
|
||||
) -> eyre::Result<()> {
|
||||
let (rx, tx) = stream.split();
|
||||
let mut framed_rx = FramedRead::new(rx, LengthDelimitedCodec::new());
|
||||
let mut framed_tx = FramedWrite::new(tx, LengthDelimitedCodec::new());
|
||||
|
||||
log::trace!("{remote_addr:?} peer stream opened");
|
||||
|
||||
loop {
|
||||
match framed_rx.next().await {
|
||||
Some(Ok(data)) => {
|
||||
log::trace!(
|
||||
"{:?} msg: (raw): {}",
|
||||
remote_addr,
|
||||
String::from_utf8_lossy(&data)
|
||||
);
|
||||
|
||||
let request = Request::decode(data.freeze());
|
||||
log::debug!("{remote_addr:?} msg: {request:?}");
|
||||
note_peer_activity(&ctx, remote_addr).await;
|
||||
framed_tx = dispatch_request(&ctx, remote_addr, request, framed_tx).await;
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
log::error!("{remote_addr:?} peer stream error: {err}");
|
||||
break;
|
||||
}
|
||||
None => {
|
||||
log::trace!("{remote_addr:?} peer stream closed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn dispatch_request(
|
||||
ctx: &PeerCtx,
|
||||
remote_addr: Option<SocketAddr>,
|
||||
request: Request,
|
||||
framed_tx: ResponseWriter,
|
||||
) -> ResponseWriter {
|
||||
match request {
|
||||
Request::Ping => send_response(framed_tx, Response::Pong, "pong").await,
|
||||
Request::Hello(hello) => {
|
||||
let ack = accept_inbound_hello(ctx, remote_addr, hello).await;
|
||||
send_response(framed_tx, Response::HelloAck(ack), "HelloAck").await
|
||||
}
|
||||
Request::ListGames => handle_list_games(ctx, framed_tx).await,
|
||||
Request::LibrarySummary(summary) => {
|
||||
handle_library_summary(ctx, remote_addr, summary).await;
|
||||
framed_tx
|
||||
}
|
||||
Request::LibrarySnapshot(snapshot) => {
|
||||
handle_library_snapshot(ctx, remote_addr, snapshot).await;
|
||||
framed_tx
|
||||
}
|
||||
Request::LibraryDelta(delta) => {
|
||||
handle_library_delta(ctx, remote_addr, delta).await;
|
||||
framed_tx
|
||||
}
|
||||
Request::GetGame { id } => handle_get_game(ctx, id, framed_tx).await,
|
||||
Request::GetGameFileData(desc) => handle_file_data_request(ctx, desc, framed_tx).await,
|
||||
Request::GetGameFileChunk {
|
||||
game_id,
|
||||
relative_path,
|
||||
offset,
|
||||
length,
|
||||
} => {
|
||||
handle_file_chunk_request(ctx, game_id, relative_path, offset, length, framed_tx).await
|
||||
}
|
||||
Request::Goodbye { peer_id } => {
|
||||
handle_goodbye(ctx, remote_addr, peer_id).await;
|
||||
framed_tx
|
||||
}
|
||||
Request::Invalid(_, _) => {
|
||||
log::error!("Received invalid request from peer");
|
||||
framed_tx
|
||||
}
|
||||
Request::AnnounceGames(games) => {
|
||||
handle_announce_games(ctx, remote_addr, games).await;
|
||||
framed_tx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn note_peer_activity(ctx: &PeerCtx, remote_addr: Option<SocketAddr>) {
|
||||
if let Some(addr) = remote_addr {
|
||||
ctx.peer_game_db
|
||||
.write()
|
||||
.await
|
||||
.update_last_seen_by_addr(&addr);
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_response(
|
||||
mut framed_tx: ResponseWriter,
|
||||
response: Response,
|
||||
label: &str,
|
||||
) -> ResponseWriter {
|
||||
if let Err(err) = framed_tx.send(response.encode()).await {
|
||||
log::error!("Failed to send {label} response: {err}");
|
||||
}
|
||||
framed_tx
|
||||
}
|
||||
|
||||
async fn handle_list_games(ctx: &PeerCtx, framed_tx: ResponseWriter) -> ResponseWriter {
|
||||
log::info!("Received ListGames request from peer");
|
||||
let snapshot = {
|
||||
let db_guard = ctx.local_game_db.read().await;
|
||||
if let Some(db) = db_guard.as_ref() {
|
||||
db.all_games().into_iter().cloned().collect::<Vec<Game>>()
|
||||
} else {
|
||||
log::info!("Local game database not yet loaded, responding with empty game list");
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
let games = if snapshot.is_empty() {
|
||||
snapshot
|
||||
} else {
|
||||
let downloading = ctx.downloading_games.read().await;
|
||||
snapshot
|
||||
.into_iter()
|
||||
.filter(|game| !downloading.contains(&game.id))
|
||||
.collect()
|
||||
};
|
||||
|
||||
send_response(framed_tx, Response::ListGames(games), "ListGames").await
|
||||
}
|
||||
|
||||
async fn handle_library_summary(
|
||||
ctx: &PeerCtx,
|
||||
remote_addr: Option<SocketAddr>,
|
||||
summary: LibrarySummary,
|
||||
) {
|
||||
let Some(addr) = remote_addr else {
|
||||
return;
|
||||
};
|
||||
|
||||
let peer_id = ensure_peer_id_for_addr(&ctx.peer_game_db, addr).await;
|
||||
let (previous_digest, previous_count, features) = {
|
||||
let db = ctx.peer_game_db.read().await;
|
||||
let (_, digest) = db.peer_library_state(&peer_id).unwrap_or((0, 0));
|
||||
(
|
||||
digest,
|
||||
db.peer_game_count(&peer_id),
|
||||
db.peer_features(&peer_id),
|
||||
)
|
||||
};
|
||||
|
||||
{
|
||||
let mut db = ctx.peer_game_db.write().await;
|
||||
db.update_peer_library(
|
||||
&peer_id,
|
||||
summary.library_rev,
|
||||
summary.library_digest,
|
||||
features,
|
||||
);
|
||||
}
|
||||
|
||||
if summary.library_digest != previous_digest || previous_count == 0 {
|
||||
tokio::spawn({
|
||||
let peer_id_arc = ctx.peer_id.clone();
|
||||
let local_library = ctx.local_library.clone();
|
||||
let peer_game_db = ctx.peer_game_db.clone();
|
||||
let tx_notify_ui = ctx.tx_notify_ui.clone();
|
||||
async move {
|
||||
if let Err(err) = perform_handshake_with_peer(
|
||||
peer_id_arc,
|
||||
local_library,
|
||||
peer_game_db,
|
||||
tx_notify_ui,
|
||||
addr,
|
||||
Some(peer_id),
|
||||
)
|
||||
.await
|
||||
{
|
||||
log::warn!("Failed to refresh library from {addr}: {err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_library_snapshot(
|
||||
ctx: &PeerCtx,
|
||||
remote_addr: Option<SocketAddr>,
|
||||
snapshot: LibrarySnapshot,
|
||||
) {
|
||||
if let Some(addr) = remote_addr {
|
||||
let peer_id = ensure_peer_id_for_addr(&ctx.peer_game_db, addr).await;
|
||||
{
|
||||
let mut db = ctx.peer_game_db.write().await;
|
||||
db.apply_library_snapshot(&peer_id, snapshot);
|
||||
}
|
||||
|
||||
events::emit_peer_game_list(&ctx.peer_game_db, &ctx.tx_notify_ui).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_library_delta(ctx: &PeerCtx, remote_addr: Option<SocketAddr>, delta: LibraryDelta) {
|
||||
let Some(addr) = remote_addr else {
|
||||
return;
|
||||
};
|
||||
|
||||
let peer_id = ensure_peer_id_for_addr(&ctx.peer_game_db, addr).await;
|
||||
let applied = {
|
||||
let mut db = ctx.peer_game_db.write().await;
|
||||
db.apply_library_delta(&peer_id, delta)
|
||||
};
|
||||
|
||||
if applied {
|
||||
events::emit_peer_game_list(&ctx.peer_game_db, &ctx.tx_notify_ui).await;
|
||||
} else {
|
||||
spawn_library_resync(
|
||||
ctx.peer_id.clone(),
|
||||
ctx.local_library.clone(),
|
||||
ctx.peer_game_db.clone(),
|
||||
ctx.tx_notify_ui.clone(),
|
||||
addr,
|
||||
peer_id,
|
||||
"resync",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_get_game(ctx: &PeerCtx, id: String, framed_tx: ResponseWriter) -> ResponseWriter {
|
||||
log::info!("Received GetGame request for {id} from peer");
|
||||
let response = get_game_response(ctx, id).await;
|
||||
send_response(framed_tx, response, "GetGame").await
|
||||
}
|
||||
|
||||
async fn get_game_response(ctx: &PeerCtx, id: String) -> Response {
|
||||
let downloading = ctx.downloading_games.read().await.contains(&id);
|
||||
if downloading {
|
||||
log::info!("Declining to serve GetGame for {id} because download is in progress");
|
||||
return Response::GameNotFound(id);
|
||||
}
|
||||
|
||||
let Some(game_dir) = ctx.game_dir.read().await.clone() else {
|
||||
return Response::GameNotFound(id);
|
||||
};
|
||||
|
||||
let has_game = {
|
||||
let db_guard = ctx.local_game_db.read().await;
|
||||
db_guard
|
||||
.as_ref()
|
||||
.is_some_and(|db| db.get_game_by_id(&id).is_some())
|
||||
};
|
||||
|
||||
if !has_game {
|
||||
return Response::GameNotFound(id);
|
||||
}
|
||||
|
||||
match get_game_file_descriptions(&id, &game_dir).await {
|
||||
Ok(file_descriptions) => Response::GetGame {
|
||||
id,
|
||||
file_descriptions,
|
||||
},
|
||||
Err(PeerError::FileSizeDetermination { path, source }) => {
|
||||
let error_msg = format!("Failed to determine file size for {path}: {source}");
|
||||
log::error!("File size determination error for game {id}: {error_msg}");
|
||||
Response::InternalPeerError(error_msg)
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to get game file descriptions for {id}: {err}");
|
||||
Response::GameNotFound(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_file_data_request(
|
||||
ctx: &PeerCtx,
|
||||
desc: GameFileDescription,
|
||||
framed_tx: ResponseWriter,
|
||||
) -> ResponseWriter {
|
||||
log::info!(
|
||||
"Received GetGameFileData request for {} from peer",
|
||||
desc.relative_path
|
||||
);
|
||||
|
||||
let Some(game_dir) = ctx.game_dir.read().await.clone() else {
|
||||
return send_invalid_request(
|
||||
framed_tx,
|
||||
desc.relative_path.as_bytes().to_vec(),
|
||||
"Game directory not set",
|
||||
)
|
||||
.await;
|
||||
};
|
||||
|
||||
let base_dir = PathBuf::from(game_dir);
|
||||
let mut tx = framed_tx.into_inner();
|
||||
send_game_file_data(&desc, &mut tx, &base_dir).await;
|
||||
FramedWrite::new(tx, LengthDelimitedCodec::new())
|
||||
}
|
||||
|
||||
async fn handle_file_chunk_request(
|
||||
ctx: &PeerCtx,
|
||||
game_id: String,
|
||||
relative_path: String,
|
||||
offset: u64,
|
||||
length: u64,
|
||||
framed_tx: ResponseWriter,
|
||||
) -> ResponseWriter {
|
||||
log::info!(
|
||||
"Received GetGameFileChunk request for {relative_path} (offset {offset}, length {length})"
|
||||
);
|
||||
|
||||
let Some(game_dir) = ctx.game_dir.read().await.clone() else {
|
||||
return send_invalid_request(
|
||||
framed_tx,
|
||||
relative_path.as_bytes().to_vec(),
|
||||
"Game directory not set",
|
||||
)
|
||||
.await;
|
||||
};
|
||||
|
||||
let base_dir = PathBuf::from(game_dir);
|
||||
let mut tx = framed_tx.into_inner();
|
||||
send_game_file_chunk(&game_id, &relative_path, offset, length, &mut tx, &base_dir).await;
|
||||
FramedWrite::new(tx, LengthDelimitedCodec::new())
|
||||
}
|
||||
|
||||
async fn send_invalid_request(
|
||||
framed_tx: ResponseWriter,
|
||||
raw_request: Vec<u8>,
|
||||
message: &str,
|
||||
) -> ResponseWriter {
|
||||
send_response(
|
||||
framed_tx,
|
||||
Response::InvalidRequest(raw_request.into(), message.to_string()),
|
||||
"InvalidRequest",
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn handle_goodbye(ctx: &PeerCtx, remote_addr: Option<SocketAddr>, peer_id: String) {
|
||||
log::info!("Received Goodbye from peer {peer_id}");
|
||||
let removed = { ctx.peer_game_db.write().await.remove_peer(&peer_id) };
|
||||
if removed.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(addr) = remote_addr {
|
||||
events::emit_peer_lost(&ctx.peer_game_db, &ctx.tx_notify_ui, addr).await;
|
||||
}
|
||||
|
||||
events::emit_peer_game_list(&ctx.peer_game_db, &ctx.tx_notify_ui).await;
|
||||
}
|
||||
|
||||
async fn handle_announce_games(ctx: &PeerCtx, remote_addr: Option<SocketAddr>, games: Vec<Game>) {
|
||||
log::info!(
|
||||
"Received {} announced games from peer {remote_addr:?}",
|
||||
games.len()
|
||||
);
|
||||
|
||||
if let Some(addr) = remote_addr {
|
||||
let aggregated_games = update_peer_from_game_list(&ctx.peer_game_db, addr, &games).await;
|
||||
events::send(
|
||||
&ctx.tx_notify_ui,
|
||||
PeerEvent::ListGames(aggregated_games),
|
||||
"ListGames",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -20,3 +20,7 @@ bytes = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[lib]
|
||||
test = false
|
||||
doctest = false
|
||||
|
||||
@@ -13,6 +13,8 @@ edition = "2024"
|
||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||
name = "lanspread_tauri_deno_ts_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
test = false
|
||||
doctest = false
|
||||
|
||||
[lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
@@ -27,7 +29,6 @@ tauri-build = { version = "2", features = [] }
|
||||
# local
|
||||
lanspread-peer = { path = "../../lanspread-peer" }
|
||||
lanspread-db = { path = "../../lanspread-db" }
|
||||
lanspread-mdns = { path = "../../lanspread-mdns" }
|
||||
lanspread-compat = { path = "../../lanspread-compat" }
|
||||
|
||||
# external
|
||||
@@ -35,8 +36,6 @@ base64 = { workspace = true }
|
||||
eyre = { workspace = true }
|
||||
log = { workspace = true }
|
||||
mimalloc = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tauri = { workspace = true }
|
||||
tauri-plugin-log = { workspace = true }
|
||||
tauri-plugin-shell = { workspace = true }
|
||||
|
||||
@@ -895,7 +895,7 @@ pub fn run() {
|
||||
log::info!("Peer system initialized successfully with games directory");
|
||||
|
||||
// Wait a moment for local game database to be loaded before starting discovery
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(2000)).await;
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
|
||||
// Start peer discovery and request games from other peers
|
||||
if let Err(e) = request_games(state).await {
|
||||
|
||||
@@ -11,4 +11,6 @@ pedantic = { level = "warn", priority = -1 }
|
||||
todo = "warn"
|
||||
unwrap_used = "warn"
|
||||
|
||||
[dependencies]
|
||||
[lib]
|
||||
test = false
|
||||
doctest = false
|
||||
|
||||
Reference in New Issue
Block a user