1#![deny(warnings)]
2#![allow(clippy::too_many_arguments, clippy::large_enum_variant)]
3#[cfg(not(target_os = "linux"))]
4compile_error!("Only Linux is supported");
5
6mod action;
7mod alarm;
8mod check;
9pub mod config;
10mod filter;
11mod measurement;
12mod process;
13mod report;
14pub mod uptime;
15mod window_buffer;
16
17pub type Result<T> = std::result::Result<T, Error>;
18type PlaceholderMap = std::collections::HashMap<String, String>;
19type ActionMap = std::collections::HashMap<String, std::sync::Arc<dyn action::Action>>;
20pub type ReportWhen = report::ReportWhen;
21
22pub fn user_agent() -> String {
23 format!("MinMon/v{}", env!("CARGO_PKG_VERSION"))
24}
25
26#[derive(Debug, Clone)]
27pub struct Error(pub String);
28impl std::error::Error for Error {}
29
30impl std::fmt::Display for Error {
31 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
32 write!(f, "{}", self.0)
33 }
34}
35
36static ENV_VARS: std::sync::OnceLock<std::collections::HashMap<String, String>> =
37 std::sync::OnceLock::new();
38
39pub fn init_env_vars(config: &config::Config) {
40 let mut env_vars = PlaceholderMap::new();
41 for (name, value) in std::env::vars() {
42 if name.starts_with(&config.general.env_var_prefix) {
43 env_vars.insert(format!("env:{name}"), value);
44 }
45 }
46 ENV_VARS.set(env_vars).unwrap();
47}
48
49fn global_placeholders() -> PlaceholderMap {
50 let mut res = ENV_VARS.get().unwrap().clone();
51 let system_uptime = uptime::system();
52 let minmon_uptime = uptime::process();
53 res.insert(
54 String::from("system_uptime"),
55 system_uptime.as_secs().to_string(),
56 );
57 res.insert(
58 String::from("system_uptime_iso"),
59 duration_iso8601(system_uptime),
60 );
61 res.insert(
62 String::from("minmon_uptime"),
63 minmon_uptime.as_secs().to_string(),
64 );
65 res.insert(
66 String::from("minmon_uptime_iso"),
67 duration_iso8601(minmon_uptime),
68 );
69 res
70}
71
72fn merge_placeholders(target: &mut PlaceholderMap, source: &PlaceholderMap) {
73 for (key, value) in source.iter() {
74 target.insert(key.clone(), value.clone());
75 }
76}
77
78fn fill_placeholders(template: &str, placeholders: &PlaceholderMap) -> String {
79 let template = text_placeholder::Template::new(template);
80 template.fill_with_hashmap(
81 &placeholders
82 .iter()
83 .map(|(k, v)| (k.as_str(), v.as_str()))
84 .collect(),
85 )
86}
87
88fn datetime_iso8601(system_time: std::time::SystemTime) -> String {
89 let date_time: chrono::DateTime<chrono::Utc> = system_time.into();
90 date_time.format("%FT%TZ").to_string()
91}
92
93fn duration_iso8601(duration: std::time::Duration) -> String {
95 const SECONDS_PER_MINUTE: u64 = 60;
96 const MINUTES_PER_HOUR: u64 = 60;
97 const HOURS_PER_DAY: u64 = 24;
98 const SECONDS_PER_HOUR: u64 = SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
99 const SECONDS_PER_DAY: u64 = SECONDS_PER_HOUR * HOURS_PER_DAY;
100 let mut remainder = duration.as_secs();
101 if remainder == 0 {
102 return String::from("PT0S");
103 }
104 let mut res = String::new();
105 let days = remainder / SECONDS_PER_DAY;
106 if days > 0 {
107 res = format!("P{days}D");
108 }
109 remainder %= SECONDS_PER_DAY;
110 if remainder == 0 {
111 return res;
112 }
113 res.push('T');
114 let hours = remainder / SECONDS_PER_HOUR;
115 if hours > 0 {
116 res = format!("{res}{hours}H");
117 }
118 remainder %= SECONDS_PER_HOUR;
119 if remainder == 0 {
120 return res;
121 }
122 let minutes = remainder / SECONDS_PER_MINUTE;
123 if minutes > 0 {
124 res = format!("{res}{minutes}M");
125 }
126 remainder %= SECONDS_PER_MINUTE;
127 if remainder > 0 {
128 res = format!("{res}{remainder}S");
129 }
130 res
131}
132
133fn init_actions(config: &config::Config) -> Result<ActionMap> {
134 log::info!("Initializing {} actions(s)..", config.actions.len());
135 let mut res = ActionMap::new();
136 for action_config in config.actions.iter() {
137 if res.contains_key(&action_config.name) {
138 return Err(Error(format!(
139 "Found duplicate action name: {}",
140 action_config.name
141 )));
142 }
143 let action = action::from_action_config(action_config)?;
144 res.insert(action_config.name.clone(), action);
145 log::info!("Action '{}' initialized.", action_config.name);
146 }
147 Ok(res)
148}
149
150fn init_report(config: &config::Config, actions: &ActionMap) -> Result<Option<report::Report>> {
151 log::info!("Initializing report..");
152 let report_config = &config.report;
153 if report_config.disable {
154 log::info!("Report is disabled.");
155 return Ok(None);
156 }
157 let report = report::from_report_config(report_config, actions)?;
158 match &report.when {
159 report::ReportWhen::Interval(interval) => {
160 log::info!(
161 "Report will be triggered every {} seconds.",
162 interval.as_secs()
163 )
164 }
165 report::ReportWhen::Cron(schedule) => {
166 log::info!("Report will be triggered by cron schedule '{schedule}'.")
167 }
168 }
169 Ok(Some(report))
170}
171
172fn init_checks(config: &config::Config, actions: &ActionMap) -> Result<Vec<Box<dyn check::Check>>> {
173 log::info!("Initializing {} check(s)..", config.checks.len());
174 let mut res: Vec<Box<dyn check::Check>> = Vec::new();
175 let mut used_names = std::collections::HashSet::new();
176 for check_config in config.checks.iter() {
177 if !used_names.insert(check_config.name.clone()) {
178 return Err(Error(format!(
179 "Found duplicate check name: {}",
180 check_config.name
181 )));
182 }
183 if check_config.disable {
184 log::info!("Check '{}' is disabled.", check_config.name);
185 continue;
186 }
187 let check = check::from_check_config(check_config, actions)?;
188 log::info!(
189 "Check '{}' will be triggered every {} seconds.",
190 check.name(),
191 check.interval().as_secs()
192 );
193 res.push(check);
194 }
195 Ok(res)
196}
197
198type ConfigState = (Option<report::Report>, Vec<Box<dyn check::Check>>);
199
200pub fn from_config(config: &config::Config) -> Result<ConfigState> {
201 let actions = init_actions(config)?;
202 let report = init_report(config, &actions)?;
203 let checks = init_checks(config, &actions)?;
204 Ok((report, checks))
205}
206
207pub fn start_delay(config: &config::Config) -> Option<std::time::Duration> {
208 let uptime = uptime::system();
209 let boot_delay = config
210 .general
211 .boot_delay
212 .map(|x| std::time::Duration::from_secs(x.into()))
213 .filter(|x| x > &uptime)
214 .map(|x| x - uptime);
215 let start_delay = config
216 .general
217 .start_delay
218 .map(|x| std::time::Duration::from_secs(x.into()));
219
220 boot_delay.map_or(start_delay, |x| {
221 start_delay.map_or(boot_delay, |y| Some(x.max(y)))
222 })
223}
224
225fn get_number<T>(error_message: &str, line: &str, column: usize) -> Result<T>
226where
227 T: std::str::FromStr,
228 <T as std::str::FromStr>::Err: std::fmt::Display,
229{
230 {
231 line.split_whitespace()
232 .nth(column)
233 .ok_or_else(|| Error(String::from("Column not found.")))?
234 .parse()
235 .map_err(|x| Error(format!("{x}")))
236 }
237 .map_err(|x| Error(format!("{error_message}: {x}")))
238}
239
240#[cfg(test)]
241mod test {
242 use super::*;
243
244 #[test]
245 fn test_merge_placeholders() {
246 let mut target = PlaceholderMap::from([(String::from("A"), String::from("?"))]);
247 let source = PlaceholderMap::from([
248 (String::from("A"), String::from("B")),
249 (String::from("C"), String::from("D")),
250 ]);
251 merge_placeholders(&mut target, &source);
252 assert_eq!(target.get("A").unwrap(), "B");
253 assert_eq!(target.get("C").unwrap(), "D");
254 }
255
256 #[test]
257 fn test_fill_placeholders() {
258 let template = "X{{A}}{{missing}}Z";
259 let placeholders = PlaceholderMap::from([(String::from("A"), String::from("Y"))]);
260 let filled = fill_placeholders(template, &placeholders);
261 assert_eq!(filled, "XYZ");
262 }
263
264 #[test]
265 fn test_datetime_iso8601() {
266 let system_time = std::time::SystemTime::UNIX_EPOCH;
267 assert_eq!(datetime_iso8601(system_time), "1970-01-01T00:00:00Z");
268 }
269
270 #[test]
271 fn test_duration_iso8601() {
272 let duration = std::time::Duration::from_secs(123630);
273 assert_eq!(duration_iso8601(duration), "P1DT10H20M30S");
274 let duration = std::time::Duration::from_secs(37230);
275 assert_eq!(duration_iso8601(duration), "T10H20M30S");
276 let duration = std::time::Duration::from_secs(0);
277 assert_eq!(duration_iso8601(duration), "PT0S");
278 }
279
280 #[test]
281 fn test_get_number() {
282 let line = "0 1 2 3 4 5";
283 assert_eq!(get_number::<u32>("error", line, 0).unwrap(), 0);
284 assert_eq!(get_number::<u32>("error", line, 5).unwrap(), 5);
285 assert!(get_number::<u32>("error", line, 6).is_err());
286 }
287}