Skip to main content

linuxutils_system/
lsns.rs

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    /// Use JSON output format
16    #[arg(short = 'J', long)]
17    json: bool,
18
19    /// Use list format output
20    #[arg(short, long)]
21    list: bool,
22
23    /// Don't print headings
24    #[arg(short = 'n', long)]
25    noheadings: bool,
26
27    /// Output columns to print
28    #[arg(short, long, value_delimiter = ',')]
29    output: Option<Vec<String>>,
30
31    /// Output all columns
32    #[arg(long)]
33    output_all: bool,
34
35    /// Use raw output format
36    #[arg(short, long)]
37    raw: bool,
38
39    /// Namespace type filter
40    #[arg(short = 't', long = "type")]
41    ns_type: Option<String>,
42
43    /// Print namespaces for a specific PID
44    #[arg(short = 'p', long = "task")]
45    task: Option<u32>,
46
47    /// Optional namespace inode to show
48    #[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    // Parse: "pid (comm) state ppid ..."
148    // comm can contain spaces and parens, so find the last ')'.
149    let comm_end = stat.rfind(')')?;
150    let after_comm = &stat[comm_end + 2..]; // skip ") "
151    let fields: Vec<&str> = after_comm.split_whitespace().collect();
152    // fields[0] = state, fields[1] = ppid
153    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        // Kernel thread — use comm.
162        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    // Format: "type:[inode]"
206    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    // Map: (ns_type, inode) -> NsInfo
218    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            // Track the lowest PID.
272            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}