innate_core/daemon/
process.rs1use super::{state::*, *};
2
3pub fn start(
4 watch_dirs: &[std::path::PathBuf],
5 db_path: &Path,
6 pid_file: &Path,
7 state_db: &Path,
8 log_file: &Path,
9) -> anyhow::Result<()> {
10 #[cfg(not(target_os = "linux"))]
11 {
12 anyhow::bail!(
13 "innate daemon is only supported on Linux. \
14 On other platforms use the SDK or CLI directly."
15 );
16 }
17
18 #[cfg(target_os = "linux")]
19 {
20 use std::os::unix::process::CommandExt;
21
22 if watch_dirs.is_empty() {
24 eprintln!(
25 "[innate daemon] warning: no --watch directories specified; \
26 daemon will start but won't monitor any logs"
27 );
28 }
29
30 if let Some(running_pid) = read_pid(pid_file) {
32 if process_alive(running_pid) {
33 anyhow::bail!(
34 "daemon already running (pid {}). \
35 Use `innate daemon stop` first.",
36 running_pid
37 );
38 }
39 }
40
41 if let Some(p) = pid_file.parent() {
43 std::fs::create_dir_all(p)?;
44 }
45 if let Some(p) = state_db.parent() {
46 std::fs::create_dir_all(p)?;
47 }
48 if let Some(p) = log_file.parent() {
49 std::fs::create_dir_all(p)?;
50 }
51
52 init_state_db(state_db)?;
54
55 let watch_strs: Vec<String> = watch_dirs
57 .iter()
58 .map(|p| p.to_string_lossy().into_owned())
59 .collect();
60 let db_str = db_path.to_string_lossy().into_owned();
61 let sdb_str = state_db.to_string_lossy().into_owned();
62 let log_str = log_file.to_string_lossy().into_owned();
63 let pid_str = pid_file.to_string_lossy().into_owned();
64
65 let self_exe = std::env::current_exe()?;
67 let mut cmd = std::process::Command::new(&self_exe);
68 cmd.arg("--daemon-internal-watch")
69 .arg("--db")
70 .arg(&db_str)
71 .arg("--state-db")
72 .arg(&sdb_str)
73 .arg("--log-file")
74 .arg(&log_str)
75 .arg("--pid-file")
76 .arg(&pid_str);
77 for w in &watch_strs {
78 cmd.arg("--watch-dir").arg(w);
79 }
80
81 unsafe {
83 cmd.pre_exec(|| {
84 libc::setsid();
85 Ok(())
86 });
87 }
88 let child = cmd
89 .stdin(std::process::Stdio::null())
90 .stdout(std::process::Stdio::null())
91 .stderr(std::process::Stdio::null())
92 .spawn()?;
93
94 std::fs::write(pid_file, child.id().to_string())?;
95 println!("daemon started (pid {})", child.id());
96 Ok(())
97 }
98}
99
100pub fn stop(pid_file: &Path) -> anyhow::Result<()> {
101 #[cfg(not(target_os = "linux"))]
102 anyhow::bail!("innate daemon is only supported on Linux.");
103
104 #[cfg(target_os = "linux")]
105 {
106 match read_pid(pid_file) {
107 None => anyhow::bail!(
108 "no pid file at {}; daemon may not be running",
109 pid_file.display()
110 ),
111 Some(pid) => {
112 if !process_alive(pid) {
113 let _ = std::fs::remove_file(pid_file);
114 println!("daemon was not running (stale pid {pid}); pid file removed");
115 return Ok(());
116 }
117 let r = unsafe { libc::kill(pid as libc::pid_t, libc::SIGTERM) };
119 if r != 0 {
120 anyhow::bail!(
121 "kill({pid}, SIGTERM) failed: {}",
122 std::io::Error::last_os_error()
123 );
124 }
125 for _ in 0..30 {
127 std::thread::sleep(std::time::Duration::from_millis(100));
128 if !process_alive(pid) {
129 let _ = std::fs::remove_file(pid_file);
130 println!("daemon stopped (pid {pid})");
131 return Ok(());
132 }
133 }
134 unsafe {
135 libc::kill(pid as libc::pid_t, libc::SIGKILL);
136 }
137 let _ = std::fs::remove_file(pid_file);
138 println!("daemon killed (pid {pid})");
139 Ok(())
140 }
141 }
142 }
143}
144
145pub fn status(state_db: &Path, pid_file: &Path) -> anyhow::Result<()> {
146 let pid = read_pid(pid_file);
147 let running = pid.is_some_and(process_alive);
148 println!(
149 "status : {}",
150 if running { "running" } else { "stopped" }
151 );
152 println!(
153 "pid : {}",
154 pid.map(|value| value.to_string())
155 .unwrap_or_else(|| "-".to_string())
156 );
157
158 if !state_db.exists() {
159 println!(
160 "daemon_state.sqlite not found at {}; daemon has never run.",
161 state_db.display()
162 );
163 return Ok(());
164 }
165 let conn = rusqlite::Connection::open(state_db)?;
166 let count: i64 = conn
167 .query_row("SELECT count(*) FROM watch_state", [], |r| r.get(0))
168 .unwrap_or(0);
169 let processed: i64 = conn
170 .query_row("SELECT count(*) FROM processed_events", [], |r| r.get(0))
171 .unwrap_or(0);
172 let errors: i64 = conn
173 .query_row("SELECT count(*) FROM daemon_errors", [], |r| r.get(0))
174 .unwrap_or(0);
175 println!("watch_state entries : {count}");
176 println!("processed events : {processed}");
177 println!("errors : {errors}");
178 let mut stmt =
180 conn.prepare("SELECT watch_path, last_processed_offset, updated_at FROM watch_state")?;
181 let rows = stmt.query_map([], |r| {
182 Ok((
183 r.get::<_, String>(0)?,
184 r.get::<_, i64>(1)?,
185 r.get::<_, String>(2)?,
186 ))
187 })?;
188 for row in rows.flatten() {
189 println!(" {} offset={} updated={}", row.0, row.1, row.2);
190 }
191 Ok(())
192}
193
194