use std::process::Stdio;
use crate::cli::output::OutputConfig;
use crate::cli::StartArgs;
use crate::config;
use crate::error::{OlError, ERR_DAEMON_START_FAILED, ERR_INVALID_CONFIG};
pub fn run_start(args: &StartArgs, output: &OutputConfig) -> Result<(), OlError> {
let cfg = config::Config::load(args.port, None, false)?;
if let Some(pid) = read_pid_file() {
if is_process_alive(pid) {
output.print_info(&format!("Daemon is already running (PID {pid})"));
return Ok(());
}
}
let token = load_or_generate_token()?;
if args.foreground {
run_daemon_foreground(cfg.port, &token)?;
} else {
let pid = spawn_daemon_background(cfg.port, &token)?;
if !wait_for_health(cfg.port, 5) {
return Err(OlError::new(
ERR_DAEMON_START_FAILED,
format!("Daemon spawned (PID {pid}) but health check failed within 5s"),
)
.with_suggestion("Check ~/.openlatch/logs/daemon.log for errors.")
.with_docs("https://docs.openlatch.ai/errors/OL-1502"));
}
output.print_step(&format!("Daemon started on port {} (PID {pid})", cfg.port));
}
Ok(())
}
pub fn run_stop(output: &OutputConfig) -> Result<(), OlError> {
let Some(pid) = read_pid_file() else {
output.print_info("Daemon is not running");
return Ok(());
};
if !is_process_alive(pid) {
output.print_info("Daemon is not running");
let _ = std::fs::remove_file(config::openlatch_dir().join("daemon.pid"));
return Ok(());
}
let cfg = config::Config::load(None, None, false)?;
let token = load_or_generate_token().unwrap_or_default();
if send_shutdown_request(cfg.port, &token) {
let start = std::time::Instant::now();
while start.elapsed() < std::time::Duration::from_secs(5) {
if !is_process_alive(pid) {
break;
}
std::thread::sleep(std::time::Duration::from_millis(200));
}
}
if !is_process_alive(pid) {
let _ = std::fs::remove_file(config::openlatch_dir().join("daemon.pid"));
output.print_step("Daemon stopped");
} else {
#[cfg(unix)]
{
unsafe {
libc::kill(pid as libc::pid_t, libc::SIGTERM);
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
let _ = std::fs::remove_file(config::openlatch_dir().join("daemon.pid"));
output.print_step("Daemon stopped");
}
Ok(())
}
pub fn run_restart(output: &OutputConfig) -> Result<(), OlError> {
run_stop(output)?;
let timeout = std::time::Duration::from_secs(5);
let start = std::time::Instant::now();
let cfg = config::Config::load(None, None, false)?;
while start.elapsed() < timeout {
let pid_file_gone = read_pid_file().is_none();
let health_down = !check_health(cfg.port);
if pid_file_gone || health_down {
break;
}
std::thread::sleep(std::time::Duration::from_millis(200));
}
let start_args = StartArgs {
foreground: false,
port: None,
};
run_start(&start_args, output)
}
pub fn spawn_daemon_background(port: u16, token: &str) -> Result<u32, OlError> {
let exe = std::env::current_exe().map_err(|e| {
OlError::new(
ERR_INVALID_CONFIG,
format!("Cannot locate current executable: {e}"),
)
})?;
#[cfg(unix)]
let child = {
use std::os::unix::process::CommandExt;
std::process::Command::new(&exe)
.args([
"daemon",
"start",
"--foreground",
"--port",
&port.to_string(),
])
.env("OPENLATCH_TOKEN", token)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.process_group(0)
.spawn()
.map_err(|e| {
OlError::new(
ERR_INVALID_CONFIG,
format!("Failed to spawn daemon process: {e}"),
)
.with_suggestion("Check that the openlatch binary is executable.")
})?
};
#[cfg(windows)]
let child = {
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
std::process::Command::new(&exe)
.args([
"daemon",
"start",
"--foreground",
"--port",
&port.to_string(),
])
.env("OPENLATCH_TOKEN", token)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.creation_flags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP)
.spawn()
.map_err(|e| {
OlError::new(
ERR_INVALID_CONFIG,
format!("Failed to spawn daemon process: {e}"),
)
.with_suggestion("Check that the openlatch binary is executable.")
})?
};
let pid = child.id();
Ok(pid)
}
pub(crate) fn read_pid_file() -> Option<u32> {
let pid_path = config::openlatch_dir().join("daemon.pid");
let content = std::fs::read_to_string(&pid_path).ok()?;
content.trim().parse::<u32>().ok()
}
pub(crate) fn is_process_alive(pid: u32) -> bool {
#[cfg(unix)]
{
let result = unsafe { libc::kill(pid as libc::pid_t, 0) };
result == 0
}
#[cfg(windows)]
{
let handle = unsafe {
winapi::um::processthreadsapi::OpenProcess(
winapi::um::winnt::PROCESS_QUERY_INFORMATION,
0,
pid,
)
};
if handle.is_null() {
return false;
}
let mut exit_code: u32 = 0;
let alive = unsafe {
winapi::um::processthreadsapi::GetExitCodeProcess(handle, &mut exit_code) != 0
&& exit_code == winapi::um::minwinbase::STILL_ACTIVE
};
unsafe { winapi::um::handleapi::CloseHandle(handle) };
alive
}
#[cfg(not(any(unix, windows)))]
{
let _ = pid;
false
}
}
fn send_shutdown_request(port: u16, token: &str) -> bool {
let url = format!("http://127.0.0.1:{port}/shutdown");
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(2))
.build();
match client {
Ok(c) => c
.post(&url)
.header("Authorization", format!("Bearer {token}"))
.send()
.is_ok(),
Err(_) => false,
}
}
pub(crate) fn wait_for_health(port: u16, timeout_secs: u64) -> bool {
let url = format!("http://127.0.0.1:{port}/health");
let start = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(timeout_secs);
while start.elapsed() < timeout {
if let Ok(resp) = reqwest::blocking::get(&url) {
if resp.status().is_success() {
return true;
}
}
std::thread::sleep(std::time::Duration::from_millis(200));
}
false
}
fn check_health(port: u16) -> bool {
let url = format!("http://127.0.0.1:{port}/health");
reqwest::blocking::get(url)
.map(|r| r.status().is_success())
.unwrap_or(false)
}
fn load_or_generate_token() -> Result<String, OlError> {
let ol_dir = config::openlatch_dir();
config::ensure_token(&ol_dir)
}
fn run_daemon_foreground(port: u16, token: &str) -> Result<(), OlError> {
let mut cfg = config::Config::load(Some(port), None, true)?;
cfg.foreground = true;
let rt = tokio::runtime::Runtime::new().map_err(|e| {
OlError::new(
ERR_INVALID_CONFIG,
format!("Failed to create async runtime: {e}"),
)
})?;
let token_owned = token.to_string();
rt.block_on(async move {
use crate::daemon;
use crate::envelope;
use crate::logging;
use crate::privacy;
let _guard = logging::daemon_log::init_daemon_logging(&cfg.log_dir);
if let Ok(deleted) = logging::cleanup_old_logs(&cfg.log_dir, cfg.retention_days) {
if deleted > 0 {
tracing::info!(deleted = deleted, "cleaned up old log files");
}
}
privacy::init_filter(&cfg.extra_patterns);
let pid = std::process::id();
let pid_path = config::openlatch_dir().join("daemon.pid");
if let Err(e) = std::fs::write(&pid_path, pid.to_string()) {
tracing::warn!(error = %e, "failed to write PID file");
}
logging::daemon_log::log_startup(
env!("CARGO_PKG_VERSION"),
cfg.port,
pid,
envelope::os_string(),
envelope::arch_string(),
);
eprintln!(
"openlatch v{} \u{2022} listening on 127.0.0.1:{} \u{2022} pid {}",
env!("CARGO_PKG_VERSION"),
cfg.port,
pid,
);
match daemon::start_server(cfg.clone(), token_owned).await {
Ok((uptime_secs, events)) => {
eprintln!(
"openlatch daemon stopped \u{2022} uptime {} \u{2022} {} events processed",
daemon::format_uptime(uptime_secs),
events
);
}
Err(e) => {
tracing::error!(error = %e, "daemon exited with error");
eprintln!("Error: daemon exited unexpectedly: {e}");
}
}
let _ = std::fs::remove_file(&pid_path);
});
Ok(())
}