mirror of
https://github.com/uutils/util-linux.git
synced 2026-06-10 16:13:52 -07:00
Implement basic lsns utility (#554)
--------- Signed-off-by: Malhar Vora <mlvora.2010@gmail.com>
This commit is contained in:
Generated
+10
@@ -1545,6 +1545,7 @@ dependencies = [
|
||||
"uu_lsipc",
|
||||
"uu_lslocks",
|
||||
"uu_lsmem",
|
||||
"uu_lsns",
|
||||
"uu_mcookie",
|
||||
"uu_mesg",
|
||||
"uu_mountpoint",
|
||||
@@ -1688,6 +1689,15 @@ dependencies = [
|
||||
"uucore 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uu_lsns"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"smartcols-sys",
|
||||
"uucore 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uu_mcookie"
|
||||
version = "0.0.1"
|
||||
|
||||
@@ -38,6 +38,7 @@ feat_common_core = [
|
||||
"lsipc",
|
||||
"lslocks",
|
||||
"lsmem",
|
||||
"lsns",
|
||||
"mcookie",
|
||||
"mesg",
|
||||
"mountpoint",
|
||||
@@ -110,6 +111,7 @@ lscpu = { optional = true, version = "0.0.1", package = "uu_lscpu", path = "src/
|
||||
lsipc = { optional = true, version = "0.0.1", package = "uu_lsipc", path = "src/uu/lsipc" }
|
||||
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" }
|
||||
lsns = { optional = true, version = "0.0.1", package = "uu_lsns", path = "src/uu/lsns" }
|
||||
mcookie = { optional = true, version = "0.0.1", package = "uu_mcookie", path = "src/uu/mcookie" }
|
||||
mesg = { optional = true, version = "0.0.1", package = "uu_mesg", path = "src/uu/mesg" }
|
||||
mountpoint = { optional = true, version = "0.0.1", package = "uu_mountpoint", path = "src/uu/mountpoint" }
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "uu_lsns"
|
||||
version = "0.0.1"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
path = "src/lsns.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "lsns"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
uucore = { workspace = true, features = ["entries"] }
|
||||
clap = { workspace = true }
|
||||
smartcols-sys = { workspace = true }
|
||||
@@ -0,0 +1,7 @@
|
||||
# lsns
|
||||
|
||||
```
|
||||
lsns [OPTION]...
|
||||
```
|
||||
|
||||
List the namespaces in the system.
|
||||
@@ -0,0 +1,105 @@
|
||||
// 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::c_int;
|
||||
use std::fmt;
|
||||
|
||||
use uucore::error::UError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LsnsError {
|
||||
/// Generic I/O error with context message
|
||||
IOError(String, std::io::Error),
|
||||
/// CString conversion error (null byte in string)
|
||||
NulError(String, std::ffi::NulError),
|
||||
/// Invalid namespace type index
|
||||
InvalidNamespaceType(usize),
|
||||
/// Unsupported platform
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
UnsupportedPlatform,
|
||||
/// Invalid namespace inode format
|
||||
InvalidNamespaceInodeFormat(String),
|
||||
/// Invalid process stat format
|
||||
InvalidProcessStatFormat(String),
|
||||
/// Failed to get UID from directory entry
|
||||
FailedToGetUid(String),
|
||||
/// Failed to get PID from directory entry
|
||||
FailedToGetPid(String),
|
||||
/// Failed to read process information
|
||||
FailedToReadProcess(String),
|
||||
}
|
||||
|
||||
impl LsnsError {
|
||||
/// Create an I/O error with a context message
|
||||
pub(crate) fn io0(message: impl Into<String>, error: impl Into<std::io::Error>) -> Self {
|
||||
Self::IOError(message.into(), error.into())
|
||||
}
|
||||
|
||||
/// Helper to convert negative errno to Result
|
||||
pub(crate) fn io_from_neg_errno(
|
||||
message: impl Into<String>,
|
||||
result: c_int,
|
||||
) -> Result<usize, LsnsError> {
|
||||
if let Ok(result) = usize::try_from(result) {
|
||||
Ok(result)
|
||||
} else {
|
||||
let err = std::io::Error::from_raw_os_error(-result);
|
||||
Err(Self::IOError(message.into(), err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for LsnsError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::IOError(message, err) => write!(f, "{message}: {err}"),
|
||||
Self::NulError(message, err) => write!(f, "{message}: {err}"),
|
||||
Self::InvalidNamespaceType(idx) => write!(f, "Invalid namespace type index: {}", idx),
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
Self::UnsupportedPlatform => write!(f, "lsns is only supported on Linux"),
|
||||
Self::InvalidNamespaceInodeFormat(s) => {
|
||||
write!(f, "Invalid namespace inode format: {}", s)
|
||||
}
|
||||
Self::InvalidProcessStatFormat(s) => {
|
||||
write!(f, "Invalid process stat format: {}", s)
|
||||
}
|
||||
Self::FailedToGetUid(s) => {
|
||||
write!(f, "Failed to get UID from directory entry: {}", s)
|
||||
}
|
||||
Self::FailedToGetPid(s) => {
|
||||
write!(f, "Failed to get PID from directory entry: {}", s)
|
||||
}
|
||||
Self::FailedToReadProcess(s) => {
|
||||
write!(f, "Failed to read process information: {}", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UError for LsnsError {
|
||||
fn code(&self) -> i32 {
|
||||
1
|
||||
}
|
||||
|
||||
fn usage(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for LsnsError {}
|
||||
|
||||
// Implement From trait for automatic conversion from std::io::Error
|
||||
impl From<std::io::Error> for LsnsError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
Self::IOError(String::new(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// Implement From trait for automatic conversion from std::ffi::NulError
|
||||
impl From<std::ffi::NulError> for LsnsError {
|
||||
fn from(err: std::ffi::NulError) -> Self {
|
||||
Self::NulError(String::new(), err)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
uucore::bin!(uu_lsns);
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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_int, c_uint};
|
||||
use std::ptr::NonNull;
|
||||
use std::{io, ptr};
|
||||
|
||||
use smartcols_sys::{
|
||||
libscols_column, libscols_line, libscols_table, scols_init_debug, scols_line_set_data,
|
||||
scols_new_table, scols_print_table, scols_table_new_column, scols_table_new_line,
|
||||
scols_unref_table,
|
||||
};
|
||||
|
||||
use crate::errors::LsnsError;
|
||||
|
||||
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, LsnsError> {
|
||||
NonNull::new(unsafe { scols_new_table() })
|
||||
.ok_or_else(|| LsnsError::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 new_column(
|
||||
&mut self,
|
||||
name: &CStr,
|
||||
width_hint: f64,
|
||||
flags: c_uint,
|
||||
) -> Result<ColumnRef, LsnsError> {
|
||||
NonNull::new(unsafe {
|
||||
scols_table_new_column(self.as_ptr(), name.as_ptr(), width_hint, flags as c_int)
|
||||
})
|
||||
.ok_or_else(|| LsnsError::io0("scols_table_new_column", io::ErrorKind::OutOfMemory))
|
||||
.map(ColumnRef)
|
||||
}
|
||||
|
||||
fn new_line(&mut self, parent: Option<&mut LineRef>) -> Result<LineRef, LsnsError> {
|
||||
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(|| LsnsError::io0("scols_table_new_line", io::ErrorKind::OutOfMemory))
|
||||
.map(LineRef)
|
||||
}
|
||||
|
||||
fn print(&self) -> Result<(), LsnsError> {
|
||||
let r = unsafe { scols_print_table(self.as_ptr()) };
|
||||
LsnsError::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<(), LsnsError> {
|
||||
let r = unsafe { scols_line_set_data(self.0.as_ptr(), cell_index, data.as_ptr()) };
|
||||
LsnsError::io_from_neg_errno("scols_line_set_data", r).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
pub(crate) struct ColumnRef(NonNull<libscols_column>);
|
||||
@@ -0,0 +1,145 @@
|
||||
// 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 uutests::new_ucmd;
|
||||
|
||||
#[test]
|
||||
fn test_invalid_arg() {
|
||||
new_ucmd!().arg("--definitely-invalid").fails().code_is(1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn test_basic_output() {
|
||||
let res = new_ucmd!().succeeds();
|
||||
let stdout = res.no_stderr().stdout_str();
|
||||
|
||||
// Check for header columns
|
||||
assert!(stdout.contains("NS"));
|
||||
assert!(stdout.contains("TYPE"));
|
||||
assert!(stdout.contains("NPROCS"));
|
||||
assert!(stdout.contains("PID"));
|
||||
assert!(stdout.contains("USER"));
|
||||
assert!(stdout.contains("COMMAND"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn test_namespace_types() {
|
||||
let res = new_ucmd!().succeeds();
|
||||
let stdout = res.no_stderr().stdout_str();
|
||||
|
||||
// We should see at least some common namespace types
|
||||
// Note: Not all may be present on all systems, so we check for at least one
|
||||
let has_namespace = stdout.contains("mnt")
|
||||
|| stdout.contains("net")
|
||||
|| stdout.contains("pid")
|
||||
|| stdout.contains("uts")
|
||||
|| stdout.contains("ipc")
|
||||
|| stdout.contains("user")
|
||||
|| stdout.contains("cgroup");
|
||||
|
||||
assert!(
|
||||
has_namespace,
|
||||
"Expected to see at least one namespace type in output"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn test_output_has_processes() {
|
||||
let res = new_ucmd!().succeeds();
|
||||
let stdout = res.no_stderr().stdout_str();
|
||||
|
||||
// The output should have at least one process (the test process itself)
|
||||
// Count lines (excluding header)
|
||||
let lines: Vec<&str> = stdout.lines().collect();
|
||||
assert!(
|
||||
lines.len() >= 2,
|
||||
"Expected at least header line and one namespace entry"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn test_unsupported_platform() {
|
||||
// On non-Linux platforms, lsns should fail with an appropriate error
|
||||
new_ucmd!().fails();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn test_output_format() {
|
||||
let res = new_ucmd!().succeeds();
|
||||
let stdout = res.no_stderr().stdout_str();
|
||||
|
||||
// Verify the output has proper table format
|
||||
/*
|
||||
Each line should have multiple columns separated by whitespace.We check
|
||||
for at least 4 columns as the minimum (NS, TYPE, NPROCS, PID). These
|
||||
fields are always present. For mnt namespaces the PID and COMMAND column
|
||||
will be empty.
|
||||
*/
|
||||
for line in stdout.lines().skip(1) {
|
||||
// Skip header
|
||||
if !line.is_empty() {
|
||||
let columns: Vec<&str> = line.split_whitespace().collect();
|
||||
assert!(
|
||||
columns.len() >= 4,
|
||||
"Each namespace entry should have at least 4 columns"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn test_namespace_ids_are_numeric() {
|
||||
let res = new_ucmd!().succeeds();
|
||||
let stdout = res.no_stderr().stdout_str();
|
||||
|
||||
/*
|
||||
The first column of each line should be the namespace ID which is an inod number
|
||||
and it should be numeric.
|
||||
*/
|
||||
|
||||
// Skip the header line and check that namespace IDs are numeric
|
||||
for line in stdout.lines().skip(1) {
|
||||
if !line.is_empty() {
|
||||
let columns: Vec<&str> = line.split_whitespace().collect();
|
||||
if !columns.is_empty() {
|
||||
let ns_id = columns[0];
|
||||
assert!(
|
||||
ns_id.chars().all(|c| c.is_ascii_digit()),
|
||||
"Namespace ID should be numeric: {}",
|
||||
ns_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn test_user_column_not_empty() {
|
||||
let res = new_ucmd!().succeeds();
|
||||
let stdout = res.no_stderr().stdout_str();
|
||||
|
||||
// Check that USER column (5th column) is not empty for entries with processes
|
||||
for line in stdout.lines().skip(1) {
|
||||
if !line.is_empty() {
|
||||
let columns: Vec<&str> = line.split_whitespace().collect();
|
||||
if columns.len() >= 5 {
|
||||
// If there's a PID (4th column is not empty), there should be a user
|
||||
if !columns[3].is_empty() && columns[3] != "0" {
|
||||
assert!(
|
||||
!columns[4].is_empty(),
|
||||
"User column should not be empty when PID is present"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,10 @@ mod test_lsmem;
|
||||
#[path = "by-util/test_lslocks.rs"]
|
||||
mod test_lslocks;
|
||||
|
||||
#[cfg(feature = "lsns")]
|
||||
#[path = "by-util/test_lsns.rs"]
|
||||
mod test_lsns;
|
||||
|
||||
#[cfg(feature = "mesg")]
|
||||
#[path = "by-util/test_mesg.rs"]
|
||||
mod test_mesg;
|
||||
|
||||
Reference in New Issue
Block a user