use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tokio::signal;
use tracing::{error, info, warn};
use hyperdb_api::{HyperProcess, Parameters, TransportMode};
use super::discovery::{self, DaemonInfo};
use super::health::{DaemonState, HealthListener};
use super::ENV_IDLE_TIMEOUT;
#[derive(Debug)]
pub struct DaemonConfig {
pub port: u16,
pub idle_timeout: Option<Duration>,
}
impl DaemonConfig {
pub fn from_args(port: u16, idle_timeout_secs: Option<u64>) -> Self {
let idle_timeout = idle_timeout_secs
.or_else(|| {
std::env::var(ENV_IDLE_TIMEOUT)
.ok()
.and_then(|v| v.parse().ok())
})
.map(Duration::from_secs);
Self { port, idle_timeout }
}
}
pub const RESTART_WINDOW: Duration = Duration::from_secs(60);
pub const RESTART_LIMIT: usize = 3;
const HYPERD_POLL_INTERVAL: Duration = Duration::from_secs(5);
struct HyperState {
hyper: Option<HyperProcess>,
restart_history: Vec<Instant>,
}
#[derive(Debug)]
enum RestartError {
TooManyRestarts,
SpawnFailed(String),
}
pub async fn run_daemon(config: DaemonConfig) -> Result<(), Box<dyn std::error::Error>> {
let listener = HealthListener::bind(config.port).map_err(|e| {
if e.kind() == std::io::ErrorKind::AddrInUse {
format!(
"Another hyperdb daemon is already running on port {}. \
Use `hyperdb-mcp daemon status` to check or `hyperdb-mcp daemon stop` to stop it.",
config.port
)
} else {
format!("Failed to bind health port {}: {e}", config.port)
}
})?;
let bound_port = listener.port;
info!(port = bound_port, "daemon health listener bound");
let hyper = HyperProcess::new(None, Some(&build_params()?))?;
let endpoint = hyper
.endpoint()
.ok_or("hyperd did not report an endpoint")?
.to_string();
info!(endpoint = %endpoint, "hyperd started");
let info = DaemonInfo {
pid: std::process::id(),
hyperd_endpoint: endpoint.clone(),
health_port: bound_port,
started_at: chrono::Utc::now().to_rfc3339(),
version: env!("CARGO_PKG_VERSION").to_string(),
};
discovery::write_discovery_file(&info)?;
info!(path = %discovery::discovery_file_path()?.display(), "discovery file written");
let state = Arc::new(DaemonState::new());
let info_arc = Arc::new(Mutex::new(info));
let hyper_state = Arc::new(Mutex::new(HyperState {
hyper: Some(hyper),
restart_history: Vec::new(),
}));
let health_state = Arc::clone(&state);
let health_info = Arc::clone(&info_arc);
let health_handle = std::thread::spawn(move || {
listener.run(health_state, health_info);
});
if let Some(d) = config.idle_timeout {
info!(idle_timeout_secs = d.as_secs(), "idle shutdown enabled");
} else {
info!("idle shutdown disabled (daemon will stay resident)");
}
let idle_fut = async {
match config.idle_timeout {
Some(d) => idle_monitor(Arc::clone(&state), d).await,
None => std::future::pending::<()>().await,
}
};
tokio::select! {
() = idle_fut => {}
() = hyperd_monitor(Arc::clone(&state), Arc::clone(&hyper_state), Arc::clone(&info_arc)) => {}
() = shutdown_signal() => {
info!("received shutdown signal");
}
}
state.request_shutdown();
info!("shutting down daemon");
discovery::remove_discovery_file();
let _ = health_handle.join();
drop(hyper_state);
Ok(())
}
#[derive(Debug, PartialEq, Eq)]
pub enum RestartAttempt {
Recorded,
LimitExceeded,
}
pub fn try_record_restart_attempt(history: &mut Vec<Instant>, now: Instant) -> RestartAttempt {
history.retain(|t| now.duration_since(*t) < RESTART_WINDOW);
if history.len() >= RESTART_LIMIT {
return RestartAttempt::LimitExceeded;
}
history.push(now);
RestartAttempt::Recorded
}
fn build_params() -> std::io::Result<Parameters> {
let log_dir = discovery::state_dir()?.join("logs");
std::fs::create_dir_all(&log_dir)?;
let mut params = Parameters::new();
params.set("log_file_max_count", "2");
params.set("log_file_size_limit", "100M");
params.set("log_dir", log_dir.to_string_lossy().as_ref());
params.set_transport_mode(TransportMode::Tcp);
Ok(params)
}
async fn idle_monitor(state: Arc<DaemonState>, idle_timeout: Duration) {
loop {
tokio::time::sleep(Duration::from_secs(10)).await;
if state.should_shutdown() {
return;
}
if state.idle_duration() >= idle_timeout {
info!(
idle_secs = idle_timeout.as_secs(),
"idle timeout reached, shutting down"
);
return;
}
}
}
async fn hyperd_monitor(
state: Arc<DaemonState>,
hyper_state: Arc<Mutex<HyperState>>,
info_arc: Arc<Mutex<DaemonInfo>>,
) {
loop {
tokio::time::sleep(HYPERD_POLL_INTERVAL).await;
if state.should_shutdown() {
return;
}
let needs_restart = {
let mut guard = hyper_state.lock().expect("HyperState mutex poisoned");
let process_dead = guard.hyper.as_mut().map_or(true, HyperProcess::has_exited);
if process_dead {
let _ = state.consume_restart_request();
true
} else {
state.consume_restart_request()
}
};
if !needs_restart {
continue;
}
match try_restart_hyperd(&hyper_state, &info_arc) {
Ok(new_endpoint) => {
info!(endpoint = %new_endpoint, "hyperd restarted");
let _ = state.consume_restart_request();
}
Err(RestartError::TooManyRestarts) => {
error!(
limit = RESTART_LIMIT,
window_secs = RESTART_WINDOW.as_secs(),
"hyperd restart limit exceeded — daemon shutting down"
);
return;
}
Err(RestartError::SpawnFailed(e)) => {
warn!(error = %e, "hyperd spawn failed during restart; will retry on next tick");
}
}
}
}
fn try_restart_hyperd(
hyper_state: &Mutex<HyperState>,
info_arc: &Mutex<DaemonInfo>,
) -> Result<String, RestartError> {
let mut guard = hyper_state.lock().expect("HyperState mutex poisoned");
if try_record_restart_attempt(&mut guard.restart_history, Instant::now())
== RestartAttempt::LimitExceeded
{
return Err(RestartError::TooManyRestarts);
}
guard.hyper = None;
let params = build_params().map_err(|e| RestartError::SpawnFailed(e.to_string()))?;
let new_hyper = HyperProcess::new(None, Some(¶ms))
.map_err(|e| RestartError::SpawnFailed(e.to_string()))?;
let new_endpoint = new_hyper
.endpoint()
.ok_or_else(|| RestartError::SpawnFailed("hyperd did not report endpoint".into()))?
.to_string();
let snapshot = {
let mut info_guard = info_arc.lock().expect("DaemonInfo mutex poisoned");
info_guard.hyperd_endpoint.clone_from(&new_endpoint);
info_guard.clone()
};
discovery::write_discovery_file(&snapshot)
.map_err(|e| RestartError::SpawnFailed(format!("discovery write: {e}")))?;
guard.hyper = Some(new_hyper);
Ok(new_endpoint)
}
async fn shutdown_signal() {
let ctrl_c = signal::ctrl_c();
#[cfg(unix)]
{
let mut sigterm =
signal::unix::signal(signal::unix::SignalKind::terminate()).expect("sigterm handler");
tokio::select! {
_ = ctrl_c => {}
_ = sigterm.recv() => {}
}
}
#[cfg(not(unix))]
{
ctrl_c.await.ok();
}
}