feat(client): identify lifecycle leave events
The relay now sends enough PeerJoined events for the client to learn the peers already in the room, but the client was still formatting PeerLeft with only the bare peer id. That made a gateway disconnect look the same as any other peer leaving. Keep a small in-memory peer map in the control-event logger. PeerJoined records or refreshes the peer identity, and PeerLeft removes it before formatting the message. Unknown leaves still use the generic fallback, so out-of-order or missed events remain understandable. Test Plan: - cargo fmt --check - cargo test -p lanparty-client-win formats_relay_lifecycle_events \ -- --nocapture - cargo test -p lanparty-client-win - cargo clippy -p lanparty-client-win --all-targets -- -D warnings - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check Refs: PLAN.md
This commit is contained in:
@@ -167,4 +167,6 @@ before bridging if the driver-reported MAC does not match the tunnel identity.
|
|||||||
It prints client diagnostics snapshots with relay reachability, route-pinning,
|
It prints client diagnostics snapshots with relay reachability, route-pinning,
|
||||||
QUIC datagram budget, TAP status/IP, frame/datagram counters, and drops.
|
QUIC datagram budget, TAP status/IP, frame/datagram counters, and drops.
|
||||||
Relay lifecycle events are logged as they arrive, including gateway joins and
|
Relay lifecycle events are logged as they arrive, including gateway joins and
|
||||||
peer leaves.
|
peer leaves. The client remembers peer identities from join and catch-up events
|
||||||
|
so later leave logs can identify a disconnected LAN gateway or client MAC when
|
||||||
|
that peer was known.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::{fs, net::SocketAddr, path::PathBuf};
|
use std::{collections::BTreeMap, fs, net::SocketAddr, path::PathBuf};
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use std::{
|
use std::{
|
||||||
sync::{Arc, mpsc},
|
sync::{Arc, mpsc},
|
||||||
@@ -22,7 +22,7 @@ use lanparty_client_route::{
|
|||||||
};
|
};
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use lanparty_client_tap::TapAdapter;
|
use lanparty_client_tap::TapAdapter;
|
||||||
use lanparty_ctrl::{ControlMessage, Role, RoomCode};
|
use lanparty_ctrl::{ControlMessage, PeerInfo, Role, RoomCode};
|
||||||
use lanparty_obs::{ClientDiagnostics, RelayDiagnostics, TapDiagnostics};
|
use lanparty_obs::{ClientDiagnostics, RelayDiagnostics, TapDiagnostics};
|
||||||
use lanparty_proto::MacAddr;
|
use lanparty_proto::MacAddr;
|
||||||
|
|
||||||
@@ -416,24 +416,73 @@ fn optional_label<T: std::fmt::Display>(value: Option<T>) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn run_control_event_log(session: &ClientSession) -> Result<()> {
|
async fn run_control_event_log(session: &ClientSession) -> Result<()> {
|
||||||
|
let mut formatter = ControlEventFormatter::default();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let event = session
|
let event = session
|
||||||
.recv_control_event()
|
.recv_control_event()
|
||||||
.await
|
.await
|
||||||
.context("failed to receive relay control event")?;
|
.context("failed to receive relay control event")?;
|
||||||
println!("{}", format_control_event(&event));
|
println!("{}", formatter.format(&event));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
fn format_control_event(event: &ControlMessage) -> String {
|
fn format_control_event(event: &ControlMessage) -> String {
|
||||||
|
ControlEventFormatter::default().format(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct ControlEventFormatter {
|
||||||
|
peers: BTreeMap<u32, PeerInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ControlEventFormatter {
|
||||||
|
fn format(&mut self, event: &ControlMessage) -> String {
|
||||||
match event {
|
match event {
|
||||||
ControlMessage::PeerJoined(peer) if peer.role() == Role::Gateway => {
|
ControlMessage::PeerJoined(peer) => {
|
||||||
|
self.peers.insert(peer.peer_id(), peer.clone());
|
||||||
|
format_peer_joined(peer)
|
||||||
|
}
|
||||||
|
ControlMessage::PeerLeft { peer_id, reason } => {
|
||||||
|
let Some(peer) = self.peers.remove(peer_id) else {
|
||||||
|
return format!("relay event: peer {peer_id} left ({reason:?})");
|
||||||
|
};
|
||||||
|
|
||||||
|
match peer.role() {
|
||||||
|
Role::Gateway => {
|
||||||
|
format!(
|
||||||
|
"relay event: LAN gateway disconnected (peer {}, {reason:?})",
|
||||||
|
peer.peer_id()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Role::Client => {
|
||||||
|
let mac = peer
|
||||||
|
.mac()
|
||||||
|
.map(|mac| mac.to_string())
|
||||||
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
format!(
|
||||||
|
"relay event: client peer {} with MAC {} left ({reason:?})",
|
||||||
|
peer.peer_id(),
|
||||||
|
mac
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => format!("relay event: {event:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_peer_joined(peer: &PeerInfo) -> String {
|
||||||
|
match peer.role() {
|
||||||
|
Role::Gateway => {
|
||||||
format!(
|
format!(
|
||||||
"relay event: LAN gateway connected as peer {}",
|
"relay event: LAN gateway connected as peer {}",
|
||||||
peer.peer_id()
|
peer.peer_id()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ControlMessage::PeerJoined(peer) => {
|
Role::Client => {
|
||||||
let mac = peer
|
let mac = peer
|
||||||
.mac()
|
.mac()
|
||||||
.map(|mac| mac.to_string())
|
.map(|mac| mac.to_string())
|
||||||
@@ -444,10 +493,6 @@ fn format_control_event(event: &ControlMessage) -> String {
|
|||||||
mac
|
mac
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ControlMessage::PeerLeft { peer_id, reason } => {
|
|
||||||
format!("relay event: peer {peer_id} left ({reason:?})")
|
|
||||||
}
|
|
||||||
_ => format!("relay event: {event:?}"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -735,22 +780,39 @@ mod tests {
|
|||||||
let gateway = ControlMessage::PeerJoined(PeerInfo::new(1, Role::Gateway, None).unwrap());
|
let gateway = ControlMessage::PeerJoined(PeerInfo::new(1, Role::Gateway, None).unwrap());
|
||||||
let client =
|
let client =
|
||||||
ControlMessage::PeerJoined(PeerInfo::new(2, Role::Client, Some(mac(2))).unwrap());
|
ControlMessage::PeerJoined(PeerInfo::new(2, Role::Client, Some(mac(2))).unwrap());
|
||||||
let left = ControlMessage::PeerLeft {
|
let unknown_left = ControlMessage::PeerLeft {
|
||||||
|
peer_id: 3,
|
||||||
|
reason: DisconnectReason::Normal,
|
||||||
|
};
|
||||||
|
let client_left = ControlMessage::PeerLeft {
|
||||||
peer_id: 2,
|
peer_id: 2,
|
||||||
reason: DisconnectReason::Normal,
|
reason: DisconnectReason::Normal,
|
||||||
};
|
};
|
||||||
|
let gateway_left = ControlMessage::PeerLeft {
|
||||||
|
peer_id: 1,
|
||||||
|
reason: DisconnectReason::Normal,
|
||||||
|
};
|
||||||
|
let mut formatter = ControlEventFormatter::default();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_control_event(&gateway),
|
formatter.format(&gateway),
|
||||||
"relay event: LAN gateway connected as peer 1"
|
"relay event: LAN gateway connected as peer 1"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_control_event(&client),
|
formatter.format(&client),
|
||||||
"relay event: client peer 2 joined with MAC 02:00:00:00:00:02"
|
"relay event: client peer 2 joined with MAC 02:00:00:00:00:02"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_control_event(&left),
|
format_control_event(&unknown_left),
|
||||||
"relay event: peer 2 left (Normal)"
|
"relay event: peer 3 left (Normal)"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
formatter.format(&client_left),
|
||||||
|
"relay event: client peer 2 with MAC 02:00:00:00:00:02 left (Normal)"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
formatter.format(&gateway_left),
|
||||||
|
"relay event: LAN gateway disconnected (peer 1, Normal)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user