1use super::{events::*, *};
2
3pub fn run_watch_loop(
4 watch_dirs: &[String],
5 db_path: &str,
6 state_db_path: &str,
7 log_path: &str,
8 pid_file: &str,
9) {
10 let _ = std::fs::write(pid_file, std::process::id().to_string());
12
13 let log_file = std::fs::OpenOptions::new()
15 .create(true)
16 .append(true)
17 .open(log_path);
18
19 let mut logger: Box<dyn std::io::Write + Send> = match log_file {
20 Ok(f) => Box::new(f),
21 Err(_) => Box::new(std::io::stderr()),
22 };
23
24 let _ = writeln!(logger, "[innate-daemon] started pid={}", std::process::id());
25
26 let state_db = match rusqlite::Connection::open(state_db_path) {
27 Ok(c) => c,
28 Err(e) => {
29 let _ = writeln!(logger, "[innate-daemon] cannot open state db: {e}");
30 return;
31 }
32 };
33 if state_db.execute_batch(DAEMON_SCHEMA).is_err() {
34 let _ = writeln!(logger, "[innate-daemon] failed to init schema");
35 return;
36 }
37
38 let mut last_evolve_poll = std::time::Instant::now();
40 const EVOLVE_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_secs(60);
41 let mut last_backup_poll = std::time::Instant::now();
42 const BACKUP_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_secs(30 * 60);
43 loop {
44 for dir in watch_dirs {
45 let dir_path = std::path::Path::new(dir);
46 if !dir_path.exists() {
47 continue;
48 }
49 if let Ok(entries) = std::fs::read_dir(dir_path) {
51 for entry in entries.flatten() {
52 let p = entry.path();
53 if p.extension().and_then(|e| e.to_str()) == Some("log") {
54 process_log_file(&p, &state_db, db_path, &mut *logger);
55 }
56 }
57 }
58 }
59 if last_evolve_poll.elapsed() >= EVOLVE_POLL_INTERVAL {
61 if let Err(error) = call_cli_evolve(db_path, "scheduled") {
62 let _ = writeln!(logger, "[innate-daemon] scheduled evolve failed: {error}");
63 record_daemon_error(
64 &state_db,
65 "<scheduler>",
66 "scheduled_evolve",
67 &error.to_string(),
68 );
69 }
70 last_evolve_poll = std::time::Instant::now();
71 }
72 if last_backup_poll.elapsed() >= BACKUP_POLL_INTERVAL {
73 let backup_enabled = crate::settings::load()
78 .map(|s| s.backup.is_some_and(|b| b.enable && b.r2.is_some()))
79 .unwrap_or(false);
80 if backup_enabled {
81 if let Err(error) = call_cli_backup(db_path) {
82 let _ = writeln!(logger, "[innate-daemon] auto-backup failed: {error}");
83 record_daemon_error(
84 &state_db,
85 "<scheduler>",
86 "auto_backup",
87 &error.to_string(),
88 );
89 }
90 }
91 last_backup_poll = std::time::Instant::now();
92 }
93 std::thread::sleep(std::time::Duration::from_millis(500));
94 }
95}
96
97pub(in crate::daemon) fn process_log_file(
98 path: &Path,
99 state_db: &rusqlite::Connection,
100 db_path: &str,
101 log: &mut dyn std::io::Write,
102) {
103 let path_str = path.to_string_lossy();
104 let meta = match std::fs::metadata(path) {
105 Ok(m) => m,
106 Err(_) => return,
107 };
108
109 #[cfg(target_os = "linux")]
111 let inode = {
112 use std::os::linux::fs::MetadataExt;
113 meta.st_ino().to_string()
114 };
115 #[cfg(not(target_os = "linux"))]
116 let inode = String::new();
117
118 let (saved_offset, saved_inode): (i64, Option<String>) = state_db.query_row(
119 "SELECT last_processed_offset, last_processed_inode FROM watch_state WHERE watch_path=?",
120 rusqlite::params![path_str.as_ref()],
121 |r| Ok((r.get(0)?, r.get(1)?)),
122 ).unwrap_or((0, None));
123
124 let file_size = meta.len() as i64;
126 let start_offset = if saved_inode.as_deref() != Some(&inode) || file_size < saved_offset {
127 0
128 } else {
129 saved_offset
130 };
131
132 if start_offset >= file_size {
133 return;
134 }
135
136 use std::io::{BufRead, Seek};
137 let mut f = match std::fs::File::open(path) {
138 Ok(f) => f,
139 Err(_) => return,
140 };
141 if f.seek(std::io::SeekFrom::Start(start_offset as u64))
142 .is_err()
143 {
144 return;
145 }
146
147 let mut reader = std::io::BufReader::new(&mut f);
148 let mut new_offset = start_offset;
149 let mut line_buf = String::new();
150
151 loop {
152 let line_start_offset = new_offset;
153 line_buf.clear();
154 let bytes_read = match reader.read_line(&mut line_buf) {
155 Ok(n) => n,
156 Err(_) => break,
157 };
158 if bytes_read == 0 {
159 break; }
161 if !line_buf.ends_with('\n') {
164 new_offset = line_start_offset;
165 break;
166 }
167 new_offset += bytes_read as i64;
168 let line = line_buf.trim_end_matches('\n').trim_end_matches('\r');
169
170 let Some(event) = parse_log_event(line) else {
171 continue;
172 };
173 let event_type = event.kind;
174
175 let event_id = event
179 .event_id
180 .clone()
181 .unwrap_or_else(|| event_id_for_line(path_str.as_ref(), &inode, new_offset, line));
182
183 let already: i64 = state_db
185 .query_row(
186 "SELECT count(*) FROM processed_events WHERE event_id=?",
187 rusqlite::params![event_id],
188 |r| r.get(0),
189 )
190 .unwrap_or(0);
191 if already > 0 {
192 continue;
193 }
194
195 if matches!(event_type, "start" | "ok" | "fail" | "feedback") {
207 continue;
208 }
209
210 let trace_id = event.trace_id.clone();
213
214 if event_type == "end" {
215 if let Some(tid) = &trace_id {
216 if let Err(e) = call_cli_record(db_path, tid, &event) {
217 let ts = crate::utils::utc_now_iso();
218 let _ = writeln!(log, "{ts} [daemon] end trace retirement failed: {e}");
219 record_daemon_error(
220 state_db,
221 path_str.as_ref(),
222 "record_session_end",
223 &e.to_string(),
224 );
225 }
226 }
227 let result = call_cli_evolve(db_path, "manual");
228 let ts = crate::utils::utc_now_iso();
229 if let Err(e) = state_db.execute(
237 "INSERT OR IGNORE INTO processed_events
238 (event_id, watch_path, trace_id, event_type, ts)
239 VALUES (?,?,?,?,?)",
240 rusqlite::params![event_id, path_str.as_ref(), trace_id, event_type, ts],
241 ) {
242 let _ = writeln!(log, "{ts} [daemon] failed to record processed end event: {e}");
243 }
244 match result {
245 Ok(()) => {
246 let _ = call_cli_evolve(db_path, "scheduled");
247 let _ = writeln!(log, "{ts} [daemon] end evolve ok");
248 }
249 Err(e) => {
250 let _ = writeln!(log, "{ts} [daemon] end evolve failed (scheduled tick will retry): {e}");
251 record_daemon_error(state_db, path_str.as_ref(), "evolve", &e.to_string());
252 }
253 }
254 continue;
255 }
256
257 }
258
259 let ts = crate::utils::utc_now_iso();
261 let _ = state_db.execute(
262 "INSERT OR REPLACE INTO watch_state(watch_path, last_processed_offset, last_processed_inode, updated_at)
263 VALUES (?,?,?,?)",
264 rusqlite::params![path_str.as_ref(), new_offset, inode, ts],
265 );
266}