use crate::utils::read_to_string;
use crate::{traits::*, utils::Size};
use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize};
use std::env::{self, var, vars};
use std::fmt::Display;
use std::process::Command;
#[derive(Debug, Serialize)]
pub struct Sys {
pub machine_id: Option<String>,
pub timezone: Option<String>,
pub env_vars: Vec<(String, String)>,
pub uptime: Uptime,
pub loadavg: LoadAVG,
pub shells: Shells,
pub hostname: Option<HostName>,
}
impl Sys {
pub fn new() -> Result<Self> {
Ok(Self {
machine_id: read_to_string("/etc/machine-id").ok(),
timezone: read_to_string("/etc/timezone").ok(),
env_vars: get_env_vars(),
uptime: Uptime::new()?,
loadavg: LoadAVG::new()?,
shells: get_shells()?,
hostname: get_hostname(),
})
}
pub fn update(&mut self) -> Result<()> {
self.uptime = Uptime::new()?;
self.loadavg = LoadAVG::new()?;
Ok(())
}
}
impl ToJson for Sys {}
#[derive(Debug, Serialize, Clone)]
pub struct Kernel {
pub uname: Option<String>,
pub cmdline: Option<String>,
pub arch: Option<String>,
pub version: Option<String>,
pub build_info: Option<String>,
pub pid_max: u32,
pub threads_max: u32,
pub user_events_max: Option<u32>,
pub enthropy_avail: Option<u16>, }
impl Kernel {
pub fn new() -> Result<Self> {
Ok(Self {
uname: read_to_string("/proc/version").ok(),
cmdline: read_to_string("/proc/cmdline").ok(),
arch: read_to_string("/proc/sys/kernel/arch").ok(),
version: read_to_string("/proc/sys/kernel/osrelease").ok(),
build_info: read_to_string("/proc/sys/kernel/version").ok(),
pid_max: read_to_string("/proc/sys/kernel/pid_max")?.parse()?,
threads_max: read_to_string("/proc/sys/kernel/threads-max")?.parse()?,
user_events_max: match read_to_string("/proc/sys/kernel/user_events_max").ok() {
Some(uem) => uem.parse().ok(),
None => None,
},
enthropy_avail: match read_to_string("/proc/sys/kernel/random/entropy_avail").ok() {
Some(ea) => ea.parse().ok(),
None => None,
},
})
}
}
impl ToJson for Kernel {}
#[derive(Debug, Serialize, Default, Clone)]
pub struct OsRelease {
pub name: String,
pub id: Option<String>,
pub id_like: Option<String>,
pub pretty_name: Option<String>,
pub cpe_name: Option<String>,
pub variant: Option<String>,
pub variant_id: Option<String>,
pub version: Option<String>,
pub version_id: Option<String>,
pub version_codename: Option<String>,
pub build_id: Option<String>,
pub image_id: Option<String>,
pub image_version: Option<String>,
pub home_url: Option<String>,
pub documentation_url: Option<String>,
pub support_url: Option<String>,
pub bug_report_url: Option<String>,
pub privacy_policy_url: Option<String>,
pub logo: Option<String>,
pub default_hostname: Option<String>,
pub sysext_level: Option<String>,
}
impl OsRelease {
pub fn new() -> Result<Self> {
let chunks = get_chunks_osrelease(read_to_string("/etc/os-release")?);
let mut osr = Self::default();
for chunk in chunks {
parse_osrelease(&mut osr, chunk);
}
Ok(osr)
}
}
impl ToJson for OsRelease {}
fn get_chunks_osrelease(contents: String) -> Vec<(Option<String>, Option<String>)> {
contents
.lines()
.map(|item| {
let mut items = item.split('=').map(sanitize_str);
(items.next(), items.next())
})
.collect::<Vec<_>>()
}
fn parse_osrelease(osr: &mut OsRelease, chunk: (Option<String>, Option<String>)) {
match chunk {
(Some(key), Some(val)) => {
let key = &key as &str;
match key {
"NAME" => osr.name = val.to_string(),
"ID" => osr.id = Some(val.to_string()),
"ID_LIKE" => osr.id_like = Some(val.to_string()),
"PRETTY_NAME" => osr.pretty_name = Some(val.to_string()),
"CPE_NAME" => osr.cpe_name = Some(val.to_string()),
"VARIANT" => osr.variant = Some(val.to_string()),
"VARIANT_ID" => osr.variant_id = Some(val.to_string()),
"VERSION" => osr.version = Some(val.to_string()),
"VERSION_CODENAME" => osr.version_codename = Some(val.to_string()),
"VERSION_ID" => osr.version_id = Some(val.to_string()),
"BUILD_ID" => osr.build_id = Some(val.to_string()),
"IMAGE_ID" => osr.image_id = Some(val.to_string()),
"IMAGE_VERSION" => osr.image_version = Some(val.to_string()),
"HOME_URL" => osr.home_url = Some(val.to_string()),
"DOCUMENTATION_URL" => osr.documentation_url = Some(val.to_string()),
"SUPPORT_URL" => osr.support_url = Some(val.to_string()),
"BUG_REPORT_URL" => osr.bug_report_url = Some(val.to_string()),
"PRIVACY_POLICY_URL" => osr.privacy_policy_url = Some(val.to_string()),
"LOGO" => osr.logo = Some(val.to_string()),
"DEFAULT_HOSTNAME" => osr.default_hostname = Some(val.to_string()),
"SYSEXT_LEVEL" => osr.sysext_level = Some(val.to_string()),
_ => {}
}
}
_ => {}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Shell {
pub path: String,
pub version: String,
}
impl Shell {
pub fn new() -> Result<Self> {
let path = env::var("SHELL")?;
let version = {
let stdout = Command::new(&path).arg("--version").output()?.stdout;
String::from_utf8(stdout)?
.lines()
.next()
.unwrap_or("")
.to_string()
};
Ok(Self { path, version })
}
}
impl Display for Shell {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", &self.version, &self.path)
}
}
#[derive(Debug, Serialize, Clone)]
pub struct Uptime(
pub f32,
pub f32,
);
impl Uptime {
pub fn new() -> Result<Self> {
let data = read_to_string("/proc/uptime")?;
let mut chunks = data.split_whitespace();
match (chunks.next(), chunks.next()) {
(Some(a), Some(b)) => Ok(Self(a.parse()?, b.parse()?)),
_ => Err(anyhow!("`/proc/uptime` file format is incorrect!")),
}
}
}
impl ToPlainText for Uptime {
fn to_plain(&self) -> String {
format!(
"\nUptime: {} seconds; downtime: {} seconds\n",
self.0, self.1
)
}
}
#[derive(Debug, Serialize, Clone)]
pub struct LoadAVG(
pub f32,
pub f32,
pub f32,
);
impl LoadAVG {
pub fn new() -> Result<Self> {
let data = read_to_string("/proc/loadavg")?;
let mut chunks = data.split_whitespace();
match (chunks.next(), chunks.next(), chunks.next()) {
(Some(a), Some(b), Some(c)) => Ok(Self(a.parse()?, b.parse()?, c.parse()?)),
_ => Err(anyhow!("`/proc/loadavg` file format is incorrect!")),
}
}
}
impl ToPlainText for LoadAVG {
fn to_plain(&self) -> String {
let mut s = format!("\nAverage system load:\n");
s += &print_val("1 minute", &self.0);
s += &print_val("5 minutes", &self.1);
s += &print_val("15 minutes", &self.2);
s
}
}
#[derive(Debug, Serialize, Clone)]
pub struct Users {
pub users: Vec<User>,
}
impl ToJson for Users {}
impl Users {
pub fn new() -> Result<Self> {
let mut users = vec![];
for user in read_to_string("/etc/passwd")?.lines() {
match User::try_from(user) {
Ok(user) => users.push(user),
Err(_) => continue,
}
}
Ok(Self { users })
}
}
#[derive(Debug, Serialize, Clone)]
pub struct User {
pub name: String,
pub uid: u32,
pub gid: u32,
pub gecos: Option<String>,
pub home_dir: String,
pub login_shell: String,
}
impl TryFrom<&str> for User {
type Error = anyhow::Error;
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
let chunks = value
.trim()
.split(':')
.map(sanitize_str)
.collect::<Vec<_>>();
if chunks.len() != 7 {
return Err(anyhow!("Field \"{value}\" is incorrect user entry"));
}
Ok(Self {
name: sanitize_str(&chunks[0]),
uid: chunks[2].parse()?,
gid: chunks[3].parse()?,
gecos: match chunks[4].is_empty() {
true => None,
false => Some(sanitize_str(&chunks[4])),
},
home_dir: sanitize_str(&chunks[5]),
login_shell: sanitize_str(&chunks[6]),
})
}
}
impl ToPlainText for User {
fn to_plain(&self) -> String {
let mut s = format!("\nUser '{}':\n", &self.name);
s += &print_val("User ID", &self.uid);
s += &print_val("Group ID", &self.gid);
s += &print_opt_val("GECOS", &self.gecos);
s += &print_val("Home directory", &self.home_dir);
s += &print_val("Login shell", &self.login_shell);
s
}
}
pub fn current_user() -> Option<String> {
std::env::var("USER").ok()
}
#[derive(Debug, Serialize, Clone)]
pub struct Groups {
pub groups: Vec<Group>,
}
impl ToJson for Groups {}
impl Groups {
pub fn new() -> Result<Self> {
let mut groups = vec![];
for group in read_to_string("/etc/group")?.lines() {
match Group::try_from(group) {
Ok(group) => groups.push(group),
Err(_) => continue,
}
}
Ok(Self { groups })
}
}
#[derive(Debug, Serialize, Clone)]
pub struct Group {
pub name: String,
pub gid: u32,
pub users: Vec<String>,
}
impl TryFrom<&str> for Group {
type Error = anyhow::Error;
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
let chunks = value
.trim()
.split(':')
.map(sanitize_str)
.collect::<Vec<_>>();
if chunks.len() != 4 {
return Err(anyhow!("Field \"{value}\" is incorrect group entry"));
}
Ok(Self {
name: chunks[0].to_string(),
gid: chunks[2].parse()?,
users: {
let mut users = vec![];
for user in chunks[3].split(',') {
if !user.is_empty() {
users.push(user.to_string());
}
}
users
},
})
}
}
pub type Shells = Vec<String>;
fn get_shells() -> Result<Shells> {
let mut shells = vec![];
for shell in read_to_string("/etc/shells")?
.lines()
.filter(|line| !line.is_empty() && !line.starts_with('#'))
{
shells.push(shell.to_string());
}
Ok(shells)
}
pub type HostName = String;
pub fn get_hostname() -> Option<HostName> {
match read_to_string("/etc/hostname") {
Ok(s) => Some(sanitize_str(&s)),
Err(_) => None,
}
}
#[derive(Debug, Serialize, Clone)]
pub struct Locale {}
fn sanitize_str(s: &str) -> String {
s.trim().replace('"', "").replace('\'', "")
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct KModules {
pub modules: Vec<Module>,
}
impl ToJson for KModules {}
impl KModules {
pub fn new() -> Result<Self> {
let contents = read_to_string("/proc/modules")?;
let contents = contents.lines();
let mut modules = Vec::new();
for s in contents {
modules.push(Module::try_from(s)?);
}
Ok(Self { modules })
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Module {
pub name: String,
pub size: Size,
pub instances: usize,
pub dependencies: String,
pub state: String,
pub memory_addrs: String,
}
impl TryFrom<&str> for Module {
type Error = anyhow::Error;
fn try_from(value: &str) -> Result<Self> {
let mut ch = value.split_whitespace();
match (
ch.next(),
ch.next(),
ch.next(),
ch.next(),
ch.next(),
ch.next(),
) {
(
Some(name),
Some(size),
Some(instances),
Some(dependencies),
Some(state),
Some(memory_addrs),
) => {
let size = size.parse::<u64>().map_err(|err| anyhow!("{err}"))?;
let instances = instances.parse::<usize>().map_err(|err| anyhow!("{err}"))?;
Ok(Self {
name: name.to_string(),
size: Size::B(size),
instances,
dependencies: dependencies.to_string(),
state: state.to_string(),
memory_addrs: memory_addrs.to_string(),
})
}
_ => Err(anyhow!("Unknown field: \"{value}\"")),
}
}
}
pub fn get_current_desktop() -> Option<String> {
var("XDG_CURRENT_DESKTOP").ok()
}
pub fn get_lang() -> Option<String> {
let lang = var("LANG").ok();
let lc_all = var("LC_ALL").ok();
if lang.is_some() { lang } else { lc_all }
}
pub fn get_env_vars() -> Vec<(String, String)> {
let mut vars = vars().collect::<Vec<(String, String)>>();
vars.sort_by_key(|v| v.0.clone());
vars
}
pub fn get_machine_id() -> Option<String> {
read_to_string("/etc/machine-id").ok()
}