refactor(peer): adopt structured concurrency with supervised shutdown
Replace the detached tokio::spawn pattern in the peer runtime with a
supervised model built on tokio_util's CancellationToken and TaskTracker.
Long-lived services and child tasks now have an explicit parent, a
cancellation path, and a join point. Tauri can request a clean shutdown
on app exit instead of leaking work into process termination.
Background
~~~~~~~~~~
start_peer() previously returned only a command sender. The four startup
services (QUIC server, mDNS discovery, peer liveness, local library
monitor) and their child tasks (ping workers, handshake jobs, download
workers, announcement fan-outs, connection/stream handlers) were spawned
with raw tokio::spawn and detached. Closing the command channel sent
Goodbye notifications but did not stop those services. The mDNS blocking
worker had no cancellation path at all. Active downloads were stored as
JoinHandle<()> and force-aborted, which could interrupt file writes
mid-chunk.
Supervisor
~~~~~~~~~~
The runtime now owns a CancellationToken and a TaskTracker, threaded
through Ctx and PeerCtx. Each long-lived service is spawned through a
small supervisor (spawn_supervised_service) that wraps the service in
catch_unwind and enforces an explicit SupervisionPolicy:
QuicServer: Required (fatal; cancels the runtime if it dies)
Discovery: Restart(5s) (matches the prior self-restart loop)
Liveness: Restart(5s)
LocalMonitor: BestEffort (logs and exits, no restart)
A Required failure emits a new RuntimeFailed { component, error } event
to the UI and cancels the runtime; the command loop and goodbye
notifications still run to completion. The Tauri layer forwards the
event as "peer-runtime-failed" so a future UI can surface it.
mDNS cancellation
~~~~~~~~~~~~~~~~~
MdnsBrowser previously blocked on receiver.recv() forever. It now
exposes next_service_timeout(Duration) returning an MdnsServicePoll
enum (Service/Timeout/Closed) via recv_timeout(). The discovery worker
polls at 250ms and checks the shutdown flag between ticks, so
cancellation reaches the blocking thread within one poll interval
instead of waiting for the next mDNS event.
Downloads
~~~~~~~~~
active_downloads is now HashMap<String, CancellationToken>. Each
download gets a child token of the runtime shutdown, checked at chunk
and peer-attempt boundaries (never inside file writes). When all peers
with a game disappear, liveness cancels the token and emits
DownloadGameFilesAllPeersGone; the download exits Ok(()) without
emitting a duplicate Failed event.
DownloadStateGuard (context.rs) is held inside the download task and
clears downloading_games + active_downloads on Drop, covering the happy
path, error returns, cancellation, and task abort. Drop falls back to
spawning the cleanup if write-lock contention prevents try_write.
Public API and Tauri integration
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
start_peer() now returns PeerRuntimeHandle exposing:
fn sender(&self) -> UnboundedSender<PeerCommand>
fn shutdown(&self)
async fn wait_stopped(&mut self)
The Tauri layer stores the handle in managed state and switches its
main loop from .run(ctx) to .build(ctx).run(|h, e| ...). On
RunEvent::Exit it calls handle.shutdown() and blocks up to 2s on
wait_stopped(), giving services time to cancel and Goodbye packets time
to flush over a healthy LAN while staying short enough not to delay
process exit noticeably on a dead network.
The command loop distinguishes graceful shutdown from unexpected
channel closure: if recv() returns None and shutdown.is_cancelled() is
set, the loop returns Ok(()) silently. Only an unexpected close (no
cancellation observed) still emits RuntimeFailed. This avoids a
spurious failure event on every normal app close.
User-visible behavior changes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Closing the app no longer leaks services into process termination;
Goodbye notifications are reliably attempted before exit.
- Downloads cancel cleanly (between chunks) instead of force-aborting
mid-write.
- A new "peer-runtime-failed" Tauri event fires when a Required service
cannot recover. No frontend handler exists yet — that is a follow-up.
Tradeoffs
~~~~~~~~~
- Workspace tokio-util now requires the "rt" feature for TaskTracker.
- The mDNS worker still runs in spawn_blocking and may stay parked
briefly between 250ms polls — acceptable for a desktop app.
- The 2s shutdown timeout on app exit is a deliberate compromise.
Tests
~~~~~
New unit tests:
- DownloadStateGuard clears tracking on completion, cancellation, and
parent-task abort (context.rs).
- Required failure cancels the runtime and emits RuntimeFailed
(startup.rs).
- Restart policy restarts until shutdown is requested (startup.rs).
- PeerRuntimeHandle.shutdown() observable via wait_stopped()
(startup.rs).
- Peers-gone cancellation emits only PeersGone, no duplicate Failed
(services/liveness.rs).
Test plan
~~~~~~~~~
cargo test --workspace
cargo clippy --workspace --all-targets
Manual smoke test on two peers on the same LAN:
1. Start a download, verify chunks transfer.
2. Close the receiving app mid-download — verify the sending peer
logs a Goodbye, not a connection-reset error.
3. Stop the sending peer mid-download — verify the receiver emits
DownloadGameFilesAllPeersGone, not Failed.
Follow-ups
~~~~~~~~~~
- Frontend handler for "peer-runtime-failed".
- Consider exposing the runtime handle's stopped watch to the frontend
for a reconnecting indicator on Required failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,15 +2,16 @@
|
||||
|
||||
use std::{collections::HashMap, net::SocketAddr, time::Duration};
|
||||
|
||||
use lanspread_mdns::{LANSPREAD_SERVICE_TYPE, MdnsAdvertiser};
|
||||
use lanspread_mdns::{DaemonEvent, LANSPREAD_SERVICE_TYPE, MdnsAdvertiser, MdnsMonitor};
|
||||
use lanspread_proto::PROTOCOL_VERSION;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{context::PeerCtx, network::select_advertise_ip};
|
||||
|
||||
pub(super) async fn start_mdns_advertiser(
|
||||
ctx: &PeerCtx,
|
||||
server_addr: SocketAddr,
|
||||
) -> eyre::Result<()> {
|
||||
) -> eyre::Result<MdnsAdvertiser> {
|
||||
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}");
|
||||
@@ -36,22 +37,34 @@ pub(super) async fn start_mdns_advertiser(
|
||||
})
|
||||
.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:?}");
|
||||
log::info!("Registered mDNS service with name: {monitor_name}");
|
||||
Ok(mdns)
|
||||
}
|
||||
|
||||
pub(super) async fn monitor_mdns_events(monitor: MdnsMonitor, shutdown: CancellationToken) {
|
||||
loop {
|
||||
let event = tokio::select! {
|
||||
() = shutdown.cancelled() => break,
|
||||
event = monitor.recv_async() => event,
|
||||
};
|
||||
|
||||
match event {
|
||||
Ok(DaemonEvent::Error(err)) => {
|
||||
log::error!("mDNS error: {err}");
|
||||
tokio::select! {
|
||||
() = shutdown.cancelled() => break,
|
||||
() = tokio::time::sleep(Duration::from_secs(1)) => {}
|
||||
}
|
||||
}
|
||||
Ok(other_event) => {
|
||||
log::trace!("mDNS event: {other_event:?}");
|
||||
}
|
||||
Err(err) => {
|
||||
log::debug!("mDNS monitor channel closed: {err}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn advertised_service_name(hostname: &str, peer_id: &str) -> String {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use lanspread_mdns::{LANSPREAD_SERVICE_TYPE, MdnsBrowser, MdnsService};
|
||||
use lanspread_mdns::{LANSPREAD_SERVICE_TYPE, MdnsBrowser, MdnsService, MdnsServicePoll};
|
||||
use lanspread_proto::PROTOCOL_VERSION;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
@@ -23,54 +23,73 @@ struct MdnsPeerInfo {
|
||||
}
|
||||
|
||||
/// Runs the peer discovery service using mDNS.
|
||||
pub async fn run_peer_discovery(tx_notify_ui: UnboundedSender<PeerEvent>, ctx: Ctx) {
|
||||
pub async fn run_peer_discovery(
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
ctx: Ctx,
|
||||
) -> eyre::Result<()> {
|
||||
log::info!("Starting peer discovery task");
|
||||
|
||||
let service_type = LANSPREAD_SERVICE_TYPE.to_string();
|
||||
let (service_tx, mut service_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let worker_shutdown = ctx.shutdown.clone();
|
||||
let service_type_clone = service_type.clone();
|
||||
|
||||
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 worker_handle = ctx
|
||||
.task_tracker
|
||||
.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");
|
||||
while !worker_shutdown.is_cancelled() {
|
||||
match browser.next_service_timeout(None, Duration::from_millis(250))? {
|
||||
MdnsServicePoll::Service(service) => {
|
||||
if service_tx.send(service).is_err() {
|
||||
log::debug!("Peer discovery consumer dropped; stopping worker");
|
||||
break;
|
||||
}
|
||||
}
|
||||
MdnsServicePoll::Timeout => {}
|
||||
MdnsServicePoll::Closed => {
|
||||
log::warn!("mDNS browser closed; stopping peer discovery 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;
|
||||
}
|
||||
loop {
|
||||
tokio::select! {
|
||||
() = ctx.shutdown.cancelled() => break,
|
||||
service = service_rx.recv() => {
|
||||
let Some(service) = service else {
|
||||
break;
|
||||
};
|
||||
|
||||
handle_discovered_peer(info, &ctx, &tx_notify_ui).await;
|
||||
}
|
||||
let info = parse_mdns_peer(&service);
|
||||
if is_self_advertisement(&info, &ctx).await {
|
||||
log::trace!("Ignoring self advertisement at {}", info.addr);
|
||||
continue;
|
||||
}
|
||||
|
||||
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}");
|
||||
handle_discovered_peer(info, &ctx, &tx_notify_ui).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
match worker_handle.await {
|
||||
Ok(Ok(())) if ctx.shutdown.is_cancelled() => Ok(()),
|
||||
Ok(Ok(())) => {
|
||||
eyre::bail!("mDNS discovery worker exited unexpectedly");
|
||||
}
|
||||
Ok(Err(err)) if ctx.shutdown.is_cancelled() => {
|
||||
log::debug!("Peer discovery worker stopped during shutdown: {err}");
|
||||
Ok(())
|
||||
}
|
||||
Ok(Err(err)) => Err(err.wrap_err("peer discovery worker failed")),
|
||||
Err(err) if ctx.shutdown.is_cancelled() => {
|
||||
log::debug!("Peer discovery worker join ended during shutdown: {err}");
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(eyre::eyre!("peer discovery worker join error: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +165,7 @@ fn spawn_protocol_negotiation(
|
||||
let local_library = ctx.local_library.clone();
|
||||
let peer_game_db = ctx.peer_game_db.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
ctx.task_tracker.spawn(async move {
|
||||
let handshake_result = if proto_ver.is_none() || proto_ver == Some(PROTOCOL_VERSION) {
|
||||
perform_handshake_with_peer(
|
||||
peer_id_arc,
|
||||
|
||||
@@ -31,11 +31,7 @@ pub(super) async fn request_games_from_peer(
|
||||
}
|
||||
|
||||
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",
|
||||
);
|
||||
events::send(&tx_notify_ui, PeerEvent::ListGames(aggregated_games));
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,8 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use tokio::{
|
||||
sync::{RwLock, mpsc::UnboundedSender},
|
||||
task::JoinHandle,
|
||||
};
|
||||
use tokio::sync::{RwLock, mpsc::UnboundedSender};
|
||||
use tokio_util::{sync::CancellationToken, task::TaskTracker};
|
||||
|
||||
use crate::{
|
||||
PeerEvent,
|
||||
@@ -24,8 +22,10 @@ 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<()>>>>,
|
||||
) {
|
||||
active_downloads: Arc<RwLock<HashMap<String, CancellationToken>>>,
|
||||
shutdown: CancellationToken,
|
||||
task_tracker: TaskTracker,
|
||||
) -> eyre::Result<()> {
|
||||
log::info!(
|
||||
"Starting ping service ({PEER_PING_INTERVAL_SECS}s interval, \
|
||||
{}s idle threshold, {}s timeout)",
|
||||
@@ -36,12 +36,18 @@ pub async fn run_ping_service(
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(PEER_PING_INTERVAL_SECS));
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
tokio::select! {
|
||||
() = shutdown.cancelled() => return Ok(()),
|
||||
_ = interval.tick() => {}
|
||||
}
|
||||
|
||||
ping_idle_peers(
|
||||
&peer_game_db,
|
||||
&downloading_games,
|
||||
&active_downloads,
|
||||
&tx_notify_ui,
|
||||
&shutdown,
|
||||
&task_tracker,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -58,8 +64,10 @@ pub async fn run_ping_service(
|
||||
async fn ping_idle_peers(
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
downloading_games: &Arc<RwLock<HashSet<String>>>,
|
||||
active_downloads: &Arc<RwLock<HashMap<String, JoinHandle<()>>>>,
|
||||
active_downloads: &Arc<RwLock<HashMap<String, CancellationToken>>>,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
shutdown: &CancellationToken,
|
||||
task_tracker: &TaskTracker,
|
||||
) {
|
||||
let peer_snapshots = { peer_game_db.read().await.peer_liveness_snapshot() };
|
||||
|
||||
@@ -72,9 +80,15 @@ async fn ping_idle_peers(
|
||||
let peer_game_db = peer_game_db.clone();
|
||||
let downloading_games = downloading_games.clone();
|
||||
let active_downloads = active_downloads.clone();
|
||||
let shutdown = shutdown.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
match ping_peer(peer_addr).await {
|
||||
task_tracker.spawn(async move {
|
||||
let ping_result = tokio::select! {
|
||||
() = shutdown.cancelled() => return,
|
||||
result = ping_peer(peer_addr) => result,
|
||||
};
|
||||
|
||||
match ping_result {
|
||||
Ok(true) => {
|
||||
peer_game_db.write().await.update_last_seen(&peer_id);
|
||||
}
|
||||
@@ -110,7 +124,7 @@ async fn ping_idle_peers(
|
||||
async fn prune_stale_peers(
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
downloading_games: &Arc<RwLock<HashSet<String>>>,
|
||||
active_downloads: &Arc<RwLock<HashMap<String, JoinHandle<()>>>>,
|
||||
active_downloads: &Arc<RwLock<HashMap<String, CancellationToken>>>,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
) {
|
||||
let stale_peers = {
|
||||
@@ -140,7 +154,7 @@ async fn prune_stale_peers(
|
||||
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<()>>>>,
|
||||
active_downloads: &Arc<RwLock<HashMap<String, CancellationToken>>>,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
peer_id: PeerId,
|
||||
log_label: &str,
|
||||
@@ -176,7 +190,7 @@ async fn remove_peer(
|
||||
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<()>>>>,
|
||||
active_downloads: &Arc<RwLock<HashMap<String, CancellationToken>>>,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
) {
|
||||
let active_ids = {
|
||||
@@ -196,23 +210,14 @@ async fn handle_active_downloads_without_peers(
|
||||
continue;
|
||||
}
|
||||
|
||||
let removed_from_tracking = {
|
||||
let mut guard = downloading_games.write().await;
|
||||
guard.remove(&id)
|
||||
};
|
||||
|
||||
if !removed_from_tracking {
|
||||
let Some(cancel_token) = active_downloads.write().await.remove(&id) else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(handle) = { active_downloads.write().await.remove(&id) } {
|
||||
handle.abort();
|
||||
}
|
||||
};
|
||||
cancel_token.cancel();
|
||||
|
||||
events::send(
|
||||
tx_notify_ui,
|
||||
PeerEvent::DownloadGameFilesAllPeersGone { id },
|
||||
"DownloadGameFilesAllPeersGone",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -221,3 +226,50 @@ async fn peers_still_have_game(peer_game_db: &Arc<RwLock<PeerGameDB>>, game_id:
|
||||
let guard = peer_game_db.read().await;
|
||||
!guard.peers_with_game(game_id).is_empty()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use super::handle_active_downloads_without_peers;
|
||||
use crate::{PeerEvent, peer_db::PeerGameDB};
|
||||
|
||||
#[tokio::test]
|
||||
async fn all_peers_gone_cancels_download_and_emits_only_peers_gone() {
|
||||
let peer_game_db = Arc::new(RwLock::new(PeerGameDB::new()));
|
||||
let downloading_games = Arc::new(RwLock::new(HashSet::from(["game".to_string()])));
|
||||
let cancel = CancellationToken::new();
|
||||
let active_downloads = Arc::new(RwLock::new(HashMap::from([(
|
||||
"game".to_string(),
|
||||
cancel.clone(),
|
||||
)])));
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
handle_active_downloads_without_peers(
|
||||
&peer_game_db,
|
||||
&downloading_games,
|
||||
&active_downloads,
|
||||
&tx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(cancel.is_cancelled());
|
||||
assert!(!active_downloads.read().await.contains_key("game"));
|
||||
|
||||
let event = rx.recv().await.expect("peers-gone event should be emitted");
|
||||
assert!(matches!(
|
||||
event,
|
||||
PeerEvent::DownloadGameFilesAllPeersGone { id } if id == "game"
|
||||
));
|
||||
assert!(
|
||||
rx.try_recv().is_err(),
|
||||
"peers-gone cancellation must not emit a duplicate failure event"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@ use crate::{
|
||||
};
|
||||
|
||||
/// Monitors the local game directory for changes.
|
||||
pub async fn run_local_game_monitor(tx_notify_ui: UnboundedSender<PeerEvent>, ctx: Ctx) {
|
||||
pub async fn run_local_game_monitor(
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
ctx: Ctx,
|
||||
) -> eyre::Result<()> {
|
||||
log::info!(
|
||||
"Starting local game directory monitor ({LOCAL_GAME_MONITOR_INTERVAL_SECS}s interval)"
|
||||
);
|
||||
@@ -21,7 +24,10 @@ pub async fn run_local_game_monitor(tx_notify_ui: UnboundedSender<PeerEvent>, ct
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(LOCAL_GAME_MONITOR_INTERVAL_SECS));
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
tokio::select! {
|
||||
() = ctx.shutdown.cancelled() => return Ok(()),
|
||||
_ = interval.tick() => {}
|
||||
}
|
||||
|
||||
let game_dir = { ctx.game_dir.read().await.clone() };
|
||||
match scan_local_library(&game_dir).await {
|
||||
|
||||
@@ -10,7 +10,10 @@ use crate::{
|
||||
config::{CERT_PEM, KEY_PEM},
|
||||
context::PeerCtx,
|
||||
events,
|
||||
services::{advertise::start_mdns_advertiser, stream::handle_peer_stream},
|
||||
services::{
|
||||
advertise::{monitor_mdns_events, start_mdns_advertiser},
|
||||
stream::handle_peer_stream,
|
||||
},
|
||||
};
|
||||
|
||||
/// Runs the QUIC server and mDNS advertiser.
|
||||
@@ -32,20 +35,33 @@ pub async fn run_server_component(
|
||||
let server_addr = server.local_addr()?;
|
||||
log::info!("Peer server listening on {server_addr}");
|
||||
|
||||
start_mdns_advertiser(&ctx, server_addr).await?;
|
||||
let mdns_advertiser = start_mdns_advertiser(&ctx, server_addr).await?;
|
||||
let mdns_monitor = mdns_advertiser.monitor.clone();
|
||||
let mdns_shutdown = ctx.shutdown.clone();
|
||||
ctx.task_tracker.spawn(async move {
|
||||
monitor_mdns_events(mdns_monitor, mdns_shutdown).await;
|
||||
});
|
||||
|
||||
loop {
|
||||
let connection = tokio::select! {
|
||||
() = ctx.shutdown.cancelled() => return Ok(()),
|
||||
connection = server.accept() => connection,
|
||||
};
|
||||
|
||||
let Some(connection) = connection else {
|
||||
eyre::bail!("QUIC server accept loop ended unexpectedly");
|
||||
};
|
||||
|
||||
while let Some(connection) = server.accept().await {
|
||||
let ctx = ctx.clone();
|
||||
let tx_notify_ui = tx_notify_ui.clone();
|
||||
let task_tracker = ctx.task_tracker.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
task_tracker.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(
|
||||
@@ -55,25 +71,27 @@ async fn handle_peer_connection(
|
||||
) -> eyre::Result<()> {
|
||||
let remote_addr = connection.remote_addr()?;
|
||||
log::info!("{remote_addr} peer connected");
|
||||
events::send(
|
||||
&tx_notify_ui,
|
||||
PeerEvent::PeerConnected(remote_addr),
|
||||
"PeerConnected",
|
||||
);
|
||||
events::send(&tx_notify_ui, PeerEvent::PeerConnected(remote_addr));
|
||||
|
||||
loop {
|
||||
let stream = tokio::select! {
|
||||
() = ctx.shutdown.cancelled() => break,
|
||||
stream = connection.accept_bidirectional_stream() => stream,
|
||||
};
|
||||
|
||||
let Some(stream) = stream? else {
|
||||
break;
|
||||
};
|
||||
|
||||
while let Ok(Some(stream)) = connection.accept_bidirectional_stream().await {
|
||||
let ctx = ctx.clone();
|
||||
tokio::spawn(async move {
|
||||
let task_tracker = ctx.task_tracker.clone();
|
||||
task_tracker.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",
|
||||
);
|
||||
events::send(&tx_notify_ui, PeerEvent::PeerDisconnected(remote_addr));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -38,7 +38,12 @@ pub(super) async fn handle_peer_stream(
|
||||
log::trace!("{remote_addr:?} peer stream opened");
|
||||
|
||||
loop {
|
||||
match framed_rx.next().await {
|
||||
let next_message = tokio::select! {
|
||||
() = ctx.shutdown.cancelled() => break,
|
||||
next_message = framed_rx.next() => next_message,
|
||||
};
|
||||
|
||||
match next_message {
|
||||
Some(Ok(data)) => {
|
||||
log::trace!(
|
||||
"{:?} msg: (raw): {}",
|
||||
@@ -191,7 +196,7 @@ async fn handle_library_summary(
|
||||
}
|
||||
|
||||
if summary.library_digest != previous_digest || previous_count == 0 {
|
||||
tokio::spawn({
|
||||
ctx.task_tracker.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();
|
||||
@@ -357,10 +362,6 @@ async fn handle_announce_games(ctx: &PeerCtx, remote_addr: Option<SocketAddr>, g
|
||||
|
||||
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",
|
||||
);
|
||||
events::send(&ctx.tx_notify_ui, PeerEvent::ListGames(aggregated_games));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user