diff --git a/src/uu/lsmem/Cargo.toml b/src/uu/lsmem/Cargo.toml index a76660a..35d5e8f 100644 --- a/src/uu/lsmem/Cargo.toml +++ b/src/uu/lsmem/Cargo.toml @@ -13,3 +13,6 @@ path = "src/main.rs" [dependencies] uucore = { workspace = true } clap = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tabled = { workspace = true } \ No newline at end of file diff --git a/src/uu/lsmem/src/lsmem.rs b/src/uu/lsmem/src/lsmem.rs index d8f3388..c9b84b6 100644 --- a/src/uu/lsmem/src/lsmem.rs +++ b/src/uu/lsmem/src/lsmem.rs @@ -3,15 +3,547 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +mod utils; + use clap::{crate_version, Command}; +use clap::{Arg, ArgAction}; +use serde::Deserialize; +use std::borrow::Borrow; +use std::fs; +use std::io::{self, BufRead, BufReader}; +use std::path::{Path, PathBuf}; +use std::str::FromStr; use uucore::{error::UResult, format_usage, help_about, help_usage}; +use tabled::{ + settings::{location::ByColumnName, object, Alignment, Disable, Modify, Style}, + Table, Tabled, +}; + const ABOUT: &str = help_about!("lsmem.md"); const USAGE: &str = help_usage!("lsmem.md"); +mod options { + pub const BYTES: &str = "bytes"; +} + +const BUFSIZ: usize = 1024; + +const PATH_SYS_MEMORY: &str = "/sys/devices/system/memory"; +const PATH_BLOCK_SIZE_BYTES: &str = "/sys/devices/system/memory/block_size_bytes"; +const PATH_VALID_ZONES: &str = "/sys/devices/system/memory/valid_zones"; +const PATH_SUB_REMOVABLE: &str = "removable"; +const PATH_SUB_STATE: &str = "state"; +const NAME_MEMORY: &str = "memory"; + +struct ColDesc { + name: &'static str, // Rust's equivalent to `const char *` + whint: f64, // Rust uses `f64` for double precision floating-point numbers + flags: i32, // Using `i32` for integers + help: &'static str, // Rust's equivalent to `const char *` +} + +#[derive(Debug, Deserialize)] +enum Columns { + #[serde(rename = "RANGE")] + Range, + #[serde(rename = "SIZE")] + Size, + #[serde(rename = "STATE")] + State, + #[serde(rename = "REMOVABLE")] + Removable, + #[serde(rename = "BLOCK")] + Block, + #[serde(rename = "NODE")] + Node, + #[serde(rename = "ZONES")] + Zones, +} +const SCOLS_FL_RIGHT: i32 = 1; // Placeholder value, replace with the actual flag value if needed. + +static COLDESCS: [ColDesc; 7] = [ + ColDesc { + name: "RANGE", + whint: 0.0, + flags: 0, + help: "start and end address of the memory range", + }, + ColDesc { + name: "SIZE", + whint: 5.0, + flags: SCOLS_FL_RIGHT, + help: "size of the memory range", + }, + ColDesc { + name: "STATE", + whint: 0.0, + flags: SCOLS_FL_RIGHT, + help: "online status of the memory range", + }, + ColDesc { + name: "REMOVABLE", + whint: 0.0, + flags: SCOLS_FL_RIGHT, + help: "memory is removable", + }, + ColDesc { + name: "BLOCK", + whint: 0.0, + flags: SCOLS_FL_RIGHT, + help: "memory block number or blocks range", + }, + ColDesc { + name: "NODE", + whint: 0.0, + flags: SCOLS_FL_RIGHT, + help: "numa node of memory", + }, + ColDesc { + name: "ZONES", + whint: 0.0, + flags: SCOLS_FL_RIGHT, + help: "valid zones for the memory range", + }, +]; + +#[derive(Debug, Deserialize, PartialEq, Clone, Copy)] +enum ZoneId { + #[serde(rename = "ZONE_DMA")] + ZoneDma, + #[serde(rename = "ZONE_DMA32")] + ZoneDma32, + #[serde(rename = "ZONE_NORMAL")] + ZoneNormal, + #[serde(rename = "ZONE_HIGHMEM")] + ZoneHighmem, + #[serde(rename = "ZONE_MOVABLE")] + ZoneMovable, + #[serde(rename = "ZONE_DEVICE")] + ZoneDevice, + #[serde(rename = "ZONE_NONE")] + ZoneNone, + #[serde(rename = "ZONE_UNKNOWN")] + ZoneUnknown, + #[serde(rename = "MAX_NR_ZONES")] + MaxNrZones, +} + +#[derive(PartialEq, Clone)] +enum MemoryState { + Online, + Offline, + GoingOffline, + Unknown, +} + +impl core::fmt::Display for MemoryState { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + match self { + MemoryState::Online => write!(f, "online"), + MemoryState::Offline => write!(f, "offline"), + MemoryState::GoingOffline => write!(f, "going-offline"), + MemoryState::Unknown => write!(f, "unknown"), + } + } +} + +impl FromStr for MemoryState { + type Err = (); + fn from_str(input: &str) -> Result { + match input { + "online" => Ok(MemoryState::Online), + "offline" => Ok(MemoryState::Offline), + "going-offline" => Ok(MemoryState::GoingOffline), + "unknown" => Ok(MemoryState::Unknown), + _ => Err(()), + } + } +} + +#[derive(Clone)] +struct MemoryBlock { + index: u64, + count: u64, + state: MemoryState, + node: i32, + nr_zones: usize, + zones: [ZoneId; ZoneId::MaxNrZones as usize], + removable: bool, +} + +impl MemoryBlock { + fn new() -> Self { + MemoryBlock { + index: 0, + count: 0, + state: MemoryState::Unknown, + node: 0, + nr_zones: 0, + zones: [ZoneId::ZoneUnknown; ZoneId::MaxNrZones as usize], + removable: true, + } + } +} + +#[derive(Tabled)] +struct TableRow { + #[tabled(rename = "RANGE")] + range: String, + #[tabled(rename = "SIZE")] + size: String, + #[tabled(rename = "STATE")] + state: String, + #[tabled(rename = "REMOVABLE")] + removable: String, + #[tabled(rename = "BLOCK")] + block: String, + #[tabled(rename = "NODE")] + node: String, + #[tabled(rename = "ZONES")] + zones: String, +} + +impl TableRow { + fn new() -> Self { + TableRow { + range: String::new(), + size: String::new(), + state: String::new(), + removable: String::new(), + block: String::new(), + node: String::new(), + zones: String::new(), + } + } +} + +struct Lsmem { + ndirs: usize, + dirs: Vec, + blocks: Vec, + nblocks: usize, + block_size: u64, + mem_online: u64, + mem_offline: u64, + + have_nodes: bool, + raw: bool, + export: bool, + json: bool, + noheadings: bool, + summary: bool, + list_all: bool, + bytes: bool, + want_summary: bool, + want_table: bool, + split_by_node: bool, + split_by_state: bool, + split_by_removable: bool, + split_by_zones: bool, + have_zones: bool, + + table: Vec>, +} + +impl Lsmem { + fn new() -> Lsmem { + Lsmem { + ndirs: 0, + dirs: Vec::default(), + blocks: Vec::default(), + nblocks: 0, + block_size: 0, + mem_online: 0, + mem_offline: 0, + + have_nodes: false, + raw: false, + export: false, + json: false, + noheadings: false, + summary: false, + list_all: false, + bytes: false, + want_summary: true, // default true + want_table: true, // default true + split_by_node: false, + split_by_state: false, + split_by_removable: false, + split_by_zones: false, + have_zones: false, + + table: Vec::default(), + } + } +} + +fn read_info(lsmem: &mut Lsmem) { + lsmem.block_size = u64::from_str_radix( + &read_file_content::(Path::new(PATH_BLOCK_SIZE_BYTES)).unwrap(), + 16, + ) + .unwrap(); + lsmem.dirs = get_block_paths(); + lsmem.dirs.sort_by(|a, b| { + let filename_a = a.to_str().unwrap().split("/").last().unwrap(); + let filename_b = b.to_str().unwrap().split("/").last().unwrap(); + let idx_a: u64 = filename_a[NAME_MEMORY.len()..].parse().unwrap(); + let idx_b: u64 = filename_b[NAME_MEMORY.len()..].parse().unwrap(); + return idx_a.cmp(&idx_b); + }); + lsmem.ndirs = lsmem.dirs.len(); + for path in lsmem.dirs.iter() { + if memory_block_get_node(&path).is_ok() { + lsmem.have_nodes = true; + } + + let mut p = path.clone(); + p.push("valid_zones"); + if fs::read_dir(p).is_ok() { + lsmem.have_zones = true; + } + + if lsmem.have_nodes && lsmem.have_zones { + break; + } + } + + for i in 0..lsmem.ndirs { + let blk = memory_block_read_attrs(&lsmem, &lsmem.dirs[i]); + if blk.state == MemoryState::Online { + lsmem.mem_online += lsmem.block_size; + } else { + lsmem.mem_offline += lsmem.block_size; + } + if is_mergeable(&lsmem, &blk) { + lsmem.blocks[lsmem.nblocks - 1].count += 1; + continue; + } + lsmem.nblocks += 1; + lsmem.blocks.push(blk.clone()); + } +} + +fn get_block_paths() -> Vec { + let mut paths = Vec::::new(); + for entry in fs::read_dir(PATH_SYS_MEMORY).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + let filename = path.to_str().unwrap().split("/").last().unwrap(); + if path.is_dir() && filename.starts_with(NAME_MEMORY) { + paths.push(path); + } + } + return paths; +} + +fn is_mergeable(lsmem: &Lsmem, blk: &MemoryBlock) -> bool { + if lsmem.nblocks == 0 { + return false; + } + + let curr_block = &lsmem.blocks[lsmem.nblocks - 1]; + if lsmem.list_all { + return false; + } + if curr_block.index + curr_block.count != blk.index { + return false; + } + if lsmem.split_by_state && curr_block.state != blk.state { + return false; + } + if lsmem.split_by_removable && curr_block.removable != blk.removable { + return false; + } + if lsmem.split_by_node && lsmem.have_nodes { + if curr_block.node != blk.node { + return false; + } + } + if lsmem.split_by_zones && lsmem.have_zones { + if curr_block.nr_zones != blk.nr_zones { + return false; + } + + for i in 0..curr_block.nr_zones { + if curr_block.zones[i] == ZoneId::ZoneUnknown || curr_block.zones[i] != blk.zones[i] { + return false; + } + } + } + + return true; +} + +fn memory_block_get_node(path: &PathBuf) -> Result::Err> { + for entry in fs::read_dir(path).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + let filename = path.to_str().unwrap().split("/").last().unwrap(); + if path.is_dir() && filename.starts_with("node") { + return filename["node".len()..].parse(); + } + } + return Ok(-1); +} + +fn memory_block_read_attrs(lsmem: &Lsmem, path: &PathBuf) -> MemoryBlock { + let mut blk = MemoryBlock::new(); + blk.count = 1; + blk.state = MemoryState::Unknown; + let filename = path.to_str().unwrap().split("/").last().unwrap(); + blk.index = filename[NAME_MEMORY.len()..].parse().unwrap(); + + let mut removable_path = path.clone(); + removable_path.push(PATH_SUB_REMOVABLE); + blk.removable = read_file_content::(&removable_path).is_ok(); + + let mut state_path = path.clone(); + state_path.push(PATH_SUB_STATE); + if let Ok(state_raw) = read_file_content::(&state_path) { + blk.state = MemoryState::from_str(&state_raw).unwrap(); + } + + if lsmem.have_nodes { + blk.node = memory_block_get_node(&path).unwrap(); + } + + blk.nr_zones = 0; + if lsmem.have_zones { + if let Ok(raw_content) = read_file_content::(Path::new(PATH_VALID_ZONES)) { + let zone_toks = raw_content.split(" ").collect::>(); + for i in 0..std::cmp::min(zone_toks.len(), ZoneId::MaxNrZones as usize) { + blk.zones[i] = serde_json::from_str(zone_toks[i]).unwrap(); + blk.nr_zones += 1; + } + } + } + return blk; +} + +fn set_summary(lsmem: &Lsmem) -> Vec { + let mut table = Vec::::new(); + + for i in 0..lsmem.nblocks { + let mut row = TableRow::new(); + + let blk = lsmem.blocks[i].borrow(); + + // Range + let start = blk.index * lsmem.block_size; + let size = blk.count * lsmem.block_size; + row.range = format!("0x{:016x}-0x{:016x}", start, start + size - 1); + + // Size + row.size = if lsmem.bytes { + format!("{}", blk.count * lsmem.block_size) + } else { + format!( + "{}", + utils::size_to_human_string(blk.count * lsmem.block_size) + ) + }; + + // State + row.state = match blk.state { + MemoryState::Online => format!("{}", MemoryState::Online.to_string()), + MemoryState::Offline => format!("{}", MemoryState::Offline.to_string()), + MemoryState::GoingOffline => format!("{}", MemoryState::GoingOffline.to_string()), + _ => format!("?"), + }; + + // Removable + row.removable = if blk.removable { + format!("yes") + } else { + format!("no") + }; + + // Block + row.block = if blk.count == 1 { + format!("{}", blk.index) + } else { + format!("{}-{}", blk.index, blk.index + blk.count - 1) + }; + + // Node + if lsmem.have_nodes { + row.node = format!("{}", blk.node); + } + + table.push(row); + } + return table; +} + +fn print_table(lsmem: &Lsmem) { + let table_row_vec: Vec = set_summary(lsmem); + let mut table = Table::new(table_row_vec); + table + .with(Style::blank()) + .with(Modify::new(object::Columns::new(1..)).with(Alignment::right())); + + // the default version + table.with(Disable::column(ByColumnName::new("NODE"))); + table.with(Disable::column(ByColumnName::new("ZONES"))); + + println!("{table}"); +} + +fn print_summary(lsmem: &Lsmem) { + if lsmem.bytes { + println!("{:<23} {:>15}", "Memory block, size:", lsmem.block_size); + println!("{:<23} {:>15}", "Total online memory:", lsmem.mem_online); + println!("{:<23} {:>15}", "Total offline memory:", lsmem.mem_offline); + } else { + println!( + "{:<23} {:>15}", + "Memory block size:", + utils::size_to_human_string(lsmem.block_size) + ); + println!( + "{:<23} {:>15}", + "Total online memory:", + utils::size_to_human_string(lsmem.mem_online) + ); + println!( + "{:<23} {:>15}", + "Total offline memory:", + utils::size_to_human_string(lsmem.mem_offline) + ); + } +} + +fn read_file_content(path: &Path) -> io::Result +where + T::Err: std::fmt::Debug, // Required to unwrap the result of T::from_str +{ + let file = fs::File::open(path)?; + let mut reader = BufReader::new(file); + let mut content = String::new(); + reader.read_line(&mut content)?; + Ok(content.trim().to_string().parse().unwrap()) +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let _matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?; + let matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?; + let opt_bytes = matches.get_flag(options::BYTES); + + let mut lsmem = Lsmem::new(); + lsmem.bytes = opt_bytes; + + read_info(&mut lsmem); + + if lsmem.want_table { + print_table(&lsmem); + } + + if lsmem.want_summary { + print_summary(&lsmem); + } + Ok(()) } @@ -21,4 +553,11 @@ pub fn uu_app() -> Command { .about(ABOUT) .override_usage(format_usage(USAGE)) .infer_long_args(true) + .arg( + Arg::new(options::BYTES) + .short('b') + .long("bytes") + .help("print SIZE in bytes rather than in human readable format") + .action(ArgAction::SetTrue), + ) } diff --git a/src/uu/lsmem/src/utils.rs b/src/uu/lsmem/src/utils.rs new file mode 100644 index 0000000..8300374 --- /dev/null +++ b/src/uu/lsmem/src/utils.rs @@ -0,0 +1,76 @@ +enum HumanStringSize { + Suffix1Letter = 0, + Suffix3Letter = (1 << 0), + SuffixSpace = (1 << 1), + Decimal2Digits = (1 << 2), +} + +pub fn size_to_human_string(bytes: u64) -> String { + let mut buf = String::with_capacity(32); + let mut dec; + let mut frac; + let letters = "BKMGTPE"; + let mut suffix = String::with_capacity(4); + + let exp = get_exp(bytes); + let c = letters + .chars() + .nth(if exp != 0 { exp / 10 } else { 0 }) + .unwrap_or('B'); + dec = if exp != 0 { + bytes / ((1 as u64) << exp) + } else { + bytes + }; + frac = if exp != 0 { + bytes % ((1 as u64) << exp) + } else { + 0 + }; + + suffix.push(c); + + // Rounding logic + if frac != 0 { + if frac >= u64::MAX / 1000 { + frac = ((frac / 1024) * 1000) / (1 << (exp - 10)); + } else { + frac = (frac * 1000) / (1 << exp); + } + + // Round to 1 decimal place + frac = ((frac + 50) / 100) * 10; + + // Check for overflow due to rounding + if frac == 100 { + dec += 1; + frac = 0; + } + } + + // Format the result + if frac != 0 { + let decimal_point = "."; + buf = format!("{}{}{:02}", dec, decimal_point, frac); + if buf.ends_with('0') { + buf.pop(); // Remove extraneous zero + } + buf += &suffix; + } else { + buf += &format!("{dec}"); + buf += &suffix; + } + + buf +} + +fn get_exp(n: u64) -> usize { + let mut shft = 10; + while shft <= 60 { + if n < (1 << shft) { + break; + } + shft += 10; + } + shft - 10 +}