1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use cols::{OutputMode, Table, WidthHint, print_table};
7use crate::sysfs::SysfsDevice;
8use std::{
9 path::{Path, PathBuf},
10 process::ExitCode,
11};
12
13#[derive(Parser)]
14#[command(
15 name = "lsmem",
16 about = "List the ranges of available memory with their online status"
17)]
18pub struct Args {
19 #[arg(short, long)]
21 all: bool,
22
23 #[arg(short, long)]
25 bytes: bool,
26
27 #[arg(short = 'J', long)]
29 json: bool,
30
31 #[arg(short = 'n', long)]
33 noheadings: bool,
34
35 #[arg(short, long, value_delimiter = ',')]
37 output: Option<Vec<String>>,
38
39 #[arg(long)]
41 output_all: bool,
42
43 #[arg(short = 'P', long)]
45 pairs: bool,
46
47 #[arg(short, long)]
49 raw: bool,
50
51 #[arg(short = 'S', long, value_delimiter = ',')]
53 split: Option<Vec<String>>,
54
55 #[arg(short, long)]
57 sysroot: Option<PathBuf>,
58
59 #[arg(long, default_value = "always")]
61 summary: SummaryMode,
62}
63
64#[derive(Clone, Debug)]
65enum SummaryMode {
66 Never,
67 Always,
68 Only,
69}
70
71impl std::str::FromStr for SummaryMode {
72 type Err = String;
73 fn from_str(s: &str) -> Result<Self, String> {
74 match s {
75 "never" => Ok(Self::Never),
76 "always" => Ok(Self::Always),
77 "only" => Ok(Self::Only),
78 _ => Err(format!("invalid summary mode: {s}")),
79 }
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84enum Col {
85 Range,
86 Size,
87 State,
88 Removable,
89 Block,
90 Node,
91 Zones,
92}
93
94impl Col {
95 fn name(self) -> &'static str {
96 match self {
97 Col::Range => "RANGE",
98 Col::Size => "SIZE",
99 Col::State => "STATE",
100 Col::Removable => "REMOVABLE",
101 Col::Block => "BLOCK",
102 Col::Node => "NODE",
103 Col::Zones => "ZONES",
104 }
105 }
106
107 fn whint(self) -> WidthHint {
108 match self {
109 Col::Range => WidthHint::Auto,
110 Col::Size => WidthHint::Fixed(5),
111 Col::State => WidthHint::Fixed(6),
112 Col::Removable => WidthHint::Fixed(9),
113 Col::Block => WidthHint::Auto,
114 Col::Node => WidthHint::Fixed(4),
115 Col::Zones => WidthHint::Auto,
116 }
117 }
118
119 fn is_right(self) -> bool {
120 matches!(self, Col::Size | Col::Removable | Col::Block)
121 }
122
123 fn from_name(name: &str) -> Option<Self> {
124 match name.to_uppercase().as_str() {
125 "RANGE" => Some(Col::Range),
126 "SIZE" => Some(Col::Size),
127 "STATE" => Some(Col::State),
128 "REMOVABLE" => Some(Col::Removable),
129 "BLOCK" => Some(Col::Block),
130 "NODE" => Some(Col::Node),
131 "ZONES" => Some(Col::Zones),
132 _ => None,
133 }
134 }
135}
136
137const DEFAULT_COLUMNS: &[Col] = &[
138 Col::Range,
139 Col::Size,
140 Col::State,
141 Col::Removable,
142 Col::Block,
143];
144const ALL_COLUMNS: &[Col] = &[
145 Col::Range,
146 Col::Size,
147 Col::State,
148 Col::Removable,
149 Col::Block,
150 Col::Node,
151 Col::Zones,
152];
153
154const DEFAULT_SPLIT: &[Col] = &[Col::State, Col::Removable, Col::Node];
156
157#[derive(Debug)]
158struct MemBlock {
159 index: u64,
160 state: String,
161 removable: bool,
162 node: Option<u64>,
163 zones: String,
164 block_size: u64,
165}
166
167impl MemBlock {
168 fn start_addr(&self) -> u64 {
169 self.index * self.block_size
170 }
171
172 fn end_addr(&self) -> u64 {
173 (self.index + 1) * self.block_size - 1
174 }
175}
176
177#[derive(Debug)]
178struct MemRange {
179 start_block: u64,
180 end_block: u64,
181 start_addr: u64,
182 end_addr: u64,
183 size: u64,
184 state: String,
185 removable: bool,
186 node: Option<u64>,
187 zones: String,
188}
189
190fn read_memory_blocks(sysroot: &Path) -> Result<(u64, Vec<MemBlock>), String> {
191 let mem_dir = SysfsDevice::new(sysroot.join("sys/devices/system/memory"));
192
193 let block_size = mem_dir
194 .read_attr_hex("block_size_bytes")
195 .map_err(|e| format!("failed to read block_size_bytes: {e}"))?;
196
197 let children = mem_dir
198 .children_with_prefix("memory")
199 .map_err(|e| format!("failed to list memory blocks: {e}"))?;
200
201 let mut blocks = Vec::new();
202 for child in children {
203 let index = child
204 .read_attr_hex("phys_index")
205 .map_err(|e| format!("failed to read phys_index: {e}"))?;
206 let state = child
207 .read_attr("state")
208 .map_err(|e| format!("failed to read state: {e}"))?;
209 let removable = child.read_attr_bool("removable").unwrap_or(false);
210 let node = find_node(&child);
211 let zones = child.read_attr("valid_zones").unwrap_or_default();
212 let zones = zones.split_whitespace().next().unwrap_or("").to_string();
214
215 blocks.push(MemBlock {
216 index,
217 state,
218 removable,
219 node,
220 zones,
221 block_size,
222 });
223 }
224
225 blocks.sort_by_key(|b| b.index);
226 Ok((block_size, blocks))
227}
228
229fn find_node(dev: &SysfsDevice) -> Option<u64> {
230 let path = dev.path();
232 if let Ok(entries) = std::fs::read_dir(path) {
233 for entry in entries.flatten() {
234 if let Some(name) = entry.file_name().to_str()
235 && let Some(n) = name.strip_prefix("node")
236 && let Ok(num) = n.parse::<u64>()
237 {
238 return Some(num);
239 }
240 }
241 }
242 None
243}
244
245fn merge_blocks(blocks: &[MemBlock], split_cols: &[Col]) -> Vec<MemRange> {
246 if blocks.is_empty() {
247 return Vec::new();
248 }
249
250 let mut ranges = Vec::new();
251 let mut start = 0;
252
253 for i in 1..blocks.len() {
254 let should_split = blocks[i].index != blocks[i - 1].index + 1
255 || split_cols.iter().any(|col| match col {
256 Col::State => blocks[i].state != blocks[start].state,
257 Col::Removable => {
258 blocks[i].removable != blocks[start].removable
259 }
260 Col::Node => blocks[i].node != blocks[start].node,
261 Col::Zones => blocks[i].zones != blocks[start].zones,
262 _ => false,
263 });
264
265 if should_split {
266 ranges.push(make_range(&blocks[start..i]));
267 start = i;
268 }
269 }
270 ranges.push(make_range(&blocks[start..]));
271 ranges
272}
273
274fn make_range(blocks: &[MemBlock]) -> MemRange {
275 let first = &blocks[0];
276 let last = &blocks[blocks.len() - 1];
277 MemRange {
278 start_block: first.index,
279 end_block: last.index,
280 start_addr: first.start_addr(),
281 end_addr: last.end_addr(),
282 size: (last.index - first.index + 1) * first.block_size,
283 state: first.state.clone(),
284 removable: first.removable,
285 node: first.node,
286 zones: first.zones.clone(),
287 }
288}
289
290fn format_size(bytes: u64, human: bool) -> String {
291 if !human {
292 return bytes.to_string();
293 }
294 const UNITS: &[&str] = &["B", "K", "M", "G", "T", "P", "E"];
295 let mut val = bytes as f64;
296 for unit in UNITS {
297 if val < 1024.0 || *unit == "E" {
298 if (val - val.round()).abs() < 0.05 {
299 return format!("{}{unit}", val.round() as u64);
300 }
301 let rounded = (val * 10.0 + 0.5).floor() / 10.0;
304 return format!("{rounded:.1}{unit}");
305 }
306 val /= 1024.0;
307 }
308 unreachable!()
309}
310
311pub fn run(args: Args) -> ExitCode {
312 let sysroot = args.sysroot.as_deref().unwrap_or(Path::new("/"));
313
314 let (block_size, blocks) = match read_memory_blocks(sysroot) {
315 Ok(v) => v,
316 Err(e) => {
317 eprintln!("lsmem: {e}");
318 return ExitCode::FAILURE;
319 }
320 };
321
322 let columns = if args.output_all {
323 ALL_COLUMNS.to_vec()
324 } else if let Some(ref names) = args.output {
325 let mut cols = Vec::new();
326 for name in names {
327 let name = name.trim();
328 match Col::from_name(name) {
329 Some(c) => cols.push(c),
330 None => {
331 eprintln!("lsmem: unknown column: {name}");
332 return ExitCode::FAILURE;
333 }
334 }
335 }
336 cols
337 } else {
338 DEFAULT_COLUMNS.to_vec()
339 };
340
341 let split_cols = if let Some(ref names) = args.split {
342 if names.len() == 1 && names[0].eq_ignore_ascii_case("none") {
343 Vec::new()
344 } else {
345 names
346 .iter()
347 .filter_map(|n| Col::from_name(n.trim()))
348 .collect()
349 }
350 } else {
351 DEFAULT_SPLIT.to_vec()
352 };
353
354 let ranges = if args.all {
355 blocks
357 .iter()
358 .map(|b| MemRange {
359 start_block: b.index,
360 end_block: b.index,
361 start_addr: b.start_addr(),
362 end_addr: b.end_addr(),
363 size: b.block_size,
364 state: b.state.clone(),
365 removable: b.removable,
366 node: b.node,
367 zones: b.zones.clone(),
368 })
369 .collect()
370 } else {
371 merge_blocks(&blocks, &split_cols)
372 };
373
374 let human = !args.bytes;
375 let show_summary = match args.summary {
376 SummaryMode::Never => false,
377 SummaryMode::Only => true,
378 SummaryMode::Always => !args.raw && !args.pairs && !args.json,
379 };
380 let show_table = !matches!(args.summary, SummaryMode::Only);
381
382 if show_table {
383 let mut table = Table::new();
384 table.name_set("memory");
385
386 if args.json {
387 table.output_mode_set(OutputMode::Json);
388 } else if args.pairs {
389 table.output_mode_set(OutputMode::Export);
390 } else if args.raw {
391 table.output_mode_set(OutputMode::Raw);
392 }
393
394 if args.noheadings {
395 table.headings_set(false);
396 }
397
398 for col in &columns {
399 let idx = table.new_column(col.name());
400 table.column_mut(idx).unwrap().width_hint_set(col.whint());
401 if col.is_right() {
402 table.column_mut(idx).unwrap().right_set(true);
403 }
404 }
405
406 for range in &ranges {
407 let line_id = table.new_line(None);
408 let line = table.line_mut(line_id);
409
410 for (ci, col) in columns.iter().enumerate() {
411 let val = match col {
412 Col::Range => format!(
413 "0x{:016x}-0x{:016x}",
414 range.start_addr, range.end_addr
415 ),
416 Col::Size => format_size(range.size, human),
417 Col::State => range.state.clone(),
418 Col::Removable => {
419 if range.removable { "yes" } else { "no" }.to_string()
420 }
421 Col::Block => {
422 if range.start_block == range.end_block {
423 range.start_block.to_string()
424 } else {
425 format!("{}-{}", range.start_block, range.end_block)
426 }
427 }
428 Col::Node => {
429 range.node.map_or(String::new(), |n| n.to_string())
430 }
431 Col::Zones => range.zones.clone(),
432 };
433 line.data_set(ci, &val);
434 }
435 }
436
437 let stdout = std::io::stdout();
438 let mut out = stdout.lock();
439 if let Err(e) = print_table(&table, &mut out) {
440 eprintln!("lsmem: {e}");
441 return ExitCode::FAILURE;
442 }
443 }
444
445 if show_summary {
446 let total_online: u64 = blocks
447 .iter()
448 .filter(|b| b.state == "online")
449 .map(|b| b.block_size)
450 .sum();
451 let total_offline: u64 = blocks
452 .iter()
453 .filter(|b| b.state != "online")
454 .map(|b| b.block_size)
455 .sum();
456
457 if show_table {
458 println!();
459 }
460 let w = 38;
461 println!(
462 "Memory block size:{:>pad$}",
463 format_size(block_size, human),
464 pad = w - 18,
465 );
466 println!(
467 "Total online memory:{:>pad$}",
468 format_size(total_online, human),
469 pad = w - 20,
470 );
471 println!(
472 "Total offline memory:{:>pad$}",
473 format_size(total_offline, human),
474 pad = w - 21,
475 );
476 }
477
478 ExitCode::SUCCESS
479}