Merge pull request #260 from mdcssw/main

Implemented lslocks.
This commit is contained in:
Sylvestre Ledru
2025-04-22 09:52:32 +02:00
committed by GitHub
14 changed files with 1520 additions and 329 deletions

124
Cargo.lock generated
View File

@@ -125,9 +125,9 @@ checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "cc"
version = "1.2.17"
version = "1.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362"
dependencies = [
"shlex",
]
@@ -270,9 +270,9 @@ dependencies = [
[[package]]
name = "deranged"
version = "0.4.1"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
dependencies = [
"powerfmt",
]
@@ -381,9 +381,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "iana-time-zone"
version = "0.1.62"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
@@ -391,7 +391,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.52.0",
"windows-core 0.61.0",
]
[[package]]
@@ -405,9 +405,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.8.0"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown",
@@ -457,7 +457,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.52.6",
]
[[package]]
name = "libmount-sys"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef1e0e42bbd34fa3f8fe6c9ffeb27906263d59d680825e4e71a5dacd7613e90a"
dependencies = [
"bindgen",
"cc",
"walkdir",
]
[[package]]
@@ -572,9 +583,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "objc2-core-foundation"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925"
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
dependencies = [
"bitflags",
]
@@ -679,9 +690,9 @@ dependencies = [
[[package]]
name = "prettyplease"
version = "0.2.31"
version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb"
checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6"
dependencies = [
"proc-macro2",
"syn",
@@ -689,9 +700,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.94"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
@@ -848,9 +859,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "1.0.3"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96"
checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf"
dependencies = [
"bitflags",
"errno",
@@ -945,9 +956,9 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
[[package]]
name = "socket2"
version = "0.5.8"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
dependencies = [
"libc",
"windows-sys 0.52.0",
@@ -998,7 +1009,7 @@ dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix 1.0.3",
"rustix 1.0.5",
"windows-sys 0.59.0",
]
@@ -1008,7 +1019,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed"
dependencies = [
"rustix 1.0.3",
"rustix 1.0.5",
"windows-sys 0.59.0",
]
@@ -1240,8 +1251,9 @@ name = "uu_lslocks"
version = "0.0.1"
dependencies = [
"clap",
"serde",
"serde_json",
"libc",
"libmount-sys",
"smartcols-sys",
"uucore",
]
@@ -1460,7 +1472,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1481,23 +1493,27 @@ dependencies = [
[[package]]
name = "windows-core"
version = "0.52.0"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
dependencies = [
"windows-implement 0.57.0",
"windows-interface 0.57.0",
"windows-result 0.1.2",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.57.0"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
"windows-implement",
"windows-interface",
"windows-result",
"windows-targets 0.52.6",
"windows-implement 0.60.0",
"windows-interface 0.59.1",
"windows-link",
"windows-result 0.3.2",
"windows-strings",
]
[[package]]
@@ -1511,6 +1527,17 @@ dependencies = [
"syn",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.57.0"
@@ -1522,6 +1549,17 @@ dependencies = [
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.1.1"
@@ -1537,6 +1575,24 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
@@ -1701,7 +1757,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e"
dependencies = [
"libc",
"rustix 1.0.3",
"rustix 1.0.5",
]
[[package]]

View File

@@ -51,6 +51,7 @@ clap_mangen = "0.2"
dns-lookup = "2.0.4"
errno = "0.3"
libc = "0.2.171"
libmount-sys = "0.1.1"
linux-raw-sys = { version = "0.9.0", features = ["ioctl"] }
md-5 = "0.10.6"
nix = { version = "0.29", default-features = false }

View File

@@ -1,7 +1,7 @@
[package]
name = "uu_lslocks"
name = "uu_lslocks"
version = "0.0.1"
edition = "2021"
edition = "2024"
[lib]
path = "src/lslocks.rs"
@@ -11,7 +11,8 @@ name = "lslocks"
path = "src/main.rs"
[dependencies]
uucore = { workspace = true }
clap = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
uucore = { workspace = true }
clap = { workspace = true }
libc = { workspace = true }
smartcols-sys = { workspace = true }
libmount-sys = { workspace = true }

View File

@@ -0,0 +1,69 @@
{
"lslocks-columns": [
{
"holder": "COMMAND",
"type": "<string>",
"description": "command of the process holding the lock"
},
{
"holder": "PID",
"type": "<integer>",
"description": "PID of the process holding the lock"
},
{
"holder": "TYPE",
"type": "<string>",
"description": "kind of lock"
},
{
"holder": "SIZE",
"type": "<string|number>",
"description": "size of the lock, use <number> if --bytes is given"
},
{
"holder": "INODE",
"type": "<integer>",
"description": "inode number"
},
{
"holder": "MAJ:MIN",
"type": "<string>",
"description": "major:minor device number"
},
{
"holder": "MODE",
"type": "<string>",
"description": "lock access mode"
},
{
"holder": "M",
"type": "<boolean>",
"description": "mandatory state of the lock: 0 (none), 1 (set)"
},
{
"holder": "START",
"type": "<integer>",
"description": "relative byte offset of the lock"
},
{
"holder": "END",
"type": "<integer>",
"description": "ending offset of the lock"
},
{
"holder": "PATH",
"type": "<string>",
"description": "path of the locked file"
},
{
"holder": "BLOCKER",
"type": "<integer>",
"description": "PID of the process blocking the lock"
},
{
"holder": "HOLDERS",
"type": "<string>",
"description": "holders of the lock"
}
]
}

View File

@@ -0,0 +1,13 @@
COMMAND <string> command\x20of\x20the\x20process\x20holding\x20the\x20lock
PID <integer> PID\x20of\x20the\x20process\x20holding\x20the\x20lock
TYPE <string> kind\x20of\x20lock
SIZE <string|number> size\x20of\x20the\x20lock,\x20use\x20<number>\x20if\x20--bytes\x20is\x20given
INODE <integer> inode\x20number
MAJ:MIN <string> major:minor\x20device\x20number
MODE <string> lock\x20access\x20mode
M <boolean> mandatory\x20state\x20of\x20the\x20lock:\x200\x20(none),\x201\x20(set)
START <integer> relative\x20byte\x20offset\x20of\x20the\x20lock
END <integer> ending\x20offset\x20of\x20the\x20lock
PATH <string> path\x20of\x20the\x20locked\x20file
BLOCKER <integer> PID\x20of\x20the\x20process\x20blocking\x20the\x20lock
HOLDERS <string> holders\x20of\x20the\x20lock

View File

@@ -0,0 +1,13 @@
COMMAND <string> command of the process holding the lock
PID <integer> PID of the process holding the lock
TYPE <string> kind of lock
SIZE <string|number> size of the lock, use <number> if --bytes is given
INODE <integer> inode number
MAJ:MIN <string> major:minor device number
MODE <string> lock access mode
M <boolean> mandatory state of the lock: 0 (none), 1 (set)
START <integer> relative byte offset of the lock
END <integer> ending offset of the lock
PATH <string> path of the locked file
BLOCKER <integer> PID of the process blocking the lock
HOLDERS <string> holders of the lock

View File

@@ -1,7 +1,10 @@
# lslocks
```
lslocks [OPTION]...
lslocks [-b|--bytes] [-i|--noinaccessible] [-J|--json|-r|--raw] [-n|--noheadings] [-o list|--output list] [--output-all] [-p pid|--pid pid] [-u|--notruncate]
lslocks {-H|--list-columns} [-J|--json|-r|--raw]
lslocks {-V|--version}
lslocks {-h|--help}
```
lists system locks
list local system locks.

View File

@@ -0,0 +1,122 @@
// 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 std::ffi::{CStr, c_uint};
use std::str::FromStr;
use smartcols_sys::{
SCOLS_FL_RIGHT, SCOLS_FL_TRUNC, SCOLS_FL_WRAP, SCOLS_JSON_ARRAY_STRING, SCOLS_JSON_BOOLEAN,
SCOLS_JSON_NUMBER, SCOLS_JSON_STRING,
};
use crate::errors::LsLocksError;
#[derive(Debug, Copy, Clone)]
pub(crate) struct ColumnInfo {
pub(crate) id: &'static CStr,
pub(crate) width_hint: f64,
pub(crate) flags: c_uint,
pub(crate) default_json_type: c_uint,
}
impl ColumnInfo {
const fn new(
id: &'static CStr,
width_hint: f64,
flags: c_uint,
default_json_type: c_uint,
) -> Self {
Self {
id,
width_hint,
flags,
default_json_type,
}
}
pub(crate) fn json_type(&self, in_bytes: bool) -> c_uint {
if in_bytes && self.id.to_bytes() == b"SIZE" {
SCOLS_JSON_NUMBER
} else {
self.default_json_type
}
}
}
pub(crate) static COLUMN_INFOS: [ColumnInfo; 13] = [
ColumnInfo::new(c"COMMAND", 15.0, 0, SCOLS_JSON_STRING),
ColumnInfo::new(c"PID", 5.0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER),
ColumnInfo::new(c"TYPE", 5.0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING),
ColumnInfo::new(c"SIZE", 4.0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING),
ColumnInfo::new(c"INODE", 5.0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER),
ColumnInfo::new(c"MAJ:MIN", 6.0, 0, SCOLS_JSON_STRING),
ColumnInfo::new(c"MODE", 5.0, 0, SCOLS_JSON_STRING),
ColumnInfo::new(c"M", 1.0, 0, SCOLS_JSON_BOOLEAN),
ColumnInfo::new(c"START", 10.0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER),
ColumnInfo::new(c"END", 10.0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER),
ColumnInfo::new(c"PATH", 0.0, SCOLS_FL_TRUNC, SCOLS_JSON_STRING),
ColumnInfo::new(c"BLOCKER", 0.0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER),
ColumnInfo::new(c"HOLDERS", 0.0, SCOLS_FL_WRAP, SCOLS_JSON_ARRAY_STRING),
];
pub(crate) static ALL: [&str; 13] = [
"COMMAND", "PID", "TYPE", "SIZE", "INODE", "MAJ:MIN", "MODE", "M", "START", "END", "PATH",
"BLOCKER", "HOLDERS",
];
pub(crate) static DEFAULT: [&str; 9] = [
"COMMAND", "PID", "TYPE", "SIZE", "MODE", "M", "START", "END", "PATH",
];
#[derive(Debug, Clone)]
pub(crate) struct OutputColumns {
pub(crate) append: bool,
pub(crate) list: Vec<&'static ColumnInfo>,
}
impl Default for OutputColumns {
fn default() -> Self {
Self {
append: true,
list: Vec::default(),
}
}
}
impl FromStr for OutputColumns {
type Err = LsLocksError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let suffix = s.strip_prefix('+');
let append = suffix.is_some();
let list: Vec<_> = suffix
.unwrap_or(s)
.split(',')
.map(|name| {
COLUMN_INFOS
.iter()
.find(|&column| column.id.to_str().unwrap() == name)
.ok_or_else(|| LsLocksError::InvalidColumnName(name.into()))
})
.collect::<Result<_, _>>()?;
if list.is_empty() {
Err(LsLocksError::InvalidColumnSequence(s.into()))
} else {
Ok(Self { append, list })
}
}
}
impl From<&'_ clap::ArgMatches> for OutputColumns {
fn from(args: &clap::ArgMatches) -> Self {
args.get_one::<Self>(crate::options::OUTPUT)
.map_or_else(Self::default, |columns| Self {
append: columns.append,
list: columns.list.clone(),
})
}
}

View File

@@ -0,0 +1,136 @@
// 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 std::borrow::Cow;
use std::ffi::{CStr, CString};
use std::sync::atomic::AtomicPtr;
use std::{io, ptr};
use crate::errors::LsLocksError;
use crate::utils::LockInfo;
fn decimal_point() -> &'static str {
use std::sync::atomic::Ordering;
static DEFAULT: &CStr = c".";
static VALUE: AtomicPtr<u8> = AtomicPtr::new(ptr::null_mut());
let mut decimal_point = VALUE.load(Ordering::Acquire);
if decimal_point.is_null() {
decimal_point = unsafe { libc::localeconv().as_ref() }
.and_then(|lc| (!lc.decimal_point.is_null()).then_some(lc.decimal_point))
.unwrap_or(DEFAULT.as_ptr().cast_mut())
.cast();
match VALUE.compare_exchange(
ptr::null_mut(),
decimal_point,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_previous_value) => {}
Err(previous_value) => decimal_point = previous_value,
}
}
unsafe { CStr::from_ptr(decimal_point.cast()) }
.to_str()
.unwrap()
}
// returns exponent (2^x=n) in range KiB..EiB (2^10..2^60).
fn bytes_exponent(bytes: u64) -> u64 {
for shift in (10..=60).step_by(10) {
if bytes < (1 << shift) {
return shift - 10;
}
}
60
}
fn size_to_human_string(bytes: u64) -> String {
static LETTERS: [char; 7] = ['B', 'K', 'M', 'G', 'T', 'P', 'E'];
let exp = bytes_exponent(bytes);
let unit = LETTERS[if exp == 0 { 0 } else { (exp / 10) as usize }];
let mut decimal = if exp == 0 { bytes } else { bytes / (1 << exp) };
let mut fractional = if exp == 0 { 0 } else { bytes % (1 << exp) };
if fractional != 0 {
fractional = if fractional >= (u64::MAX / 1000) {
((fractional / 1024) * 1000) / (1 << (exp - 10))
} else {
(fractional * 1000) / (1 << exp)
};
fractional = ((fractional + 50) / 100) * 10;
if fractional == 100 {
decimal += 1;
fractional = 0;
}
}
if fractional == 0 {
format!("{decimal}{unit}")
} else {
format!("{decimal}{}{fractional:02}{unit}", decimal_point())
}
}
pub(crate) fn describe_integer<T: ToString>(n: T) -> Option<Cow<'static, CStr>> {
Some(Cow::Owned(CString::new(n.to_string()).unwrap()))
}
pub(crate) fn describe_size(size: u64, in_bytes: bool) -> Option<Cow<'static, CStr>> {
let value = if in_bytes {
size.to_string()
} else {
size_to_human_string(size)
};
Some(Cow::Owned(CString::new(value).unwrap()))
}
pub(crate) fn describe_holders(
proc_lock: &LockInfo,
pid_locks: &[LockInfo],
) -> Result<CString, LsLocksError> {
let lock_compare = move |lock: &&LockInfo| {
lock.range == proc_lock.range
&& lock.inode == proc_lock.inode
&& lock.device_id == proc_lock.device_id
&& lock.mandatory == proc_lock.mandatory
&& lock.blocked == proc_lock.blocked
&& lock.kind == proc_lock.kind
&& lock.mode == proc_lock.mode
};
let mut separator: &[u8] = &[];
let append_holder = move |mut buffer: Vec<u8>, lock: &LockInfo| {
buffer.extend(separator);
separator = b"\n";
buffer.extend(lock.process_id.to_string().into_bytes());
buffer.push(b',');
if let Some(command_line) = lock.command_name.as_deref().map(CStr::to_bytes) {
buffer.extend(command_line);
}
buffer.push(b',');
buffer.extend(lock.file_descriptor.to_string().into_bytes());
buffer
};
let buffer = pid_locks
.iter()
.filter(lock_compare)
.fold(Vec::default(), append_holder);
CString::new(buffer).map_err(|_| LsLocksError::io0("invalid data", io::ErrorKind::InvalidData))
}

