Skip to main content

innate_core/daemon/
process.rs

1use 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        // Validate: warn if no watch dirs.
23        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        // Already running?
31        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        // Create parent dirs.
42        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 daemon_state.sqlite.
53        init_state_db(state_db)?;
54
55        // Fork: parent writes pid and returns; child runs the watch loop.
56        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        // Re-exec self with a hidden marker flag so the child enters watch_loop directly.
66        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        // Detach from terminal.
82        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                // SIGTERM
118                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                // Wait up to 3 s then SIGKILL.
126                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    // List watch paths.
179    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// ── Internal: watch loop (called in the forked child) ───────────────────────