use crate::{log::update_service, Config, Status, GLOBAL_ERROR, STATUS}; use anyhow::{anyhow, bail, Context, Result}; use chrono::Utc; use futures::{stream::FuturesUnordered, StreamExt}; use log::info; use serde::Deserialize; use std::{sync::Arc, time::Duration}; use tokio::{ process::Command, time::{sleep, timeout}, }; #[derive(Debug, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum Check { Systemd(String), SystemdGlobal, Pacman(String), Http { title: Option, url: String, }, 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(sname) => { let output = Command::new("systemctl") .arg("show") .arg("--no-pager") .arg(sname) .output() .await?; let output = String::from_utf8(output.stdout).context("systemctl output")?; let mut active = ""; let mut sub = ""; for line in output.split("\n") { if let Some((key, value)) = line.split_once("=") { match key { "ActiveState" => active = value, "SubState" => sub = value, _ => (), } } } let s = format!("{active} ({sub})"); if active != "active" { Err(anyhow!(s)) } else { Ok(s) } } Check::SystemdGlobal => { let output = Command::new("systemctl") .arg("show") .arg("--no-pager") .output() .await?; let output = String::from_utf8(output.stdout).context("systemctl output")?; let mut nfailed = 0; for line in output.split("\n") { if let Some((key, value)) = line.split_once("=") { match key { "NFailedUnits" => { nfailed = value.parse().context("systemctl nfailed output")? } _ => (), } } } if nfailed > 0 { Err(anyhow!( "{nfailed} unit{} failed", if nfailed > 1 { "s" } else { "" } )) } else { Ok("running".to_string()) } } 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, .. } => { let r = reqwest::get(url).await?; let s = format!( "{} {}", r.status().as_str(), r.status().canonical_reason().unwrap_or_default() ); if r.status().is_success() { 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(), } } }