Skip to main content

linuxutils_misc/
lslocks.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    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    /// Print SIZE in bytes rather than human-readable format
15    #[arg(short, long)]
16    bytes: bool,
17
18    /// Use JSON output format
19    #[arg(short = 'J', long)]
20    json: bool,
21
22    /// Ignore locks without read permissions on the file
23    #[arg(short = 'i', long)]
24    noinaccessible: bool,
25
26    /// Don't print headings
27    #[arg(short = 'n', long)]
28    noheadings: bool,
29
30    /// Define which output columns to use
31    #[arg(short = 'o', long, value_delimiter = ',')]
32    output: Option<Vec<String>>,
33
34    /// Output all available columns
35    #[arg(long)]
36    output_all: bool,
37
38    /// Display only locks held by this PID
39    #[arg(short = 'p', long, value_name = "pid")]
40    pid: Option<u32>,
41
42    /// Use the raw output format
43    #[arg(short = 'r', long)]
44    raw: bool,
45
46    /// Don't truncate text in columns
47    #[arg(short = 'u', long)]
48    notruncate: bool,
49}
50
51#[derive(Debug)]
52struct Lock {
53    lock_type: String, // FLOCK, POSIX, OFDLCK, LEASE, ...
54    mandatory: bool,
55    mode: String, // READ, WRITE
56    pid: u32,
57    major: u64,
58    minor: u64,
59    inode: u64,
60    start: u64,
61    end: Option<u64>, // None = EOF
62    blocker: Option<u32>,
63    command: String,
64    path: String,
65}
66
67impl Lock {
68    fn file_size(&self) -> Option<u64> {
69        // Don't show size for zero-range locks (start=0, end=0).
70        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
103// Parse /proc/locks. Returns a list of Lock entries.
104// Format: "id: TYPE ADVISORY|MANDATORY ACCESS PID MAJOR:MINOR:INODE START END"
105// Blocked locks are indicated by a leading space or by repeated lock id.
106fn 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        // Expected: id: TYPE ADVISORY PID DEVICE START END
122        // or:       id: TYPE ADVISORY PID DEVICE START END (blocked=id)
123        if fields.len() < 8 {
124            continue;
125        }
126
127        // fields[0] = "N:" (lock id, possibly with leading space for blocked)
128        // Check if this is a blocked lock (kernel marks them with " ->" style)
129        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        // fields[5] = "MAJOR:MINOR:INODE"
141        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    // Format: "MAJOR:MINOR:INODE" where MAJOR and MINOR are hex, INODE is decimal.
174    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
181// Build a map from (pid, inode) → path by scanning /proc/<pid>/fd/.
182// We key by (pid, inode) rather than (dev, inode) because the block device
183// reported in /proc/locks may differ from st_dev (e.g. btrfs subvolumes).
184fn 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            // Stat via the fd symlink to get the inode.
202            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    // Filter by PID if requested.
332    if let Some(filter_pid) = args.pid {
333        locks.retain(|l| l.pid == filter_pid);
334    }
335
336    // Collect unique PIDs for path + command resolution.
337    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    // Resolve paths from /proc/<pid>/fd/.
345    let path_cache = build_path_cache(&pids);
346
347    // Fill in command and path for each lock.
348    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(), // not implemented
421            };
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}