use crate::{log::update_service, Config, Status, GLOBAL_ERROR, STATUS}; use anyhow::{anyhow, bail, Result}; use chrono::Utc; use futures::{stream::FuturesUnordered, StreamExt}; use log::info; use serde::Deserialize; use std::{path::PathBuf, sync::Arc, time::Duration}; use tokio::{ fs::read_to_string, process::Command, time::{sleep, timeout}, }; use crate::dbus::*; #[derive(Debug, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum Check { Systemd(String), SystemdGlobal, SystemdUser { user: String, name: String, }, SystemdUserGlobal(String), Pacman(String), Http { title: Option, url: String, expected: Option, }, Shell { title: String, command: String, #[serde(default)] output: bool, }, } pub async fn check_loop(config: Arc, i: usize) { loop { check_service(&config, i).await; sleep(Duration::from_secs(config.interval)).await; } } async fn check_service(config: &Arc, i: usize) { let service = &config.services[i]; let mut futs = FuturesUnordered::from_iter(service.checks.iter().enumerate().map( |(j, check)| async move { let r = match timeout(Duration::from_secs(30), check.check()).await { Ok(Ok(succ)) => Ok(succ), Ok(Err(e)) => Err(e), Err(_) => Err(anyhow!("timed out")), }; info!("check {i}:{j} => {r:?}"); let status = Status { time: Utc::now(), status: r.map_err(|e| format!("{e:?}")), }; { let mut g = STATUS.write().await; g.insert((i, j), status.clone()); } let config = config.clone(); tokio::task::spawn(async move { if let Err(e) = update_service(config.clone(), i, j, status).await { *GLOBAL_ERROR.write().await = Some(e); } }) }, )); while let Some(_) = futs.next().await {} } impl Check { pub async fn check(&self) -> Result { match self { Check::Pacman(package) => { let output = Command::new("pacman") .arg("-Q") .arg(package) .output() .await?; output.status.exit_ok()?; Ok(String::from_utf8(output.stdout)?) } Check::Systemd(name) => check_systemd_unit(None, name).await, Check::SystemdGlobal => check_systemd_all(None).await, Check::SystemdUser { user, name } => check_systemd_unit(Some(user), name).await, Check::SystemdUserGlobal(user) => check_systemd_all(Some(user)).await, Check::Shell { command, output, .. } => { let args = shlex::split(&command).ok_or(anyhow!("command syntax invalid"))?; let status = Command::new(args.get(0).ok_or(anyhow!("argv0 missing"))?) .args(&args[1..]) .output() .await; if *output { match status { Ok(status) if status.status.success() => { Ok(String::from_utf8_lossy(&status.stdout).to_string()) } Ok(status) => bail!("{}", String::from_utf8_lossy(&status.stdout)), Err(e) => bail!("command failed to execute: {e}"), } } else { match status { Ok(status) if status.status.success() => Ok(Default::default()), Ok(status) => { bail!("failed with code {}", status.status.code().unwrap_or(1)) } Err(e) => bail!("command failed to execute: {e}"), } } } Check::Http { url, expected, .. } => { let r = reqwest::get(url).await?; let mut s = format!( "{} {}", r.status().as_str(), r.status().canonical_reason().unwrap_or_default() ); let mut succ = r.status().is_success(); if let Some(path) = expected { let actual = r.text().await?; let expected = read_to_string(path).await?; if actual == expected { s += " (content identical)" } else { succ = false; s += " (content differs)" } } if succ { Ok(s) } else { bail!("{s}") } } } } } impl Check { pub fn display(&self) -> String { match self { Check::Systemd(_) => "Service".to_string(), Check::Http { title, .. } => title.clone().unwrap_or("HTTP".to_string()), Check::Shell { title, .. } => title.to_owned(), Check::Pacman(_) => "Installed".to_string(), Check::SystemdGlobal => "System Services".to_string(), Check::SystemdUser { user, .. } => format!("User service for {user}"), Check::SystemdUserGlobal(username) => format!("User services for {username}"), } } }