From c2571cb6fac365151a48c42342b1baf40c47b1a1 Mon Sep 17 00:00:00 2001 From: alxndrv <> Date: Tue, 18 Feb 2025 16:18:49 +0200 Subject: [PATCH 1/5] `lslocks`: Set up `lslocks` entry point --- Cargo.lock | 11 +++++++++++ Cargo.toml | 5 +++++ src/uu/lslocks/Cargo.toml | 17 +++++++++++++++++ src/uu/lslocks/src/lslocks.rs | 21 +++++++++++++++++++++ src/uu/lslocks/src/main.rs | 1 + 5 files changed, 55 insertions(+) create mode 100644 src/uu/lslocks/Cargo.toml create mode 100644 src/uu/lslocks/src/lslocks.rs create mode 100644 src/uu/lslocks/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 87b3096..59bafca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -984,6 +984,7 @@ dependencies = [ "uu_fsfreeze", "uu_last", "uu_lscpu", + "uu_lslocks", "uu_lsmem", "uu_mountpoint", "uu_rev", @@ -1056,6 +1057,16 @@ dependencies = [ "uucore", ] +[[package]] +name = "uu_lslocks" +version = "0.0.1" +dependencies = [ + "clap", + "serde", + "serde_json", + "uucore", +] + [[package]] name = "uu_lsmem" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 3c625d7..5ca63cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,12 +32,16 @@ feat_common_core = [ "fsfreeze", "last", "lscpu", + "lslocks", "lsmem", "mountpoint", "rev", "setsid", ] +[workspace] +members = ["src/uu/lslocks"] + [workspace.dependencies] clap = { version = "4.4", features = ["wrap_help", "cargo"] } clap_complete = "4.4" @@ -76,6 +80,7 @@ dmesg = { optional = true, version = "0.0.1", package = "uu_dmesg", path = "src/ fsfreeze = { optional = true, version = "0.0.1", package = "uu_fsfreeze", path = "src/uu/fsfreeze" } last = { optional = true, version = "0.0.1", package = "uu_last", path = "src/uu/last" } lscpu = { optional = true, version = "0.0.1", package = "uu_lscpu", path = "src/uu/lscpu" } +lslocks = { optional = true, version = "0.0.1", package = "uu_lslocks", path = "src/uu/lslocks" } lsmem = { optional = true, version = "0.0.1", package = "uu_lsmem", path = "src/uu/lsmem" } mountpoint = { optional = true, version = "0.0.1", package = "uu_mountpoint", path = "src/uu/mountpoint" } rev = { optional = true, version = "0.0.1", package = "uu_rev", path = "src/uu/rev" } diff --git a/src/uu/lslocks/Cargo.toml b/src/uu/lslocks/Cargo.toml new file mode 100644 index 0000000..4d2a756 --- /dev/null +++ b/src/uu/lslocks/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "uu_lslocks" +version = "0.0.1" +edition = "2021" + +[lib] +path = "src/lslocks.rs" + +[[bin]] +name = "lslocks" +path = "src/main.rs" + +[dependencies] +uucore = { workspace = true } +clap = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } diff --git a/src/uu/lslocks/src/lslocks.rs b/src/uu/lslocks/src/lslocks.rs new file mode 100644 index 0000000..716d745 --- /dev/null +++ b/src/uu/lslocks/src/lslocks.rs @@ -0,0 +1,21 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use clap::{crate_version, Command}; +use uucore::error::UResult; + +#[uucore::main] +pub fn uumain(_args: impl uucore::Args) -> UResult<()> { + println!("lslocks: Hello world"); + Ok(()) +} + +pub fn uu_app() -> Command { + Command::new(uucore::util_name()) + .version(crate_version!()) + //.about(ABOUT) + //.override_usage(format_usage(USAGE)) + .infer_long_args(true) +} diff --git a/src/uu/lslocks/src/main.rs b/src/uu/lslocks/src/main.rs new file mode 100644 index 0000000..0adaa21 --- /dev/null +++ b/src/uu/lslocks/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_lslocks); From c2b189ff53fa08db7550db188fc98f199a7921e5 Mon Sep 17 00:00:00 2001 From: alxndrv <> Date: Tue, 18 Feb 2025 19:19:39 +0200 Subject: [PATCH 2/5] `lslocks`: Implement parsing of /proc/locks --- src/uu/lslocks/src/lslocks.rs | 141 +++++++++++++++++++++++++++++++++- 1 file changed, 140 insertions(+), 1 deletion(-) diff --git a/src/uu/lslocks/src/lslocks.rs b/src/uu/lslocks/src/lslocks.rs index 716d745..a2e35a3 100644 --- a/src/uu/lslocks/src/lslocks.rs +++ b/src/uu/lslocks/src/lslocks.rs @@ -3,12 +3,151 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +use std::{fs, str::FromStr}; + use clap::{crate_version, Command}; use uucore::error::UResult; +// See https://www.man7.org/linux/man-pages/man5/proc_locks.5.html for details on each field's meaning +#[derive(Debug)] +struct Lock { + ord: usize, + lock_type: LockType, + strictness: Strictness, + variant: Variant, + pid: Option, // This value is -1 for OFD locks, hence the Option + major_minor: String, + inode: usize, + start_offset: usize, // Byte offset to start of lock + end_offset: Option, // None = lock does not have an explicit end offset and applies until the end of the file +} + +impl FromStr for Lock { + type Err = (); + fn from_str(input: &str) -> Result { + let mut parts = input.split_whitespace(); + + // Ordinal position comes in the form of `:`, so we need to strip away the `:` + let ord = parts + .next() + .and_then(|s| s.strip_suffix(":")) + .unwrap() + .parse::() + .unwrap(); + let lock_type = parts + .next() + .and_then(|part| LockType::from_str(part).ok()) + .unwrap(); + let strictness = parts + .next() + .and_then(|part| Strictness::from_str(part).ok()) + .unwrap(); + let variant = parts + .next() + .and_then(|part| Variant::from_str(part).ok()) + .unwrap(); + let pid: Option = parts.next().and_then(|pid_str| match pid_str { + "-1" => None, + other => other.parse::().ok(), + }); + + if lock_type == LockType::OFDLCK && pid.is_some() { + println!("Unexpected PID value on OFD lock: '{}'", input); + return Err(()); + }; + + // This field has a format of MAJOR:MINOR:INODE + let maj_min_inode: Vec<_> = parts.next().unwrap().split(":").collect(); + assert_eq!(maj_min_inode.len(), 3); + let major_minor = [maj_min_inode[0], maj_min_inode[1]].join(":"); + let inode = maj_min_inode[2].parse::().unwrap(); + + let start_offset = parts.next().unwrap().parse::().unwrap(); + let end_offset: Option = parts.next().and_then(|offset_str| match offset_str { + "EOF" => None, + other => other.parse::().ok(), + }); + + Ok(Self { + ord, + lock_type, + strictness, + variant, + pid, + major_minor, + inode, + start_offset, + end_offset, + }) + } +} + +#[derive(Debug, PartialEq, Eq)] +enum LockType { + FLOCK, // BSD file lock + OFDLCK, // Open file descriptor + POSIX, // POSIX byte-range lock +} + +impl FromStr for LockType { + type Err = (); + fn from_str(input: &str) -> Result { + match input { + "FLOCK" => Ok(Self::FLOCK), + "OFDLCK" => Ok(Self::OFDLCK), + "POSIX" => Ok(Self::POSIX), + _ => Err(()), + } + } +} + +#[derive(Debug)] +enum Strictness { + Advisory, + Mandatory, +} + +impl FromStr for Strictness { + type Err = (); + fn from_str(input: &str) -> Result { + match input { + "ADVISORY" => Ok(Self::Advisory), + "MANDATORY" => Ok(Self::Mandatory), + _ => Err(()), + } + } +} + +#[derive(Debug)] +enum Variant { + Read, + Write, +} + +impl FromStr for Variant { + type Err = (); + fn from_str(input: &str) -> Result { + match input { + "WRITE" => Ok(Self::Write), + "READ" => Ok(Self::Read), + _ => Err(()), + } + } +} + #[uucore::main] pub fn uumain(_args: impl uucore::Args) -> UResult<()> { - println!("lslocks: Hello world"); + let locks: Vec<_> = match fs::read_to_string("/proc/locks") { + Ok(content) => content + .lines() + .map(|line| Lock::from_str(line).unwrap()) + .collect(), + Err(e) => panic!("Could not read /proc/locks: {}", e), + }; + + for lock in locks { + println!("{:?}", lock); + } Ok(()) } From 9dd6bc139168b1628156a8b13aae111019757d17 Mon Sep 17 00:00:00 2001 From: alxndrv <> Date: Tue, 18 Feb 2025 21:52:30 +0200 Subject: [PATCH 3/5] `lslocks`: Print output in a table format similar to the original --- src/uu/lslocks/lslocks.md | 7 ++ src/uu/lslocks/src/lslocks.rs | 210 ++++++++++++++++++++++++++++------ 2 files changed, 185 insertions(+), 32 deletions(-) create mode 100644 src/uu/lslocks/lslocks.md diff --git a/src/uu/lslocks/lslocks.md b/src/uu/lslocks/lslocks.md new file mode 100644 index 0000000..8982646 --- /dev/null +++ b/src/uu/lslocks/lslocks.md @@ -0,0 +1,7 @@ +# lslocks + +``` +lslocks [OPTION]... +``` + +lists system locks diff --git a/src/uu/lslocks/src/lslocks.rs b/src/uu/lslocks/src/lslocks.rs index a2e35a3..63b03b9 100644 --- a/src/uu/lslocks/src/lslocks.rs +++ b/src/uu/lslocks/src/lslocks.rs @@ -3,18 +3,18 @@ // For the full copyright and license information, please view the LICENSE // 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 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 #[derive(Debug)] struct Lock { - ord: usize, + _ord: usize, lock_type: LockType, - strictness: Strictness, - variant: Variant, + mandatory: bool, + mode: LockMode, pid: Option, // This value is -1 for OFD locks, hence the Option major_minor: String, inode: usize, @@ -22,6 +22,39 @@ struct Lock { end_offset: Option, // 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("".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 { type Err = (); fn from_str(input: &str) -> Result { @@ -38,13 +71,17 @@ impl FromStr for Lock { .next() .and_then(|part| LockType::from_str(part).ok()) .unwrap(); - let strictness = parts + let mandatory = parts .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(); - let variant = parts + let mode = parts .next() - .and_then(|part| Variant::from_str(part).ok()) + .and_then(|part| LockMode::from_str(part).ok()) .unwrap(); let pid: Option = parts.next().and_then(|pid_str| match pid_str { "-1" => None, @@ -69,10 +106,10 @@ impl FromStr for Lock { }); Ok(Self { - ord, + _ord: ord, lock_type, - strictness, - variant, + mandatory, + mode, pid, major_minor, inode, @@ -83,6 +120,7 @@ impl FromStr for Lock { } #[derive(Debug, PartialEq, Eq)] +#[allow(clippy::upper_case_acronyms)] enum LockType { FLOCK, // BSD file lock OFDLCK, // Open file descriptor @@ -101,30 +139,23 @@ impl FromStr for LockType { } } -#[derive(Debug)] -enum Strictness { - Advisory, - Mandatory, -} - -impl FromStr for Strictness { - type Err = (); - fn from_str(input: &str) -> Result { - match input { - "ADVISORY" => Ok(Self::Advisory), - "MANDATORY" => Ok(Self::Mandatory), - _ => Err(()), +impl fmt::Display for LockType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LockType::FLOCK => write!(f, "FLOCK"), + LockType::OFDLCK => write!(f, "OFDLCK"), + LockType::POSIX => write!(f, "POSIX"), } } } #[derive(Debug)] -enum Variant { +enum LockMode { Read, Write, } -impl FromStr for Variant { +impl FromStr for LockMode { type Err = (); fn from_str(input: &str) -> Result { 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 { + 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//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, +} + +fn print_output(locks: Vec, 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!("{: = output_opts + .cols + .iter() + .enumerate() + .map(|(i, col)| format!("{: UResult<()> { + let output_opts = OutputOptions { + cols: Vec::from(DEFAULT_COLS), + }; + let locks: Vec<_> = match fs::read_to_string("/proc/locks") { Ok(content) => content .lines() @@ -145,16 +290,17 @@ pub fn uumain(_args: impl uucore::Args) -> UResult<()> { Err(e) => panic!("Could not read /proc/locks: {}", e), }; - for lock in locks { - println!("{:?}", lock); - } + print_output(locks, output_opts); Ok(()) } +const ABOUT: &str = help_about!("lslocks.md"); +const USAGE: &str = help_usage!("lslocks.md"); + pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(crate_version!()) - //.about(ABOUT) - //.override_usage(format_usage(USAGE)) + .about(ABOUT) + .override_usage(format_usage(USAGE)) .infer_long_args(true) } From ee14c4454028d507117df64db3d48cd51b58077d Mon Sep 17 00:00:00 2001 From: alxndrv <> Date: Fri, 21 Feb 2025 18:20:53 +0200 Subject: [PATCH 4/5] `lslocks`: Small code cleanup --- Cargo.toml | 3 --- src/uu/lslocks/src/lslocks.rs | 14 +++++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5ca63cf..256df54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,9 +39,6 @@ feat_common_core = [ "setsid", ] -[workspace] -members = ["src/uu/lslocks"] - [workspace.dependencies] clap = { version = "4.4", features = ["wrap_help", "cargo"] } clap_complete = "4.4" diff --git a/src/uu/lslocks/src/lslocks.rs b/src/uu/lslocks/src/lslocks.rs index 63b03b9..129d6cb 100644 --- a/src/uu/lslocks/src/lslocks.rs +++ b/src/uu/lslocks/src/lslocks.rs @@ -33,7 +33,7 @@ impl Lock { Column::Type => self.lock_type.to_string(), Column::Size => todo!(), Column::Inode => self.inode.to_string(), - Column::MajMin => self.major_minor.clone(), + Column::MajorMinor => self.major_minor.clone(), Column::Mode => self.mode.to_string(), Column::Mandatory => { if self.mandatory { @@ -94,10 +94,10 @@ impl FromStr for Lock { }; // This field has a format of MAJOR:MINOR:INODE - let maj_min_inode: Vec<_> = parts.next().unwrap().split(":").collect(); - assert_eq!(maj_min_inode.len(), 3); - let major_minor = [maj_min_inode[0], maj_min_inode[1]].join(":"); - let inode = maj_min_inode[2].parse::().unwrap(); + let major_minor_inode: Vec<_> = parts.next().unwrap().split(":").collect(); + assert_eq!(major_minor_inode.len(), 3); + let major_minor = [major_minor_inode[0], major_minor_inode[1]].join(":"); + let inode = major_minor_inode[2].parse::().unwrap(); let start_offset = parts.next().unwrap().parse::().unwrap(); let end_offset: Option = parts.next().and_then(|offset_str| match offset_str { @@ -184,7 +184,7 @@ enum Column { Type, Size, Inode, - MajMin, + MajorMinor, Mode, Mandatory, Start, @@ -202,7 +202,7 @@ impl Column { Self::Type => "TYPE", Self::Size => "SIZE", Self::Inode => "INODE", - Self::MajMin => "MAJ:MIN", + Self::MajorMinor => "MAJ:MIN", Self::Mode => "MODE", Self::Mandatory => "M", Self::Start => "START", From 7e0fde94dd9373d665dce62f285b868222ded814 Mon Sep 17 00:00:00 2001 From: alxndrv <> Date: Fri, 21 Feb 2025 22:23:57 +0200 Subject: [PATCH 5/5] `lslocks`: Add test skeleton --- tests/by-util/test_lslocks.rs | 22 ++++++++++++++++++++++ tests/tests.rs | 4 ++++ 2 files changed, 26 insertions(+) create mode 100644 tests/by-util/test_lslocks.rs diff --git a/tests/by-util/test_lslocks.rs b/tests/by-util/test_lslocks.rs new file mode 100644 index 0000000..b56dcf8 --- /dev/null +++ b/tests/by-util/test_lslocks.rs @@ -0,0 +1,22 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::common::util::TestScenario; + +#[test] +#[cfg(target_os = "linux")] +fn test_column_headers() { + let res = new_ucmd!().succeeds(); + let stdout = res.no_stderr().stdout_str(); + + let header_line = stdout.lines().next().unwrap(); + let cols: Vec<_> = header_line.split_whitespace().collect(); + + assert_eq!(cols.len(), 7); + assert_eq!( + cols, + vec!["COMMAND", "PID", "TYPE", "MODE", "M", "START", "END"] + ); +} diff --git a/tests/tests.rs b/tests/tests.rs index 23c3f90..ae15cde 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -13,6 +13,10 @@ mod test_lscpu; #[path = "by-util/test_lsmem.rs"] mod test_lsmem; +#[cfg(feature = "lslocks")] +#[path = "by-util/test_lslocks.rs"] +mod test_lslocks; + #[cfg(feature = "mountpoint")] #[path = "by-util/test_mountpoint.rs"] mod test_mountpoint;