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 #[arg(short = 'J', long)]
21 json: bool,
22
23 #[arg(short = 'P', long)]
25 pairs: bool,
26
27 #[arg(short = 'n', long)]
29 noheadings: bool,
30
31 #[arg(short, long, value_delimiter = ',')]
33 output: Option<Vec<String>>,
34
35 #[arg(short, long)]
37 sort: Option<String>,
38
39 #[arg(short = 'S', long)]
41 softirq: bool,
42
43 #[arg(short, long)]
45 threshold: Option<String>,
46
47 #[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 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 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 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 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 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}