/* Varanus: client/server system monitor for PCs. * * Copyright (c) 2024 Scott Lawrence. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software * and associated documentation files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or * substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* TODO * * Accept command-line arguments to query only part of JSON. (Maybe also a formatting string?) * * Forecast * * Installation / packaging * * Desktop display (a la conky?) * */ use std::error; use std::fs::{read_dir,read_to_string,remove_file}; use std::io::{Read,Write}; use std::path::{Path,PathBuf}; use std::os::unix::net::{UnixStream,UnixListener}; use std::str; use std::sync::{Arc,Mutex}; use std::thread::{sleep,spawn}; use std::time::{Duration,Instant,SystemTime}; use clap::Parser; use serde::{Serialize,Deserialize}; use signal_hook::{consts::SIGINT,iterator::Signals}; mod forecast; type Result = std::result::Result>; fn read_line>(p: P) -> Result { let s = read_to_string(p)?; Ok(s.lines().next().ok_or("")?.to_string()) } #[derive(Default)] #[repr(C)] struct Sysinfo { uptime: cty::c_long, loads: [cty::c_ulong; 3], totalram: cty::c_ulong, freeram: cty::c_ulong, sharedram: cty::c_ulong, bufferram: cty::c_ulong, totalswap: cty::c_ulong, freeswap: cty::c_ulong, procs: cty::c_ushort, totalhigh: cty::c_ulong, freehigh: cty::c_ulong, mem_unit: cty::c_uint, /*char _f[20-2*sizeof(long)-sizeof(int)];*/ } extern "C" { fn sysinfo(si: *mut Sysinfo) -> cty::c_int; } impl Sysinfo { fn new() -> Self { let mut si = Sysinfo::default(); unsafe { sysinfo(&mut si) }; return si; } } #[derive(Parser)] struct Cli { #[arg(short='d', long)] daemon: bool, #[arg(short='v', long)] verbose: bool, #[arg(short='D', long, default_value="2.0")] delay: f64, #[arg(short='s', long, default_value="varanus.sock")] socket: String, #[arg(short='f', long, default_value="")] format: String, #[arg(long)] power: bool, #[arg(long)] memory: bool, } #[derive(Debug, Serialize, Deserialize, Default)] struct Battery { path: PathBuf, charge: f64, } impl Battery { fn new(p: &Path) -> Self { Battery { path: p.to_path_buf(), charge: 1.0, } } fn update(&mut self) { let cfp = self.path.join("charge_full"); let cnp = self.path.join("charge_now"); let cf: i32 = read_line(cfp).unwrap().parse().unwrap(); let cn: i32 = read_line(cnp).unwrap().parse().unwrap(); self.charge = cn as f64 / cf as f64 ; } } #[derive(Debug, Serialize, Deserialize, Default)] struct Mains { online: bool, } #[derive(Debug, Serialize, Deserialize, Default)] struct Power { batteries: Vec, mains: Vec, } impl Power { fn new() -> Self { let mut p = Power::default(); for path in read_dir("/sys/class/power_supply").unwrap() { match path { Ok(de) => { let typefile = de.path().join("type"); let line = read_line(typefile).unwrap(); if line == "Battery" { let bat = Battery::new(&de.path()); p.batteries.push(bat); } }, Err(_) => (), } } return p } fn update(&mut self) { for bat in &mut self.batteries { bat.update(); } } } #[derive(Debug, Serialize, Deserialize)] struct Process { } #[derive(Debug, Serialize, Deserialize)] struct ProcFS { } impl ProcFS { } #[derive(Debug, Serialize, Deserialize, Default)] struct Memory { total: u64, free: u64, } impl Memory { fn new(si: Sysinfo) -> Self { let mut mem = Memory::default(); mem.update(si); return mem; } fn update(&mut self, si: Sysinfo) { self.total = si.totalram; self.free = si.freeram; } } #[derive(Debug, Serialize, Deserialize)] struct State { asof: SystemTime, uptime: i64, power: Power, memory: Memory, } impl State { fn new() -> Self { let asof = SystemTime::now(); let si = Sysinfo::new(); return State { asof: asof, uptime: si.uptime, power: Power::new(), memory: Memory::new(si) } } fn update(&mut self) { let si = Sysinfo::new(); self.asof = SystemTime::now(); self.uptime = si.uptime; self.memory.update(si); self.power.update(); } } fn update_state(state: &mut State) { state.update(); } fn get_state(sockfile: String) -> Result { let mut socket = UnixStream::connect(sockfile)?; let mut buf = vec![]; socket.read_to_end(&mut buf)?; Ok(serde_json::from_str::(str::from_utf8(&buf)?)?) } struct Listener { file: String, state: Arc>, } impl Listener { fn new(socket: String, state_mutex: &Arc>) -> Self { return Listener { file: socket, state: Arc::clone(state_mutex), } } fn listen(&self) { let state_mutex = Arc::clone(&self.state); let sfn1 = self.file.clone(); let sfn2 = self.file.clone(); spawn(move || { match Signals::new([SIGINT]) { Ok(mut signals) => for _sig in signals.forever() { let _ = remove_file(&sfn1); std::process::exit(0) }, Err(_) => () } }); spawn(move || { let _ = remove_file(&sfn2); let listener = UnixListener::bind(sfn2).unwrap(); loop { match listener.accept() { Ok((mut socket,_addr)) => { let json = { let state = state_mutex.lock().unwrap(); serde_json::to_string(&*state).unwrap() }; socket.write_all(json.as_bytes()).unwrap(); }, Err(e) => println!("error: {:?}", e) } } }); } } impl Drop for Listener { fn drop(&mut self) { let _ = remove_file(&self.file); } } fn human(k: u64) -> String { let mut x: f64 = k as f64; let mut i = 0; let cs = ["", "K", "M", "G", "T", "P"]; while x >= 1000. && i < cs.len() { x /= 1000.; i += 1; } let c = cs[i]; format!("{x:.1}{c}") } fn display_power(state: &State) { } fn display_memory(state: &State) { let mem = &state.memory; let fs = human(mem.free); let us = human(mem.total - mem.free); let ts = human(mem.total); let per = 100. * (mem.total - mem.free) as f64 / mem.total as f64; println!("Memory used: {us} / {ts} ({per:.0}%)") } fn format(fmt: &str, state: &State) { } fn main() { let args = Cli::parse(); if args.daemon { let start = Instant::now(); let delay_ms: u64 = (args.delay * 1000.0) as u64; let mut cycle: u64 = 0; let state_mutex = Arc::new(Mutex::new(State::new())); let listener = Listener::new(args.socket, &state_mutex); listener.listen(); loop { if args.verbose { println!("{:?} elapsed; updating state...", start.elapsed()); } { let mut state = state_mutex.lock().unwrap(); update_state(&mut state); } cycle += 1; sleep(Duration::from_millis(cycle*delay_ms) - start.elapsed()); } } else { match get_state(args.socket.clone()) { Ok(state) => { if args.power { display_power(&state); } if args.memory { display_memory(&state); } if args.format.len() > 0 { format(&args.format, &state); } //println!("{:#?}", state) }, Err(_) => eprintln!("Couldn't open {}", &args.socket) } } }