//! Data types and associated methods for monitoring and configuring go-sbot. use std::{fs, fs::File, io, io::Write, path::PathBuf, process::Command, str}; use crate::config_manager; use serde::{Deserialize, Serialize}; use crate::error::PeachError; /* HELPER FUNCTIONS */ // iterate over the given directory path to determine the size of the directory fn dir_size(path: impl Into) -> io::Result { fn dir_size(mut dir: fs::ReadDir) -> io::Result { dir.try_fold(0, |acc, file| { let file = file?; let size = match file.metadata()? { data if data.is_dir() => dir_size(fs::read_dir(file.path())?)?, data => data.len(), }; Ok(acc + size) }) } dir_size(fs::read_dir(path.into())?) } /* SBOT-RELATED TYPES AND METHODS */ /// go-sbot process status. #[derive(Debug, Serialize, Deserialize)] pub struct SbotStatus { /// Current process state. pub state: Option, /// Current process boot state. pub boot_state: Option, /// Current process memory usage in bytes. pub memory: Option, /// Uptime for the process (if state is `active`). pub uptime: Option, /// Downtime for the process (if state is `inactive`). pub downtime: Option, /// Size of the blobs directory in bytes. pub blobstore: Option, } /// Default builder for `SbotStatus`. impl Default for SbotStatus { fn default() -> Self { Self { state: None, boot_state: None, memory: None, uptime: None, downtime: None, blobstore: None, } } } impl SbotStatus { /// Retrieve statistics for the go-sbot systemd process by querying `systemctl`. pub fn read() -> Result { let mut status = SbotStatus::default(); // note this command does not need to be run as sudo // because non-privileged users are able to run systemctl show let info_output = Command::new("systemctl") .arg("show") .arg(config_manager::get_config_value("GO_SBOT_SERVICE")?) .arg("--no-page") .output()?; let service_info = std::str::from_utf8(&info_output.stdout)?; for line in service_info.lines() { if line.starts_with("ActiveState=") { if let Some(state) = line.strip_prefix("ActiveState=") { status.state = Some(state.to_string()) } } else if line.starts_with("MemoryCurrent=") { if let Some(memory) = line.strip_prefix("MemoryCurrent=") { status.memory = memory.parse().ok() } } } // note this command does not need to be run as sudo // because non-privileged users are able to run systemctl status let status_output = Command::new("systemctl") .arg("status") .arg(config_manager::get_config_value("GO_SBOT_SERVICE")?) .output()?; let service_status = str::from_utf8(&status_output.stdout)?; //.map_err(PeachError::Utf8ToStr)?; for line in service_status.lines() { // example of the output line we're looking for: // `Loaded: loaded (/home/glyph/.config/systemd/user/go-sbot.service; enabled; vendor // preset: enabled)` if line.contains("Loaded:") { let before_boot_state = line.find(';'); let after_boot_state = line.rfind(';'); if let (Some(start), Some(end)) = (before_boot_state, after_boot_state) { // extract the enabled / disabled from the `Loaded: ...` line // using the index of the first ';' + 2 and the last ';' status.boot_state = Some(line[start + 2..end].to_string()); } // example of the output line we're looking for here: // `Active: active (running) since Mon 2022-01-24 16:22:51 SAST; 4min 14s ago` } else 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 == Some("active".to_string()) { status.uptime = time.map(|t| t.to_string()) // if service is inactive then the `time` reading is downtime } else if status.state == Some("inactive".to_string()) { status.downtime = time.map(|t| t.to_string()) } } } } // get path to blobstore let blobstore_path = format!("{}/blobs/sha256", config_manager::get_config_value("GO_SBOT_DATADIR")?); // determine the size of the blobstore directory in bytes status.blobstore = dir_size(blobstore_path).ok(); Ok(status) } } /// go-sbot configuration parameters. #[derive(Debug, Serialize, Deserialize)] #[serde(default)] pub struct SbotConfig { // TODO: maybe define as a Path type? /// Directory path for the log and indexes. pub repo: String, /// Directory path for writing debug output. pub debugdir: String, /// Secret-handshake app-key (aka. network key). pub shscap: String, /// HMAC hash used to sign messages. pub hmac: String, /// Replication hops (1: friends, 2: friends of friends). pub hops: u8, /// Address to listen on. pub lis: String, /// Address to listen on for WebSocket connections. pub wslis: String, /// Address to for metrics and pprof HTTP server. pub debuglis: String, /// Enable sending local UDP broadcasts. pub localadv: bool, /// Enable listening for UDP broadcasts and connecting. pub localdiscov: bool, /// Enable syncing by using epidemic-broadcast-trees (EBT). #[serde(rename(serialize = "enable_ebt", deserialize = "enable-ebt"))] pub enable_ebt: bool, /// Bypass graph auth and fetch remote's feed (useful for pubs that are restoring their data /// from peer; user beware - caveats about). pub promisc: bool, /// Disable the UNIX socket RPC interface. pub nounixsock: bool, /// Attempt to repair the filesystem before starting. pub repair: bool, } /// Default configuration values for go-sbot. impl Default for SbotConfig { fn default() -> Self { Self { repo: ".ssb-go".to_string(), debugdir: "".to_string(), shscap: "1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=".to_string(), hmac: "".to_string(), hops: 1, lis: ":8008".to_string(), wslis: ":8989".to_string(), debuglis: "localhost:6078".to_string(), localadv: false, localdiscov: false, enable_ebt: false, promisc: false, nounixsock: false, repair: false, } } } impl SbotConfig { /// Read the go-sbot `config.toml` file from file and deserialize into `SbotConfig`. pub fn read() -> Result { // determine path of user's go-sbot config.toml let config_path = format!("{}/config.toml", config_manager::get_config_value("GO_SBOT_DATADIR")?); let config_contents = fs::read_to_string(config_path)?; let config: SbotConfig = toml::from_str(&config_contents)?; Ok(config) } /// Write the given `SbotConfig` to the go-sbot `config.toml` file. pub fn write(config: SbotConfig) -> Result<(), PeachError> { let repo_comment = "# For details about go-sbot configuration, please visit the repo: https://github.com/cryptoscope/ssb\n".to_string(); // convert the provided `SbotConfig` instance to a string let config_string = toml::to_string(&config)?; // determine path of user's go-sbot config.toml let config_path = format!("{}/config.toml", config_manager::get_config_value("GO_SBOT_DATADIR")?); // open config file for writing let mut file = File::create(config_path)?; // write the repo comment to file write!(file, "{}", repo_comment)?; // write the config string to file write!(file, "{}", config_string)?; Ok(()) } }