add cg ls, prepare for rc subcommands

This commit is contained in:
Mikaël Cluseau
2026-04-12 19:55:56 +02:00
parent 0f116e21b9
commit c75d4febb3
7 changed files with 343 additions and 0 deletions
Generated
+85
View File
@@ -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"
+1
View File
@@ -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"] }
+14
View File
@@ -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?),
},
}
}
+205
View File
@@ -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<Cgroup> {
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<u64>,
low: Option<u64>,
high: Option<Max>,
min: Option<u64>,
max: Option<Max>,
stat: MemoryStat,
}
impl Memory {
/// working set as defined by cAdvisor
/// (https://github.com/google/cadvisor/blob/master/container/libcontainer/handler.go#L853-L862)
fn working_set(&self) -> Option<u64> {
let cur = self.current?;
let inactive = self.stat.inactive_file?;
(inactive <= cur).then(|| cur - inactive)
}
}
#[derive(Default)]
struct MemoryStat {
anon: Option<u64>,
file: Option<u64>,
kernel: Option<u64>,
kernel_stack: Option<u64>,
pagetables: Option<u64>,
shmem: Option<u64>,
inactive_file: Option<u64>,
}
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<Self, <Self as FromStr>::Err> {
Ok(match s {
"max" => Self::Max,
s => Self::Num(s.parse()?),
})
}
}
async fn read(dir: &str, mut f: impl FnMut(String)) -> Result<Cgroup> {
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<T: FromStr>(path: &str) -> Result<Option<T>>
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::<u64>().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(())
}
}
+35
View File
@@ -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<T: Human> Human for Option<T> {
fn human(&self) -> String {
match self {
Some(h) => h.human(),
None => "".to_string(),
}
}
}
+3
View File
@@ -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;
View File