View File

@@ -0,0 +1,67 @@
// This file is part of the uutils hostname package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use std::ffi::c_int;
use std::fmt;
use std::path::PathBuf;
use uucore::error::UError;
#[derive(Debug)]
pub enum LsLocksError {
InvalidColumnName(String),
InvalidColumnSequence(String),
IO0(String, std::io::Error),
IO1(String, PathBuf, std::io::Error),
}
impl LsLocksError {
pub(crate) fn io0(message: impl Into<String>, error: impl Into<std::io::Error>) -> Self {
Self::IO0(message.into(), error.into())
}
pub(crate) fn io1(
message: impl Into<String>,
path: impl Into<PathBuf>,
error: impl Into<std::io::Error>,
) -> Self {
Self::IO1(message.into(), path.into(), error.into())
}
pub(crate) fn io_from_neg_errno(
message: impl Into<String>,
result: c_int,
) -> Result<usize, LsLocksError> {
if let Ok(result) = usize::try_from(result) {
Ok(result)
} else {
let err = std::io::Error::from_raw_os_error(-result);
Err(Self::IO0(message.into(), err))
}
}
}
impl fmt::Display for LsLocksError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::IO0(message, err) => write!(f, "{message}: {err}"),
Self::IO1(message, path, err) => write!(f, "{message} '{}': {err}", path.display()),
Self::InvalidColumnName(name) => write!(f, "invalid column name: {name}"),
Self::InvalidColumnSequence(seq) => write!(f, "invalid column sequence: {seq}"),
}
}
}
impl UError for LsLocksError {
fn code(&self) -> i32 {
1
}
fn usage(&self) -> bool {
false
}
}
impl std::error::Error for LsLocksError {}

