use crate::utils::clock::Instant;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct ClientId(u32);
impl ClientId {
pub fn new(id: u32) -> Self {
Self(id)
}
pub fn as_u32(&self) -> u32 {
self.0
}
pub fn from_string(s: &str) -> Option<Self> {
let s = s.trim();
let id_str = s.strip_prefix("client-").unwrap_or(s);
id_str.parse::<u32>().ok().map(Self)
}
}
impl std::fmt::Display for ClientId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "client-{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ClientStatus {
Connecting,
Connected,
Disconnected,
Error(String),
}
impl ClientStatus {
pub fn as_str(&self) -> &str {
match self {
Self::Connecting => "Connecting",
Self::Connected => "Connected",
Self::Disconnected => "Disconnected",
Self::Error(_) => "Error",
}
}
}
impl std::fmt::Display for ClientStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Connecting => write!(f, "Connecting"),
Self::Connected => write!(f, "Connected"),
Self::Disconnected => write!(f, "Disconnected"),
Self::Error(msg) => write!(f, "Error: {}", msg),
}
}
}
#[derive(Debug, Clone)]
pub struct ClientConnectionState {
pub id: ClientId,
pub remote_addr: String,
pub connected_addr: Option<SocketAddr>,
pub local_addr: Option<SocketAddr>,
pub bytes_sent: u64,
pub bytes_received: u64,
pub packets_sent: u64,
pub packets_received: u64,
pub last_activity: Instant,
pub status: ClientStatus,
pub status_changed_at: Instant,
pub protocol_info: super::server::ProtocolConnectionInfo,
}
pub const CLIENT_CONNECTION_HISTORY_CAPACITY: usize = 20;
#[derive(Debug, Clone, serde::Serialize)]
pub struct ClientConnectionAttempt {
pub remote_addr: String,
pub started_unix_ms: u64,
pub ended_unix_ms: Option<u64>,
pub outcome: String,
}
fn now_unix_ms() -> u64 {
crate::utils::clock::SystemTime::now()
.duration_since(crate::utils::clock::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
#[derive(Debug, Clone)]
pub struct ClientInstance {
pub id: ClientId,
pub remote_addr: String,
pub protocol_name: String,
pub instruction: String,
pub memory: String,
pub status: ClientStatus,
pub connection: Option<ClientConnectionState>,
pub created_at: Instant,
pub status_changed_at: Instant,
pub startup_params: Option<serde_json::Value>,
pub event_handler_config: Option<crate::scripting::EventHandlerConfig>,
pub protocol_data: serde_json::Value,
pub log_files: HashMap<String, PathBuf>,
pub feedback_instructions: Option<String>,
pub feedback_buffer: Vec<serde_json::Value>,
pub last_feedback_processed: Option<Instant>,
pub connection_history: std::collections::VecDeque<ClientConnectionAttempt>,
}
impl ClientInstance {
pub fn new(
id: ClientId,
remote_addr: String,
protocol_name: String,
instruction: String,
) -> Self {
let now = Instant::now();
Self {
id,
remote_addr,
protocol_name,
instruction,
memory: String::new(),
status: ClientStatus::Connecting,
connection: None,
created_at: now,
status_changed_at: now,
startup_params: None,
event_handler_config: None,
protocol_data: serde_json::Value::Object(serde_json::Map::new()),
log_files: HashMap::new(),
feedback_instructions: None,
feedback_buffer: Vec::new(),
last_feedback_processed: None,
connection_history: std::collections::VecDeque::new(),
}
}
pub fn record_status_transition(&mut self, status: &ClientStatus) {
let live_tail = self
.connection_history
.back_mut()
.filter(|attempt| attempt.ended_unix_ms.is_none());
match status {
ClientStatus::Connecting => {
if let Some(attempt) = live_tail {
attempt.ended_unix_ms = Some(now_unix_ms());
}
self.connection_history.push_back(ClientConnectionAttempt {
remote_addr: self.remote_addr.clone(),
started_unix_ms: now_unix_ms(),
ended_unix_ms: None,
outcome: "connecting".to_string(),
});
let excess = self
.connection_history
.len()
.saturating_sub(CLIENT_CONNECTION_HISTORY_CAPACITY);
for _ in 0..excess {
self.connection_history.pop_front();
}
}
ClientStatus::Connected => {
if let Some(attempt) = live_tail {
attempt.outcome = "connected".to_string();
} else {
self.connection_history.push_back(ClientConnectionAttempt {
remote_addr: self.remote_addr.clone(),
started_unix_ms: now_unix_ms(),
ended_unix_ms: None,
outcome: "connected".to_string(),
});
}
}
ClientStatus::Disconnected => {
if let Some(attempt) = live_tail {
attempt.ended_unix_ms = Some(now_unix_ms());
attempt.outcome = "disconnected".to_string();
}
}
ClientStatus::Error(message) => {
if let Some(attempt) = live_tail {
attempt.ended_unix_ms = Some(now_unix_ms());
attempt.outcome = format!("error: {message}");
}
}
}
}
pub fn get_protocol_data<T: serde::de::DeserializeOwned>(
&self,
) -> Result<T, serde_json::Error> {
serde_json::from_value(self.protocol_data.clone())
}
pub fn set_protocol_data<T: serde::Serialize>(
&mut self,
data: T,
) -> Result<(), serde_json::Error> {
self.protocol_data = serde_json::to_value(data)?;
Ok(())
}
pub fn get_protocol_field(&self, key: &str) -> Option<&serde_json::Value> {
self.protocol_data.get(key)
}
pub fn set_protocol_field(&mut self, key: String, value: serde_json::Value) {
if let Some(obj) = self.protocol_data.as_object_mut() {
obj.insert(key, value);
} else {
let mut map = serde_json::Map::new();
map.insert(key, value);
self.protocol_data = serde_json::Value::Object(map);
}
}
pub fn get_or_create_log_path(&mut self, output_name: &str) -> anyhow::Result<PathBuf> {
crate::state::server::validate_log_output_name(output_name)?;
if let Some(path) = self.log_files.get(output_name) {
return Ok(path.clone());
}
let now = crate::utils::clock::SystemTime::now();
let elapsed = self.created_at.elapsed();
let created_system_time = now - elapsed;
let timestamp = crate::utils::clock::to_local_datetime(created_system_time);
let timestamp_str = timestamp.format("%Y_%m_%d_%H_%M_%S").to_string();
let log_filename = format!("netget_{}_{}.log", output_name, timestamp_str);
let log_path = PathBuf::from(log_filename);
self.log_files
.insert(output_name.to_string(), log_path.clone());
Ok(log_path)
}
pub fn summary(&self) -> String {
let duration = self.created_at.elapsed();
let duration_str = if duration.as_secs() < 60 {
format!("{}s", duration.as_secs())
} else if duration.as_secs() < 3600 {
format!("{}m", duration.as_secs() / 60)
} else {
format!("{}h", duration.as_secs() / 3600)
};
format!(
"#{} {} → {} ({}) - {}",
self.id.as_u32(),
self.protocol_name,
self.remote_addr,
self.status.as_str(),
duration_str
)
}
}