1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use cols::{OutputMode, Table, WidthHint, print_table};
7use std::{collections::HashMap, fs, process::ExitCode};
8
9const NS_TYPES: &[&str] =
10 &["mnt", "net", "ipc", "user", "pid", "uts", "cgroup", "time"];
11
12#[derive(Parser)]
13#[command(name = "lsns", about = "List system namespaces")]
14pub struct Args {
15 #[arg(short = 'J', long)]
17 json: bool,
18
19 #[arg(short, long)]
21 list: bool,
22
23 #[arg(short = 'n', long)]
25 noheadings: bool,
26
27 #[arg(short, long, value_delimiter = ',')]
29 output: Option<Vec<String>>,
30
31 #[arg(long)]
33 output_all: bool,
34
35 #[arg(short, long)]
37 raw: bool,
38
39 #[arg(short = 't', long = "type")]
41 ns_type: Option<String>,
42
43 #[arg(short = 'p', long = "task")]
45 task: Option<u32>,
46
47 #[arg()]
49 namespace: Option<u64>,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53enum Col {
54 Ns,
55 Type,
56 Nprocs,
57 Pid,
58 Ppid,
59 Command,
60 Uid,
61 User,
62}
63
64impl Col {
65 fn name(self) -> &'static str {
66 match self {
67 Col::Ns => "NS",
68 Col::Type => "TYPE",
69 Col::Nprocs => "NPROCS",
70 Col::Pid => "PID",
71 Col::Ppid => "PPID",
72 Col::Command => "COMMAND",
73 Col::Uid => "UID",
74 Col::User => "USER",
75 }
76 }
77
78 fn whint(self) -> WidthHint {
79 match self {
80 Col::Ns => WidthHint::Fixed(10),
81 Col::Type => WidthHint::Fixed(6),
82 Col::Nprocs => WidthHint::Fixed(6),
83 Col::Pid => WidthHint::Fixed(6),
84 Col::Ppid => WidthHint::Fixed(6),
85 Col::Command => WidthHint::Auto,
86 Col::Uid => WidthHint::Fixed(6),
87 Col::User => WidthHint::Fixed(8),
88 }
89 }
90
91 fn is_right(self) -> bool {
92 matches!(
93 self,
94 Col::Ns | Col::Nprocs | Col::Pid | Col::Ppid | Col::Uid
95 )
96 }
97
98 fn from_name(name: &str) -> Option<Self> {
99 match name.to_uppercase().as_str() {
100 "NS" => Some(Col::Ns),
101 "TYPE" => Some(Col::Type),
102 "NPROCS" => Some(Col::Nprocs),
103 "PID" => Some(Col::Pid),
104 "PPID" => Some(Col::Ppid),
105 "COMMAND" => Some(Col::Command),
106 "UID" => Some(Col::Uid),
107 "USER" => Some(Col::User),
108 _ => None,
109 }
110 }
111}
112
113const DEFAULT_COLUMNS: &[Col] = &[
114 Col::Ns,
115 Col::Type,
116 Col::Nprocs,
117 Col::Pid,
118 Col::User,
119 Col::Command,
120];
121
122const ALL_COLUMNS: &[Col] = &[
123 Col::Ns,
124 Col::Type,
125 Col::Nprocs,
126 Col::Pid,
127 Col::Ppid,
128 Col::Command,
129 Col::Uid,
130 Col::User,
131];
132
133#[derive(Debug)]
134struct NsInfo {
135 ino: u64,
136 ns_type: String,
137 nprocs: u32,
138 pid: u32,
139 ppid: u32,
140 uid: u32,
141 user: String,
142 command: String,
143}
144
145fn read_proc_pid(pid: u32) -> Option<(u32, u32, String)> {
146 let stat = fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
147 let comm_end = stat.rfind(')')?;
150 let after_comm = &stat[comm_end + 2..]; let fields: Vec<&str> = after_comm.split_whitespace().collect();
152 let ppid: u32 = fields.get(1)?.parse().ok()?;
154
155 let cmdline = fs::read_to_string(format!("/proc/{pid}/cmdline"))
156 .ok()
157 .map(|s| s.replace('\0', " ").trim().to_string())
158 .unwrap_or_default();
159
160 let command = if cmdline.is_empty() {
161 let comm_start = stat.find('(')? + 1;
163 format!("[{}]", &stat[comm_start..comm_end])
164 } else {
165 cmdline
166 };
167
168 Some((ppid, 0, command))
169}
170
171fn get_uid(pid: u32) -> u32 {
172 fs::read_to_string(format!("/proc/{pid}/status"))
173 .ok()
174 .and_then(|s| {
175 for line in s.lines() {
176 if let Some(rest) = line.strip_prefix("Uid:") {
177 return rest.split_whitespace().next()?.parse().ok();
178 }
179 }
180 None
181 })
182 .unwrap_or(0)
183}
184
185fn uid_to_name(uid: u32) -> String {
186 fs::read_to_string("/etc/passwd")
187 .ok()
188 .and_then(|content| {
189 for line in content.lines() {
190 let fields: Vec<&str> = line.split(':').collect();
191 if fields.len() >= 3
192 && let Ok(u) = fields[2].parse::<u32>()
193 && u == uid
194 {
195 return Some(fields[0].to_string());
196 }
197 }
198 None
199 })
200 .unwrap_or_else(|| uid.to_string())
201}
202
203fn read_ns_inode(pid: u32, ns_type: &str) -> Option<u64> {
204 let link = fs::read_link(format!("/proc/{pid}/ns/{ns_type}")).ok()?;
205 let s = link.to_str()?;
207 let start = s.find('[')? + 1;
208 let end = s.find(']')?;
209 s[start..end].parse().ok()
210}
211
212fn scan_namespaces(
213 type_filter: Option<&str>,
214 task_filter: Option<u32>,
215 ns_filter: Option<u64>,
216) -> Vec<NsInfo> {
217 let mut ns_map: HashMap<(String, u64), NsInfo> = HashMap::new();
219
220 let pids: Vec<u32> = if let Some(pid) = task_filter {
221 vec![pid]
222 } else {
223 fs::read_dir("/proc")
224 .ok()
225 .map(|entries| {
226 entries
227 .flatten()
228 .filter_map(|e| e.file_name().to_str()?.parse::<u32>().ok())
229 .collect()
230 })
231 .unwrap_or_default()
232 };
233
234 let types: Vec<&str> = if let Some(t) = type_filter {
235 NS_TYPES.iter().copied().filter(|&ns| ns == t).collect()
236 } else {
237 NS_TYPES.to_vec()
238 };
239
240 for &pid in &pids {
241 for &ns_type in &types {
242 let ino = match read_ns_inode(pid, ns_type) {
243 Some(i) => i,
244 None => continue,
245 };
246
247 if let Some(filter) = ns_filter
248 && ino != filter
249 {
250 continue;
251 }
252
253 let key = (ns_type.to_string(), ino);
254 let entry = ns_map.entry(key).or_insert_with(|| {
255 let uid = get_uid(pid);
256 let (ppid, _, command) =
257 read_proc_pid(pid).unwrap_or((0, 0, String::new()));
258 NsInfo {
259 ino,
260 ns_type: ns_type.to_string(),
261 nprocs: 0,
262 pid,
263 ppid,
264 uid,
265 user: uid_to_name(uid),
266 command,
267 }
268 });
269 entry.nprocs += 1;
270
271 if pid < entry.pid {
273 let uid = get_uid(pid);
274 let (ppid, _, command) =
275 read_proc_pid(pid).unwrap_or((0, 0, String::new()));
276 entry.pid = pid;
277 entry.ppid = ppid;
278 entry.uid = uid;
279 entry.user = uid_to_name(uid);
280 entry.command = command;
281 }
282 }
283 }
284
285 let mut result: Vec<NsInfo> = ns_map.into_values().collect();
286 result.sort_by_key(|ns| ns.ino);
287 result
288}
289
290pub fn run(args: Args) -> ExitCode {
291 let columns = if args.output_all {
292 ALL_COLUMNS.to_vec()
293 } else if let Some(ref names) = args.output {
294 let mut cols = Vec::new();
295 for name in names {
296 match Col::from_name(name.trim()) {
297 Some(c) => cols.push(c),
298 None => {
299 eprintln!("lsns: unknown column: {name}");
300 return ExitCode::FAILURE;
301 }
302 }
303 }
304 cols
305 } else {
306 DEFAULT_COLUMNS.to_vec()
307 };
308
309 let type_filter = args.ns_type.as_deref();
310 let namespaces = scan_namespaces(type_filter, args.task, args.namespace);
311
312 let mut table = Table::new();
313 table.name_set("namespaces");
314
315 if args.json {
316 table.output_mode_set(OutputMode::Json);
317 } else if args.raw {
318 table.output_mode_set(OutputMode::Raw);
319 }
320
321 if args.noheadings {
322 table.headings_set(false);
323 }
324
325 for col in &columns {
326 let idx = table.new_column(col.name());
327 table.column_mut(idx).unwrap().width_hint_set(col.whint());
328 if col.is_right() {
329 table.column_mut(idx).unwrap().right_set(true);
330 }
331 }
332
333 for ns in &namespaces {
334 let line_id = table.new_line(None);
335 let line = table.line_mut(line_id);
336
337 for (ci, col) in columns.iter().enumerate() {
338 let val = match col {
339 Col::Ns => ns.ino.to_string(),
340 Col::Type => ns.ns_type.clone(),
341 Col::Nprocs => ns.nprocs.to_string(),
342 Col::Pid => ns.pid.to_string(),
343 Col::Ppid => ns.ppid.to_string(),
344 Col::Command => ns.command.clone(),
345 Col::Uid => ns.uid.to_string(),
346 Col::User => ns.user.clone(),
347 };
348 line.data_set(ci, &val);
349 }
350 }
351
352 let stdout = std::io::stdout();
353 let mut out = stdout.lock();
354 if let Err(e) = print_table(&table, &mut out) {
355 eprintln!("lsns: {e}");
356 return ExitCode::FAILURE;
357 }
358
359 ExitCode::SUCCESS
360}