View File

@@ -3,295 +3,61 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use std::{fmt, fs, str::FromStr};
// Remove this if the tool is ported to Non-UNIX platforms.
#![cfg_attr(
not(target_os = "linux"),
allow(dead_code, non_camel_case_types, unused_imports)
)]
use clap::{crate_version, Command};
#[cfg(target_os = "linux")]
mod column;
#[cfg(target_os = "linux")]
mod display;
mod errors;
#[cfg(target_os = "linux")]
mod smartcols;
#[cfg(target_os = "linux")]
mod utils;
use std::borrow::Cow;
use std::ffi::{CStr, CString, OsStr};
use std::io::Write;
use std::path::Path;
use std::ptr;
use std::str::FromStr;
use clap::{Arg, ArgAction, ArgMatches, Command, crate_version, value_parser};
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,
lock_type: LockType,
mandatory: bool,
mode: LockMode,
pid: Option<usize>, // 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<usize>, // None = lock does not have an explicit end offset and applies until the end of the file
}
#[cfg(target_os = "linux")]
use crate::column::{ColumnInfo, OutputColumns};
#[cfg(target_os = "linux")]
use crate::display::{describe_holders, describe_integer, describe_size};
use crate::errors::LsLocksError;
#[cfg(target_os = "linux")]
use crate::smartcols::{Table, TableOperations};
#[cfg(target_os = "linux")]
use crate::utils::{
_PATH_PROC, _PATH_PROC_LOCKS, BinFileLineIter, LockInfo, entry_is_dir_or_unknown,
proc_pid_command_name,
};
#[cfg(target_os = "linux")]
use libc::pid_t;
#[cfg(not(target_os = "linux"))]
type pid_t = i32;
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::MajorMinor => 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<Self, Self::Err> {
let mut parts = input.split_whitespace();
// Ordinal position comes in the form of `<value>:`, so we need to strip away the `:`
let ord = parts
.next()
.and_then(|s| s.strip_suffix(":"))
.unwrap()
.parse::<usize>()
.unwrap();
let lock_type = parts
.next()
.and_then(|part| LockType::from_str(part).ok())
.unwrap();
let mandatory = parts
.next()
.map(|part| match part {
"MANDATORY" => true,
"ADVISORY" => false,
_ => panic!("Unrecognized value in lock line: {}", part),
})
.unwrap();
let mode = parts
.next()
.and_then(|part| LockMode::from_str(part).ok())
.unwrap();
let pid: Option<usize> = parts.next().and_then(|pid_str| match pid_str {
"-1" => None,
other => other.parse::<usize>().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 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::<usize>().unwrap();
let start_offset = parts.next().unwrap().parse::<usize>().unwrap();
let end_offset: Option<usize> = parts.next().and_then(|offset_str| match offset_str {
"EOF" => None,
other => other.parse::<usize>().ok(),
});
Ok(Self {
_ord: ord,
lock_type,
mandatory,
mode,
pid,
major_minor,
inode,
start_offset,
end_offset,
})
}
}
#[derive(Debug, PartialEq, Eq)]
#[allow(clippy::upper_case_acronyms)]
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<Self, Self::Err> {
match input {
"FLOCK" => Ok(Self::FLOCK),
"OFDLCK" => Ok(Self::OFDLCK),
"POSIX" => Ok(Self::POSIX),
_ => 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 LockMode {
Read,
Write,
}
impl FromStr for LockMode {
type Err = ();
fn from_str(input: &str) -> Result<Self, Self::Err> {
match input {
"WRITE" => Ok(Self::Write),
"READ" => Ok(Self::Read),
_ => Err(()),
}
}
}
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,
MajorMinor,
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::MajorMinor => "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]
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") {
Ok(content) => content
.lines()
.map(|line| Lock::from_str(line).unwrap())
.collect(),
Err(e) => panic!("Could not read /proc/locks: {}", e),
};
print_output(locks, output_opts);
Ok(())
mod options {
pub static BYTES: &str = "bytes";
pub static JSON: &str = "json";
pub static LIST_COLUMNS: &str = "list-columns";
pub static LIST: &str = "list";
pub static NO_HEADINGS: &str = "noheadings";
pub static NO_INACCESSIBLE: &str = "noinaccessible";
pub static NO_TRUNCATE: &str = "notruncate";
pub static OUTPUT_ALL: &str = "output-all";
pub static OUTPUT: &str = "output";
pub static PID: &str = "pid";
pub static RAW: &str = "raw";
}
const ABOUT: &str = help_about!("lslocks.md");
@@ -303,4 +69,447 @@ pub fn uu_app() -> Command {
.about(ABOUT)
.override_usage(format_usage(USAGE))
.infer_long_args(true)
.arg(
Arg::new(options::BYTES)
.short('b')
.long(options::BYTES)
.action(ArgAction::SetTrue)
.help("print SIZE in bytes rather than in human readable format"),
)
.arg(
Arg::new(options::NO_INACCESSIBLE)
.short('i')
.long(options::NO_INACCESSIBLE)
.action(ArgAction::SetTrue)
.help("ignore locks without read permissions"),
)
.arg(
Arg::new(options::JSON)
.short('J')
.long(options::JSON)
.action(ArgAction::SetTrue)
.conflicts_with(options::RAW)
.help("use JSON output format"),
)
.arg(
Arg::new(options::LIST_COLUMNS)
.short('H')
.long(options::LIST_COLUMNS)
.action(ArgAction::SetTrue)
.conflicts_with_all([
options::BYTES,
options::NO_INACCESSIBLE,
options::NO_HEADINGS,
options::OUTPUT,
options::OUTPUT_ALL,
options::PID,
options::NO_TRUNCATE,
])
.help("list the available columns"),
)
.arg(
Arg::new(options::NO_HEADINGS)
.short('n')
.long(options::NO_HEADINGS)
.action(ArgAction::SetTrue)
.help("don't print headings"),
)
.arg(
Arg::new(options::OUTPUT)
.short('o')
.long(options::OUTPUT)
.value_name(options::LIST)
.value_parser(OutputColumns::from_str)
.action(ArgAction::Set)
.help("output columns (see --list-columns)"),
)
.arg(
Arg::new(options::OUTPUT_ALL)
.long(options::OUTPUT_ALL)
.action(ArgAction::SetTrue)
.help("output all columns"),
)
.arg(
Arg::new(options::PID)
.short('p')
.long(options::PID)
.value_name(options::PID)
.value_parser(value_parser!(pid_t))
.action(ArgAction::Set)
.help("display only locks held by this process"),
)
.arg(
Arg::new(options::NO_TRUNCATE)
.short('u')
.long(options::NO_TRUNCATE)
.action(ArgAction::SetTrue)
.help("don't truncate text in columns"),
)
.arg(
Arg::new(options::RAW)
.short('r')
.long(options::RAW)
.action(ArgAction::SetTrue)
.conflicts_with(options::JSON)
.help("use the raw output format"),
)
}
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let args = uu_app().try_get_matches_from_mut(args)?;
let output_mode = OutputMode::from(&args);
if args.get_flag(options::LIST_COLUMNS) {
list_columns(output_mode)?;
return Ok(());
}
lslocks(&args, output_mode).map_err(From::from)
}
// The Linux implementation resides in `crate::column`.
#[cfg(not(target_os = "linux"))]
#[derive(Debug, Clone)]
struct OutputColumns;
#[cfg(not(target_os = "linux"))]
impl FromStr for OutputColumns {
type Err = LsLocksError;
fn from_str(_s: &str) -> Result<Self, Self::Err> {
unimplemented!()
}
}
#[cfg(target_os = "linux")]
fn lslocks(args: &ArgMatches, output_mode: OutputMode) -> Result<(), LsLocksError> {
let columns = collect_output_columns(args)?;
let in_bytes = args.get_flag(options::BYTES);
let mut table = setup_table(args, output_mode, in_bytes, &columns)?;
let no_inaccessible = args.get_flag(options::NO_INACCESSIBLE);
let pid_locks = collect_pid_locks(no_inaccessible)?;
let path = Path::new(_PATH_PROC_LOCKS);
let mut lines = BinFileLineIter::open(path)?;
let mut proc_locks = Vec::default();
while let Some(line) = lines.next_line()? {
if let Some(lock) =
LockInfo::parse(no_inaccessible, path, None, -1, c"", Some(&pid_locks), line)?
{
proc_locks.push(lock);
}
}
let target_pid = args.get_one::<pid_t>(options::PID).copied();
fill_table(
output_mode,
&columns,
target_pid,
in_bytes,
&mut table,
&pid_locks,
&proc_locks,
)?;
table.print()
}
#[cfg(not(target_os = "linux"))]
fn lslocks(_args: &ArgMatches, _output_mode: OutputMode) -> Result<(), LsLocksError> {
unimplemented!()
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
pub(crate) enum OutputMode {
None = 0,
Raw,
Json,
}
impl From<&'_ ArgMatches> for OutputMode {
fn from(args: &ArgMatches) -> Self {
if args.get_flag(options::JSON) {
Self::Json
} else if args.get_flag(options::RAW) {
Self::Raw
} else {
Self::None
}
}
}
fn list_columns(output_mode: OutputMode) -> Result<(), LsLocksError> {
let mut stdout = std::io::stdout().lock();
match output_mode {
OutputMode::None => stdout.write_all(include_bytes!("../columns.txt")),
OutputMode::Raw => stdout.write_all(include_bytes!("../columns.raw")),
OutputMode::Json => stdout.write_all(include_bytes!("../columns.json")),
}
.map_err(|err| LsLocksError::io0("stdout", err))
}
#[cfg(target_os = "linux")]
fn collect_output_columns(args: &ArgMatches) -> Result<Vec<&'static ColumnInfo>, LsLocksError> {
let columns = OutputColumns::from(args);
let columns = if columns.append {
let default_names: &[&str] = if args.get_flag(options::OUTPUT_ALL) {
&column::ALL
} else {
&column::DEFAULT
};
let mut default_columns = default_names
.iter()
.map(|&name| {
column::COLUMN_INFOS
.iter()
.find(|&column| column.id.to_str().unwrap() == name)
.ok_or_else(|| LsLocksError::InvalidColumnName(name.into()))
})
.collect::<Result<Vec<_>, _>>()?;
default_columns.extend(columns.list);
default_columns
} else {
columns.list
};
Ok(columns)
}
#[cfg(target_os = "linux")]
fn setup_table(
args: &ArgMatches,
output_mode: OutputMode,
in_bytes: bool,
columns: &[&ColumnInfo],
) -> Result<Table, LsLocksError> {
use smartcols_sys::{
SCOLS_FL_TRUNC, SCOLS_FL_WRAP, scols_wrapnl_chunksize, scols_wrapnl_nextchunk,
};
smartcols::initialize();
let mut table = Table::new()?;
if args.get_flag(options::JSON) {
table.set_name(c"locks")?;
}
if args.get_flag(options::NO_HEADINGS) {
table.enable_headings(false)?;
}
match output_mode {
OutputMode::Raw => table.enable_raw(true)?,
OutputMode::Json => table.enable_json(true)?,
OutputMode::None => {}
}
let no_truncate = args.get_flag(options::NO_TRUNCATE);
for &column_info in columns {
let mut flags = column_info.flags;
if no_truncate {
flags &= !SCOLS_FL_TRUNC;
}
let mut column = table.new_column(column_info.id, column_info.width_hint, flags)?;
if (flags & SCOLS_FL_WRAP) != 0 {
column.set_wrap_func(
Some(scols_wrapnl_chunksize),
Some(scols_wrapnl_nextchunk),
ptr::null_mut(),
)?;
column.set_safe_chars(c"\n")?;
}
if output_mode == OutputMode::Json {
column.set_json_type(column_info.json_type(in_bytes))?;
}
}
Ok(table)
}
#[cfg(target_os = "linux")]
fn collect_pid_locks(no_inaccessible: bool) -> Result<Vec<LockInfo>, LsLocksError> {
let path = Path::new(_PATH_PROC);
let dir_entries = path
.read_dir()
.map_err(|err| LsLocksError::io1("reading directory", path, err))?;
let mut pid_locks = Vec::default();
for entry in dir_entries {
let entry = entry.map_err(|err| LsLocksError::io1("reading directory entry", path, err))?;
let Some(process_id) = entry.file_name().to_str().and_then(|s| s.parse().ok()) else {
continue;
};
let path = entry.path();
let file_type = entry
.file_type()
.map_err(|err| LsLocksError::io1("reading directory entry type", &path, err))?;
if !entry_is_dir_or_unknown(&file_type) {
continue;
}
let command_name = proc_pid_command_name(&path);
// We should report the error instead of silently continuing.
let _ignored = append_pid_locks(
no_inaccessible,
&path.join("fdinfo"),
process_id,
command_name.as_deref().unwrap_or(c""),
&mut pid_locks,
);
}
Ok(pid_locks)
}
#[cfg(target_os = "linux")]
fn append_pid_locks(
no_inaccessible: bool,
path: &Path,
process_id: pid_t,
command_name: &CStr,
pid_locks: &mut Vec<LockInfo>,
) -> Result<(), LsLocksError> {
let Ok(dir_entries) = path.read_dir() else {
//eprintln!("{}", LsLocksError::io1("reading directory", &path, err));
return Ok(()); // We should report the error instead of silently continuing.
};
for entry in dir_entries {
let entry = entry.map_err(|err| LsLocksError::io1("reading directory entry", path, err))?;
let Some(file_descriptor) = entry.file_name().to_str().and_then(|s| s.parse().ok()) else {
continue;
};
let path = entry.path();
let mut lines = BinFileLineIter::open(&path)?;
// This silently ignores potential errors that prevent us from reading the next line.
// We should report the error instead of silently ignoring it.
while let Ok(Some(line)) = lines.next_line() {
let Some(suffix) = line.strip_prefix(b"lock:").map(<[u8]>::trim_ascii) else {
continue;
};
if let Some(lock) = LockInfo::parse(
no_inaccessible,
&path,
Some(process_id),
file_descriptor,
command_name,
None,
suffix,
)? {
pid_locks.push(lock);
}
}
}
Ok(())
}
#[cfg(target_os = "linux")]
fn fill_table(
output_mode: OutputMode,
columns: &[&ColumnInfo],
target_pid: Option<pid_t>,
in_bytes: bool,
table: &mut Table,
pid_locks: &[LockInfo],
proc_locks: &[LockInfo],
) -> Result<(), LsLocksError> {
use std::os::unix::ffi::OsStrExt;
for proc_lock in proc_locks
.iter()
.rev()
.filter(|lock| target_pid.is_none_or(|target_pid| lock.process_id == target_pid))
{
let mut line = table.new_line(None)?;
for (cell_index, &column) in columns.iter().enumerate() {
let data_str = match column.id.to_bytes() {
b"PID" => describe_integer(proc_lock.process_id),
b"INODE" => describe_integer(proc_lock.inode),
b"M" => describe_integer(u8::from(proc_lock.mandatory)),
b"START" => describe_integer(proc_lock.range.start),
b"END" => describe_integer(proc_lock.range.end),
b"SIZE" => proc_lock
.size
.and_then(|size| describe_size(size, in_bytes)),
b"TYPE" => Some(Cow::Borrowed(proc_lock.kind.as_c_str())),
b"MAJ:MIN" => {
let major = libc::major(proc_lock.device_id);
let minor = libc::minor(proc_lock.device_id);
let value = if matches!(output_mode, OutputMode::Json | OutputMode::Raw) {
format!("{major}:{minor}")
} else {
format!("{major:3}:{minor:<3}")
};
Some(Cow::Owned(CString::new(value).unwrap()))
}
b"MODE" => {
if proc_lock.blocked {
let mut buffer = proc_lock.mode.clone().into_bytes();
buffer.push(b'*');
Some(Cow::Owned(CString::new(buffer).unwrap()))
} else {
Some(Cow::Borrowed(proc_lock.mode.as_c_str()))
}
}
b"COMMAND" => proc_lock.command_name.as_deref().map(Cow::Borrowed),
b"PATH" => proc_lock
.path
.as_deref()
.map(Path::as_os_str)
.map(OsStr::as_bytes)
.map(|path| Cow::Owned(CString::new(path).unwrap())),
b"BLOCKER" => (proc_lock.blocked && proc_lock.id != -1)
.then(|| proc_locks.iter())
.and_then(|mut iter| {
iter.find(|&lock| !lock.blocked && lock.id == proc_lock.id)
})
.and_then(|found| describe_integer(found.process_id)),
b"HOLDERS" => describe_holders(proc_lock, pid_locks)
.map(Cow::Owned)
.map(Some)?,
_ => continue,
};
if let Some(data_str) = data_str {
line.set_data(cell_index, &data_str)?;
}
}
}
Ok(())
}

