Skip to main content

linuxutils_system/
lsmem.rs

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    /// List each individual memory block
20    #[arg(short, long)]
21    all: bool,
22
23    /// Print SIZE in bytes rather than human-readable format
24    #[arg(short, long)]
25    bytes: bool,
26
27    /// Use JSON output format
28    #[arg(short = 'J', long)]
29    json: bool,
30
31    /// Don't print headings
32    #[arg(short = 'n', long)]
33    noheadings: bool,
34
35    /// Output columns to print
36    #[arg(short, long, value_delimiter = ',')]
37    output: Option<Vec<String>>,
38
39    /// Output all available columns
40    #[arg(long)]
41    output_all: bool,
42
43    /// Use key="value" output format
44    #[arg(short = 'P', long)]
45    pairs: bool,
46
47    /// Use raw output format
48    #[arg(short, long)]
49    raw: bool,
50
51    /// Split ranges by specified columns
52    #[arg(short = 'S', long, value_delimiter = ',')]
53    split: Option<Vec<String>>,
54
55    /// Use specified directory as system root
56    #[arg(short, long)]
57    sysroot: Option<PathBuf>,
58
59    /// Print summary information (never, always, only)
60    #[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
154/// Attributes that control how memory blocks are split into ranges.
155const 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        // Only the first zone matters for display.
213        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    // NUMA node is in a symlink like /sys/devices/system/memory/memoryN/nodeN
231    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            // Use C-style rounding (half-up) instead of Rust's default
302            // banker's rounding: add a tiny epsilon before formatting.
303            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        // --all: each block is its own range
356        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}