1const TASK_NAME: &str = "Hematite Health Check";
2const TASK_SWEEP_NAME: &str = "Hematite Maintenance Sweep";
3const TASK_TIMELINE_NAME: &str = "Hematite Timeline Capture";
4const TASK_ALERT_NAME: &str = "Hematite Alert Rules";
5
6pub fn register_alert_task(cadence: &str, exe_path: &str) -> Result<String, String> {
7 #[cfg(not(target_os = "windows"))]
8 {
9 let _ = (cadence, exe_path);
10 return Err("Scheduled tasks require Windows (schtasks.exe).\n\
11 On Linux/macOS use cron instead:\n\
12 0 * * * * hematite --alert-rule-run"
13 .into());
14 }
15
16 #[cfg(target_os = "windows")]
17 {
18 let task_run = format!("\"{}\" --alert-rule-run", exe_path);
19 let (schedule_type, extra_args, label): (&str, &[&str], &str) = match cadence {
20 "daily" => ("daily", &[], "daily at 03:00"),
21 _ => ("hourly", &[], "hourly"),
22 };
23 let mut args: Vec<String> = vec![
24 "/create".into(),
25 "/tn".into(),
26 TASK_ALERT_NAME.into(),
27 "/tr".into(),
28 task_run.clone(),
29 "/sc".into(),
30 schedule_type.into(),
31 "/st".into(),
32 "03:00".into(),
33 ];
34 for a in extra_args {
35 args.push(a.to_string());
36 }
37 args.push("/f".into());
38 let out = std::process::Command::new("schtasks")
39 .args(&args)
40 .output()
41 .map_err(|e| format!("Failed to run schtasks: {}", e))?;
42 if out.status.success() {
43 Ok(format!(
44 "Task \"{}\" registered — runs {}.\n\
45 Action: {}\n\
46 Run `hematite --alert-rule-run --schedule status` to confirm.",
47 TASK_ALERT_NAME, label, task_run
48 ))
49 } else {
50 let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
51 let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string();
52 Err(if !stderr.is_empty() { stderr } else { stdout })
53 }
54 }
55}
56
57pub fn remove_alert_task() -> Result<String, String> {
58 #[cfg(not(target_os = "windows"))]
59 return Err("Scheduled tasks require Windows.".into());
60
61 #[cfg(target_os = "windows")]
62 {
63 let out = std::process::Command::new("schtasks")
64 .args(["/delete", "/tn", TASK_ALERT_NAME, "/f"])
65 .output()
66 .map_err(|e| format!("Failed to run schtasks: {}", e))?;
67 if out.status.success() {
68 Ok(format!("Task \"{}\" removed.", TASK_ALERT_NAME))
69 } else {
70 let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
71 Err(if !stderr.is_empty() {
72 stderr
73 } else {
74 format!("Task \"{}\" not found.", TASK_ALERT_NAME)
75 })
76 }
77 }
78}
79
80pub fn query_alert_task() -> String {
81 #[cfg(not(target_os = "windows"))]
82 return "Scheduled tasks are Windows-only.".to_string();
83
84 #[cfg(target_os = "windows")]
85 {
86 let out = std::process::Command::new("schtasks")
87 .args(["/query", "/tn", TASK_ALERT_NAME, "/fo", "LIST"])
88 .output();
89 match out {
90 Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).trim().to_string(),
91 Ok(o) => {
92 let stderr = String::from_utf8_lossy(&o.stderr).to_ascii_lowercase();
93 if stderr.contains("cannot find") || stderr.contains("does not exist") {
94 format!("Task \"{}\" is not registered.", TASK_ALERT_NAME)
95 } else {
96 format!(
97 "Not registered: {}",
98 String::from_utf8_lossy(&o.stderr).trim()
99 )
100 }
101 }
102 Err(e) => format!("Error querying task: {}", e),
103 }
104 }
105}
106
107pub fn register_timeline_task(exe_path: &str) -> Result<String, String> {
108 #[cfg(not(target_os = "windows"))]
109 {
110 let _ = exe_path;
111 return Err("Scheduled tasks require Windows (schtasks.exe).\n\
112 On Linux/macOS use cron instead:\n\
113 0 3 * * * hematite --timeline-capture"
114 .into());
115 }
116
117 #[cfg(target_os = "windows")]
118 {
119 let task_run = format!("\"{}\" --timeline-capture", exe_path);
120 let args: Vec<String> = vec![
121 "/create".into(),
122 "/tn".into(),
123 TASK_TIMELINE_NAME.into(),
124 "/tr".into(),
125 task_run.clone(),
126 "/sc".into(),
127 "daily".into(),
128 "/st".into(),
129 "03:00".into(),
130 "/f".into(),
131 ];
132 let out = std::process::Command::new("schtasks")
133 .args(&args)
134 .output()
135 .map_err(|e| format!("Failed to run schtasks: {}", e))?;
136
137 if out.status.success() {
138 let dir = crate::tools::file_ops::hematite_dir().join("timeline");
139 Ok(format!(
140 "Task \"{}\" registered — runs daily at 03:00.\n\
141 Action: {}\n\
142 Timeline entries will save to: {}\n\
143 Run `hematite --timeline` to view history.",
144 TASK_TIMELINE_NAME,
145 task_run,
146 dir.display()
147 ))
148 } else {
149 let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
150 let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string();
151 Err(if !stderr.is_empty() { stderr } else { stdout })
152 }
153 }
154}
155
156pub fn remove_timeline_task() -> Result<String, String> {
157 #[cfg(not(target_os = "windows"))]
158 return Err("Scheduled tasks require Windows.".into());
159
160 #[cfg(target_os = "windows")]
161 {
162 let out = std::process::Command::new("schtasks")
163 .args(["/delete", "/tn", TASK_TIMELINE_NAME, "/f"])
164 .output()
165 .map_err(|e| format!("Failed to run schtasks: {}", e))?;
166
167 if out.status.success() {
168 Ok(format!("Task \"{}\" removed.", TASK_TIMELINE_NAME))
169 } else {
170 let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
171 Err(if !stderr.is_empty() {
172 stderr
173 } else {
174 format!(
175 "Task \"{}\" not found — nothing to remove.",
176 TASK_TIMELINE_NAME
177 )
178 })
179 }
180 }
181}
182
183pub fn query_timeline_task() -> String {
184 #[cfg(not(target_os = "windows"))]
185 return "Scheduled tasks are Windows-only.".to_string();
186
187 #[cfg(target_os = "windows")]
188 {
189 let out = std::process::Command::new("schtasks")
190 .args(["/query", "/tn", TASK_TIMELINE_NAME, "/fo", "LIST"])
191 .output();
192
193 match out {
194 Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).trim().to_string(),
195 Ok(o) => {
196 let stderr = String::from_utf8_lossy(&o.stderr).to_ascii_lowercase();
197 if stderr.contains("cannot find") || stderr.contains("does not exist") {
198 format!("Task \"{}\" is not registered.", TASK_TIMELINE_NAME)
199 } else {
200 format!(
201 "Not registered: {}",
202 String::from_utf8_lossy(&o.stderr).trim()
203 )
204 }
205 }
206 Err(e) => format!("Error querying task: {}", e),
207 }
208 }
209}
210
211pub fn register_sweep_task(cadence: &str, exe_path: &str) -> Result<String, String> {
212 #[cfg(not(target_os = "windows"))]
213 {
214 let _ = (cadence, exe_path);
215 return Err("Scheduled tasks require Windows (schtasks.exe).\n\
216 On Linux/macOS use cron instead:\n\
217 hematite --fix-all --report-format html"
218 .into());
219 }
220
221 #[cfg(target_os = "windows")]
222 {
223 let task_run = format!("\"{}\" --fix-all --report-format html --quiet", exe_path);
224
225 let (schedule_type, extra_args, label): (&str, &[&str], &str) = match cadence {
226 "daily" => ("daily", &[], "daily at 03:00"),
227 _ => ("weekly", &["/d", "SUN"], "weekly on Sunday at 03:00"),
228 };
229
230 let mut args: Vec<String> = vec![
231 "/create".into(),
232 "/tn".into(),
233 TASK_SWEEP_NAME.into(),
234 "/tr".into(),
235 task_run.clone(),
236 "/sc".into(),
237 schedule_type.into(),
238 "/st".into(),
239 "03:00".into(),
240 ];
241 for a in extra_args {
242 args.push(a.to_string());
243 }
244 args.push("/f".into());
245
246 let out = std::process::Command::new("schtasks")
247 .args(&args)
248 .output()
249 .map_err(|e| format!("Failed to run schtasks: {}", e))?;
250
251 if out.status.success() {
252 let reports_dir = crate::tools::file_ops::hematite_dir().join("reports");
253 Ok(format!(
254 "Task \"{}\" registered — runs {}.\n\
255 Action: {}\n\
256 Sweep reports will save to: {}\n\
257 Run `hematite --fix-all --schedule status` to confirm.",
258 TASK_SWEEP_NAME,
259 label,
260 task_run,
261 reports_dir.display()
262 ))
263 } else {
264 let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
265 let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string();
266 Err(if !stderr.is_empty() { stderr } else { stdout })
267 }
268 }
269}
270
271pub fn remove_sweep_task() -> Result<String, String> {
272 #[cfg(not(target_os = "windows"))]
273 return Err("Scheduled tasks require Windows.".into());
274
275 #[cfg(target_os = "windows")]
276 {
277 let out = std::process::Command::new("schtasks")
278 .args(["/delete", "/tn", TASK_SWEEP_NAME, "/f"])
279 .output()
280 .map_err(|e| format!("Failed to run schtasks: {}", e))?;
281
282 if out.status.success() {
283 Ok(format!("Task \"{}\" removed.", TASK_SWEEP_NAME))
284 } else {
285 let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
286 Err(if !stderr.is_empty() {
287 stderr
288 } else {
289 format!(
290 "Task \"{}\" not found — nothing to remove.",
291 TASK_SWEEP_NAME
292 )
293 })
294 }
295 }
296}
297
298pub fn query_sweep_task() -> String {
299 #[cfg(not(target_os = "windows"))]
300 return "Scheduled tasks are Windows-only. Use cron for recurring sweeps:\n\
301 hematite --fix-all --report-format html"
302 .to_string();
303
304 #[cfg(target_os = "windows")]
305 {
306 let out = std::process::Command::new("schtasks")
307 .args(["/query", "/tn", TASK_SWEEP_NAME, "/fo", "LIST"])
308 .output();
309
310 match out {
311 Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).trim().to_string(),
312 Ok(o) => {
313 let stderr = String::from_utf8_lossy(&o.stderr).to_ascii_lowercase();
314 if stderr.contains("cannot find") || stderr.contains("does not exist") {
315 format!("Task \"{}\" is not registered.", TASK_SWEEP_NAME)
316 } else {
317 format!(
318 "Not registered: {}",
319 String::from_utf8_lossy(&o.stderr).trim()
320 )
321 }
322 }
323 Err(e) => format!("Error querying task: {}", e),
324 }
325 }
326}
327
328pub fn register_scheduled_task(cadence: &str, exe_path: &str) -> Result<String, String> {
329 #[cfg(not(target_os = "windows"))]
330 {
331 let _ = (cadence, exe_path);
332 return Err("Scheduled tasks require Windows (schtasks.exe).\n\
333 On Linux/macOS use cron instead:\n\
334 hematite --triage --report-format html"
335 .into());
336 }
337
338 #[cfg(target_os = "windows")]
339 {
340 let task_run = format!("\"{}\" --triage --report-format html", exe_path);
341
342 let (schedule_type, extra_args, label): (&str, &[&str], &str) = match cadence {
343 "daily" => ("daily", &[], "daily at 08:00"),
344 _ => ("weekly", &["/d", "MON"], "weekly on Monday at 08:00"),
345 };
346
347 let mut args: Vec<String> = vec![
348 "/create".into(),
349 "/tn".into(),
350 TASK_NAME.into(),
351 "/tr".into(),
352 task_run.clone(),
353 "/sc".into(),
354 schedule_type.into(),
355 "/st".into(),
356 "08:00".into(),
357 ];
358 for a in extra_args {
359 args.push(a.to_string());
360 }
361 args.push("/f".into());
362
363 let out = std::process::Command::new("schtasks")
364 .args(&args)
365 .output()
366 .map_err(|e| format!("Failed to run schtasks: {}", e))?;
367
368 if out.status.success() {
369 let reports_dir = crate::tools::file_ops::hematite_dir().join("reports");
370 Ok(format!(
371 "Task \"{}\" registered — runs {}.\n\
372 Action: {}\n\
373 Reports will save to: {}\n\
374 Run `hematite --schedule status` to confirm.",
375 TASK_NAME,
376 label,
377 task_run,
378 reports_dir.display()
379 ))
380 } else {
381 let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
382 let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string();
383 Err(if !stderr.is_empty() { stderr } else { stdout })
384 }
385 }
386}
387
388pub fn remove_scheduled_task() -> Result<String, String> {
389 #[cfg(not(target_os = "windows"))]
390 return Err("Scheduled tasks require Windows.".into());
391
392 #[cfg(target_os = "windows")]
393 {
394 let out = std::process::Command::new("schtasks")
395 .args(["/delete", "/tn", TASK_NAME, "/f"])
396 .output()
397 .map_err(|e| format!("Failed to run schtasks: {}", e))?;
398
399 if out.status.success() {
400 Ok(format!("Task \"{}\" removed.", TASK_NAME))
401 } else {
402 let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
403 Err(if !stderr.is_empty() {
404 stderr
405 } else {
406 format!("Task \"{}\" not found — nothing to remove.", TASK_NAME)
407 })
408 }
409 }
410}
411
412pub fn query_scheduled_task() -> String {
413 #[cfg(not(target_os = "windows"))]
414 return "Scheduled tasks are Windows-only. Use cron for recurring triage:\n\
415 hematite --triage --report-format html"
416 .to_string();
417
418 #[cfg(target_os = "windows")]
419 {
420 let out = std::process::Command::new("schtasks")
421 .args(["/query", "/tn", TASK_NAME, "/fo", "LIST"])
422 .output();
423
424 match out {
425 Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).trim().to_string(),
426 Ok(o) => {
427 let stderr = String::from_utf8_lossy(&o.stderr).to_ascii_lowercase();
428 if stderr.contains("cannot find") || stderr.contains("does not exist") {
429 format!("Task \"{}\" is not registered.", TASK_NAME)
430 } else {
431 format!(
432 "Not registered: {}",
433 String::from_utf8_lossy(&o.stderr).trim()
434 )
435 }
436 }
437 Err(e) => format!("Error querying task: {}", e),
438 }
439 }
440}