View File

@@ -0,0 +1,137 @@
// 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 std::ffi::{CStr, c_char, c_int, c_uint, c_void};
use std::ptr::NonNull;
use std::{io, ptr};
use smartcols_sys::{
libscols_column, libscols_line, libscols_table, scols_column_set_json_type,
scols_column_set_safechars, scols_column_set_wrapfunc, scols_init_debug, scols_line_set_data,
scols_new_table, scols_print_table, scols_table_enable_json, scols_table_enable_noheadings,
scols_table_enable_raw, scols_table_new_column, scols_table_new_line, scols_table_set_name,
scols_unref_table,
};
use crate::errors::LsLocksError;
pub(crate) fn initialize() {
unsafe { scols_init_debug(0) };
}
#[repr(transparent)]
pub(crate) struct Table(NonNull<libscols_table>);
impl Table {
pub(crate) fn new() -> Result<Self, LsLocksError> {
NonNull::new(unsafe { scols_new_table() })
.ok_or_else(|| LsLocksError::io0("scols_new_table", io::ErrorKind::OutOfMemory))
.map(Self)
}
}
impl TableOperations for Table {
fn as_ptr(&self) -> *mut libscols_table {
self.0.as_ptr()
}
}
impl Drop for Table {
fn drop(&mut self) {
unsafe { scols_unref_table(self.0.as_ptr()) }
}
}
pub(crate) trait TableOperations: Sized {
fn as_ptr(&self) -> *mut libscols_table;
fn enable_headings(&mut self, enable: bool) -> Result<(), LsLocksError> {
let no_headings = c_int::from(!enable);
let r = unsafe { scols_table_enable_noheadings(self.as_ptr(), no_headings) };
LsLocksError::io_from_neg_errno("scols_table_enable_noheadings", r).map(|_| ())
}
fn enable_raw(&mut self, enable: bool) -> Result<(), LsLocksError> {
let r = unsafe { scols_table_enable_raw(self.as_ptr(), c_int::from(enable)) };
LsLocksError::io_from_neg_errno("scols_table_enable_raw", r).map(|_| ())
}
fn enable_json(&mut self, enable: bool) -> Result<(), LsLocksError> {
let r = unsafe { scols_table_enable_json(self.as_ptr(), c_int::from(enable)) };
LsLocksError::io_from_neg_errno("scols_table_enable_json", r).map(|_| ())
}
fn new_column(
&mut self,
name: &CStr,
width_hint: f64,
flags: c_uint,
) -> Result<ColumnRef, LsLocksError> {
NonNull::new(unsafe {
scols_table_new_column(self.as_ptr(), name.as_ptr(), width_hint, flags as c_int)
})
.ok_or_else(|| LsLocksError::io0("scols_table_new_column", io::ErrorKind::OutOfMemory))
.map(ColumnRef)
}
fn new_line(&mut self, parent: Option<&mut LineRef>) -> Result<LineRef, LsLocksError> {
let parent = parent.map_or(ptr::null_mut(), |parent| parent.0.as_ptr());
NonNull::new(unsafe { scols_table_new_line(self.as_ptr(), parent) })
.ok_or_else(|| LsLocksError::io0("scols_table_new_line", io::ErrorKind::OutOfMemory))
.map(LineRef)
}
fn set_name(&mut self, name: &CStr) -> Result<(), LsLocksError> {
let r = unsafe { scols_table_set_name(self.as_ptr(), name.as_ptr()) };
LsLocksError::io_from_neg_errno("scols_table_set_name", r).map(|_| ())
}
fn print(&self) -> Result<(), LsLocksError> {
let r = unsafe { scols_print_table(self.as_ptr()) };
LsLocksError::io_from_neg_errno("scols_print_table", r).map(|_| ())
}
}
#[repr(transparent)]
pub(crate) struct LineRef(NonNull<libscols_line>);
impl LineRef {
pub(crate) fn set_data(&mut self, cell_index: usize, data: &CStr) -> Result<(), LsLocksError> {
let r = unsafe { scols_line_set_data(self.0.as_ptr(), cell_index, data.as_ptr()) };
LsLocksError::io_from_neg_errno("scols_line_set_data", r).map(|_| ())
}
}
#[repr(transparent)]
pub(crate) struct ColumnRef(NonNull<libscols_column>);
impl ColumnRef {
pub(crate) fn set_json_type(&mut self, json_type: c_uint) -> Result<(), LsLocksError> {
let r = unsafe { scols_column_set_json_type(self.0.as_ptr(), json_type as c_int) };
LsLocksError::io_from_neg_errno("scols_column_set_json_type", r).map(|_| ())
}
pub(crate) fn set_safe_chars(&mut self, safe: &CStr) -> Result<(), LsLocksError> {
let r = unsafe { scols_column_set_safechars(self.0.as_ptr(), safe.as_ptr()) };
LsLocksError::io_from_neg_errno("scols_column_set_safechars", r).map(|_| ())
}
pub(crate) fn set_wrap_func(
&mut self,
wrap_chunk_size: Option<
unsafe extern "C" fn(*const libscols_column, *const c_char, *mut c_void) -> usize,
>,
wrap_next_chunk: Option<
unsafe extern "C" fn(*const libscols_column, *mut c_char, *mut c_void) -> *mut c_char,
>,
user_data: *mut c_void,
) -> Result<(), LsLocksError> {
let r = unsafe {
scols_column_set_wrapfunc(self.0.as_ptr(), wrap_chunk_size, wrap_next_chunk, user_data)
};
LsLocksError::io_from_neg_errno("scols_column_set_wrapfunc", r).map(|_| ())
}
}

