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 collections::HashMap, fs, os::unix::fs::MetadataExt, process::ExitCode,
9};
10
11#[derive(Parser)]
12#[command(name = "lslocks", about = "List local system locks")]
13pub struct Args {
14 #[arg(short, long)]
16 bytes: bool,
17
18 #[arg(short = 'J', long)]
20 json: bool,
21
22 #[arg(short = 'i', long)]
24 noinaccessible: bool,
25
26 #[arg(short = 'n', long)]
28 noheadings: bool,
29
30 #[arg(short = 'o', long, value_delimiter = ',')]
32 output: Option<Vec<String>>,
33
34 #[arg(long)]
36 output_all: bool,
37
38 #[arg(short = 'p', long, value_name = "pid")]
40 pid: Option<u32>,
41
42 #[arg(short = 'r', long)]
44 raw: bool,
45
46 #[arg(short = 'u', long)]
48 notruncate: bool,
49}
50
51#[derive(Debug)]
52struct Lock {
53 lock_type: String, mandatory: bool,
55 mode: String, pid: u32,
57 major: u64,
58 minor: u64,
59 inode: u64,
60 start: u64,
61 end: Option<u64>, blocker: Option<u32>,
63 command: String,
64 path: String,
65}
66
67impl Lock {
68 fn file_size(&self) -> Option<u64> {
69 if self.start == 0 && self.end == Some(0) {
71 return None;
72 }
73 if self.path.is_empty() {
74 return None;
75 }
76 fs::metadata(&self.path).ok().map(|m| m.len())
77 }
78
79 fn size_str(&self, human: bool) -> String {
80 match self.file_size() {
81 None => String::new(),
82 Some(b) if human => human_size(b),
83 Some(b) => b.to_string(),
84 }
85 }
86}
87
88fn human_size(bytes: u64) -> String {
89 const UNITS: &[&str] = &["B", "K", "M", "G", "T"];
90 let mut val = bytes as f64;
91 let mut unit = 0;
92 while val >= 1024.0 && unit + 1 < UNITS.len() {
93 val /= 1024.0;
94 unit += 1;
95 }
96 if unit == 0 {
97 format!("{bytes}B")
98 } else {
99 format!("{:.0}{}", val, UNITS[unit])
100 }
101}
102
103fn parse_proc_locks() -> Vec<Lock> {
107 let content = match fs::read_to_string("/proc/locks") {
108 Ok(s) => s,
109 Err(_) => return Vec::new(),
110 };
111
112 let mut locks = Vec::new();
113
114 for line in content.lines() {
115 let line = line.trim();
116 if line.is_empty() {
117 continue;
118 }
119
120 let fields: Vec<&str> = line.split_whitespace().collect();
121 if fields.len() < 8 {
124 continue;
125 }
126
127 let _lock_id = fields[0].trim_end_matches(':');
130
131 let lock_type = fields[1].to_string();
132 let mandatory = fields[2] == "MANDATORY";
133 let mode = fields[3].to_string();
134
135 let pid: u32 = match fields[4].parse() {
136 Ok(p) => p,
137 Err(_) => continue,
138 };
139
140 let (major, minor, inode) = match parse_device_inode(fields[5]) {
142 Some(v) => v,
143 None => continue,
144 };
145
146 let start: u64 = fields[6].parse().unwrap_or(0);
147 let end: Option<u64> = if fields[7] == "EOF" {
148 None
149 } else {
150 fields[7].parse().ok()
151 };
152
153 locks.push(Lock {
154 lock_type,
155 mandatory,
156 mode,
157 pid,
158 major,
159 minor,
160 inode,
161 start,
162 end,
163 blocker: None,
164 command: String::new(),
165 path: String::new(),
166 });
167 }
168
169 locks
170}
171
172fn parse_device_inode(s: &str) -> Option<(u64, u64, u64)> {
173 let mut parts = s.splitn(3, ':');
175 let major = u64::from_str_radix(parts.next()?, 16).ok()?;
176 let minor = u64::from_str_radix(parts.next()?, 16).ok()?;
177 let inode: u64 = parts.next()?.parse().ok()?;
178 Some((major, minor, inode))
179}
180
181fn build_path_cache(pids: &[u32]) -> HashMap<(u32, u64), String> {
185 let mut cache = HashMap::new();
186
187 for &pid in pids {
188 let fd_dir = format!("/proc/{pid}/fd");
189 let entries = match fs::read_dir(&fd_dir) {
190 Ok(e) => e,
191 Err(_) => continue,
192 };
193
194 for entry in entries.flatten() {
195 let link = match fs::read_link(entry.path()) {
196 Ok(p) => p,
197 Err(_) => continue,
198 };
199 let path_str = link.to_string_lossy().into_owned();
200
201 if let Ok(meta) = fs::metadata(entry.path()) {
203 let ino = meta.ino();
204 cache.entry((pid, ino)).or_insert(path_str);
205 }
206 }
207 }
208
209 cache
210}
211
212fn read_command(pid: u32) -> String {
213 fs::read_to_string(format!("/proc/{pid}/comm"))
214 .ok()
215 .map(|s| s.trim().to_string())
216 .unwrap_or_default()
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220enum Col {
221 Command,
222 Pid,
223 Type,
224 Size,
225 Mode,
226 Mandatory,
227 Start,
228 End,
229 Path,
230 Inode,
231 MajMin,
232 Blocker,
233 Holders,
234}
235
236impl Col {
237 fn name(self) -> &'static str {
238 match self {
239 Col::Command => "COMMAND",
240 Col::Pid => "PID",
241 Col::Type => "TYPE",
242 Col::Size => "SIZE",
243 Col::Mode => "MODE",
244 Col::Mandatory => "M",
245 Col::Start => "START",
246 Col::End => "END",
247 Col::Path => "PATH",
248 Col::Inode => "INODE",
249 Col::MajMin => "MAJ:MIN",
250 Col::Blocker => "BLOCKER",
251 Col::Holders => "HOLDERS",
252 }
253 }
254
255 fn whint(self) -> WidthHint {
256 match self {
257 Col::Command => WidthHint::Fixed(14),
258 Col::Pid => WidthHint::Fixed(5),
259 Col::Type => WidthHint::Fixed(5),
260 Col::Size => WidthHint::Fixed(5),
261 Col::Mode => WidthHint::Fixed(5),
262 Col::Mandatory => WidthHint::Fixed(1),
263 Col::Start => WidthHint::Fixed(10),
264 Col::End => WidthHint::Fixed(10),
265 Col::Path => WidthHint::Auto,
266 Col::Inode => WidthHint::Fixed(8),
267 Col::MajMin => WidthHint::Fixed(6),
268 Col::Blocker => WidthHint::Fixed(7),
269 Col::Holders => WidthHint::Auto,
270 }
271 }
272
273 fn is_right(self) -> bool {
274 matches!(
275 self,
276 Col::Pid | Col::Start | Col::End | Col::Inode | Col::Blocker
277 )
278 }
279
280 fn from_name(s: &str) -> Option<Self> {
281 match s.to_uppercase().as_str() {
282 "COMMAND" => Some(Col::Command),
283 "PID" => Some(Col::Pid),
284 "TYPE" => Some(Col::Type),
285 "SIZE" => Some(Col::Size),
286 "MODE" => Some(Col::Mode),
287 "M" => Some(Col::Mandatory),
288 "START" => Some(Col::Start),
289 "END" => Some(Col::End),
290 "PATH" => Some(Col::Path),
291 "INODE" => Some(Col::Inode),
292 "MAJ:MIN" => Some(Col::MajMin),
293 "BLOCKER" => Some(Col::Blocker),
294 "HOLDERS" => Some(Col::Holders),
295 _ => None,
296 }
297 }
298}
299
300const DEFAULT_COLUMNS: &[Col] = &[
301 Col::Command,
302 Col::Pid,
303 Col::Type,
304 Col::Size,
305 Col::Mode,
306 Col::Mandatory,
307 Col::Start,
308 Col::End,
309 Col::Path,
310];
311
312const ALL_COLUMNS: &[Col] = &[
313 Col::Command,
314 Col::Pid,
315 Col::Type,
316 Col::Size,
317 Col::Inode,
318 Col::MajMin,
319 Col::Mode,
320 Col::Mandatory,
321 Col::Start,
322 Col::End,
323 Col::Path,
324 Col::Blocker,
325 Col::Holders,
326];
327
328pub fn run(args: Args) -> ExitCode {
329 let mut locks = parse_proc_locks();
330
331 if let Some(filter_pid) = args.pid {
333 locks.retain(|l| l.pid == filter_pid);
334 }
335
336 let pids: Vec<u32> = {
338 let mut v: Vec<u32> = locks.iter().map(|l| l.pid).collect();
339 v.sort_unstable();
340 v.dedup();
341 v
342 };
343
344 let path_cache = build_path_cache(&pids);
346
347 for lock in &mut locks {
349 lock.command = read_command(lock.pid);
350 if let Some(p) = path_cache.get(&(lock.pid, lock.inode)) {
351 lock.path = p.clone();
352 }
353 }
354
355 if args.noinaccessible {
356 locks.retain(|l| !l.path.is_empty());
357 }
358
359 let columns: Vec<Col> = if args.output_all {
360 ALL_COLUMNS.to_vec()
361 } else if let Some(ref names) = args.output {
362 let mut cols = Vec::new();
363 for name in names {
364 match Col::from_name(name.trim()) {
365 Some(c) => cols.push(c),
366 None => {
367 eprintln!("lslocks: unknown column: {name}");
368 return ExitCode::FAILURE;
369 }
370 }
371 }
372 cols
373 } else {
374 DEFAULT_COLUMNS.to_vec()
375 };
376
377 let mut table = Table::new();
378 table.name_set("locks");
379
380 if args.json {
381 table.output_mode_set(OutputMode::Json);
382 } else if args.raw {
383 table.output_mode_set(OutputMode::Raw);
384 }
385
386 if args.noheadings {
387 table.headings_set(false);
388 }
389
390 for col in &columns {
391 let idx = table.new_column(col.name());
392 table.column_mut(idx).unwrap().width_hint_set(col.whint());
393 if col.is_right() {
394 table.column_mut(idx).unwrap().right_set(true);
395 }
396 }
397
398 for lock in &locks {
399 let line_id = table.new_line(None);
400 let line = table.line_mut(line_id);
401
402 for (ci, col) in columns.iter().enumerate() {
403 let val = match col {
404 Col::Command => lock.command.clone(),
405 Col::Pid => lock.pid.to_string(),
406 Col::Type => lock.lock_type.clone(),
407 Col::Size => lock.size_str(!args.bytes),
408 Col::Mode => lock.mode.clone(),
409 Col::Mandatory => (lock.mandatory as u8).to_string(),
410 Col::Start => lock.start.to_string(),
411 Col::End => {
412 lock.end.map_or("EOF".to_string(), |e| e.to_string())
413 }
414 Col::Path => lock.path.clone(),
415 Col::Inode => lock.inode.to_string(),
416 Col::MajMin => format!("{}:{}", lock.major, lock.minor),
417 Col::Blocker => {
418 lock.blocker.map_or(String::new(), |b| b.to_string())
419 }
420 Col::Holders => String::new(), };
422 line.data_set(ci, &val);
423 }
424 }
425
426 let stdout = std::io::stdout();
427 let mut out = stdout.lock();
428 if let Err(e) = print_table(&table, &mut out) {
429 eprintln!("lslocks: {e}");
430 return ExitCode::FAILURE;
431 }
432
433 ExitCode::SUCCESS
434}