diff --git a/peach-stats/src/error.rs b/peach-stats/src/error.rs index d782e35..e75a5e2 100644 --- a/peach-stats/src/error.rs +++ b/peach-stats/src/error.rs @@ -1,7 +1,7 @@ //! Custom error type for `peach-stats`. use probes::ProbeError; -use std::{error, fmt, io::Error as IoError}; +use std::{error, fmt, io::Error as IoError, str::Utf8Error}; /// Custom error type encapsulating all possible errors when retrieving system /// statistics. @@ -17,6 +17,10 @@ pub enum StatsError { MemStat(ProbeError), /// Failed to retrieve system uptime. Uptime(IoError), + /// Systemctl command returned an error. + Systemctl(IoError), + /// Failed to interpret sequence of `u8` as a string. + Utf8String(Utf8Error), } impl error::Error for StatsError {} @@ -39,6 +43,12 @@ impl fmt::Display for StatsError { StatsError::Uptime(ref source) => { write!(f, "Failed to retrieve system uptime: {}", source) } + StatsError::Systemctl(ref source) => { + write!(f, "Systemctl command returned an error: {}", source) + } + StatsError::Utf8String(ref source) => { + write!(f, "Failed to convert stdout to string: {}", source) + } } } } diff --git a/peach-stats/src/lib.rs b/peach-stats/src/lib.rs index cae7d50..7a0e0b0 100644 --- a/peach-stats/src/lib.rs +++ b/peach-stats/src/lib.rs @@ -43,6 +43,7 @@ //! ``` pub mod error; +pub mod sbot; pub mod stats; pub use crate::error::StatsError; diff --git a/peach-stats/src/sbot.rs b/peach-stats/src/sbot.rs new file mode 100644 index 0000000..b67a417 --- /dev/null +++ b/peach-stats/src/sbot.rs @@ -0,0 +1,90 @@ +//! Systemd go-sbot process statistics retrieval functions and associated data types. + +use std::{process::Command, str}; + +use crate::StatsError; + +/// go-sbot process statistics. +#[derive(Debug)] +pub struct SbotStat { + /// Current process state. + state: String, + /// Current process memory usage in bytes. + memory: Option, + /// Uptime for the process (if state is `active`). + uptime: Option, + /// Downtime for the process (if state is `inactive`). + downtime: Option, +} + +impl SbotStat { + /// Default builder for `SbotStat`. + fn default() -> Self { + Self { + state: String::new(), + memory: None, + uptime: None, + downtime: None, + } + } +} + +/// Retrieve statistics for the go-sbot systemd process by querying `systemctl`. +pub fn sbot_stats() -> Result { + let mut status = SbotStat::default(); + + let info_output = Command::new("/usr/bin/systemctl") + .arg("--user") + .arg("show") + .arg("go-sbot.service") + .arg("--no-page") + .output() + .map_err(StatsError::Systemctl)?; + + let service_info = std::str::from_utf8(&info_output.stdout).map_err(StatsError::Utf8String)?; + + for line in service_info.lines() { + if line.starts_with("ActiveState=") { + if let Some(state) = line.strip_prefix("ActiveState=") { + status.state = state.to_string() + } + } else if line.starts_with("MemoryCurrent=") { + if let Some(memory) = line.strip_prefix("MemoryCurrent=") { + status.memory = memory.parse().ok() + } + } + } + + let status_output = Command::new("/usr/bin/systemctl") + .arg("--user") + .arg("status") + .arg("go-sbot.service") + .output() + .unwrap(); + + let service_status = str::from_utf8(&status_output.stdout).map_err(StatsError::Utf8String)?; + + // example of the output line we're looking for: + // `Active: active (running) since Mon 2022-01-24 16:22:51 SAST; 4min 14s ago` + + for line in service_status.lines() { + if line.contains("Active:") { + let before_time = line.find(';'); + let after_time = line.find(" ago"); + if let (Some(start), Some(end)) = (before_time, after_time) { + // extract the uptime / downtime from the `Active: ...` line + // using the index of ';' + 2 and the index of " ago" + let time = Some(&line[start + 2..end]); + // if service is active then the `time` reading is uptime + if status.state == "active" { + status.uptime = time.map(|t| t.to_string()) + // if service is inactive then the `time` reading is downtime + } else if status.state == "inactive" { + status.downtime = time.map(|t| t.to_string()) + } + } + } + } + + Ok(status) +}