use std::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tracing::{debug, warn};
use super::discovery::DaemonInfo;
pub const PONG_TOKEN: &str = "hyperdb-mcp";
fn pong_response() -> String {
format!("PONG {PONG_TOKEN} {}\n", crate::version::MCP_VERSION)
}
#[derive(Debug)]
pub struct HealthListener {
listener: TcpListener,
pub port: u16,
}
#[derive(Debug)]
pub struct DaemonState {
pub last_activity: Mutex<Instant>,
pub shutdown: AtomicBool,
pub restart_requested: AtomicBool,
}
impl Default for DaemonState {
fn default() -> Self {
Self::new()
}
}
impl DaemonState {
pub fn new() -> Self {
Self {
last_activity: Mutex::new(Instant::now()),
shutdown: AtomicBool::new(false),
restart_requested: AtomicBool::new(false),
}
}
pub fn touch(&self) {
*self.last_activity.lock().expect("mutex poisoned") = Instant::now();
}
pub fn idle_duration(&self) -> Duration {
self.last_activity.lock().expect("mutex poisoned").elapsed()
}
pub fn request_shutdown(&self) {
self.shutdown.store(true, Ordering::Release);
}
pub fn should_shutdown(&self) -> bool {
self.shutdown.load(Ordering::Acquire)
}
pub fn request_restart(&self) {
self.restart_requested.store(true, Ordering::Release);
}
pub fn consume_restart_request(&self) -> bool {
self.restart_requested.swap(false, Ordering::AcqRel)
}
}
impl HealthListener {
pub fn bind(port: u16) -> std::io::Result<Self> {
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], port));
let listener = TcpListener::bind(addr)?;
listener.set_nonblocking(true)?;
let port = listener.local_addr()?.port();
Ok(Self { listener, port })
}
#[expect(
clippy::needless_pass_by_value,
reason = "Arcs are cloned into per-connection threads"
)]
pub fn run(self, state: Arc<DaemonState>, info: Arc<Mutex<DaemonInfo>>) {
loop {
if state.should_shutdown() {
break;
}
match self.listener.accept() {
Ok((stream, _addr)) => {
let state = Arc::clone(&state);
let info = Arc::clone(&info);
std::thread::spawn(move || {
handle_client(stream, &state, &info);
});
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
std::thread::sleep(Duration::from_millis(100));
}
Err(e) => {
warn!(error = %e, "health listener accept error");
std::thread::sleep(Duration::from_millis(500));
}
}
}
debug!("health listener shut down");
}
}
#[expect(
clippy::needless_pass_by_value,
reason = "TcpStream must be owned for BufReader"
)]
fn handle_client(stream: TcpStream, state: &DaemonState, info: &Mutex<DaemonInfo>) {
let _ = stream.set_read_timeout(Some(Duration::from_secs(5)));
let mut reader = BufReader::new(&stream);
let mut writer = &stream;
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line) {
Ok(0) => break,
Ok(_) => {
let cmd = line.trim();
let response = match cmd {
"PING" => pong_response(),
"HEARTBEAT" => {
state.touch();
"OK\n".to_string()
}
"STOP" => {
state.request_shutdown();
"STOPPING\n".to_string()
}
"STATUS" => {
let snapshot = info.lock().expect("DaemonInfo mutex poisoned").clone();
let json = serde_json::to_string(&snapshot).unwrap_or_default();
format!("{json}\n")
}
"REPORT_HYPERD_ERROR" => {
state.request_restart();
"OK\n".to_string()
}
_ => "ERR unknown command\n".to_string(),
};
if writer.write_all(response.as_bytes()).is_err() {
break;
}
}
Err(_) => break,
}
}
}
pub fn send_command(port: u16, command: &str) -> std::io::Result<String> {
send_command_with_timeout(
port,
command,
Duration::from_secs(2),
Duration::from_secs(5),
)
}
pub fn report_hyperd_error_to_daemon() {
let port = super::discovery::resolve_port();
let timeout = Duration::from_millis(200);
match send_command_with_timeout(port, "REPORT_HYPERD_ERROR", timeout, timeout) {
Ok(response) => {
debug!(response = %response.trim(), "reported hyperd error to daemon");
}
Err(e) => {
debug!(error = %e, "could not report hyperd error to daemon (best-effort)");
}
}
}
pub fn send_command_with_timeout(
port: u16,
command: &str,
connect_timeout: Duration,
read_timeout: Duration,
) -> std::io::Result<String> {
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], port));
let mut stream = TcpStream::connect_timeout(&addr, connect_timeout)?;
stream.set_read_timeout(Some(read_timeout))?;
let msg = format!("{command}\n");
stream.write_all(msg.as_bytes())?;
stream.flush()?;
let mut reader = BufReader::new(&stream);
let mut response = String::new();
reader.read_line(&mut response)?;
Ok(response)
}
pub fn ping_identified(
port: u16,
connect_timeout: Duration,
read_timeout: Duration,
) -> Option<String> {
let response = send_command_with_timeout(port, "PING", connect_timeout, read_timeout).ok()?;
let mut tokens = response.split_whitespace();
if tokens.next() != Some("PONG") || tokens.next() != Some(PONG_TOKEN) {
return None;
}
Some(tokens.next().unwrap_or("").to_string())
}