//! Data types and associated methods for monitoring and configuring solar-sbot. use std::{fs, fs::File, io, io::Write, path::PathBuf, process::Command, str}; use std::os::linux::raw::ino_t; use tilde_client::{TildeClient}; use log::debug; 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 */ /// solar-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 solar-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 service_name = config_manager::get_config_value("TILDE_SBOT_SERVICE")?; let info_output = Command::new("systemctl") .arg("show") .arg(service_name) .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("TILDE_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/solar-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()) } } } } // TOOD restore this // get path to blobstore // let blobstore_path = format!( // "{}/blobs/sha256", // config_manager::get_config_value("TILDE_SBOT_DATADIR")? // ); let blobstore_path = format!( "{}", config_manager::get_config_value("TILDE_SBOT_DATADIR")? ); // determine the size of the blobstore directory in bytes status.blobstore = dir_size(blobstore_path).ok(); Ok(status) } } /// solar-sbot configuration parameters. #[derive(Debug, Serialize, Deserialize, Default)] #[serde(default)] pub struct Config { // TODO: maybe define as a Path type? /// Directory path for the log and indexes. pub database_directory: 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 replication_hops: u8, /// Ip address of pub pub ip: String, /// Address to listen on. pub ssb_port: 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, } // TODO: make this real #[derive(Debug, Deserialize, Serialize)] #[serde(default)] pub struct SbotConfig { pub database_directory: String, pub shscap: String, pub hmac: String, pub replication_hops: i8, pub ip: String, pub ssb_port: String, // TODO: below settings have not been configured with tilde pub localadv: bool, pub localdiscov: bool, pub enable_ebt: bool, pub promisc: bool, pub nounixsock: bool, } /// Default configuration values for solar-sbot. impl Default for SbotConfig { fn default() -> Self { Self { database_directory: "~/.local/share/tildefriends".to_string(), shscap: "1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=".to_string(), hmac: "".to_string(), replication_hops: 1, ip: "".to_string(), ssb_port: "8008".to_string(), localadv: false, localdiscov: false, enable_ebt: false, promisc: false, nounixsock: false, } } } impl SbotConfig { /// Read the solar-sbot `config.toml` file from file and deserialize into `SbotConfig`. pub fn read() -> Result { // determine path of user's solar-sbot config.toml let config_path = format!( "{}/tilde-sbot.toml", config_manager::get_config_value("TILDE_SBOT_DATADIR")? ); println!("TILDE_SBOT_CONFIG_PATH: {}", config_path); 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 tilde-sbot `tilde-sbot.toml` file. pub fn write(config: SbotConfig) -> Result<(), PeachError> { let repo_comment = "# For details about tilde-sbot configuration, please visit tilde friends documentation\n".to_string(); // convert the provided `SbotConfig` instance to a string let config_string = toml::to_string(&config)?; // determine path of user's solar-sbot config.toml let config_path = format!( "{}/tilde-sbot.toml", config_manager::get_config_value("TILDE_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(()) } } /// Initialise an sbot client pub async fn init_sbot() -> Result { // read sbot config let sbot_config = match SbotConfig::read() { Ok(config) => config, // build default config if an error is returned from the read attempt Err(_) => SbotConfig::default(), }; debug!("Initialising an sbot client with configuration parameters"); let database_path = format!("{}/db.sqlite", config_manager::get_config_value("TILDE_SBOT_DATADIR")?); let sbot_client = TildeClient { ssb_port: sbot_config.ssb_port, tilde_binary_path: config_manager::get_config_value("TILDE_BINARY_PATH")?, tilde_database_path: database_path, }; Ok(sbot_client) }