365
src/uu/lslocks/src/utils.rs Normal file
View File

@@ -0,0 +1,365 @@
// 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 std::ffi::{CStr, CString, c_int, c_uint};
use std::fs::{File, FileType};
use std::io;
use std::io::{BufRead, BufReader};
use std::ops::Range;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::{FileTypeExt, MetadataExt};
use std::path::{Path, PathBuf};
use libmount_sys::{
MNT_ITER_BACKWARD, mnt_fs_get_target, mnt_new_table_from_file, mnt_table_find_devno,
};
use crate::errors::LsLocksError;
pub(crate) static _PATH_PROC: &str = "/proc";
pub(crate) static _PATH_PROC_LOCKS: &str = "/proc/locks";
static _PATH_PROC_MOUNTINFO: &str = "/proc/self/mountinfo";
static _PATH_PROC_MOUNTINFO_C: &CStr = c"/proc/self/mountinfo";
pub(crate) fn entry_is_dir_or_unknown(file_type: &FileType) -> bool {
file_type.is_dir()
|| (!file_type.is_file()
&& !file_type.is_symlink()
&& !file_type.is_block_device()
&& !file_type.is_char_device()
&& !file_type.is_fifo()
&& !file_type.is_socket())
}
pub(crate) fn proc_pid_command_name(proc_path: &Path) -> Result<CString, LsLocksError> {
let path = proc_path.join("comm");
let mut contents =
std::fs::read(&path).map_err(|err| LsLocksError::io1("reading file", &path, err))?;
if contents.last().copied() == Some(0_u8) {
contents.pop();
}
if contents.last().copied() == Some(b'\n') {
contents.pop();
}
CString::new(contents).map_err(|_| {
let err = io::ErrorKind::InvalidData;
LsLocksError::io1("invalid data", &path, err)
})
}
fn pid_command_name(pid: libc::pid_t) -> Result<CString, LsLocksError> {
proc_pid_command_name(&Path::new(_PATH_PROC).join(pid.to_string()))
}
fn path_and_size_of_inode_opened_by_process(
pid: libc::pid_t,
inode: libc::ino_t,
) -> Result<(PathBuf, u64), LsLocksError> {
let path = Path::new(_PATH_PROC).join(format!("{pid}")).join("fd");
let dir_entries = path
.read_dir()
.map_err(|err| LsLocksError::io1("reading directory", &path, err))?;
for entry in dir_entries {
let entry =
entry.map_err(|err| LsLocksError::io1("reading directory entry", &path, err))?;
if entry
.file_name()
.as_bytes()
.iter()
.any(|&b| !b.is_ascii_digit())
{
continue;
}
let path = entry.path();
let md = path
.metadata()
.map_err(|err| LsLocksError::io1("reading file metadata", &path, err))?;
if md.ino() == inode {
let path = entry
.path()
.read_link()
.map_err(|err| LsLocksError::io1("reading symbolic link", &path, err))?;
return Ok((path, md.len()));
}
}
Err(LsLocksError::io0(
"looking for inode open in process",
io::ErrorKind::NotFound,
))
}
pub(crate) struct BinFileLineIter {
path: PathBuf,
input: BufReader<File>,
buffer: Vec<u8>,
}
impl BinFileLineIter {
pub(crate) fn open(path: &Path) -> Result<Self, LsLocksError> {
let input = File::open(path)
.map(BufReader::new)
.map_err(|err| LsLocksError::io1("opening file", path, err))?;
Ok(Self {
path: path.into(),
input,
buffer: Vec::default(),
})
}
pub(crate) fn next_line(&mut self) -> Result<Option<&[u8]>, LsLocksError> {
self.buffer.clear();
let count = self
.input
.read_until(b'\n', &mut self.buffer)
.map_err(|err| LsLocksError::io1("reading file", &self.path, err))?;
if count == 0 {
return Ok(None);
}
if self.buffer.last().copied() == Some(b'\n') {
self.buffer.pop();
}
if self.buffer.last().copied() == Some(b'\r') {
self.buffer.pop();
}
Ok(Some(&self.buffer))
}
}
pub(crate) struct LockInfo {
pub(crate) command_name: Option<CString>,
pub(crate) process_id: libc::pid_t,
pub(crate) path: Option<PathBuf>,
pub(crate) kind: CString,
pub(crate) mode: CString,
pub(crate) range: Range<u64>,
pub(crate) inode: libc::ino_t,
pub(crate) device_id: libc::dev_t,
pub(crate) mandatory: bool,
pub(crate) blocked: bool,
pub(crate) size: Option<u64>,
pub(crate) file_descriptor: c_int,
pub(crate) id: i64,
}
impl LockInfo {
pub(crate) fn parse(
no_inaccessible: bool,
fdinfo_path: &Path,
process_id: Option<libc::pid_t>,
file_descriptor: c_int,
command_name: &CStr,
pid_locks: Option<&[Self]>,
line: &[u8],
) -> Result<Option<Self>, LsLocksError> {
let err_map = || {
let err = io::ErrorKind::InvalidData;
LsLocksError::io1("parsing lock information", fdinfo_path, err)
};
let mut elements = line
.split(|&b| b.is_ascii_whitespace())
.filter(|&b| !b.is_empty());
let element = elements.next().ok_or_else(err_map)?;
let id = if process_id.is_none() {
element
.strip_suffix(b":")
.ok_or_else(err_map)
.and_then(|b| std::str::from_utf8(b).map_err(|_| err_map()))?
.parse()
.map_err(|_| err_map())?
} else {
-1
};
let mut blocked = false;
let kind = loop {
let element = elements.next().ok_or_else(err_map)?;
if element != b"->" {
break CString::new(element).map_err(|_| err_map())?;
}
blocked = true;
};
let mandatory = elements.next().ok_or_else(err_map)?.starts_with(b"M");
let element = elements.next().ok_or_else(err_map)?;
let mode = CString::new(element).map_err(|_| err_map())?;
let mut unknown_command_name = false;
let element = elements.next().ok_or_else(err_map)?;
let (mut process_id, mut command_name) = if let Some(process_id) = process_id {
(process_id, Some(CString::from(command_name)))
} else {
let process_id: libc::pid_t = std::str::from_utf8(element)
.map_err(|_| err_map())?
.parse()
.map_err(|_| err_map())?;
let command_name = if process_id > 0 {
if let Ok(cmd_line) = pid_command_name(process_id).map(Some) {
cmd_line
} else {
unknown_command_name = true;
None
}
} else {
None
};
(process_id, command_name)
};
let mut iter = elements.next().ok_or_else(err_map)?.split(|&b| b == b':');
let major = iter
.next()
.ok_or_else(err_map)
.and_then(|b| std::str::from_utf8(b).map_err(|_| err_map()))
.and_then(|s| c_uint::from_str_radix(s, 16).map_err(|_| err_map()))?;
let minor = iter
.next()
.ok_or_else(err_map)
.and_then(|b| std::str::from_utf8(b).map_err(|_| err_map()))
.and_then(|s| c_uint::from_str_radix(s, 16).map_err(|_| err_map()))?;
let inode = iter
.next()
.ok_or_else(err_map)
.and_then(|b| std::str::from_utf8(b).map_err(|_| err_map()))?
.parse()
.map_err(|_| err_map())?;
let device_id = libc::makedev(major, minor);
let element = elements.next().ok_or_else(err_map)?;
let start = if element == b"EOF" {
0
} else {
std::str::from_utf8(element)
.map_err(|_| err_map())?
.parse()
.map_err(|_| err_map())?
};
let element = elements.next().ok_or_else(err_map)?;
let end = if element == b"EOF" {
0
} else {
std::str::from_utf8(element)
.map_err(|_| err_map())?
.parse()
.map_err(|_| err_map())?
};
let range = start..end;
if let Some(pid_locks) = pid_locks {
if command_name.is_none() && !blocked {
let lock_compare = |lock: &&LockInfo| {
lock.range == range
&& lock.inode == inode
&& lock.device_id == device_id
&& lock.mandatory == mandatory
&& lock.blocked == blocked
&& lock.kind == kind
&& lock.mode == mode
};
if let Some(found) = pid_locks.iter().find(lock_compare) {
process_id = found.process_id;
command_name = found.command_name.clone();
}
}
}
if command_name.is_none() {
command_name = if unknown_command_name {
Some(CString::from(c"(unknown)"))
} else {
Some(CString::from(c"(undefined)"))
};
}
let (mut path, size) = path_and_size_of_inode_opened_by_process(process_id, inode)
.ok()
.map_or((None, None), |(path, size)| (Some(path), Some(size)));
if path.is_none() {
if no_inaccessible {
return Ok(None);
}
path = fall_back_file_name(device_id).ok();
}
Ok(Some(Self {
command_name,
process_id,
path,
kind,
mode,
range,
inode,
device_id,
mandatory,
blocked,
size,
file_descriptor,
id,
}))
}
}
fn fall_back_file_name(device_id: libc::dev_t) -> Result<PathBuf, LsLocksError> {
let table = unsafe { mnt_new_table_from_file(_PATH_PROC_MOUNTINFO_C.as_ptr()) };
if table.is_null() {
return Err(LsLocksError::io1(
"mnt_new_table_from_file",
_PATH_PROC_MOUNTINFO,
io::ErrorKind::InvalidData,
));
};
let fs = unsafe { mnt_table_find_devno(table, device_id, MNT_ITER_BACKWARD as c_int) };
if fs.is_null() {
let err = io::ErrorKind::NotFound;
return Err(LsLocksError::io0("mnt_table_find_devno", err));
};
let target = unsafe { mnt_fs_get_target(fs) };
if target.is_null() {
let err = io::ErrorKind::NotFound;
return Err(LsLocksError::io0("mnt_fs_get_target", err));
};
let target = unsafe { CStr::from_ptr(target) }
.to_str()
.map_err(|_| LsLocksError::io0("data is not UTF-8", io::ErrorKind::InvalidData))?;
Ok(PathBuf::from(format!(
"{target}{}...",
if target.ends_with("/") { "" } else { "/" }
)))
}

View File

@@ -15,9 +15,8 @@ fn test_column_headers() {
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"]
["COMMAND", "PID", "TYPE", "SIZE", "MODE", "M", "START", "END", "PATH"]
);
}