Skip to main content

linuxutils_system/
lsirq.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 std::{
8    io::{self, BufRead},
9    path::PathBuf,
10    process::ExitCode,
11};
12
13#[derive(Parser)]
14#[command(
15    name = "lsirq",
16    about = "Utility to display kernel interrupt information"
17)]
18pub struct Args {
19    /// Use JSON output format
20    #[arg(short = 'J', long)]
21    json: bool,
22
23    /// Use key="value" output format
24    #[arg(short = 'P', long)]
25    pairs: bool,
26
27    /// Don't print headings
28    #[arg(short = 'n', long)]
29    noheadings: bool,
30
31    /// Output columns to print
32    #[arg(short, long, value_delimiter = ',')]
33    output: Option<Vec<String>>,
34
35    /// Sort by column name
36    #[arg(short, long)]
37    sort: Option<String>,
38
39    /// Show softirqs instead of interrupts
40    #[arg(short = 'S', long)]
41    softirq: bool,
42
43    /// Only IRQs with counters above this threshold
44    #[arg(short, long)]
45    threshold: Option<String>,
46
47    /// Only show counters for these CPUs
48    #[arg(short = 'C', long, value_delimiter = ',')]
49    cpu_list: Option<Vec<String>>,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53enum Col {
54    Irq,
55    Total,
56    Name,
57}
58
59impl Col {
60    fn name(self) -> &'static str {
61        match self {
62            Col::Irq => "IRQ",
63            Col::Total => "TOTAL",
64            Col::Name => "NAME",
65        }
66    }
67
68    fn whint(self) -> WidthHint {
69        match self {
70            Col::Irq => WidthHint::Auto,
71            Col::Total => WidthHint::Auto,
72            Col::Name => WidthHint::Auto,
73        }
74    }
75
76    fn is_right(self) -> bool {
77        matches!(self, Col::Irq | Col::Total)
78    }
79
80    fn from_name(name: &str) -> Option<Self> {
81        match name.to_uppercase().as_str() {
82            "IRQ" => Some(Col::Irq),
83            "TOTAL" => Some(Col::Total),
84            "NAME" => Some(Col::Name),
85            _ => None,
86        }
87    }
88}
89
90const DEFAULT_COLUMNS: &[Col] = &[Col::Irq, Col::Total, Col::Name];
91
92#[derive(Debug)]
93struct IrqEntry {
94    irq: String,
95    total: u64,
96    name: String,
97}
98
99fn parse_threshold(s: &str) -> Option<u64> {
100    let s = s.trim();
101    if s.is_empty() {
102        return None;
103    }
104
105    let (num_part, suffix) = if s.ends_with("KiB") || s.ends_with("K") {
106        (s.trim_end_matches("KiB").trim_end_matches('K'), 1000u64)
107    } else if s.ends_with("MiB") || s.ends_with("M") {
108        (s.trim_end_matches("MiB").trim_end_matches('M'), 1_000_000)
109    } else if s.ends_with("GiB") || s.ends_with("G") {
110        (
111            s.trim_end_matches("GiB").trim_end_matches('G'),
112            1_000_000_000,
113        )
114    } else {
115        (s, 1)
116    };
117
118    let val: f64 = num_part.trim().parse().ok()?;
119    Some((val * suffix as f64) as u64)
120}
121
122fn parse_cpu_list(list: &[String]) -> Vec<usize> {
123    let mut cpus = Vec::new();
124    for item in list {
125        for part in item.split(',') {
126            let part = part.trim();
127            if let Some((start, end)) = part.split_once('-') {
128                if let (Ok(s), Ok(e)) =
129                    (start.trim().parse::<usize>(), end.trim().parse::<usize>())
130                {
131                    cpus.extend(s..=e);
132                }
133            } else if let Ok(n) = part.parse::<usize>() {
134                cpus.push(n);
135            }
136        }
137    }
138    cpus.sort();
139    cpus.dedup();
140    cpus
141}
142
143fn read_interrupts(
144    softirq: bool,
145    cpu_filter: &Option<Vec<usize>>,
146) -> Result<Vec<IrqEntry>, io::Error> {
147    let path = if softirq {
148        PathBuf::from("/proc/softirqs")
149    } else {
150        PathBuf::from("/proc/interrupts")
151    };
152
153    let file = std::fs::File::open(&path)?;
154    let reader = io::BufReader::new(file);
155    let mut lines = reader.lines();
156
157    // First line is the CPU header — parse to get CPU count.
158    let header = lines.next().ok_or_else(|| {
159        io::Error::new(io::ErrorKind::InvalidData, "empty interrupts file")
160    })??;
161    let _num_cpus = header.split_whitespace().count();
162
163    let mut entries = Vec::new();
164    for line in lines {
165        let line = line?;
166        let line = line.trim();
167        if line.is_empty() {
168            continue;
169        }
170
171        // Format: "IRQ: count0 count1 ... countN  description"
172        // or for named IRQs without colon: "NAME count0 count1 ... countN"
173        let (irq_name, rest) = if let Some((name, rest)) = line.split_once(':')
174        {
175            (name.trim().to_string(), rest.trim())
176        } else {
177            continue;
178        };
179
180        let parts: Vec<&str> = rest.split_whitespace().collect();
181
182        // Parse per-CPU counts. Some IRQs (like ERR, MIS) have fewer
183        // counters than CPUs.
184        let mut per_cpu: Vec<u64> = Vec::new();
185        let mut desc_start = 0;
186        for (i, part) in parts.iter().enumerate() {
187            if let Ok(n) = part.parse::<u64>() {
188                per_cpu.push(n);
189            } else {
190                desc_start = i;
191                break;
192            }
193            desc_start = i + 1;
194        }
195
196        let total = if let Some(cpus) = cpu_filter {
197            cpus.iter().filter_map(|&c| per_cpu.get(c)).sum()
198        } else {
199            per_cpu.iter().sum()
200        };
201
202        let name = parts[desc_start..].join(" ");
203
204        entries.push(IrqEntry {
205            irq: irq_name,
206            total,
207            name,
208        });
209    }
210
211    Ok(entries)
212}
213
214pub fn run(args: Args) -> ExitCode {
215    let cpu_filter = args.cpu_list.as_ref().map(|l| parse_cpu_list(l));
216
217    let mut entries = match read_interrupts(args.softirq, &cpu_filter) {
218        Ok(e) => e,
219        Err(e) => {
220            eprintln!("lsirq: failed to read interrupts: {e}");
221            return ExitCode::FAILURE;
222        }
223    };
224
225    // Apply threshold filter.
226    if let Some(ref threshold_str) = args.threshold {
227        if let Some(threshold) = parse_threshold(threshold_str) {
228            entries.retain(|e| e.total >= threshold);
229        } else {
230            eprintln!("lsirq: invalid threshold: {threshold_str}");
231            return ExitCode::FAILURE;
232        }
233    }
234
235    let columns = if let Some(ref names) = args.output {
236        let mut cols = Vec::new();
237        for name in names {
238            match Col::from_name(name.trim()) {
239                Some(c) => cols.push(c),
240                None => {
241                    eprintln!("lsirq: unknown column: {name}");
242                    return ExitCode::FAILURE;
243                }
244            }
245        }
246        cols
247    } else {
248        DEFAULT_COLUMNS.to_vec()
249    };
250
251    // Sort entries. Default is by TOTAL descending.
252    let sort_col = args
253        .sort
254        .as_ref()
255        .and_then(|s| Col::from_name(s))
256        .unwrap_or(Col::Total);
257
258    match sort_col {
259        Col::Irq => entries.sort_by(|a, b| a.irq.cmp(&b.irq)),
260        Col::Total => entries.sort_by(|a, b| b.total.cmp(&a.total)),
261        Col::Name => entries.sort_by(|a, b| a.name.cmp(&b.name)),
262    }
263
264    let mut table = Table::new();
265    table.name_set("interrupts");
266
267    if args.json {
268        table.output_mode_set(OutputMode::Json);
269    } else if args.pairs {
270        table.output_mode_set(OutputMode::Export);
271    }
272
273    if args.noheadings {
274        table.headings_set(false);
275    }
276
277    for col in &columns {
278        let idx = table.new_column(col.name());
279        table.column_mut(idx).unwrap().width_hint_set(col.whint());
280        if col.is_right() {
281            table.column_mut(idx).unwrap().right_set(true);
282        }
283    }
284
285    for entry in &entries {
286        let line_id = table.new_line(None);
287        let line = table.line_mut(line_id);
288
289        for (ci, col) in columns.iter().enumerate() {
290            let val = match col {
291                Col::Irq => entry.irq.clone(),
292                Col::Total => entry.total.to_string(),
293                Col::Name => entry.name.clone(),
294            };
295            line.data_set(ci, &val);
296        }
297    }
298
299    let stdout = std::io::stdout();
300    let mut out = stdout.lock();
301    if let Err(e) = print_table(&table, &mut out) {
302        eprintln!("lsirq: {e}");
303        return ExitCode::FAILURE;
304    }
305
306    ExitCode::SUCCESS
307}