use linuxutils_common::man::ManContent;
pub const MAN: ManContent = ManContent::empty();
use clap::Parser;
use cols::{OutputMode, Table, WidthHint, print_table};
use std::{
collections::HashMap, fs, os::unix::fs::MetadataExt, process::ExitCode,
};
#[derive(Parser)]
#[command(name = "lslocks", about = "List local system locks")]
pub struct Args {
#[arg(short, long)]
bytes: bool,
#[arg(short = 'J', long)]
json: bool,
#[arg(short = 'i', long)]
noinaccessible: bool,
#[arg(short = 'n', long)]
noheadings: bool,
#[arg(short = 'o', long, value_delimiter = ',')]
output: Option<Vec<String>>,
#[arg(long)]
output_all: bool,
#[arg(short = 'p', long, value_name = "pid")]
pid: Option<u32>,
#[arg(short = 'r', long)]
raw: bool,
#[arg(short = 'u', long)]
notruncate: bool,
}
#[derive(Debug)]
struct Lock {
lock_type: String, mandatory: bool,
mode: String, pid: u32,
major: u64,
minor: u64,
inode: u64,
start: u64,
end: Option<u64>, blocker: Option<u32>,
command: String,
path: String,
}
impl Lock {
fn file_size(&self) -> Option<u64> {
if self.start == 0 && self.end == Some(0) {
return None;
}
if self.path.is_empty() {
return None;
}
fs::metadata(&self.path).ok().map(|m| m.len())
}
fn size_str(&self, human: bool) -> String {
match self.file_size() {
None => String::new(),
Some(b) if human => human_size(b),
Some(b) => b.to_string(),
}
}
}
fn human_size(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "K", "M", "G", "T"];
let mut val = bytes as f64;
let mut unit = 0;
while val >= 1024.0 && unit + 1 < UNITS.len() {
val /= 1024.0;
unit += 1;
}
if unit == 0 {
format!("{bytes}B")
} else {
format!("{:.0}{}", val, UNITS[unit])
}
}
fn parse_proc_locks() -> Vec<Lock> {
let content = match fs::read_to_string("/proc/locks") {
Ok(s) => s,
Err(_) => return Vec::new(),
};
let mut locks = Vec::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() < 8 {
continue;
}
let _lock_id = fields[0].trim_end_matches(':');
let lock_type = fields[1].to_string();
let mandatory = fields[2] == "MANDATORY";
let mode = fields[3].to_string();
let pid: u32 = match fields[4].parse() {
Ok(p) => p,
Err(_) => continue,
};
let (major, minor, inode) = match parse_device_inode(fields[5]) {
Some(v) => v,
None => continue,
};
let start: u64 = fields[6].parse().unwrap_or(0);
let end: Option<u64> = if fields[7] == "EOF" {
None
} else {
fields[7].parse().ok()
};
locks.push(Lock {
lock_type,
mandatory,
mode,
pid,
major,
minor,
inode,
start,
end,
blocker: None,
command: String::new(),
path: String::new(),
});
}
locks
}
fn parse_device_inode(s: &str) -> Option<(u64, u64, u64)> {
let mut parts = s.splitn(3, ':');
let major = u64::from_str_radix(parts.next()?, 16).ok()?;
let minor = u64::from_str_radix(parts.next()?, 16).ok()?;
let inode: u64 = parts.next()?.parse().ok()?;
Some((major, minor, inode))
}
fn build_path_cache(pids: &[u32]) -> HashMap<(u32, u64), String> {
let mut cache = HashMap::new();
for &pid in pids {
let fd_dir = format!("/proc/{pid}/fd");
let entries = match fs::read_dir(&fd_dir) {
Ok(e) => e,
Err(_) => continue,
};
for entry in entries.flatten() {
let link = match fs::read_link(entry.path()) {
Ok(p) => p,
Err(_) => continue,
};
let path_str = link.to_string_lossy().into_owned();
if let Ok(meta) = fs::metadata(entry.path()) {
let ino = meta.ino();
cache.entry((pid, ino)).or_insert(path_str);
}
}
}
cache
}
fn read_command(pid: u32) -> String {
fs::read_to_string(format!("/proc/{pid}/comm"))
.ok()
.map(|s| s.trim().to_string())
.unwrap_or_default()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Col {
Command,
Pid,
Type,
Size,
Mode,
Mandatory,
Start,
End,
Path,
Inode,
MajMin,
Blocker,
Holders,
}
impl Col {
fn name(self) -> &'static str {
match self {
Col::Command => "COMMAND",
Col::Pid => "PID",
Col::Type => "TYPE",
Col::Size => "SIZE",
Col::Mode => "MODE",
Col::Mandatory => "M",
Col::Start => "START",
Col::End => "END",
Col::Path => "PATH",
Col::Inode => "INODE",
Col::MajMin => "MAJ:MIN",
Col::Blocker => "BLOCKER",
Col::Holders => "HOLDERS",
}
}
fn whint(self) -> WidthHint {
match self {
Col::Command => WidthHint::Fixed(14),
Col::Pid => WidthHint::Fixed(5),
Col::Type => WidthHint::Fixed(5),
Col::Size => WidthHint::Fixed(5),
Col::Mode => WidthHint::Fixed(5),
Col::Mandatory => WidthHint::Fixed(1),
Col::Start => WidthHint::Fixed(10),
Col::End => WidthHint::Fixed(10),
Col::Path => WidthHint::Auto,
Col::Inode => WidthHint::Fixed(8),
Col::MajMin => WidthHint::Fixed(6),
Col::Blocker => WidthHint::Fixed(7),
Col::Holders => WidthHint::Auto,
}
}
fn is_right(self) -> bool {
matches!(
self,
Col::Pid | Col::Start | Col::End | Col::Inode | Col::Blocker
)
}
fn from_name(s: &str) -> Option<Self> {
match s.to_uppercase().as_str() {
"COMMAND" => Some(Col::Command),
"PID" => Some(Col::Pid),
"TYPE" => Some(Col::Type),
"SIZE" => Some(Col::Size),
"MODE" => Some(Col::Mode),
"M" => Some(Col::Mandatory),
"START" => Some(Col::Start),
"END" => Some(Col::End),
"PATH" => Some(Col::Path),
"INODE" => Some(Col::Inode),
"MAJ:MIN" => Some(Col::MajMin),
"BLOCKER" => Some(Col::Blocker),
"HOLDERS" => Some(Col::Holders),
_ => None,
}
}
}
const DEFAULT_COLUMNS: &[Col] = &[
Col::Command,
Col::Pid,
Col::Type,
Col::Size,
Col::Mode,
Col::Mandatory,
Col::Start,
Col::End,
Col::Path,
];
const ALL_COLUMNS: &[Col] = &[
Col::Command,
Col::Pid,
Col::Type,
Col::Size,
Col::Inode,
Col::MajMin,
Col::Mode,
Col::Mandatory,
Col::Start,
Col::End,
Col::Path,
Col::Blocker,
Col::Holders,
];
pub fn run(args: Args) -> ExitCode {
let mut locks = parse_proc_locks();
if let Some(filter_pid) = args.pid {
locks.retain(|l| l.pid == filter_pid);
}
let pids: Vec<u32> = {
let mut v: Vec<u32> = locks.iter().map(|l| l.pid).collect();
v.sort_unstable();
v.dedup();
v
};
let path_cache = build_path_cache(&pids);
for lock in &mut locks {
lock.command = read_command(lock.pid);
if let Some(p) = path_cache.get(&(lock.pid, lock.inode)) {
lock.path = p.clone();
}
}
if args.noinaccessible {
locks.retain(|l| !l.path.is_empty());
}
let columns: Vec<Col> = if args.output_all {
ALL_COLUMNS.to_vec()
} else if let Some(ref names) = args.output {
let mut cols = Vec::new();
for name in names {
match Col::from_name(name.trim()) {
Some(c) => cols.push(c),
None => {
eprintln!("lslocks: unknown column: {name}");
return ExitCode::FAILURE;
}
}
}
cols
} else {
DEFAULT_COLUMNS.to_vec()
};
let mut table = Table::new();
table.name_set("locks");
if args.json {
table.output_mode_set(OutputMode::Json);
} else if args.raw {
table.output_mode_set(OutputMode::Raw);
}
if args.noheadings {
table.headings_set(false);
}
for col in &columns {
let idx = table.new_column(col.name());
table.column_mut(idx).unwrap().width_hint_set(col.whint());
if col.is_right() {
table.column_mut(idx).unwrap().right_set(true);
}
}
for lock in &locks {
let line_id = table.new_line(None);
let line = table.line_mut(line_id);
for (ci, col) in columns.iter().enumerate() {
let val = match col {
Col::Command => lock.command.clone(),
Col::Pid => lock.pid.to_string(),
Col::Type => lock.lock_type.clone(),
Col::Size => lock.size_str(!args.bytes),
Col::Mode => lock.mode.clone(),
Col::Mandatory => (lock.mandatory as u8).to_string(),
Col::Start => lock.start.to_string(),
Col::End => {
lock.end.map_or("EOF".to_string(), |e| e.to_string())
}
Col::Path => lock.path.clone(),
Col::Inode => lock.inode.to_string(),
Col::MajMin => format!("{}:{}", lock.major, lock.minor),
Col::Blocker => {
lock.blocker.map_or(String::new(), |b| b.to_string())
}
Col::Holders => String::new(), };
line.data_set(ci, &val);
}
}
let stdout = std::io::stdout();
let mut out = stdout.lock();
if let Err(e) = print_table(&table, &mut out) {
eprintln!("lslocks: {e}");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}