use linuxutils_common::man::ManContent;
pub const MAN: ManContent = ManContent::empty();
use clap::Parser;
use cols::{OutputMode, Table, WidthHint, print_table};
use std::{collections::HashMap, fs, process::ExitCode};
const NS_TYPES: &[&str] =
&["mnt", "net", "ipc", "user", "pid", "uts", "cgroup", "time"];
#[derive(Parser)]
#[command(name = "lsns", about = "List system namespaces")]
pub struct Args {
#[arg(short = 'J', long)]
json: bool,
#[arg(short, long)]
list: bool,
#[arg(short = 'n', long)]
noheadings: bool,
#[arg(short, long, value_delimiter = ',')]
output: Option<Vec<String>>,
#[arg(long)]
output_all: bool,
#[arg(short, long)]
raw: bool,
#[arg(short = 't', long = "type")]
ns_type: Option<String>,
#[arg(short = 'p', long = "task")]
task: Option<u32>,
#[arg()]
namespace: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Col {
Ns,
Type,
Nprocs,
Pid,
Ppid,
Command,
Uid,
User,
}
impl Col {
fn name(self) -> &'static str {
match self {
Col::Ns => "NS",
Col::Type => "TYPE",
Col::Nprocs => "NPROCS",
Col::Pid => "PID",
Col::Ppid => "PPID",
Col::Command => "COMMAND",
Col::Uid => "UID",
Col::User => "USER",
}
}
fn whint(self) -> WidthHint {
match self {
Col::Ns => WidthHint::Fixed(10),
Col::Type => WidthHint::Fixed(6),
Col::Nprocs => WidthHint::Fixed(6),
Col::Pid => WidthHint::Fixed(6),
Col::Ppid => WidthHint::Fixed(6),
Col::Command => WidthHint::Auto,
Col::Uid => WidthHint::Fixed(6),
Col::User => WidthHint::Fixed(8),
}
}
fn is_right(self) -> bool {
matches!(
self,
Col::Ns | Col::Nprocs | Col::Pid | Col::Ppid | Col::Uid
)
}
fn from_name(name: &str) -> Option<Self> {
match name.to_uppercase().as_str() {
"NS" => Some(Col::Ns),
"TYPE" => Some(Col::Type),
"NPROCS" => Some(Col::Nprocs),
"PID" => Some(Col::Pid),
"PPID" => Some(Col::Ppid),
"COMMAND" => Some(Col::Command),
"UID" => Some(Col::Uid),
"USER" => Some(Col::User),
_ => None,
}
}
}
const DEFAULT_COLUMNS: &[Col] = &[
Col::Ns,
Col::Type,
Col::Nprocs,
Col::Pid,
Col::User,
Col::Command,
];
const ALL_COLUMNS: &[Col] = &[
Col::Ns,
Col::Type,
Col::Nprocs,
Col::Pid,
Col::Ppid,
Col::Command,
Col::Uid,
Col::User,
];
#[derive(Debug)]
struct NsInfo {
ino: u64,
ns_type: String,
nprocs: u32,
pid: u32,
ppid: u32,
uid: u32,
user: String,
command: String,
}
fn read_proc_pid(pid: u32) -> Option<(u32, u32, String)> {
let stat = fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
let comm_end = stat.rfind(')')?;
let after_comm = &stat[comm_end + 2..]; let fields: Vec<&str> = after_comm.split_whitespace().collect();
let ppid: u32 = fields.get(1)?.parse().ok()?;
let cmdline = fs::read_to_string(format!("/proc/{pid}/cmdline"))
.ok()
.map(|s| s.replace('\0', " ").trim().to_string())
.unwrap_or_default();
let command = if cmdline.is_empty() {
let comm_start = stat.find('(')? + 1;
format!("[{}]", &stat[comm_start..comm_end])
} else {
cmdline
};
Some((ppid, 0, command))
}
fn get_uid(pid: u32) -> u32 {
fs::read_to_string(format!("/proc/{pid}/status"))
.ok()
.and_then(|s| {
for line in s.lines() {
if let Some(rest) = line.strip_prefix("Uid:") {
return rest.split_whitespace().next()?.parse().ok();
}
}
None
})
.unwrap_or(0)
}
fn uid_to_name(uid: u32) -> String {
fs::read_to_string("/etc/passwd")
.ok()
.and_then(|content| {
for line in content.lines() {
let fields: Vec<&str> = line.split(':').collect();
if fields.len() >= 3
&& let Ok(u) = fields[2].parse::<u32>()
&& u == uid
{
return Some(fields[0].to_string());
}
}
None
})
.unwrap_or_else(|| uid.to_string())
}
fn read_ns_inode(pid: u32, ns_type: &str) -> Option<u64> {
let link = fs::read_link(format!("/proc/{pid}/ns/{ns_type}")).ok()?;
let s = link.to_str()?;
let start = s.find('[')? + 1;
let end = s.find(']')?;
s[start..end].parse().ok()
}
fn scan_namespaces(
type_filter: Option<&str>,
task_filter: Option<u32>,
ns_filter: Option<u64>,
) -> Vec<NsInfo> {
let mut ns_map: HashMap<(String, u64), NsInfo> = HashMap::new();
let pids: Vec<u32> = if let Some(pid) = task_filter {
vec![pid]
} else {
fs::read_dir("/proc")
.ok()
.map(|entries| {
entries
.flatten()
.filter_map(|e| e.file_name().to_str()?.parse::<u32>().ok())
.collect()
})
.unwrap_or_default()
};
let types: Vec<&str> = if let Some(t) = type_filter {
NS_TYPES.iter().copied().filter(|&ns| ns == t).collect()
} else {
NS_TYPES.to_vec()
};
for &pid in &pids {
for &ns_type in &types {
let ino = match read_ns_inode(pid, ns_type) {
Some(i) => i,
None => continue,
};
if let Some(filter) = ns_filter
&& ino != filter
{
continue;
}
let key = (ns_type.to_string(), ino);
let entry = ns_map.entry(key).or_insert_with(|| {
let uid = get_uid(pid);
let (ppid, _, command) =
read_proc_pid(pid).unwrap_or((0, 0, String::new()));
NsInfo {
ino,
ns_type: ns_type.to_string(),
nprocs: 0,
pid,
ppid,
uid,
user: uid_to_name(uid),
command,
}
});
entry.nprocs += 1;
if pid < entry.pid {
let uid = get_uid(pid);
let (ppid, _, command) =
read_proc_pid(pid).unwrap_or((0, 0, String::new()));
entry.pid = pid;
entry.ppid = ppid;
entry.uid = uid;
entry.user = uid_to_name(uid);
entry.command = command;
}
}
}
let mut result: Vec<NsInfo> = ns_map.into_values().collect();
result.sort_by_key(|ns| ns.ino);
result
}
pub fn run(args: Args) -> ExitCode {
let columns = if args.output_all {
ALL_COLUMNS.to_vec()
} else if let Some(ref names) = args.output {
let mut cols = Vec::new();
for name in names {
match Col::from_name(name.trim()) {
Some(c) => cols.push(c),
None => {
eprintln!("lsns: unknown column: {name}");
return ExitCode::FAILURE;
}
}
}
cols
} else {
DEFAULT_COLUMNS.to_vec()
};
let type_filter = args.ns_type.as_deref();
let namespaces = scan_namespaces(type_filter, args.task, args.namespace);
let mut table = Table::new();
table.name_set("namespaces");
if args.json {
table.output_mode_set(OutputMode::Json);
} else if args.raw {
table.output_mode_set(OutputMode::Raw);
}
if args.noheadings {
table.headings_set(false);
}
for col in &columns {
let idx = table.new_column(col.name());
table.column_mut(idx).unwrap().width_hint_set(col.whint());
if col.is_right() {
table.column_mut(idx).unwrap().right_set(true);
}
}
for ns in &namespaces {
let line_id = table.new_line(None);
let line = table.line_mut(line_id);
for (ci, col) in columns.iter().enumerate() {
let val = match col {
Col::Ns => ns.ino.to_string(),
Col::Type => ns.ns_type.clone(),
Col::Nprocs => ns.nprocs.to_string(),
Col::Pid => ns.pid.to_string(),
Col::Ppid => ns.ppid.to_string(),
Col::Command => ns.command.clone(),
Col::Uid => ns.uid.to_string(),
Col::User => ns.user.clone(),
};
line.data_set(ci, &val);
}
}
let stdout = std::io::stdout();
let mut out = stdout.lock();
if let Err(e) = print_table(&table, &mut out) {
eprintln!("lsns: {e}");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}