use super::*;
use rusqlite::OptionalExtension;
pub(in crate::daemon) fn init_state_db(path: &Path) -> Result<()> {
let conn = rusqlite::Connection::open(path)?;
conn.execute_batch(DAEMON_SCHEMA)?;
Ok(())
}
pub(in crate::daemon) fn read_pid(pid_file: &Path) -> Option<u32> {
std::fs::read_to_string(pid_file).ok()?.trim().parse().ok()
}
pub fn is_running(pid_file: &Path) -> bool {
read_pid(pid_file).is_some_and(process_alive)
}
#[cfg(target_os = "linux")]
pub(in crate::daemon) fn process_alive(pid: u32) -> bool {
std::path::Path::new(&format!("/proc/{pid}")).exists()
}
#[cfg(not(target_os = "linux"))]
pub(in crate::daemon) fn process_alive(_pid: u32) -> bool {
false
}
pub fn health(state_db: &Path, pid_file: &Path, now_iso: &str) -> serde_json::Value {
use serde_json::json;
let running = read_pid(pid_file).is_some_and(process_alive);
let pid = read_pid(pid_file);
if !state_db.exists() {
return json!({"state": "never_run", "running": running, "pid": pid});
}
let conn = match rusqlite::Connection::open_with_flags(
state_db,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
) {
Ok(c) => c,
Err(e) => {
return json!({"state": "unknown", "running": running, "pid": pid, "error": e.to_string()})
}
};
let read = || -> rusqlite::Result<serde_json::Value> {
let cutoff_24h = days_ago_iso(now_iso, 1);
let cutoff_7d = days_ago_iso(now_iso, 7);
let mut by_op = serde_json::Map::new();
{
let mut stmt = conn.prepare(
"SELECT operation, COUNT(*) FROM daemon_errors
WHERE ts >= ?1 GROUP BY operation ORDER BY COUNT(*) DESC",
)?;
let rows =
stmt.query_map([&cutoff_7d], |r| Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)?)))?;
for row in rows.flatten() {
by_op.insert(row.0, json!(row.1));
}
}
let errors_24h: i64 =
conn.query_row("SELECT COUNT(*) FROM daemon_errors WHERE ts >= ?1", [&cutoff_24h], |r| {
r.get(0)
})?;
let errors_7d: i64 =
conn.query_row("SELECT COUNT(*) FROM daemon_errors WHERE ts >= ?1", [&cutoff_7d], |r| {
r.get(0)
})?;
let last_error = conn
.query_row(
"SELECT operation, message, ts FROM daemon_errors ORDER BY ts DESC LIMIT 1",
[],
|r| {
Ok(json!({
"operation": r.get::<_, String>(0)?,
"message": r.get::<_, String>(1)?.chars().take(200).collect::<String>(),
"ts": r.get::<_, String>(2)?,
}))
},
)
.optional()?
.unwrap_or(serde_json::Value::Null);
let processed: i64 = conn
.query_row("SELECT COUNT(*) FROM processed_events", [], |r| r.get(0))
.unwrap_or(0);
let mut watches = Vec::new();
{
let mut stmt =
conn.prepare("SELECT watch_path, last_processed_offset, updated_at FROM watch_state")?;
let rows = stmt.query_map([], |r| {
Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)?, r.get::<_, String>(2)?))
})?;
for row in rows.flatten() {
let size = std::fs::metadata(&row.0).map(|m| m.len() as i64).unwrap_or(-1);
let lag = if size >= 0 { (size - row.1).max(0) } else { -1 };
watches.push(json!({
"path": row.0, "offset": row.1, "lag_bytes": lag, "updated_at": row.2,
}));
}
}
Ok(json!({
"state": if running { "running" } else { "stopped" },
"running": running,
"pid": pid,
"errors_by_operation": by_op,
"errors_24h": errors_24h,
"errors_7d": errors_7d,
"last_error": last_error,
"processed_events": processed,
"watches": watches,
}))
};
match read() {
Ok(v) => v,
Err(e) => json!({"state": "unknown", "running": running, "pid": pid, "error": e.to_string()}),
}
}
fn days_ago_iso(now_iso: &str, days: i64) -> String {
use chrono::{DateTime, Duration, Utc};
match now_iso.parse::<DateTime<Utc>>() {
Ok(dt) => (dt - Duration::days(days))
.format("%Y-%m-%dT%H:%M:%S%.3fZ")
.to_string(),
Err(_) => now_iso.to_string(),
}
}