lslocks: Print output in a table format similar to the original

This commit is contained in:
alxndrv
2025-02-18 21:52:30 +02:00
parent c2b189ff53
commit 9dd6bc1391
2 changed files with 185 additions and 32 deletions

View File

@@ -0,0 +1,7 @@
# lslocks
```
lslocks [OPTION]...
```
lists system locks

View File

@@ -3,18 +3,18 @@
// For the full copyright and license information, please view the LICENSE // For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code. // file that was distributed with this source code.
use std::{fs, str::FromStr}; use std::{fmt, fs, str::FromStr};
use clap::{crate_version, Command}; use clap::{crate_version, Command};
use uucore::error::UResult; use uucore::{error::UResult, format_usage, help_about, help_usage};
// See https://www.man7.org/linux/man-pages/man5/proc_locks.5.html for details on each field's meaning // See https://www.man7.org/linux/man-pages/man5/proc_locks.5.html for details on each field's meaning
#[derive(Debug)] #[derive(Debug)]
struct Lock { struct Lock {
ord: usize, _ord: usize,
lock_type: LockType, lock_type: LockType,
strictness: Strictness, mandatory: bool,
variant: Variant, mode: LockMode,
pid: Option<usize>, // This value is -1 for OFD locks, hence the Option pid: Option<usize>, // This value is -1 for OFD locks, hence the Option
major_minor: String, major_minor: String,
inode: usize, inode: usize,
@@ -22,6 +22,39 @@ struct Lock {
end_offset: Option<usize>, // None = lock does not have an explicit end offset and applies until the end of the file end_offset: Option<usize>, // None = lock does not have an explicit end offset and applies until the end of the file
} }
impl Lock {
fn get_value(&self, col: &Column) -> String {
match col {
Column::Command => resolve_command(self).unwrap_or("<unknown>".to_string()),
Column::Pid => self
.pid
.map(|pid| pid.to_string())
.unwrap_or("-".to_string()),
Column::Type => self.lock_type.to_string(),
Column::Size => todo!(),
Column::Inode => self.inode.to_string(),
Column::MajMin => self.major_minor.clone(),
Column::Mode => self.mode.to_string(),
Column::Mandatory => {
if self.mandatory {
"1".to_string()
} else {
"0".to_string()
}
}
Column::Start => self.start_offset.to_string(),
// TODO: In the case of EOF end_offset, we should actually resolve the actual file size and display that as the end offset
Column::End => self
.end_offset
.map(|offset| offset.to_string())
.unwrap_or("EOF".to_string()),
Column::Path => todo!(), // TODO: Resolve filepath of the lock target
Column::Blocker => todo!(), // TODO: Check if lock is blocker (and by what)
Column::Holders => todo!(), // TODO: Resolve all holders of the lock (this would also let us display a process/PID for OFD locks)
}
}
}
impl FromStr for Lock { impl FromStr for Lock {
type Err = (); type Err = ();
fn from_str(input: &str) -> Result<Self, Self::Err> { fn from_str(input: &str) -> Result<Self, Self::Err> {
@@ -38,13 +71,17 @@ impl FromStr for Lock {
.next() .next()
.and_then(|part| LockType::from_str(part).ok()) .and_then(|part| LockType::from_str(part).ok())
.unwrap(); .unwrap();
let strictness = parts let mandatory = parts
.next() .next()
.and_then(|part| Strictness::from_str(part).ok()) .map(|part| match part {
"MANDATORY" => true,
"ADVISORY" => false,
_ => panic!("Unrecognized value in lock line: {}", part),
})
.unwrap(); .unwrap();
let variant = parts let mode = parts
.next() .next()
.and_then(|part| Variant::from_str(part).ok()) .and_then(|part| LockMode::from_str(part).ok())
.unwrap(); .unwrap();
let pid: Option<usize> = parts.next().and_then(|pid_str| match pid_str { let pid: Option<usize> = parts.next().and_then(|pid_str| match pid_str {
"-1" => None, "-1" => None,
@@ -69,10 +106,10 @@ impl FromStr for Lock {
}); });
Ok(Self { Ok(Self {
ord, _ord: ord,
lock_type, lock_type,
strictness, mandatory,
variant, mode,
pid, pid,
major_minor, major_minor,
inode, inode,
@@ -83,6 +120,7 @@ impl FromStr for Lock {
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
#[allow(clippy::upper_case_acronyms)]
enum LockType { enum LockType {
FLOCK, // BSD file lock FLOCK, // BSD file lock
OFDLCK, // Open file descriptor OFDLCK, // Open file descriptor
@@ -101,30 +139,23 @@ impl FromStr for LockType {
} }
} }
#[derive(Debug)] impl fmt::Display for LockType {
enum Strictness { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Advisory, match self {
Mandatory, LockType::FLOCK => write!(f, "FLOCK"),
} LockType::OFDLCK => write!(f, "OFDLCK"),
LockType::POSIX => write!(f, "POSIX"),
impl FromStr for Strictness {
type Err = ();
fn from_str(input: &str) -> Result<Self, Self::Err> {
match input {
"ADVISORY" => Ok(Self::Advisory),
"MANDATORY" => Ok(Self::Mandatory),
_ => Err(()),
} }
} }
} }
#[derive(Debug)] #[derive(Debug)]
enum Variant { enum LockMode {
Read, Read,
Write, Write,
} }
impl FromStr for Variant { impl FromStr for LockMode {
type Err = (); type Err = ();
fn from_str(input: &str) -> Result<Self, Self::Err> { fn from_str(input: &str) -> Result<Self, Self::Err> {
match input { match input {
@@ -135,8 +166,122 @@ impl FromStr for Variant {
} }
} }
impl fmt::Display for LockMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LockMode::Write => write!(f, "WRITE"),
LockMode::Read => write!(f, "READ"),
}
}
}
// All of the columns that need to be supported in the final version
#[derive(Clone)]
#[allow(dead_code)]
enum Column {
Command,
Pid,
Type,
Size,
Inode,
MajMin,
Mode,
Mandatory,
Start,
End,
Path,
Blocker,
Holders,
}
impl Column {
fn header_text(&self) -> &'static str {
match self {
Self::Command => "COMMAND",
Self::Pid => "PID",
Self::Type => "TYPE",
Self::Size => "SIZE",
Self::Inode => "INODE",
Self::MajMin => "MAJ:MIN",
Self::Mode => "MODE",
Self::Mandatory => "M",
Self::Start => "START",
Self::End => "END",
Self::Path => "PATH",
Self::Blocker => "BLOCKER",
Self::Holders => "HOLDERS",
}
}
}
fn resolve_command(lock: &Lock) -> Option<String> {
if let Some(pid) = lock.pid {
return fs::read_to_string(format!("/proc/{}/comm", pid))
.map(|content| content.trim().to_string())
.ok();
}
// File descriptor locks don't have a real notion of an "owner process", since it can be shared by multiple processes.
// The original `lslocks` goes through `/proc/<pid>/fdinfo/*` to find *any* of the processes that reference the same device/inode combo from the lock
// We don't implement this behaviour yet, and just show "unknown" for the locks which don't have a clear owner
None
}
const DEFAULT_COLS: &[Column] = &[
Column::Command,
Column::Pid,
Column::Type,
//TODO: Implement Column::Size here
Column::Mode,
Column::Mandatory,
Column::Start,
Column::End,
//TODO: Implements Column::Path here
];
struct OutputOptions {
cols: Vec<Column>,
}
fn print_output(locks: Vec<Lock>, output_opts: OutputOptions) {
let mut column_widths: Vec<_> = output_opts
.cols
.iter()
.map(|col| col.header_text().len())
.collect();
for lock in &locks {
for (i, col) in output_opts.cols.iter().enumerate() {
column_widths[i] = column_widths[i].max(lock.get_value(col).len());
}
}
let headers: Vec<_> = output_opts
.cols
.iter()
.enumerate()
.map(|(i, col)| format!("{:<width$}", col.header_text(), width = column_widths[i]))
.collect();
println!("{}", headers.join(" "));
for lock in &locks {
let values: Vec<_> = output_opts
.cols
.iter()
.enumerate()
.map(|(i, col)| format!("{:<width$}", lock.get_value(col), width = column_widths[i]))
.collect();
println!("{}", values.join(" "));
}
}
#[uucore::main] #[uucore::main]
pub fn uumain(_args: impl uucore::Args) -> UResult<()> { pub fn uumain(_args: impl uucore::Args) -> UResult<()> {
let output_opts = OutputOptions {
cols: Vec::from(DEFAULT_COLS),
};
let locks: Vec<_> = match fs::read_to_string("/proc/locks") { let locks: Vec<_> = match fs::read_to_string("/proc/locks") {
Ok(content) => content Ok(content) => content
.lines() .lines()
@@ -145,16 +290,17 @@ pub fn uumain(_args: impl uucore::Args) -> UResult<()> {
Err(e) => panic!("Could not read /proc/locks: {}", e), Err(e) => panic!("Could not read /proc/locks: {}", e),
}; };
for lock in locks { print_output(locks, output_opts);
println!("{:?}", lock);
}
Ok(()) Ok(())
} }
const ABOUT: &str = help_about!("lslocks.md");
const USAGE: &str = help_usage!("lslocks.md");
pub fn uu_app() -> Command { pub fn uu_app() -> Command {
Command::new(uucore::util_name()) Command::new(uucore::util_name())
.version(crate_version!()) .version(crate_version!())
//.about(ABOUT) .about(ABOUT)
//.override_usage(format_usage(USAGE)) .override_usage(format_usage(USAGE))
.infer_long_args(true) .infer_long_args(true)
} }