ChatGPT Codex 5.2 xhigh refactored > 45min
This commit is contained in:
@@ -10,15 +10,23 @@ use std::{
|
||||
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use lanspread_db::db::Game;
|
||||
use lanspread_mdns::{LANSPREAD_SERVICE_TYPE, MdnsAdvertiser, MdnsBrowser};
|
||||
use lanspread_proto::{Message, Request, Response};
|
||||
use lanspread_mdns::{LANSPREAD_SERVICE_TYPE, MdnsAdvertiser, MdnsBrowser, MdnsService};
|
||||
use lanspread_proto::{
|
||||
Hello,
|
||||
HelloAck,
|
||||
LibraryDelta,
|
||||
LibrarySnapshot,
|
||||
Message,
|
||||
PROTOCOL_VERSION,
|
||||
Request,
|
||||
Response,
|
||||
};
|
||||
use s2n_quic::{Connection, Server, provider::limits::Limits, stream::BidirectionalStream};
|
||||
use tokio::{
|
||||
sync::{RwLock, mpsc::UnboundedSender},
|
||||
task::JoinHandle,
|
||||
};
|
||||
use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
PeerEvent,
|
||||
@@ -32,10 +40,25 @@ use crate::{
|
||||
context::{Ctx, PeerCtx},
|
||||
error::PeerError,
|
||||
handlers::{emit_peer_game_list, update_and_announce_games},
|
||||
local_games::{get_game_file_descriptions, scan_local_games},
|
||||
network::{fetch_games_from_peer, ping_peer, select_advertise_ip},
|
||||
identity::default_features,
|
||||
library::{
|
||||
LocalLibraryState,
|
||||
build_library_snapshot,
|
||||
build_library_summary,
|
||||
compute_library_digest,
|
||||
},
|
||||
local_games::{get_game_file_descriptions, scan_local_library},
|
||||
network::{
|
||||
exchange_hello,
|
||||
fetch_games_from_peer,
|
||||
ping_peer,
|
||||
select_advertise_ip,
|
||||
send_library_delta,
|
||||
send_library_snapshot,
|
||||
send_library_summary,
|
||||
},
|
||||
peer::{send_game_file_chunk, send_game_file_data},
|
||||
peer_db::PeerGameDB,
|
||||
peer_db::{PeerGameDB, PeerId, PeerUpsert},
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
@@ -70,7 +93,7 @@ pub async fn run_server_component(
|
||||
}
|
||||
|
||||
// Start mDNS advertising for peer discovery
|
||||
let peer_id = Uuid::now_v7().simple().to_string();
|
||||
let peer_id = ctx.peer_id.as_ref().clone();
|
||||
let hostname = gethostname::gethostname();
|
||||
let hostname_str = hostname.to_str().unwrap_or("");
|
||||
|
||||
@@ -83,13 +106,32 @@ pub async fn run_server_component(
|
||||
};
|
||||
|
||||
let combined_str = if truncated_hostname.is_empty() {
|
||||
peer_id
|
||||
peer_id.clone()
|
||||
} else {
|
||||
format!("{truncated_hostname}-{peer_id}")
|
||||
};
|
||||
|
||||
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.clone());
|
||||
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_str.is_empty() {
|
||||
properties.insert("hostname".to_string(), hostname_str.to_string());
|
||||
}
|
||||
|
||||
let mdns = tokio::task::spawn_blocking(move || {
|
||||
MdnsAdvertiser::new(LANSPREAD_SERVICE_TYPE, &combined_str, advertise_addr)
|
||||
MdnsAdvertiser::new(
|
||||
LANSPREAD_SERVICE_TYPE,
|
||||
&combined_str,
|
||||
advertise_addr,
|
||||
Some(properties),
|
||||
)
|
||||
})
|
||||
.await??;
|
||||
|
||||
@@ -157,6 +199,279 @@ async fn handle_peer_connection(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
enum LibraryUpdate {
|
||||
Delta(LibraryDelta),
|
||||
Snapshot(LibrarySnapshot),
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
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 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 {
|
||||
return None;
|
||||
}
|
||||
|
||||
if 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,
|
||||
)))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fn summary_from_game(game: &Game) -> lanspread_proto::GameSummary {
|
||||
lanspread_proto::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: lanspread_proto::Availability::Ready,
|
||||
}
|
||||
}
|
||||
|
||||
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 = {
|
||||
let mut db = peer_game_db.write().await;
|
||||
let upsert = db.upsert_peer(ack.peer_id.clone(), peer_addr);
|
||||
db.update_peer_library(
|
||||
&ack.peer_id,
|
||||
ack.library_rev,
|
||||
ack.library_digest,
|
||||
ack.features.clone(),
|
||||
);
|
||||
upsert
|
||||
};
|
||||
|
||||
if upsert.is_new {
|
||||
if let Err(e) = tx_notify_ui.send(PeerEvent::PeerDiscovered(peer_addr)) {
|
||||
log::error!("Failed to send PeerDiscovered event: {e}");
|
||||
}
|
||||
|
||||
let current_peer_count = { peer_game_db.read().await.get_peer_addresses().len() };
|
||||
if let Err(e) = tx_notify_ui.send(PeerEvent::PeerCountUpdated(current_peer_count)) {
|
||||
log::error!("Failed to send PeerCountUpdated event: {e}");
|
||||
}
|
||||
|
||||
let summary = {
|
||||
let library_guard = local_library.read().await;
|
||||
build_library_summary(&library_guard)
|
||||
};
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = send_library_summary(peer_addr, summary).await {
|
||||
log::warn!("Failed to send library summary to {peer_addr}: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(update) =
|
||||
select_library_update(&local_library, ack.library_rev, ack.library_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(e) = result {
|
||||
log::warn!("Failed to send library update to {peer_addr}: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct MdnsPeerInfo {
|
||||
addr: SocketAddr,
|
||||
peer_id: Option<PeerId>,
|
||||
proto_ver: Option<u32>,
|
||||
library_rev: u64,
|
||||
library_digest: u64,
|
||||
}
|
||||
|
||||
fn parse_mdns_peer(service: &MdnsService) -> MdnsPeerInfo {
|
||||
let peer_id = service.properties.get("peer_id").cloned();
|
||||
let proto_ver = service
|
||||
.properties
|
||||
.get("proto_ver")
|
||||
.and_then(|value| value.parse::<u32>().ok());
|
||||
let library_rev = service
|
||||
.properties
|
||||
.get("library_rev")
|
||||
.and_then(|value| value.parse::<u64>().ok())
|
||||
.unwrap_or(0);
|
||||
let library_digest = service
|
||||
.properties
|
||||
.get("library_digest")
|
||||
.and_then(|value| value.parse::<u64>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
MdnsPeerInfo {
|
||||
addr: service.addr,
|
||||
peer_id,
|
||||
proto_ver,
|
||||
library_rev,
|
||||
library_digest,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
.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);
|
||||
if let Err(e) = tx_notify_ui.send(PeerEvent::PeerDiscovered(info.addr)) {
|
||||
log::error!("Failed to send PeerDiscovered event: {e}");
|
||||
}
|
||||
|
||||
let current_peer_count = ctx.peer_game_db.read().await.get_peer_addresses().len();
|
||||
if let Err(e) = tx_notify_ui.send(PeerEvent::PeerCountUpdated(current_peer_count)) {
|
||||
log::error!("Failed to send PeerCountUpdated event: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
if upsert.is_new || upsert.addr_changed {
|
||||
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_clone = tx_notify_ui.clone();
|
||||
let peer_id_hint = Some(peer_id.clone());
|
||||
|
||||
tokio::spawn(async move {
|
||||
let handshake_result =
|
||||
if info.proto_ver.is_none() || info.proto_ver == Some(PROTOCOL_VERSION) {
|
||||
perform_handshake_with_peer(
|
||||
peer_id_arc,
|
||||
local_library,
|
||||
peer_game_db.clone(),
|
||||
tx_notify_ui_clone.clone(),
|
||||
info.addr,
|
||||
peer_id_hint.clone(),
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Err(eyre::eyre!("Skipping hello for legacy peer"))
|
||||
};
|
||||
|
||||
if handshake_result.is_err()
|
||||
&& let Err(e) =
|
||||
request_games_from_peer(info.addr, tx_notify_ui_clone, peer_game_db, 0).await
|
||||
{
|
||||
log::error!("Failed to request games from peer {}: {e}", info.addr);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles a bidirectional stream from a peer.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn handle_peer_stream(
|
||||
@@ -183,22 +498,124 @@ async fn handle_peer_stream(
|
||||
let request = Request::decode(data.freeze());
|
||||
log::debug!("{remote_addr:?} msg: {request:?}");
|
||||
|
||||
if let Some(addr) = remote_addr {
|
||||
ctx.peer_game_db
|
||||
.write()
|
||||
.await
|
||||
.update_last_seen_by_addr(&addr);
|
||||
}
|
||||
|
||||
match request {
|
||||
Request::Ping => {
|
||||
// Respond with pong
|
||||
if let Err(e) = framed_tx.send(Response::Pong.encode()).await {
|
||||
log::error!("Failed to send pong: {e}");
|
||||
}
|
||||
}
|
||||
Request::Hello(hello) => {
|
||||
if hello.peer_id == *ctx.peer_id {
|
||||
log::trace!("Ignoring hello from self");
|
||||
let ack = build_hello_ack(&ctx).await;
|
||||
if let Err(e) = framed_tx.send(Response::HelloAck(ack).encode()).await {
|
||||
log::error!("Failed to send HelloAck: {e}");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if hello.proto_ver != PROTOCOL_VERSION {
|
||||
log::warn!(
|
||||
"Incompatible protocol from {remote_addr:?}: {}",
|
||||
hello.proto_ver
|
||||
);
|
||||
let ack = build_hello_ack(&ctx).await;
|
||||
if let Err(e) = framed_tx.send(Response::HelloAck(ack).encode()).await {
|
||||
log::error!("Failed to send HelloAck: {e}");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let upsert = if let Some(addr) = remote_addr {
|
||||
let mut db = ctx.peer_game_db.write().await;
|
||||
let upsert = db.upsert_peer(hello.peer_id.clone(), addr);
|
||||
db.update_peer_library(
|
||||
&hello.peer_id,
|
||||
hello.library_rev,
|
||||
hello.library_digest,
|
||||
hello.features.clone(),
|
||||
);
|
||||
upsert
|
||||
} else {
|
||||
PeerUpsert {
|
||||
is_new: false,
|
||||
addr_changed: false,
|
||||
}
|
||||
};
|
||||
|
||||
if upsert.is_new
|
||||
&& let Some(addr) = remote_addr
|
||||
{
|
||||
if let Err(e) = ctx.tx_notify_ui.send(PeerEvent::PeerDiscovered(addr)) {
|
||||
log::error!("Failed to send PeerDiscovered event: {e}");
|
||||
}
|
||||
|
||||
let current_peer_count =
|
||||
{ ctx.peer_game_db.read().await.get_peer_addresses().len() };
|
||||
if let Err(e) = ctx
|
||||
.tx_notify_ui
|
||||
.send(PeerEvent::PeerCountUpdated(current_peer_count))
|
||||
{
|
||||
log::error!("Failed to send PeerCountUpdated event: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
let ack = build_hello_ack(&ctx).await;
|
||||
if let Err(e) = framed_tx.send(Response::HelloAck(ack).encode()).await {
|
||||
log::error!("Failed to send HelloAck: {e}");
|
||||
}
|
||||
|
||||
if let Some(addr) = remote_addr {
|
||||
if upsert.is_new {
|
||||
let summary = {
|
||||
let library_guard = ctx.local_library.read().await;
|
||||
build_library_summary(&library_guard)
|
||||
};
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = send_library_summary(addr, summary).await {
|
||||
log::warn!("Failed to send library summary to {addr}: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(update) = select_library_update(
|
||||
&ctx.local_library,
|
||||
hello.library_rev,
|
||||
hello.library_digest,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tokio::spawn(async move {
|
||||
let result = match update {
|
||||
LibraryUpdate::Delta(delta) => {
|
||||
send_library_delta(addr, delta).await
|
||||
}
|
||||
LibraryUpdate::Snapshot(snapshot) => {
|
||||
send_library_snapshot(addr, snapshot).await
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
log::warn!("Failed to send library update to {addr}: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Request::ListGames => {
|
||||
// Return list of games from this peer
|
||||
log::info!("Received ListGames request from peer");
|
||||
let snapshot = {
|
||||
let db_guard = ctx.local_game_db.read().await;
|
||||
if let Some(ref db) = *db_guard {
|
||||
db.all_games().into_iter().cloned().collect::<Vec<Game>>()
|
||||
} else {
|
||||
// Local database not loaded yet, return empty result
|
||||
log::info!(
|
||||
"Local game database not yet loaded, responding with empty game list"
|
||||
);
|
||||
@@ -219,6 +636,94 @@ async fn handle_peer_stream(
|
||||
log::error!("Failed to send ListGames response: {e}");
|
||||
}
|
||||
}
|
||||
Request::LibrarySummary(summary) => {
|
||||
if let Some(addr) = remote_addr {
|
||||
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 {
|
||||
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();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = 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}: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Request::LibrarySnapshot(snapshot) => {
|
||||
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);
|
||||
}
|
||||
|
||||
emit_peer_game_list(&ctx.peer_game_db, &ctx.tx_notify_ui).await;
|
||||
}
|
||||
}
|
||||
Request::LibraryDelta(delta) => {
|
||||
if let Some(addr) = remote_addr {
|
||||
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 {
|
||||
emit_peer_game_list(&ctx.peer_game_db, &ctx.tx_notify_ui).await;
|
||||
} else {
|
||||
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();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = perform_handshake_with_peer(
|
||||
peer_id_arc,
|
||||
local_library,
|
||||
peer_game_db,
|
||||
tx_notify_ui,
|
||||
addr,
|
||||
Some(peer_id),
|
||||
)
|
||||
.await
|
||||
{
|
||||
log::warn!("Failed to resync library from {addr}: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Request::GetGame { id } => {
|
||||
log::info!("Received GetGame request for {id} from peer");
|
||||
let downloading = ctx.downloading_games.read().await.contains(&id);
|
||||
@@ -274,10 +779,8 @@ async fn handle_peer_stream(
|
||||
let maybe_game_dir = ctx.game_dir.read().await.clone();
|
||||
if let Some(game_dir) = maybe_game_dir {
|
||||
let base_dir = PathBuf::from(game_dir);
|
||||
// For file data, we need the raw stream, so we unwrap the FramedWrite
|
||||
let mut tx = framed_tx.into_inner();
|
||||
send_game_file_data(&desc, &mut tx, &base_dir).await;
|
||||
// Re-wrap for next iteration (though usually stream closes after file transfer)
|
||||
framed_tx = FramedWrite::new(tx, LengthDelimitedCodec::new());
|
||||
} else if let Err(e) = framed_tx
|
||||
.send(
|
||||
@@ -305,7 +808,6 @@ async fn handle_peer_stream(
|
||||
let maybe_game_dir = ctx.game_dir.read().await.clone();
|
||||
if let Some(game_dir) = maybe_game_dir {
|
||||
let base_dir = PathBuf::from(game_dir);
|
||||
// For file data, we need the raw stream, so we unwrap the FramedWrite
|
||||
let mut tx = framed_tx.into_inner();
|
||||
send_game_file_chunk(
|
||||
&game_id,
|
||||
@@ -316,7 +818,6 @@ async fn handle_peer_stream(
|
||||
&base_dir,
|
||||
)
|
||||
.await;
|
||||
// Re-wrap for next iteration
|
||||
framed_tx = FramedWrite::new(tx, LengthDelimitedCodec::new());
|
||||
} else if let Err(e) = framed_tx
|
||||
.send(
|
||||
@@ -331,6 +832,28 @@ async fn handle_peer_stream(
|
||||
log::error!("Failed to send GetGameFileChunk error: {e}");
|
||||
}
|
||||
}
|
||||
Request::Goodbye { peer_id } => {
|
||||
log::info!("Received Goodbye from peer {peer_id}");
|
||||
let removed = { ctx.peer_game_db.write().await.remove_peer(&peer_id) };
|
||||
if removed.is_some() {
|
||||
if let Some(addr) = remote_addr {
|
||||
if let Err(e) = ctx.tx_notify_ui.send(PeerEvent::PeerLost(addr)) {
|
||||
log::error!("Failed to send PeerLost event: {e}");
|
||||
}
|
||||
|
||||
let current_peer_count =
|
||||
{ ctx.peer_game_db.read().await.get_peer_addresses().len() };
|
||||
if let Err(e) = ctx
|
||||
.tx_notify_ui
|
||||
.send(PeerEvent::PeerCountUpdated(current_peer_count))
|
||||
{
|
||||
log::error!("Failed to send PeerCountUpdated event: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
emit_peer_game_list(&ctx.peer_game_db, &ctx.tx_notify_ui).await;
|
||||
}
|
||||
}
|
||||
Request::Invalid(_, _) => {
|
||||
log::error!("Received invalid request from peer");
|
||||
}
|
||||
@@ -340,9 +863,18 @@ async fn handle_peer_stream(
|
||||
games.len()
|
||||
);
|
||||
if let Some(addr) = remote_addr {
|
||||
let peer_id = ensure_peer_id_for_addr(&ctx.peer_game_db, addr).await;
|
||||
let summaries: Vec<_> = games.iter().map(summary_from_game).collect();
|
||||
let mut map = HashMap::with_capacity(summaries.len());
|
||||
for summary in &summaries {
|
||||
map.insert(summary.id.clone(), summary.clone());
|
||||
}
|
||||
let digest = compute_library_digest(&map);
|
||||
let aggregated_games = {
|
||||
let mut db = ctx.peer_game_db.write().await;
|
||||
db.update_peer_games(addr, games);
|
||||
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()
|
||||
};
|
||||
|
||||
@@ -376,24 +908,20 @@ async fn handle_peer_stream(
|
||||
// =============================================================================
|
||||
|
||||
/// Runs the peer discovery service using mDNS.
|
||||
pub async fn run_peer_discovery(
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
peer_game_db: Arc<RwLock<PeerGameDB>>,
|
||||
local_peer_addr: Arc<RwLock<Option<SocketAddr>>>,
|
||||
) {
|
||||
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 (addr_tx, mut addr_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
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(addr) = browser.next_address(None)? {
|
||||
if addr_tx.send(addr).is_err() {
|
||||
if let Some(service) = browser.next_service(None)? {
|
||||
if service_tx.send(service).is_err() {
|
||||
log::debug!("Peer discovery consumer dropped; stopping worker");
|
||||
break;
|
||||
}
|
||||
@@ -405,55 +933,14 @@ pub async fn run_peer_discovery(
|
||||
Ok(())
|
||||
});
|
||||
|
||||
while let Some(peer_addr) = addr_rx.recv().await {
|
||||
let is_self = {
|
||||
let guard = local_peer_addr.read().await;
|
||||
guard.as_ref().is_some_and(|addr| *addr == peer_addr)
|
||||
};
|
||||
|
||||
if is_self {
|
||||
log::trace!("Ignoring self advertisement at {peer_addr}");
|
||||
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;
|
||||
}
|
||||
|
||||
let is_new_peer = {
|
||||
let mut db = peer_game_db.write().await;
|
||||
if db.contains_peer(&peer_addr) {
|
||||
db.update_last_seen(&peer_addr);
|
||||
false
|
||||
} else {
|
||||
db.add_peer(peer_addr);
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
if is_new_peer {
|
||||
log::info!("Discovered peer at: {peer_addr}");
|
||||
|
||||
if let Err(e) = tx_notify_ui.send(PeerEvent::PeerDiscovered(peer_addr)) {
|
||||
log::error!("Failed to send PeerDiscovered event: {e}");
|
||||
}
|
||||
|
||||
let current_peer_count = { peer_game_db.read().await.get_peer_addresses().len() };
|
||||
if let Err(e) = tx_notify_ui.send(PeerEvent::PeerCountUpdated(current_peer_count)) {
|
||||
log::error!("Failed to send PeerCountUpdated event: {e}");
|
||||
}
|
||||
|
||||
let tx_notify_ui_clone = tx_notify_ui.clone();
|
||||
let peer_game_db_clone = peer_game_db.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = request_games_from_peer(
|
||||
peer_addr,
|
||||
tx_notify_ui_clone,
|
||||
peer_game_db_clone,
|
||||
0,
|
||||
)
|
||||
.await
|
||||
{
|
||||
log::error!("Failed to request games from peer {peer_addr}: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
handle_discovered_peer(info, &ctx, &tx_notify_ui).await;
|
||||
}
|
||||
|
||||
match worker_handle.await {
|
||||
@@ -491,9 +978,21 @@ async fn request_games_from_peer(
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut map = HashMap::with_capacity(games.len());
|
||||
let mut summaries = Vec::with_capacity(games.len());
|
||||
for game in &games {
|
||||
let summary = summary_from_game(game);
|
||||
map.insert(summary.id.clone(), summary.clone());
|
||||
summaries.push(summary);
|
||||
}
|
||||
let digest = compute_library_digest(&map);
|
||||
let peer_id = ensure_peer_id_for_addr(&peer_game_db, peer_addr).await;
|
||||
|
||||
let aggregated_games = {
|
||||
let mut db = peer_game_db.write().await;
|
||||
db.update_peer_games(peer_addr, games);
|
||||
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()
|
||||
};
|
||||
|
||||
@@ -521,7 +1020,8 @@ pub async fn run_ping_service(
|
||||
) {
|
||||
log::info!(
|
||||
"Starting ping service ({PEER_PING_INTERVAL_SECS}s interval, \
|
||||
{}s timeout)",
|
||||
{}s idle threshold, {}s timeout)",
|
||||
crate::config::PEER_PING_IDLE_SECS,
|
||||
peer_stale_timeout().as_secs()
|
||||
);
|
||||
|
||||
@@ -530,9 +1030,13 @@ pub async fn run_ping_service(
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
let peer_addresses = { peer_game_db.read().await.get_peer_addresses() };
|
||||
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(crate::config::PEER_PING_IDLE_SECS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for peer_addr in peer_addresses {
|
||||
let tx_notify_ui_clone = tx_notify_ui.clone();
|
||||
let peer_game_db_clone = peer_game_db.clone();
|
||||
let downloading_games_clone = downloading_games.clone();
|
||||
@@ -542,26 +1046,19 @@ pub async fn run_ping_service(
|
||||
match ping_peer(peer_addr).await {
|
||||
Ok(is_alive) => {
|
||||
if is_alive {
|
||||
// Update last seen time
|
||||
peer_game_db_clone
|
||||
.write()
|
||||
.await
|
||||
.update_last_seen(&peer_addr);
|
||||
peer_game_db_clone.write().await.update_last_seen(&peer_id);
|
||||
} else {
|
||||
log::warn!("Peer {peer_addr} failed ping check");
|
||||
|
||||
// Remove stale peer
|
||||
let removed_peer =
|
||||
peer_game_db_clone.write().await.remove_peer(&peer_addr);
|
||||
if removed_peer.is_some() {
|
||||
log::info!("Removed stale peer: {peer_addr}");
|
||||
peer_game_db_clone.write().await.remove_peer(&peer_id);
|
||||
if let Some(peer) = removed_peer {
|
||||
log::info!("Removed stale peer: {}", peer.addr);
|
||||
if let Err(e) =
|
||||
tx_notify_ui_clone.send(PeerEvent::PeerLost(peer_addr))
|
||||
tx_notify_ui_clone.send(PeerEvent::PeerLost(peer.addr))
|
||||
{
|
||||
log::error!("Failed to send PeerLost event: {e}");
|
||||
}
|
||||
|
||||
// Send updated peer count
|
||||
let current_peer_count =
|
||||
{ peer_game_db_clone.read().await.get_peer_addresses().len() };
|
||||
if let Err(e) = tx_notify_ui_clone
|
||||
@@ -583,17 +1080,14 @@ pub async fn run_ping_service(
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to ping peer {peer_addr}: {e}");
|
||||
|
||||
// Remove peer on error
|
||||
let removed_peer = peer_game_db_clone.write().await.remove_peer(&peer_addr);
|
||||
if removed_peer.is_some() {
|
||||
log::info!("Removed peer due to ping error: {peer_addr}");
|
||||
if let Err(e) = tx_notify_ui_clone.send(PeerEvent::PeerLost(peer_addr))
|
||||
let removed_peer = peer_game_db_clone.write().await.remove_peer(&peer_id);
|
||||
if let Some(peer) = removed_peer {
|
||||
log::info!("Removed peer due to ping error: {}", peer.addr);
|
||||
if let Err(e) = tx_notify_ui_clone.send(PeerEvent::PeerLost(peer.addr))
|
||||
{
|
||||
log::error!("Failed to send PeerLost event: {e}");
|
||||
}
|
||||
|
||||
// Send updated peer count
|
||||
let current_peer_count =
|
||||
{ peer_game_db_clone.read().await.get_peer_addresses().len() };
|
||||
if let Err(e) = tx_notify_ui_clone
|
||||
@@ -621,14 +1115,14 @@ pub async fn run_ping_service(
|
||||
peer_game_db
|
||||
.read()
|
||||
.await
|
||||
.get_stale_peers(peer_stale_timeout())
|
||||
.get_stale_peer_ids(peer_stale_timeout())
|
||||
};
|
||||
let mut removed_any = false;
|
||||
for stale_addr in stale_peers {
|
||||
let removed_peer = peer_game_db.write().await.remove_peer(&stale_addr);
|
||||
if removed_peer.is_some() {
|
||||
log::info!("Removed stale peer: {stale_addr}");
|
||||
if let Err(e) = tx_notify_ui.send(PeerEvent::PeerLost(stale_addr)) {
|
||||
for stale_peer_id in stale_peers {
|
||||
let removed_peer = peer_game_db.write().await.remove_peer(&stale_peer_id);
|
||||
if let Some(peer) = removed_peer {
|
||||
log::info!("Removed stale peer: {}", peer.addr);
|
||||
if let Err(e) = tx_notify_ui.send(PeerEvent::PeerLost(peer.addr)) {
|
||||
log::error!("Failed to send PeerLost event: {e}");
|
||||
}
|
||||
|
||||
@@ -718,9 +1212,9 @@ pub async fn run_local_game_monitor(tx_notify_ui: UnboundedSender<PeerEvent>, ct
|
||||
};
|
||||
|
||||
if let Some(ref game_dir) = game_dir {
|
||||
match scan_local_games(game_dir).await {
|
||||
Ok(current_games) => {
|
||||
update_and_announce_games(&ctx, &tx_notify_ui, current_games).await;
|
||||
match scan_local_library(game_dir).await {
|
||||
Ok(scan) => {
|
||||
update_and_announce_games(&ctx, &tx_notify_ui, scan).await;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to scan local games directory: {e}");
|
||||
|
||||
Reference in New Issue
Block a user