Files
OpenPXE/crates/core/src/client.rs
T
2026-05-21 02:13:08 -04:00

108 lines
3.1 KiB
Rust

//! In-memory client state registry — the "who has contacted us" table the
//! web UI displays. Not persisted: PXE sessions are ephemeral by nature.
use crate::arch::ClientArch;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::Arc;
use time::OffsetDateTime;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ClientEvent {
DhcpDiscover,
DhcpRequest,
PxeBootServerRequest,
TftpRead { file: String },
HttpScriptFetch { target: String },
HttpIsoAsset { file: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientSnapshot {
pub mac: String,
pub last_ip: Option<IpAddr>,
pub arch: Option<ClientArch>,
pub hostname: Option<String>,
#[serde(with = "time::serde::rfc3339")]
pub first_seen: OffsetDateTime,
#[serde(with = "time::serde::rfc3339")]
pub last_seen: OffsetDateTime,
// Events are left with default serialization (9-tuple) — they're
// diagnostic only and not consumed by the UI today.
pub events: Vec<(OffsetDateTime, ClientEvent)>,
/// The boot target (ISO id) last selected via the iPXE menu, if any.
pub selected_target: Option<String>,
}
#[derive(Debug, Default)]
pub struct ClientRegistry {
inner: RwLock<HashMap<String, ClientSnapshot>>,
}
impl ClientRegistry {
#[must_use]
pub fn new() -> Arc<Self> {
Arc::new(Self::default())
}
pub fn record(
&self,
mac: &str,
ip: Option<IpAddr>,
arch: Option<ClientArch>,
event: ClientEvent,
) {
let mut guard = self.inner.write();
let now = OffsetDateTime::now_utc();
let entry = guard
.entry(mac.to_string())
.or_insert_with(|| ClientSnapshot {
mac: mac.to_string(),
last_ip: ip,
arch,
hostname: None,
first_seen: now,
last_seen: now,
events: Vec::new(),
selected_target: None,
});
entry.last_seen = now;
if ip.is_some() {
entry.last_ip = ip;
}
if arch.is_some() {
entry.arch = arch;
}
entry.events.push((now, event));
// Cap event history per client to keep memory bounded.
const MAX_EVENTS: usize = 64;
if entry.events.len() > MAX_EVENTS {
let drop_n = entry.events.len() - MAX_EVENTS;
entry.events.drain(..drop_n);
}
}
pub fn set_selected_target(&self, mac: &str, target: Option<String>) {
let mut guard = self.inner.write();
if let Some(c) = guard.get_mut(mac) {
c.selected_target = target;
}
}
#[must_use]
pub fn list(&self) -> Vec<ClientSnapshot> {
let guard = self.inner.read();
let mut v: Vec<_> = guard.values().cloned().collect();
// Reverse-chronological by last-seen (most recent first).
v.sort_by_key(|c| std::cmp::Reverse(c.last_seen));
v
}
#[must_use]
pub fn get(&self, mac: &str) -> Option<ClientSnapshot> {
self.inner.read().get(mac).cloned()
}
}