Skip to main content

schwab_cli/agent/
daemon.rs

1use std::fs;
2use std::path::Path;
3use std::process::{Command, Stdio};
4
5use anyhow::{Context, Result};
6
7use super::paths::{log_path, pid_path};
8
9pub fn spawn_background(rules_path: &Path, extra_args: &[String]) -> Result<u32> {
10    let pid_file = pid_path(rules_path);
11    if let Ok(existing) = read_pid(&pid_file) {
12        if process_alive(existing) {
13            anyhow::bail!(
14                "agent already running with pid {existing} (pid file: {})",
15                pid_file.display()
16            );
17        }
18    }
19
20    let exe = std::env::current_exe().context("current exe")?;
21    let log = log_path(rules_path);
22    let log_file = fs::OpenOptions::new()
23        .create(true)
24        .append(true)
25        .open(&log)
26        .with_context(|| format!("open log {}", log.display()))?;
27
28    let err_file = log_file
29        .try_clone()
30        .with_context(|| format!("clone log handle {}", log.display()))?;
31
32    let mut cmd = Command::new(&exe);
33    cmd.arg("agent")
34        .arg("run")
35        .arg(rules_path)
36        .args(extra_args)
37        .stdout(Stdio::from(log_file))
38        .stderr(Stdio::from(err_file));
39
40    #[cfg(unix)]
41    {
42        use std::os::unix::process::CommandExt;
43        unsafe {
44            cmd.pre_exec(|| {
45                libc::setsid();
46                Ok(())
47            });
48        }
49    }
50
51    let child = cmd.spawn().context("spawn background agent")?;
52    let pid = child.id();
53    fs::write(&pid_file, pid.to_string())?;
54    Ok(pid)
55}
56
57pub fn stop_daemon(rules_path: &Path) -> Result<()> {
58    let pid_file = pid_path(rules_path);
59    let pid = read_pid(&pid_file).with_context(|| {
60        format!(
61            "no running agent (missing pid file at {})",
62            pid_file.display()
63        )
64    })?;
65
66    if !process_alive(pid) {
67        fs::remove_file(&pid_file).ok();
68        anyhow::bail!("agent pid {pid} is not running; removed stale pid file");
69    }
70
71    #[cfg(unix)]
72    {
73        let rc = unsafe { libc::kill(pid as i32, libc::SIGTERM) };
74        if rc != 0 {
75            anyhow::bail!("failed to send SIGTERM to pid {pid}");
76        }
77    }
78
79    #[cfg(not(unix))]
80    {
81        anyhow::bail!("stop is only supported on Unix");
82    }
83
84    for _ in 0..20 {
85        if !process_alive(pid) {
86            fs::remove_file(&pid_file).ok();
87            return Ok(());
88        }
89        std::thread::sleep(std::time::Duration::from_millis(250));
90    }
91
92    #[cfg(unix)]
93    {
94        unsafe {
95            libc::kill(pid as i32, libc::SIGKILL);
96        }
97        fs::remove_file(&pid_file).ok();
98    }
99
100    Ok(())
101}
102
103fn read_pid(path: &Path) -> Result<u32> {
104    let content = fs::read_to_string(path)?;
105    content
106        .trim()
107        .parse::<u32>()
108        .with_context(|| format!("invalid pid in {}", path.display()))
109}
110
111fn process_alive(pid: u32) -> bool {
112    #[cfg(unix)]
113    {
114        unsafe { libc::kill(pid as i32, 0) == 0 }
115    }
116    #[cfg(not(unix))]
117    {
118        let _ = pid;
119        false
120    }
121}
122
123/// Background agent process metadata for dashboard / status UIs.
124#[derive(Debug, Clone)]
125pub struct DaemonStatus {
126    pub running: bool,
127    pub pid: Option<u32>,
128    pub pid_file: std::path::PathBuf,
129    pub log_file: std::path::PathBuf,
130}
131
132pub fn daemon_status(rules_path: &Path) -> DaemonStatus {
133    let pid_file = pid_path(rules_path);
134    let log_file = log_path(rules_path);
135    match read_pid(&pid_file) {
136        Ok(pid) if process_alive(pid) => DaemonStatus {
137            running: true,
138            pid: Some(pid),
139            pid_file,
140            log_file,
141        },
142        Ok(pid) => DaemonStatus {
143            running: false,
144            pid: Some(pid),
145            pid_file,
146            log_file,
147        },
148        Err(_) => DaemonStatus {
149            running: false,
150            pid: None,
151            pid_file,
152            log_file,
153        },
154    }
155}