hematite/agent/
scheduler.rs1const TASK_NAME: &str = "Hematite Health Check";
2
3pub fn register_scheduled_task(cadence: &str, exe_path: &str) -> Result<String, String> {
4 #[cfg(not(target_os = "windows"))]
5 {
6 let _ = (cadence, exe_path);
7 return Err("Scheduled tasks require Windows (schtasks.exe).\n\
8 On Linux/macOS use cron instead:\n\
9 hematite --triage --report-format html"
10 .into());
11 }
12
13 #[cfg(target_os = "windows")]
14 {
15 let task_run = format!("\"{}\" --triage --report-format html", exe_path);
16
17 let (schedule_type, extra_args, label): (&str, &[&str], &str) = match cadence {
18 "daily" => ("daily", &[], "daily at 08:00"),
19 _ => ("weekly", &["/d", "MON"], "weekly on Monday at 08:00"),
20 };
21
22 let mut args: Vec<String> = vec![
23 "/create".into(),
24 "/tn".into(),
25 TASK_NAME.into(),
26 "/tr".into(),
27 task_run.clone(),
28 "/sc".into(),
29 schedule_type.into(),
30 "/st".into(),
31 "08:00".into(),
32 ];
33 for a in extra_args {
34 args.push(a.to_string());
35 }
36 args.push("/f".into());
37
38 let out = std::process::Command::new("schtasks")
39 .args(&args)
40 .output()
41 .map_err(|e| format!("Failed to run schtasks: {}", e))?;
42
43 if out.status.success() {
44 let reports_dir = crate::tools::file_ops::hematite_dir().join("reports");
45 Ok(format!(
46 "Task \"{}\" registered — runs {}.\n\
47 Action: {}\n\
48 Reports will save to: {}\n\
49 Run `hematite --schedule status` to confirm.",
50 TASK_NAME,
51 label,
52 task_run,
53 reports_dir.display()
54 ))
55 } else {
56 let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
57 let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string();
58 Err(if !stderr.is_empty() { stderr } else { stdout })
59 }
60 }
61}
62
63pub fn remove_scheduled_task() -> Result<String, String> {
64 #[cfg(not(target_os = "windows"))]
65 return Err("Scheduled tasks require Windows.".into());
66
67 #[cfg(target_os = "windows")]
68 {
69 let out = std::process::Command::new("schtasks")
70 .args(["/delete", "/tn", TASK_NAME, "/f"])
71 .output()
72 .map_err(|e| format!("Failed to run schtasks: {}", e))?;
73
74 if out.status.success() {
75 Ok(format!("Task \"{}\" removed.", TASK_NAME))
76 } else {
77 let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
78 Err(if !stderr.is_empty() {
79 stderr
80 } else {
81 format!("Task \"{}\" not found — nothing to remove.", TASK_NAME)
82 })
83 }
84 }
85}
86
87pub fn query_scheduled_task() -> String {
88 #[cfg(not(target_os = "windows"))]
89 return "Scheduled tasks are Windows-only. Use cron for recurring triage:\n\
90 hematite --triage --report-format html"
91 .to_string();
92
93 #[cfg(target_os = "windows")]
94 {
95 let out = std::process::Command::new("schtasks")
96 .args(["/query", "/tn", TASK_NAME, "/fo", "LIST"])
97 .output();
98
99 match out {
100 Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).trim().to_string(),
101 Ok(o) => {
102 let stderr = String::from_utf8_lossy(&o.stderr).to_ascii_lowercase();
103 if stderr.contains("cannot find") || stderr.contains("does not exist") {
104 format!("Task \"{}\" is not registered.", TASK_NAME)
105 } else {
106 format!(
107 "Not registered: {}",
108 String::from_utf8_lossy(&o.stderr).trim()
109 )
110 }
111 }
112 Err(e) => format!("Error querying task: {}", e),
113 }
114 }
115}