use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
use anyhow::{Context, Result};
use crate::ipc;
fn data_dir() -> PathBuf {
dirs::data_local_dir()
.unwrap_or_else(|| dirs::home_dir().unwrap_or_default().join(".local/share"))
.join("lean-ctx")
}
pub fn daemon_pid_path() -> PathBuf {
data_dir().join("daemon.pid")
}
pub fn daemon_addr() -> ipc::DaemonAddr {
ipc::DaemonAddr::default_for_current_os()
}
pub fn is_daemon_running() -> bool {
let pid_path = daemon_pid_path();
let Ok(contents) = fs::read_to_string(&pid_path) else {
return false;
};
let Ok(pid) = contents.trim().parse::<u32>() else {
return false;
};
if ipc::process::is_alive(pid) {
return true;
}
let _ = fs::remove_file(&pid_path);
ipc::cleanup(&daemon_addr());
false
}
pub fn read_daemon_pid() -> Option<u32> {
let contents = fs::read_to_string(daemon_pid_path()).ok()?;
contents.trim().parse::<u32>().ok()
}
pub fn start_daemon(args: &[String]) -> Result<()> {
if is_daemon_running() {
let pid = read_daemon_pid().unwrap_or(0);
anyhow::bail!("Daemon already running (PID {pid}). Use --stop to stop it first.");
}
ipc::cleanup(&daemon_addr());
if let Ok(data_dir) = crate::core::data_dir::lean_ctx_data_dir() {
crate::config_io::cleanup_legacy_backups(&data_dir);
}
let exe = std::env::current_exe().context("cannot determine own executable path")?;
let mut cmd_args = vec!["serve".to_string()];
for arg in args {
if arg == "--daemon" || arg == "-d" {
continue;
}
cmd_args.push(arg.clone());
}
cmd_args.push("--_foreground-daemon".to_string());
let log_dir = data_dir();
let _ = fs::create_dir_all(&log_dir);
let stderr_log = log_dir.join("daemon-stderr.log");
let stderr_file = fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&stderr_log);
let stderr_cfg = match stderr_file {
Ok(f) => std::process::Stdio::from(f),
Err(_) => std::process::Stdio::inherit(),
};
let child = Command::new(&exe)
.args(&cmd_args)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(stderr_cfg)
.spawn()
.with_context(|| format!("failed to spawn daemon: {}", exe.display()))?;
let pid = child.id();
write_pid_file(pid)?;
std::thread::sleep(std::time::Duration::from_millis(200));
if !ipc::process::is_alive(pid) {
let _ = fs::remove_file(daemon_pid_path());
let stderr_content = fs::read_to_string(&stderr_log).unwrap_or_default();
let stderr_trimmed = stderr_content.trim();
if stderr_trimmed.is_empty() {
anyhow::bail!("Daemon process exited immediately. Check logs for errors.");
}
anyhow::bail!("Daemon process exited immediately:\n{stderr_trimmed}");
}
let addr = daemon_addr();
if crate::core::protocol::meta_visible() {
eprintln!(
"lean-ctx daemon started (PID {pid})\n Endpoint: {}\n PID file: {}",
addr.display(),
daemon_pid_path().display()
);
}
Ok(())
}
pub fn stop_daemon() -> Result<()> {
let pid_path = daemon_pid_path();
let Some(pid) = read_daemon_pid() else {
eprintln!("No daemon PID file found. Nothing to stop.");
return Ok(());
};
if !ipc::process::is_alive(pid) {
eprintln!("Daemon (PID {pid}) is not running. Cleaning up stale files.");
ipc::cleanup(&daemon_addr());
let _ = fs::remove_file(&pid_path);
return Ok(());
}
let http_shutdown_ok = try_http_shutdown();
if http_shutdown_ok {
for _ in 0..30 {
std::thread::sleep(std::time::Duration::from_millis(100));
if !ipc::process::is_alive(pid) {
break;
}
}
}
if ipc::process::is_alive(pid) {
let _ = ipc::process::terminate_gracefully(pid);
for _ in 0..20 {
std::thread::sleep(std::time::Duration::from_millis(100));
if !ipc::process::is_alive(pid) {
break;
}
}
}
if ipc::process::is_alive(pid) {
eprintln!("Daemon (PID {pid}) did not stop gracefully, force killing.");
let _ = ipc::process::force_kill(pid);
std::thread::sleep(std::time::Duration::from_millis(100));
}
let _ = fs::remove_file(&pid_path);
ipc::cleanup(&daemon_addr());
eprintln!("lean-ctx daemon stopped (PID {pid}).");
Ok(())
}
fn try_http_shutdown() -> bool {
let Ok(rt) = tokio::runtime::Runtime::new() else {
return false;
};
rt.block_on(async {
crate::daemon_client::daemon_request("POST", "/v1/shutdown", "")
.await
.is_ok()
})
}
pub fn daemon_status() -> String {
let addr = daemon_addr();
if let Some(pid) = read_daemon_pid() {
if ipc::process::is_alive(pid) {
let listening = addr.is_listening();
return format!(
"Daemon running (PID {pid})\n Endpoint: {} ({})\n PID file: {}",
addr.display(),
if listening { "ready" } else { "missing" },
daemon_pid_path().display()
);
}
return format!("Daemon not running (stale PID file for PID {pid})");
}
"Daemon not running".to_string()
}
fn write_pid_file(pid: u32) -> Result<()> {
let pid_path = daemon_pid_path();
if let Some(parent) = pid_path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("cannot create dir: {}", parent.display()))?;
}
let mut f = fs::File::create(&pid_path)
.with_context(|| format!("cannot write PID file: {}", pid_path.display()))?;
write!(f, "{pid}")?;
Ok(())
}
pub fn init_foreground_daemon() -> Result<()> {
let pid = std::process::id();
write_pid_file(pid)?;
Ok(())
}
pub fn cleanup_daemon_files() {
let _ = fs::remove_file(daemon_pid_path());
ipc::cleanup(&daemon_addr());
}