schwab_cli/agent/
daemon.rs1use 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#[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}