monitoring kit
-rw-r--r--Cargo.toml11
-rw-r--r--cpu/Cargo.toml2
-rw-r--r--cpu/src/cpu.rs8
-rw-r--r--cpu/src/lib.rs91
-rw-r--r--cpu/src/temps.rs14
-rw-r--r--disk/Cargo.toml26
-rw-r--r--disk/src/disk.rs112
-rw-r--r--grapher/Cargo.toml2
-rw-r--r--grapher/src/lib.rs15
-rw-r--r--hwmons/Cargo.toml12
-rw-r--r--hwmons/src/lib.rs68
-rw-r--r--launch.py6
12 files changed, 293 insertions, 74 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 2da1056..954e19e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,14 @@
[workspace]
-members = ["cpu", "grapher", "memory", "gpu", "battery", "network"]
+members = [
+ "hwmons",
+ "cpu",
+ "grapher",
+ "memory",
+ "gpu",
+ "battery",
+ "network",
+ "disk",
+]
resolver = "3"
[profile.release]
diff --git a/cpu/Cargo.toml b/cpu/Cargo.toml
index 9fca4ee..d961a56 100644
--- a/cpu/Cargo.toml
+++ b/cpu/Cargo.toml
@@ -16,6 +16,8 @@ parking_lot = "0.12.3"
regex = "1.11.1"
termion = "4.0.4"
grapher = { path = "../grapher" }
+hwmons = { version = "1.0.0", path = "../hwmons" }
+implicit-fn = "0.1.0"
[[bin]]
diff --git a/cpu/src/cpu.rs b/cpu/src/cpu.rs
index 00feb75..9356bc6 100644
--- a/cpu/src/cpu.rs
+++ b/cpu/src/cpu.rs
@@ -9,7 +9,7 @@
use anyhow::*;
use comat::cwrite;
use cpu::*;
-use grapher::Grapher;
+use grapher::{truncwrite, Grapher};
use std::array;
use std::convert::identity;
use std::io::Write;
@@ -37,7 +37,7 @@ fn main() -> Result<()> {
ViewCore::One(x) => ensure!(x < info.count, "not enough cores"),
_ => (),
}
- let mut t = Temps::load()?;
+ let mut t = temps()?;
let mut g = Grapher::new()?;
g.push_point(usage()?.max(0.01));
@@ -82,9 +82,9 @@ fn main() -> Result<()> {
let speed = speed()?;
let temp = t.read()?;
let fps = (1f32 / d).round();
- cwrite!(
+ truncwrite!(
g.buffer,
- " {fps}fps ──── {name} {core} @ {speed:.2} GHz ──── {red}{temp}{reset} °C",
+ " {fps}fps ──── {name} {core} @ {speed:.2} GHz ──── \u{1b}[0;34;31m{temp:.0}\u{1b}[0m °C",
)?;
stdout.write_all(&g.buffer)?;
stdout.flush()?;
diff --git a/cpu/src/lib.rs b/cpu/src/lib.rs
index 5ddb34c..5a9e899 100644
--- a/cpu/src/lib.rs
+++ b/cpu/src/lib.rs
@@ -9,6 +9,7 @@
use anyhow::*;
use atools::prelude::*;
use collar::CollectArray;
+use hwmons::{hwmons, Hwmon};
use parking_lot::Mutex;
use std::collections::HashMap;
use std::fmt::Display;
@@ -42,46 +43,21 @@ pub struct CpuInfo {
pub count: u64,
}
-pub struct Temps(File);
-impl Temps {
- pub fn load() -> Result<Self> {
- for hwmon in std::fs::read_dir("/sys/class/hwmon/")?.filter_map(Result::ok) {
- if !read(Path::join(&hwmon.path(), "name"))?.starts_with("coretemp") {
- continue;
- }
- for f in std::fs::read_dir(hwmon.path())?.filter_map(Result::ok) {
- if let Some(n) = f.file_name().to_str()
- && n.starts_with("temp")
- && n.ends_with("input")
- {
- let i = n
- .bytes()
- .filter(u8::is_ascii_digit)
- .fold(0, |acc, x| acc * 10 + (x - b'0') as u64);
-
- let name = read(Path::join(&hwmon.path(), format!("temp{i}_label")))
- .context("alas")?;
- if let Some(c) = core() {
- if !name.contains(&c.to_string()) {
- continue;
- }
- } else if !name.contains("Package") {
- continue;
- }
-
- let f = File::open(f.path())?;
- return Ok(Self(f));
- }
- }
+#[implicit_fn::implicit_fn]
+pub fn temps() -> Result<Hwmon> {
+ if let x @ Some(_) = hwmons().find(_.contains("Package")) {
+ if let Some(c) = core() {
+ hwmons().find(_.contains(&c.to_string()))
+ } else {
+ x
}
- bail!("h")
- }
- pub fn read(&mut self) -> Result<f64> {
- let mut o = String::default();
- self.0.seek(std::io::SeekFrom::Start(0))?;
- self.0.read_to_string(&mut o)?;
- Ok(o.trim().parse::<f64>().context("reading temps")? / 1000.0)
+ .ok_or(anyhow!("no intel hwmon"))?
+ } else {
+ hwmons()
+ .find(_.contains("Tccd"))
+ .ok_or(anyhow!("no amd hwmon"))?
}
+ .load()
}
impl CpuInfo {
@@ -98,21 +74,30 @@ impl CpuInfo {
.lines()
.filter_map(|l| l.split_once(":").map(|(a, b)| (a, b.trim())))
.collect::<HashMap<_, _>>();
- let name = x["Model name"]
- .replace("Core", "")
- .replace("(TM)", "")
- .replace("Intel", "")
- .replace("(R)", "");
- let name = regex::Regex::new(r"@ [0-9]+\.[0-9]+GHz")
- .unwrap()
- .replace(&name, "");
- let name = regex::Regex::new(r"[0-9]+th Gen")
- .unwrap()
- .replace(&name, "")
- .replace(" ", " ")
- .trim()
- .replace(" ", " ")
- .to_lowercase();
+ let name = x["Model name"];
+ let name = if name.contains("Intel") {
+ let name = name
+ .replace("Core", "")
+ .replace("(TM)", "")
+ .replace("Intel", "")
+ .replace("(R)", "");
+ let name = regex::Regex::new(r"[0-9]+th Gen")
+ .unwrap()
+ .replace(&name, "")
+ .replace(" ", " ")
+ .trim()
+ .replace(" ", " ");
+ regex::Regex::new(r"@ [0-9]+\.[0-9]+GHz")
+ .unwrap()
+ .replace(&name, "")
+ .to_lowercase()
+ } else {
+ name.replace("AMD Ryzen", "Ryzen")
+ .replace("Processor", "")
+ .trim()
+ .to_owned()
+ };
+
Ok(Self {
name: name,
speed: x["CPU max MHz"].parse::<f64>().context("cpu mhz??")? / 1000.0,
diff --git a/cpu/src/temps.rs b/cpu/src/temps.rs
index 485b427..81c7883 100644
--- a/cpu/src/temps.rs
+++ b/cpu/src/temps.rs
@@ -2,7 +2,7 @@
use anyhow::{ensure, Result};
use comat::cwrite;
use cpu::*;
-use grapher::Grapher;
+use grapher::{truncwrite, Grapher};
use std::array;
use std::convert::identity;
use std::io::Write;
@@ -30,7 +30,7 @@ fn main() -> Result<()> {
ViewCore::One(x) => ensure!(x < info.count, "not enough cores"),
_ => (),
}
- let mut t = Temps::load()?;
+ let mut t = temps()?;
let mut g = Grapher::new()?;
g.push_point(t.read()? / 105.0);
@@ -75,10 +75,12 @@ fn main() -> Result<()> {
let speed = speed()?;
let temp = t.read()?;
let fps = (1f32 / d).round();
- cwrite!(
- g.buffer,
- " {fps}fps ──── {name} {core} @ {speed:.2} GHz ──── {red}{temp}{reset} °C",
- )?;
+ let core = if !name.contains("Ryzen") {
+ core.to_string() + " "
+ } else {
+ "".to_string()
+ };
+ truncwrite!(g.buffer," {fps}fps ──── {name} {core}@ {speed:.2} GHz ──── \u{1b}[0;34;31m{temp:.0}\u{1b}[0m °C",)?;
stdout.write_all(&g.buffer)?;
stdout.flush()?;
diff --git a/disk/Cargo.toml b/disk/Cargo.toml
new file mode 100644
index 0000000..80c961d
--- /dev/null
+++ b/disk/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "disk"
+version = "1.0.0"
+edition = "2024"
+authors = ["bend-n <[email protected]>"]
+repository = "https://github.com/bend-n/monitorkit"
+license = "MIT"
+rust-version = "1.85"
+
+[dependencies]
+anyhow = "1.0.97"
+termion = "4.0.4"
+grapher = { path = "../grapher" }
+ping-rs = "0.1.2"
+oneshot = { version = "0.1.11", default-features = false, features = ["std"] }
+libc = "0.2.171"
+atools = "0.1.6"
+implicit-fn = "0.1.0"
+collar = "1.0.1"
+human_bytes = "0.4.3"
+
+
+[[bin]]
+name = "disk"
+doc = false
+path = "src/disk.rs"
diff --git a/disk/src/disk.rs b/disk/src/disk.rs
new file mode 100644
index 0000000..677fd1c
--- /dev/null
+++ b/disk/src/disk.rs
@@ -0,0 +1,112 @@
+#![feature(portable_simd, string_deref_patterns, duration_millis_float)]
+use anyhow::*;
+use collar::CollectArray;
+use grapher::{Grapher, truncwrite};
+use human_bytes::human_bytes;
+use std::array;
+use std::fs::read_to_string as read;
+use std::io::Write;
+use std::io::{Read, stdout};
+use std::process::exit;
+use std::thread::sleep;
+use std::time::{Duration, Instant};
+use termion::color::*;
+use termion::cursor::Hide;
+use termion::raw::IntoRawMode;
+use termion::screen::IntoAlternateScreen;
+use termion::{async_stdin, clear, cursor, style};
+#[implicit_fn::implicit_fn]
+fn main() -> Result<()> {
+ let disk = match std::env::args().nth(1) {
+ None => {
+ let fs = std::fs::read_dir("/sys/block")?
+ .filter_map(Result::ok)
+ .filter_map(_.file_name().into_string().ok())
+ .collect::<Vec<_>>();
+ eprintln!("no args! must be one of {fs:?}");
+ exit(1)
+ }
+ Some(x) => x,
+ };
+ let write = match std::env::args().nth(2) {
+ Some("r") => false,
+ Some("w") => true,
+ Some(_) | None => {
+ eprintln!("requires r/w arg");
+ exit(1)
+ }
+ };
+ let drive = std::fs::read_dir("/sys/block")?
+ .filter_map(Result::ok)
+ .find(_.file_name().to_string_lossy().starts_with(&disk))
+ .ok_or(anyhow!("no network"))?
+ .path();
+
+ let name = read(drive.join("device").join("model"))?;
+ let name = name.trim();
+
+ // https://www.kernel.org/doc/html/latest/block/stat.html
+ let ticks = || {
+ let [_rio, _rm, rs, _rt, _wio, _wm, ws, _wt] = read(drive.join("stat"))?
+ .split_whitespace()
+ .filter_map(_.parse::<u32>().ok())
+ .collect_array();
+
+ Ok(write.then_some(ws).unwrap_or(rs))
+ };
+
+ let mut g = Grapher::new()?;
+ let mut d = 0.1;
+
+ let mut stdout = stdout().into_raw_mode()?.into_alternate_screen()?;
+ let mut stdin = async_stdin();
+ write!(stdout, "{}{}{}", Hide, clear::All, style::Reset).unwrap();
+ let mut max = 1f64;
+ let mut last = ticks()?;
+
+ let mut i = Instant::now();
+ 'out: loop {
+ sleep(Duration::from_secs_f32(d));
+
+ let pass = i.elapsed().as_millis_f64();
+ let new = ticks()?;
+ i = Instant::now();
+
+ let δ = (new - last) * 512;
+ let r = δ as f64 / pass;
+ g.push_point(r);
+ max = max.max(r);
+ last = new;
+
+ let mut key = 0;
+ while stdin.read(array::from_mut(&mut key)).unwrap() != 0 {
+ match key {
+ b'q' => break 'out,
+ b'+' => d = (d + 0.1f32).min(1.0),
+ b'-' if d >= 0.2 => d -= 0.1,
+ b'-' if d >= 0.02 => d -= 0.01,
+ b'-' => d = 0.00833,
+ _ => (),
+ }
+ }
+
+ g.draw(|_| None, _ / max as f64)?;
+ write!(g.buffer, "{}{}", White.fg_str(), cursor::Goto(1, 1))?;
+
+ let fps = (1f32 / d).round();
+ truncwrite!(
+ g.buffer,
+ " {fps}fps ──── {name} ──── ceil {}/s ──── curr {}/s ─ {}",
+ human_bytes(max as f64 * 1e3),
+ human_bytes(r as f64 * 1e3), // 1e3
+ ["read", "write"][write as usize],
+ )?;
+
+ write!(stdout, "{}", Rgb(106, 186, 212).fg_string())?;
+
+ stdout.write_all(&g.buffer)?;
+ stdout.flush()?;
+ }
+ println!("\x1B[?7l");
+ return Ok(());
+}
diff --git a/grapher/Cargo.toml b/grapher/Cargo.toml
index b03bb37..6cbfdb7 100644
--- a/grapher/Cargo.toml
+++ b/grapher/Cargo.toml
@@ -9,6 +9,8 @@ rust-version = "1.85"
[dependencies]
anyhow = "1.0.97"
+comat = "0.1.3"
+implicit-fn = "0.1.0"
itertools = "0.14.0"
termion = "4.0.4"
unicode-width = "0.2.0"
diff --git a/grapher/src/lib.rs b/grapher/src/lib.rs
index b6d2df2..6f98eed 100644
--- a/grapher/src/lib.rs
+++ b/grapher/src/lib.rs
@@ -104,7 +104,7 @@ fn bl(x: u8) -> [u8; 3] {
.encode_utf8(&mut b);
b
}
-
+#[implicit_fn::implicit_fn]
pub fn truncate_to(x: &str, cols: u16) -> String {
if x.width() < cols as _ {
return x.to_string();
@@ -114,14 +114,13 @@ pub fn truncate_to(x: &str, cols: u16) -> String {
Some((x, *length))
});
use itertools::Itertools;
- let mut o = i
- .take_while_ref(|&(_, length)| length < cols as usize)
- .map(|x| x.0)
+ let o = i
+ .take_while_ref(_.1 < cols as usize)
+ .map(_.0)
.collect::<Vec<char>>();
- if i.next().is_some() {
- *o.last_mut().unwrap() = '…';
- }
- o.into_iter().collect::<String>()
+ o.into_iter()
+ .chain(i.next().map(|_| '…'))
+ .collect::<String>()
}
pub fn truncate(x: &str) -> anyhow::Result<String> {
diff --git a/hwmons/Cargo.toml b/hwmons/Cargo.toml
new file mode 100644
index 0000000..102f261
--- /dev/null
+++ b/hwmons/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "hwmons"
+version = "1.0.0"
+edition = "2024"
+authors = ["bend-n <[email protected]>"]
+repository = "https://github.com/bend-n/monitorkit"
+license = "MIT"
+rust-version = "1.85"
+
+[dependencies]
+anyhow = "1.0.97"
+implicit-fn = "0.1.0"
diff --git a/hwmons/src/lib.rs b/hwmons/src/lib.rs
new file mode 100644
index 0000000..55ad3df
--- /dev/null
+++ b/hwmons/src/lib.rs
@@ -0,0 +1,68 @@
+use anyhow::*;
+use std::ffi::OsStr;
+use std::fs::{File, read_to_string as read};
+use std::io::{Read, Seek};
+use std::ops::Deref;
+use std::path::PathBuf;
+#[derive(Debug)]
+pub struct Hwmon(String, PathBuf, Option<File>);
+impl Deref for Hwmon {
+ type Target = str;
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+impl Hwmon {
+ pub fn load(self) -> Result<Self> {
+ File::open(&self.1)
+ .map(|x| Self(self.0, self.1, Some(x)))
+ .context("couldnt load hwmon")
+ }
+ #[implicit_fn::implicit_fn]
+ pub fn read(&mut self) -> Result<f64> {
+ let mut o = String::default();
+ let f = self.2.as_mut().unwrap();
+ f.seek(std::io::SeekFrom::Start(0))?;
+ f.read_to_string(&mut o)?;
+ o.trim()
+ .parse::<f64>()
+ .map(_ / 1000.0)
+ .context("parsing hwmon")
+ }
+}
+#[implicit_fn::implicit_fn]
+pub fn hwmons() -> impl Iterator<Item = Hwmon> {
+ std::fs::read_dir("/sys/class/hwmon/").into_iter().flat_map(
+ _.into_iter()
+ .filter_map(Result::ok)
+ .map(_.path())
+ .map(std::fs::read_dir)
+ .filter_map(Result::ok)
+ .flatten()
+ .filter_map(Result::ok)
+ .map(_.path())
+ .filter(
+ _.file_name()
+ .and_then(OsStr::to_str)
+ .is_some_and(|x| x.starts_with("temp") & x.ends_with("input")),
+ )
+ .filter_map(|f| {
+ Some(Hwmon(
+ read(f.parent()?.join(format!("temp{}_label",
+ f.file_name()?.to_str()?.bytes().skip(4)
+ .take_while(u8::is_ascii_digit)
+ .fold(0, |acc, x| acc * 10 + (x - b'0') as u64))))
+ .ok()?
+ .trim()
+ .to_string(),
+ f,
+ None,
+ ))
+ }),
+ )
+}
+
+#[test]
+fn x() {
+ dbg!(hwmons().collect::<Vec<_>>());
+}
diff --git a/launch.py b/launch.py
index 9787136..cc89fa4 100644
--- a/launch.py
+++ b/launch.py
@@ -1,5 +1,7 @@
for n in range(16):
kitty ./.target/release/cpu @(n) &
kitty ./.target/release/temps &
-kitty sudo ./.target/release/intel &
-./.target/release/memory \ No newline at end of file
+kitty ./.target/release/disk nvme r &
+kitty ./.target/release/ping google.com &
+# kitty sudo ./.target/release/intel &
+./.target/release/memory