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 = "snake_case")] pub enum Check { Systemd(String), Pacman(String), Http { title: Option, url: String }, Shell { title: String, command: String }, } 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::Shell { command, .. } => { 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..]) .status() .await; match status { Ok(status) if status.success() => Ok(Default::default()), Ok(status) => bail!("failed with code {}", 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}") } } } } }