diff --git a/Cargo.lock b/Cargo.lock index 1eba973..7ce0d46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,6 +171,12 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + [[package]] name = "bytes" version = "1.11.1" @@ -356,6 +362,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "tabled", "thiserror", "tokio", ] @@ -421,6 +428,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -1106,6 +1119,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "papergrid" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6978128c8b51d8f4080631ceb2302ab51e32cc6e8615f735ee2f83fd269ae3f1" +dependencies = [ + "bytecount", + "fnv", + "unicode-width", +] + [[package]] name = "paste" version = "1.0.15" @@ -1170,6 +1194,28 @@ dependencies = [ "syn", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1509,6 +1555,30 @@ dependencies = [ "syn", ] +[[package]] +name = "tabled" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e39a2ee1fbcd360805a771e1b300f78cc88fec7b8d3e2f71cd37bbf23e725c7d" +dependencies = [ + "papergrid", + "tabled_derive", + "testing_table", +] + +[[package]] +name = "tabled_derive" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea5d1b13ca6cff1f9231ffd62f15eefd72543dab5e468735f1a456728a02846" +dependencies = [ + "heck", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -1522,6 +1592,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "testing_table" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f8daae29995a24f65619e19d8d31dea5b389f3d853d8bf297bbf607cd0014cc" +dependencies = [ + "unicode-width", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -1678,6 +1757,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" diff --git a/Cargo.toml b/Cargo.toml index 9e57756..e76dc4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ rust-argon2 = "3.0.0" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" serde_yaml = "0.9.34" +tabled = "0.20.0" thiserror = "2.0.12" tokio = { version = "1.45.1", features = ["fs", "io-std", "macros", "process", "rt"] } diff --git a/src/bin/dkl.rs b/src/bin/dkl.rs index e9e6de3..39a84d1 100644 --- a/src/bin/dkl.rs +++ b/src/bin/dkl.rs @@ -81,6 +81,16 @@ enum Command { #[arg(long, default_value = "5s")] timeout: Duration, }, + + Cg { + #[command(subcommand)] + cmd: CgCmd, + }, +} + +#[derive(Subcommand)] +enum CgCmd { + Ls, } #[tokio::main(flavor = "current_thread")] @@ -157,6 +167,10 @@ async fn main() -> Result<()> { .run() .await .map(|_| ())?), + + C::Cg { cmd } => match cmd { + CgCmd::Ls => Ok(dkl::cgroup::ls().await?), + }, } } diff --git a/src/cgroup.rs b/src/cgroup.rs new file mode 100644 index 0000000..d4bca68 --- /dev/null +++ b/src/cgroup.rs @@ -0,0 +1,205 @@ +use log::warn; +use std::fmt::Display; +use std::io::Result; +use std::str::FromStr; +use tokio::fs; + +use crate::human::Human; + +const CGROUP_ROOT: &str = "/sys/fs/cgroup/"; + +pub async fn ls() -> Result<()> { + let mut cgs = walk(CGROUP_ROOT.to_string()).await; + + cgs.sort_by(|a, b| a.path.cmp(&b.path)); + + let mut table = tabled::builder::Builder::new(); + table.push_record(["cgroup", "workg set", "anon", "max"]); + + for cg in cgs { + let name = cg.path.strip_prefix(CGROUP_ROOT).unwrap(); + table.push_record([ + name, + &cg.memory.working_set().human(), + &cg.memory.stat.anon.human(), + &cg.memory.max.human(), + ]); + } + + use tabled::settings::{ + object::{Column, Row}, + Alignment, Modify, + }; + let mut table = table.build(); + table.with(tabled::settings::Style::psql()); + table.with(Alignment::right()); + table.with(Modify::list(Column::from(0), Alignment::left())); + table.with(Modify::list(Row::from(0), Alignment::left())); + + println!("{}", table); + + Ok(()) +} + +async fn walk(root: String) -> Vec { + let mut todo = vec![root]; + + let mut results = Vec::new(); + + while let Some(path) = todo.pop() { + match read(&path, |d| todo.push(d)).await { + Ok(cg) => results.push(cg), + Err(e) => { + warn!("reading dir {path} failed: {e}"); + continue; + } + }; + } + + results +} + +struct Cgroup { + path: String, + memory: Memory, +} + +#[derive(Default)] +struct Memory { + current: Option, + low: Option, + high: Option, + min: Option, + max: Option, + stat: MemoryStat, +} + +impl Memory { + /// working set as defined by cAdvisor + /// (https://github.com/google/cadvisor/blob/e1ccfa9b4cf2e17d74e0f5526b6487b74b704503/container/libcontainer/handler.go#L853-L862) + fn working_set(&self) -> Option { + let cur = self.current?; + let inactive = self.stat.inactive_file?; + (inactive <= cur).then(|| cur - inactive) + } +} + +#[derive(Default)] +struct MemoryStat { + anon: Option, + file: Option, + kernel: Option, + kernel_stack: Option, + pagetables: Option, + shmem: Option, + inactive_file: Option, +} + +enum Max { + Num(u64), + Max, +} + +impl Display for Max { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Num(n) => write!(f, "{n}"), + Self::Max => f.write_str("max"), + } + } +} + +impl Human for Max { + fn human(&self) -> String { + match self { + Self::Num(n) => n.human(), + Self::Max => "+∞".into(), + } + } +} + +impl FromStr for Max { + type Err = std::num::ParseIntError; + fn from_str(s: &str) -> std::result::Result::Err> { + Ok(match s { + "max" => Self::Max, + s => Self::Num(s.parse()?), + }) + } +} + +async fn read(dir: &str, mut f: impl FnMut(String)) -> Result { + let mut rd = fs::read_dir(dir).await?; + + let mut cg = Cgroup { + path: dir.to_string(), + memory: Memory::default(), + }; + + while let Some(entry) = rd.next_entry().await? { + let path = entry.path(); + let Some(path) = path.to_str() else { + continue; + }; + + if entry.file_type().await?.is_dir() { + f(path.to_string()); + continue; + } + + let Some((_, name)) = path.rsplit_once('/') else { + continue; + }; + let Some((controller, param)) = name.split_once('.') else { + continue; + }; + + match controller { + "memory" => match param { + "current" => cg.memory.current = read_parse(path).await?, + "low" => cg.memory.low = read_parse(path).await?, + "high" => cg.memory.high = read_parse(path).await?, + "min" => cg.memory.min = read_parse(path).await?, + "max" => cg.memory.max = read_parse(path).await?, + "stat" => cg.memory.stat.read_from(path).await?, + _ => {} + }, + _ => {} + } + } + + Ok(cg) +} + +async fn read_parse(path: &str) -> Result> +where + T::Err: Display, +{ + (fs::read_to_string(path).await?) + .trim_ascii() + .parse() + .map_err(|e| std::io::Error::other(format!("{path}: parse failed: {e}"))) + .map(|v| Some(v)) +} + +impl MemoryStat { + async fn read_from(&mut self, path: &str) -> Result<()> { + for line in (fs::read_to_string(path).await?).lines() { + let Some((key, value)) = line.split_once(' ') else { + continue; + }; + let value = value.parse::().ok(); + match key { + "anon" => self.anon = value, + "file" => self.file = value, + "kernel" => self.kernel = value, + "kernel_stack" => self.kernel_stack = value, + "pagetables" => self.pagetables = value, + "shmem" => self.shmem = value, + "inactive_file" => self.inactive_file = value, + _ => {} + } + } + Ok(()) + } +} diff --git a/src/human.rs b/src/human.rs new file mode 100644 index 0000000..e1f3ef2 --- /dev/null +++ b/src/human.rs @@ -0,0 +1,35 @@ +use human_units::FormatSize; +use std::fmt::{Display, Formatter, Result}; + +pub trait Human { + fn human(&self) -> String; +} + +pub struct Quantity(u64); + +impl Display for Quantity { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + self.0.format_size().fmt(f) + } +} + +impl Human for Quantity { + fn human(&self) -> String { + self.to_string() + } +} + +impl Human for u64 { + fn human(&self) -> String { + self.format_size().to_string() + } +} + +impl Human for Option { + fn human(&self) -> String { + match self { + Some(h) => h.human(), + None => "◌".to_string(), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 7d1bd9e..35cb920 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,7 @@ pub mod apply; +pub mod rc; +pub mod cgroup; +pub mod human; pub mod bootstrap; pub mod dls; pub mod dynlay; diff --git a/src/rc.rs b/src/rc.rs new file mode 100644 index 0000000..e69de29