use crate::bridge::fnc_session_store::SessionStoreSummary;
use crate::config::{ResolvedBridgeCommand, parse_duration};
use serde::Serialize;
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct BridgeStatus {
pub mode: String,
pub bind: Option<String>,
pub socket: Option<String>,
pub max_bytes: Option<u64>,
pub ttl_ms: Option<u64>,
pub sessions_active: usize,
pub last_activity_ms_ago: Option<u64>,
pub clipboard_enabled: bool,
pub clipboard_applied: u64,
pub token_required: bool,
pub last_abort_reason: Option<String>,
pub size_violations: u64,
pub ttl_violations: u64,
pub uptime_ms: Option<u64>,
pub ttl_remaining_ms: Option<u64>,
}
pub fn build_status(resolved: &ResolvedBridgeCommand, summary: Option<SessionStoreSummary>) -> BridgeStatus {
let (mode, bind, socket, max_bytes, ttl_str) = match resolved {
ResolvedBridgeCommand::Status(s) => (
s.mode.to_string(),
Some(s.bind.clone()),
Some(s.socket.to_string_lossy().to_string()),
None,
None,
),
ResolvedBridgeCommand::Listen(l) => (
l.mode.to_string(),
Some(l.bind.clone()),
None,
Some(l.max_bytes),
l.ttl.clone(),
),
ResolvedBridgeCommand::Connect(c) => (
c.mode.to_string(),
None,
Some(c.socket.to_string_lossy().to_string()),
Some(c.max_bytes),
None,
),
};
let ttl_ms = ttl_str
.and_then(|s| parse_duration(s.as_str()))
.map(|d| d.as_millis() as u64);
let (sessions_active, last_activity_ms_ago, clipboard_applied, size_violations, ttl_violations, last_abort_reason, uptime_ms) = match summary {
Some(sum) => {
let now = std::time::Instant::now();
let ago = sum.last_activity.map(|ts| now.duration_since(ts)).map(|d| d.as_millis() as u64);
let up = now.duration_since(sum.start_instant).as_millis() as u64;
(
sum.active,
ago,
sum.clipboard_applied,
sum.size_violations,
sum.ttl_violations,
sum.last_abort_reason.clone(),
Some(up),
)
}
None => (0, None, 0, 0, 0, None, None),
};
let token_required = match resolved {
ResolvedBridgeCommand::Listen(l) => l.token.is_some() || l.token_file.is_some(),
ResolvedBridgeCommand::Connect(c) => c.token.is_some() || c.token_file.is_some(),
ResolvedBridgeCommand::Status(_) => false,
};
let ttl_remaining_ms = ttl_ms;
BridgeStatus {
mode,
bind,
socket,
max_bytes,
ttl_ms,
sessions_active,
last_activity_ms_ago,
clipboard_enabled: true,
clipboard_applied,
token_required,
last_abort_reason,
size_violations,
ttl_violations,
uptime_ms,
ttl_remaining_ms,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{Duration, Instant};
#[test]
fn status_reports_clipboard_counts() {
let resolved = ResolvedBridgeCommand::Listen(crate::config::ResolvedBridgeListen {
bind: "127.0.0.1:0".into(),
mode: crate::config::BridgeMode::Tcp,
idle_exit: None,
ttl: Some("1000".into()),
max_bytes: 1024,
token: None,
token_file: None,
daemon: false,
log_file: None,
});
let summary = SessionStoreSummary {
active: 2,
last_activity: Some(Instant::now() - Duration::from_millis(500)),
clipboard_applied: 3,
size_violations: 0,
ttl_violations: 0,
last_abort_reason: None,
start_instant: Instant::now() - Duration::from_secs(10),
};
let status = build_status(&resolved, Some(summary));
assert_eq!(status.sessions_active, 2);
assert_eq!(status.clipboard_applied, 3);
assert!(status.clipboard_enabled);
assert_eq!(status.ttl_ms, Some(1000));
assert_eq!(status.token_required, false);
assert_eq!(status.size_violations, 0);
assert_eq!(status.ttl_violations, 0);
assert_eq!(status.last_abort_reason, None);
}
#[test]
fn status_exposes_violation_counts_and_abort_reason() {
use crate::bridge::fnc_session_store::{SessionStore, SessionStoreConfig};
let cfg = SessionStoreConfig {
default_max_bytes: 1024,
default_ttl: Duration::from_secs(60),
default_idle_exit: Duration::from_secs(60),
spill_threshold: 512,
};
let store = SessionStore::new(cfg);
{
let mut s = store.lock().unwrap();
s.record_size_violation("too big");
s.record_ttl_violation("expired");
s.record_abort_reason("token mismatch");
}
let summary = store.lock().unwrap().summary();
let resolved = ResolvedBridgeCommand::Listen(crate::config::ResolvedBridgeListen {
bind: "127.0.0.1:0".into(),
mode: crate::config::BridgeMode::Tcp,
idle_exit: None,
ttl: None,
max_bytes: 1024,
token: None,
token_file: None,
daemon: false,
log_file: None,
});
let status = build_status(&resolved, Some(summary));
assert_eq!(status.size_violations, 1);
assert_eq!(status.ttl_violations, 1);
assert_eq!(status.last_abort_reason.as_deref(), Some("token mismatch"));
}
}