/* 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 * * Handle "no socket file" more gracefully * */ 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; 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, } #[derive(Debug, Serialize, Deserialize, Default)] struct Battery { path: PathBuf, } impl Battery { fn new(p: &Path) -> Self { Battery { path: p.to_path_buf(), } } } #[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) { } } #[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); } } type Result = std::result::Result>; 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 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 { let state = get_state(args.socket).unwrap(); println!("{:#?}", state) } }