use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use anyhow::{Context, Result};
use crate::ipc;
static IS_FOREGROUND_DAEMON: AtomicBool = AtomicBool::new(false);
pub fn mark_foreground_daemon() {
IS_FOREGROUND_DAEMON.store(true, Ordering::Relaxed);
}
#[must_use]
pub fn is_foreground_daemon() -> bool {
IS_FOREGROUND_DAEMON.load(Ordering::Relaxed)
}
fn data_dir() -> PathBuf {
ipc::endpoint_dir()
}
pub fn daemon_pid_path() -> PathBuf {
data_dir().join("daemon.pid")
}
fn daemon_identity_path() -> PathBuf {
data_dir().join("daemon.identity.json")
}
#[derive(serde::Serialize, serde::Deserialize)]
struct DaemonIdentityFile {
pid: u32,
identity: ipc::process::ProcessIdentity,
}
#[must_use]
pub fn read_daemon_record() -> Option<ipc::process::RecordedPid> {
let pid = read_daemon_pid()?;
let identity = fs::read(daemon_identity_path())
.ok()
.filter(|bytes| bytes.len() <= 16 * 1024)
.and_then(|bytes| serde_json::from_slice::<DaemonIdentityFile>(&bytes).ok())
.filter(|file| file.pid == pid)
.map(|file| file.identity);
Some(ipc::process::RecordedPid { pid, identity })
}
fn remove_daemon_record() {
let _ = fs::remove_file(daemon_pid_path());
let _ = fs::remove_file(daemon_identity_path());
}
#[must_use]
pub fn verified_daemon() -> Option<ipc::process::ServiceProcess> {
let record = read_daemon_record()?;
ipc::process::find_service_processes(
&[record],
ipc::process::ServiceSweep::Only(ipc::process::ServiceKind::Daemon),
)
.into_iter()
.next()
}
fn stop_legacy_daemon() {
if cfg!(test) || crate::core::paths::dir_override_active() {
return;
}
let legacy = ipc::legacy_endpoint_dir();
if legacy == data_dir() {
return;
}
let pid_path = legacy.join("daemon.pid");
let Some(pid) = fs::read_to_string(&pid_path)
.ok()
.and_then(|contents| contents.trim().parse::<u32>().ok())
else {
return;
};
let report = ipc::process::stop_services(
&[ipc::process::RecordedPid::bare(pid)],
ipc::process::ServiceSweep::Only(ipc::process::ServiceKind::Daemon),
);
if !report.stopped.is_empty() {
eprintln!("Stopped the daemon of an earlier release (PID {pid}).");
}
let _ = fs::remove_file(&pid_path);
#[cfg(unix)]
let _ = fs::remove_file(legacy.join("daemon.sock"));
}
pub fn daemon_addr() -> ipc::DaemonAddr {
ipc::DaemonAddr::default_for_current_os()
}
pub fn is_daemon_running() -> bool {
let Some(record) = read_daemon_record() else {
return false;
};
if record.pid == std::process::id() {
return true;
}
let pid_alive = ipc::process::is_alive(record.pid);
match record.identity.as_ref() {
Some(identity) if pid_alive && ipc::process::is_same_process(record.pid, identity) => {
return true;
}
None if pid_alive => return legacy_record_verified(record.pid),
Some(_) | None => {}
}
remove_daemon_record();
ipc::cleanup(&daemon_addr());
false
}
fn legacy_record_verified(pid: u32) -> bool {
static LAST: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let last = LAST.load(Ordering::Relaxed);
if last & (1 << 33) != 0 && last as u32 == pid {
return last & (1 << 32) != 0;
}
let verified = verified_daemon().is_some_and(|daemon| daemon.pid == pid);
LAST.store(
(1 << 33) | (u64::from(verified) << 32) | u64::from(pid),
Ordering::Relaxed,
);
verified
}
pub fn read_daemon_pid() -> Option<u32> {
let contents = fs::read_to_string(daemon_pid_path()).ok()?;
contents.trim().parse::<u32>().ok()
}
fn acquire_start_lock() -> Option<fs::File> {
use fs2::FileExt;
let lock_path = data_dir().join("daemon.start.lock");
if let Some(parent) = lock_path.parent() {
let _ = fs::create_dir_all(parent);
}
let file = fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(&lock_path)
.ok()?;
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
loop {
match file.try_lock_exclusive() {
Ok(()) => return Some(file),
Err(_) if std::time::Instant::now() < deadline => {
std::thread::sleep(std::time::Duration::from_millis(50));
}
Err(_) => return None,
}
}
}
pub fn start_daemon(args: &[String]) -> Result<()> {
start_daemon_with_binary(args, None)
}
pub fn start_daemon_with_binary(args: &[String], binary: Option<&std::path::Path>) -> Result<()> {
let _start_lock = acquire_start_lock();
if let Some(daemon) = verified_daemon() {
anyhow::bail!(
"Daemon already running (PID {}). Use --stop to stop it first.",
daemon.pid
);
}
if let Some(pid) = read_daemon_pid() {
eprintln!("{}", stale_pid_message(pid));
remove_daemon_record();
}
stop_legacy_daemon();
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 = binary.map_or_else(
|| std::path::PathBuf::from(crate::core::portable_binary::resolve_portable_binary()),
std::path::Path::to_path_buf,
);
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) => ipc::process::DetachedStderr::File(f),
Err(error) => {
eprintln!(
"Cannot open {}: {error}; daemon stderr disabled.",
stderr_log.display()
);
ipc::process::DetachedStderr::Null
}
};
let mut cmd = Command::new(&exe);
cmd.args(&cmd_args);
if crate::core::pathutil::process_is_tcc_standalone() {
cmd.env("LEAN_CTX_TCC_STANDALONE", "1");
}
let child = ipc::process::spawn_detached(&mut cmd, stderr_cfg)
.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) {
remove_daemon_record();
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(())
}
fn stale_pid_message(pid: u32) -> String {
format!("No daemon was running (stale PID file for PID {pid} removed).")
}
pub fn stop_daemon() -> Result<()> {
stop_legacy_daemon();
let Some(record) = read_daemon_record() else {
eprintln!("No daemon PID file found. Nothing to stop.");
return Ok(());
};
let pid = record.pid;
let Some(daemon) = ipc::process::find_service_processes(
std::slice::from_ref(&record),
ipc::process::ServiceSweep::Only(ipc::process::ServiceKind::Daemon),
)
.into_iter()
.next() else {
if ipc::process::is_alive(pid) {
let _ = try_http_shutdown();
}
eprintln!("{}", stale_pid_message(pid));
ipc::cleanup(&daemon_addr());
remove_daemon_record();
return Ok(());
};
if try_http_shutdown() {
for _ in 0..30 {
std::thread::sleep(std::time::Duration::from_millis(100));
if !daemon.is_running() {
break;
}
}
}
if daemon.is_running() {
daemon.signal(ipc::process::StopSignal::Terminate);
for _ in 0..20 {
std::thread::sleep(std::time::Duration::from_millis(100));
if !daemon.is_running() {
break;
}
}
}
if daemon.is_running() {
eprintln!("Daemon (PID {pid}) did not stop gracefully, force killing.");
daemon.signal(ipc::process::StopSignal::Kill);
std::thread::sleep(std::time::Duration::from_millis(200));
}
remove_daemon_record();
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 verified_daemon().is_some() {
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}")?;
let identity_path = daemon_identity_path();
match ipc::process::identity(pid) {
Some(identity) => {
let file = DaemonIdentityFile { pid, identity };
if let Ok(bytes) = serde_json::to_vec(&file) {
let _ = crate::core::updater::atomic_write_bytes(&identity_path, &bytes);
}
}
None => {
let _ = fs::remove_file(&identity_path);
}
}
Ok(())
}
pub fn init_foreground_daemon() -> Result<()> {
mark_foreground_daemon();
crate::core::layout_pin::heal();
crate::core::agents::reaper::spawn_reaper();
crate::core::intelligence_runtime::cockpit::install_if_missing_in_background();
let pid = std::process::id();
write_pid_file(pid)?;
Ok(())
}
#[must_use]
pub fn owned_service_pids() -> Vec<ipc::process::RecordedPid> {
let mut pids = Vec::new();
pids.extend(read_daemon_record());
pids.extend(crate::proxy_autostart::recorded_proxy_pid().map(ipc::process::RecordedPid::bare));
pids.extend(crate::core::cockpit_launch_record::read_record().map(|r| {
ipc::process::RecordedPid {
pid: r.pid,
identity: r.identity,
}
}));
pids
}
pub fn stop_owned_services(sweep: ipc::process::ServiceSweep) -> ipc::process::ServiceStopReport {
ipc::process::stop_services(&owned_service_pids(), sweep)
}
#[must_use]
pub fn unrecorded_services() -> Vec<ipc::process::ServiceProcess> {
let recorded: Vec<u32> = owned_service_pids().iter().map(|r| r.pid).collect();
ipc::process::find_unrecorded_services(&recorded)
}
#[must_use]
pub fn unrecorded_services_warning() -> Option<(String, String)> {
ipc::process::unrecorded_services_lines(&unrecorded_services(), cfg!(windows))
}
pub fn restore_stopped_proxy(
report: &ipc::process::ServiceStopReport,
autostart_restarted: bool,
) -> Option<std::io::Result<u32>> {
let args = ipc::process::proxy_respawn_args(&report.stopped, autostart_restarted)?;
let binary = crate::core::portable_binary::resolve_portable_binary();
let mut cmd = Command::new(binary);
cmd.args(&args);
Some(
ipc::process::spawn_detached(&mut cmd, ipc::process::DetachedStderr::Null)
.map(|child| child.id()),
)
}
pub fn cleanup_daemon_files() {
remove_daemon_record();
ipc::cleanup(&daemon_addr());
}
#[cfg(test)]
mod tests {
#[test]
fn stale_pid_message_is_neutral() {
let line = super::stale_pid_message(36492);
assert_eq!(
line,
"No daemon was running (stale PID file for PID 36492 removed)."
);
assert!(!line.contains("Cleaning up"));
}
}