diff --git a/sys/ipcrm.rs b/sys/ipcrm.rs index 6b8f57847..5df1cd452 100644 --- a/sys/ipcrm.rs +++ b/sys/ipcrm.rs @@ -25,53 +25,56 @@ use libc::{semctl, semget, shmctl, shmget, shmid_ds}; struct Args { #[arg( short = 's', - long, - help = gettext("Remove the shared memory identifier semid from the system") + action = clap::ArgAction::Append, + help = gettext("Remove the semaphore identifier semid from the system") )] - semid: Option, + semid: Vec, #[arg( short = 'S', - long, - help = gettext("Remove the shared memory identifier, created with key semkey, from the system") + action = clap::ArgAction::Append, + help = gettext("Remove the semaphore identifier, created with key semkey, from the system") )] - semkey: Option, + semkey: Vec, #[arg( short = 'm', - long, + action = clap::ArgAction::Append, help = gettext("Remove the shared memory identifier shmid from the system") )] - shmid: Option, + shmid: Vec, #[arg( short = 'M', - long, + action = clap::ArgAction::Append, help = gettext("Remove the shared memory identifier, created with key shmkey, from the system") )] - shmkey: Option, + shmkey: Vec, #[cfg(not(target_os = "macos"))] #[arg( short = 'q', - long, + action = clap::ArgAction::Append, help = gettext("Remove the message queue identifier msgid from the system") )] - msgid: Option, + msgid: Vec, #[cfg(not(target_os = "macos"))] #[arg( short = 'Q', - long, + action = clap::ArgAction::Append, help = gettext("Remove the message queue identifier, created with key msgkey, from the system") )] - msgkey: Option, + msgkey: Vec, } #[cfg(not(target_os = "macos"))] fn msg_key_lookup(msgkey: i32) -> io::Result { if msgkey == libc::IPC_PRIVATE { - return Err(Error::new(ErrorKind::Other, "Invalid key")); + return Err(Error::new( + ErrorKind::InvalidInput, + gettext("invalid key: IPC_PRIVATE"), + )); } let res: i32 = unsafe { msgget(msgkey, 0) }; @@ -83,7 +86,7 @@ fn msg_key_lookup(msgkey: i32) -> io::Result { } #[cfg(not(target_os = "macos"))] -fn msg_rm(msgid: i32) -> io::Result { +fn msg_rm(msgid: i32) -> io::Result<()> { let res: i32 = unsafe { msgctl( msgid, @@ -95,13 +98,16 @@ fn msg_rm(msgid: i32) -> io::Result { if res < 0 { Err(io::Error::last_os_error()) } else { - Ok(res) + Ok(()) } } fn shm_key_lookup(shmkey: i32) -> io::Result { if shmkey == libc::IPC_PRIVATE { - return Err(Error::new(ErrorKind::Other, "Invalid key")); + return Err(Error::new( + ErrorKind::InvalidInput, + gettext("invalid key: IPC_PRIVATE"), + )); } let res: i32 = unsafe { shmget(shmkey, 0, 0) }; @@ -112,7 +118,7 @@ fn shm_key_lookup(shmkey: i32) -> io::Result { } } -fn shm_rm(shmid: i32) -> io::Result { +fn shm_rm(shmid: i32) -> io::Result<()> { let res: i32 = unsafe { shmctl( shmid, @@ -124,13 +130,16 @@ fn shm_rm(shmid: i32) -> io::Result { if res < 0 { Err(io::Error::last_os_error()) } else { - Ok(res) + Ok(()) } } fn sem_key_lookup(semkey: i32) -> io::Result { if semkey == libc::IPC_PRIVATE { - return Err(Error::new(ErrorKind::Other, "Invalid key")); + return Err(Error::new( + ErrorKind::InvalidInput, + gettext("invalid key: IPC_PRIVATE"), + )); } let res: i32 = unsafe { semget(semkey, 0, 0) }; @@ -141,16 +150,15 @@ fn sem_key_lookup(semkey: i32) -> io::Result { } } -// Define the union semun as per your requirements +// Define the union semun as per POSIX requirements #[repr(C)] union semun { val: c_int, // for SETVAL buf: *mut libc::semid_ds, // for IPC_STAT and IPC_SET array: *mut c_ushort, // for GETALL and SETALL - // Depending on your platform, you might need to add other fields as well } -fn sem_rm(semid: i32) -> io::Result { +fn sem_rm(semid: i32) -> io::Result<()> { let arg = semun { val: 0 }; let res: i32 = unsafe { semctl(semid, 0, libc::IPC_RMID, arg) }; @@ -158,42 +166,105 @@ fn sem_rm(semid: i32) -> io::Result { if res < 0 { Err(io::Error::last_os_error()) } else { - Ok(res) + Ok(()) } } -fn remove_ipcs(args: &Args) -> io::Result<()> { - // remove semaphores - if let Some(semkey) = args.semkey { - let semid = sem_key_lookup(semkey)?; - sem_rm(semid)?; +fn remove_ipcs(args: &Args) -> i32 { + let mut exit_code = 0; + + // Remove semaphores by key + for semkey in &args.semkey { + match sem_key_lookup(*semkey) { + Ok(semid) => { + if let Err(e) = sem_rm(semid) { + eprintln!("ipcrm: {}: {}", gettext("semaphore key"), e); + exit_code = 1; + } + } + Err(e) => { + eprintln!( + "ipcrm: {}: 0x{:x}: {}", + gettext("semaphore key"), + *semkey, + e + ); + exit_code = 1; + } + } } - if let Some(semid) = args.semid { - sem_rm(semid)?; + + // Remove semaphores by ID + for semid in &args.semid { + if let Err(e) = sem_rm(*semid) { + eprintln!("ipcrm: {}: {}: {}", gettext("semaphore id"), semid, e); + exit_code = 1; + } } - // remove shared memory segments - if let Some(shmkey) = args.shmkey { - let shmid = shm_key_lookup(shmkey)?; - shm_rm(shmid)?; + // Remove shared memory segments by key + for shmkey in &args.shmkey { + match shm_key_lookup(*shmkey) { + Ok(shmid) => { + if let Err(e) = shm_rm(shmid) { + eprintln!("ipcrm: {}: {}", gettext("shared memory key"), e); + exit_code = 1; + } + } + Err(e) => { + eprintln!( + "ipcrm: {}: 0x{:x}: {}", + gettext("shared memory key"), + *shmkey, + e + ); + exit_code = 1; + } + } } - if let Some(shmid) = args.shmid { - shm_rm(shmid)?; + + // Remove shared memory segments by ID + for shmid in &args.shmid { + if let Err(e) = shm_rm(*shmid) { + eprintln!("ipcrm: {}: {}: {}", gettext("shared memory id"), shmid, e); + exit_code = 1; + } } - // remove message queues + // Remove message queues (Linux only) #[cfg(not(target_os = "macos"))] { - if let Some(msgkey) = args.msgkey { - let msgid = msg_key_lookup(msgkey)?; - msg_rm(msgid)?; + // Remove message queues by key + for msgkey in &args.msgkey { + match msg_key_lookup(*msgkey) { + Ok(msgid) => { + if let Err(e) = msg_rm(msgid) { + eprintln!("ipcrm: {}: {}", gettext("message queue key"), e); + exit_code = 1; + } + } + Err(e) => { + eprintln!( + "ipcrm: {}: 0x{:x}: {}", + gettext("message queue key"), + *msgkey, + e + ); + exit_code = 1; + } + } } - if let Some(msgid) = args.msgid { - msg_rm(msgid)?; + + // Remove message queues by ID + for msgid in &args.msgid { + if let Err(e) = msg_rm(*msgid) { + eprintln!("ipcrm: {}: {}: {}", gettext("message queue id"), msgid, e); + exit_code = 1; + } } } - Ok(()) + exit_code } fn main() -> Result<(), Box> { @@ -203,12 +274,7 @@ fn main() -> Result<(), Box> { let args = Args::parse(); - let mut exit_code = 0; - - if let Err(e) = remove_ipcs(&args) { - exit_code = 1; - eprintln!("{}", e); - } + let exit_code = remove_ipcs(&args); std::process::exit(exit_code) } diff --git a/sys/ipcs.rs b/sys/ipcs.rs index e4fa40445..dae3f44d1 100644 --- a/sys/ipcs.rs +++ b/sys/ipcs.rs @@ -7,48 +7,45 @@ // SPDX-License-Identifier: MIT // -use chrono::Local; +use chrono::{Local, TimeZone}; use clap::Parser; use gettextrs::{bind_textdomain_codeset, gettext, setlocale, textdomain, LocaleCategory}; +use std::ffi::CStr; + +#[cfg(target_os = "linux")] +use std::io::{self, BufRead}; -/// ipcs - report XSI interprocess communication facilities status #[derive(Parser)] -#[command(version, about)] +#[command( + version, + about = gettext("ipcs - report XSI interprocess communication facilities status") +)] struct Args { - /// Write information about active message queues. - #[arg(short = 'q', long = "queues")] + #[arg(short = 'q', long = "queues", help = gettext("Write information about active message queues"))] message_queues: bool, - /// Write information about active shared memory segments. - #[arg(short = 'm', long = "shmems")] + #[arg(short = 'm', long = "shmems", help = gettext("Write information about active shared memory segments"))] shared_memory: bool, - /// Write information about active semaphore sets. - #[arg(short, long)] + #[arg(short, long, help = gettext("Write information about active semaphore sets"))] semaphores: bool, - /// Use all print options. - #[arg(short, long)] + #[arg(short, long, help = gettext("Use all print options (-b, -c, -o, -p, -t)"))] all: bool, - /// Write information on maximum allowable size. - #[arg(short = 'b', long = "max-size")] + #[arg(short = 'b', long = "max-size", help = gettext("Write information on maximum allowable size"))] max_size: bool, - /// Write creator's user name and group name. - #[arg(short, long)] + #[arg(short, long, help = gettext("Write creator's user name and group name"))] creator: bool, - /// Write information on outstanding usage. - #[arg(short, long)] + #[arg(short, long, help = gettext("Write information on outstanding usage"))] outstanding: bool, - /// Write process number information. - #[arg(short, long)] + #[arg(short, long, help = gettext("Write process number information"))] pid: bool, - /// Write time information. - #[arg(short, long)] + #[arg(short, long, help = gettext("Write time information"))] time: bool, } @@ -66,184 +63,565 @@ fn enable_all_facilities(args: &mut Args) { args.semaphores = true; } -fn display_message_queues(_args: &Args) { - #[cfg(not(target_os = "macos"))] - use std::ffi::CStr; - - println!("{}", gettext("Message Queues:")); - println!("T ID KEY MODE OWNER GROUP"); +/// Get username from UID, returning UID as string if not found +fn get_username(uid: u32) -> String { + unsafe { + let pw = libc::getpwuid(uid); + if pw.is_null() { + uid.to_string() + } else { + CStr::from_ptr((*pw).pw_name) + .to_str() + .unwrap_or(&uid.to_string()) + .to_string() + } + } +} - #[cfg(not(target_os = "macos"))] - { - use libc::{msgctl, msqid_ds, IPC_STAT}; +/// Get group name from GID, returning GID as string if not found +fn get_groupname(gid: u32) -> String { + unsafe { + let gr = libc::getgrgid(gid); + if gr.is_null() { + gid.to_string() + } else { + CStr::from_ptr((*gr).gr_name) + .to_str() + .unwrap_or(&gid.to_string()) + .to_string() + } + } +} - let mut msqid: i32 = 0; - let mut msg_ds: msqid_ds = unsafe { std::mem::zeroed() }; +/// Truncate a string to at most `max_chars` characters (not bytes). +/// This is safe for UTF-8 strings with multi-byte characters. +fn truncate_to_chars(s: &str, max_chars: usize) -> &str { + match s.char_indices().nth(max_chars) { + Some((idx, _)) => &s[..idx], + None => s, + } +} - loop { - if unsafe { msgctl(msqid, IPC_STAT, &mut msg_ds) } == -1 { - break; - } +/// Format permission mode as 11-character string per POSIX +/// Format: [S-][RC-][rwx][rwx][rwx][ +] +/// - First char: S if process waiting on msgsnd, else - +/// - Second char: R if process waiting on msgrcv, C if shm cleared on attach, else - +/// - Chars 3-11: owner/group/other permissions (r/w/a for read/write/alter) +/// - Char 12: space or + for additional access control +fn format_mode(mode: u16, facility: char, _waiting_send: bool, _waiting_recv: bool) -> String { + // First two special characters + let c1 = '-'; // We don't have waiting info in current implementation + let c2 = match facility { + 'm' => '-', // Could be 'C' for SHM_DEST + _ => '-', + }; + + // Permission bits + let owner_r = if mode & 0o400 != 0 { 'r' } else { '-' }; + let owner_w = if mode & 0o200 != 0 { 'w' } else { '-' }; + let owner_a = if mode & 0o100 != 0 { 'a' } else { '-' }; + + let group_r = if mode & 0o040 != 0 { 'r' } else { '-' }; + let group_w = if mode & 0o020 != 0 { 'w' } else { '-' }; + let group_a = if mode & 0o010 != 0 { 'a' } else { '-' }; + + let other_r = if mode & 0o004 != 0 { 'r' } else { '-' }; + let other_w = if mode & 0o002 != 0 { 'w' } else { '-' }; + let other_a = if mode & 0o001 != 0 { 'a' } else { '-' }; + + format!( + "{}{}{}{}{}{}{}{}{}{}{}", + c1, c2, owner_r, owner_w, owner_a, group_r, group_w, group_a, other_r, other_w, other_a + ) +} - let key = msg_ds.msg_perm.__key; // Ensure the correct field name for your system - let mode = msg_ds.msg_perm.mode; - let uid = msg_ds.msg_perm.uid; - let gid = msg_ds.msg_perm.gid; - - let owner = unsafe { - CStr::from_ptr(libc::getpwuid(uid).as_ref().unwrap().pw_name) - .to_str() - .unwrap() - }; - let group = unsafe { - CStr::from_ptr(libc::getgrgid(gid).as_ref().unwrap().gr_name) - .to_str() - .unwrap() - }; - - let mode_str = format!( - "{}{}{}", - if mode & 0o400 != 0 { "r" } else { "-" }, - if mode & 0o200 != 0 { "w" } else { "-" }, - if mode & 0o100 != 0 { "a" } else { "-" } - ); - - println!( - "q {:<5} 0x{:08x} {:<10} {:<8} {:<8}", - msqid, key, mode_str, owner, group - ); - - msqid += 1; +/// Format time as HH:MM:SS or " no-entry" per POSIX +fn format_time(timestamp: i64) -> String { + if timestamp == 0 { + " no-entry".to_string() + } else { + match Local.timestamp_opt(timestamp, 0).single() { + Some(dt) => dt.format("%H:%M:%S").to_string(), + None => " no-entry".to_string(), // Invalid or ambiguous timestamp } } +} - #[cfg(target_os = "macos")] - { - println!("{}", gettext("Message Queue facility not in system.")); - } +/// Get current date in POSIX locale format (matching `date` command) +fn get_current_date() -> String { + let now = Local::now(); + now.format("%a %b %e %H:%M:%S %Z %Y").to_string() } -fn display_shared_memory(_args: &Args) { - use libc::{shmctl, shmid_ds, IPC_STAT}; - use std::ffi::CStr; +// ============================================================================ +// Linux-specific IPC enumeration via /proc/sysvipc +// ============================================================================ - #[cfg(target_os = "macos")] - const SHM_INFO: libc::c_int = 14; // SHM_INFO is typically 14 in Linux but this is not standard +#[cfg(target_os = "linux")] +fn read_proc_msg() -> io::Result> { + let mut entries = Vec::new(); + let file = std::fs::File::open("/proc/sysvipc/msg")?; + let reader = io::BufReader::new(file); - #[cfg(not(target_os = "macos"))] - const SHM_INFO: libc::c_int = 14; + for (i, line) in reader.lines().enumerate() { + if i == 0 { + continue; + } // Skip header + let line = line?; + let fields: Vec<&str> = line.split_whitespace().collect(); + if fields.len() >= 14 { + entries.push(MsgQueueInfo { + key: i32::from_str_radix(fields[0], 10).unwrap_or(0), + msqid: fields[1].parse().unwrap_or(0), + perms: fields[2].parse().unwrap_or(0), + cbytes: fields[3].parse().unwrap_or(0), + qnum: fields[4].parse().unwrap_or(0), + lspid: fields[5].parse().unwrap_or(0), + lrpid: fields[6].parse().unwrap_or(0), + uid: fields[7].parse().unwrap_or(0), + gid: fields[8].parse().unwrap_or(0), + cuid: fields[9].parse().unwrap_or(0), + cgid: fields[10].parse().unwrap_or(0), + stime: fields[11].parse().unwrap_or(0), + rtime: fields[12].parse().unwrap_or(0), + ctime: fields[13].parse().unwrap_or(0), + }); + } + } + Ok(entries) +} - let mut shmbuf: shmid_ds = unsafe { std::mem::zeroed() }; +#[cfg(target_os = "linux")] +fn read_proc_shm() -> io::Result> { + let mut entries = Vec::new(); + let file = std::fs::File::open("/proc/sysvipc/shm")?; + let reader = io::BufReader::new(file); - let maxid = unsafe { shmctl(0, SHM_INFO, &mut shmbuf) }; - if maxid < 0 { - println!("{}", gettext("Shared Memory facility not in system.")); - return; + for (i, line) in reader.lines().enumerate() { + if i == 0 { + continue; + } // Skip header + let line = line?; + let fields: Vec<&str> = line.split_whitespace().collect(); + if fields.len() >= 14 { + entries.push(ShmInfo { + key: i32::from_str_radix(fields[0], 10).unwrap_or(0), + shmid: fields[1].parse().unwrap_or(0), + perms: fields[2].parse().unwrap_or(0), + size: fields[3].parse().unwrap_or(0), + cpid: fields[4].parse().unwrap_or(0), + lpid: fields[5].parse().unwrap_or(0), + nattch: fields[6].parse().unwrap_or(0), + uid: fields[7].parse().unwrap_or(0), + gid: fields[8].parse().unwrap_or(0), + cuid: fields[9].parse().unwrap_or(0), + cgid: fields[10].parse().unwrap_or(0), + atime: fields[11].parse().unwrap_or(0), + dtime: fields[12].parse().unwrap_or(0), + ctime: fields[13].parse().unwrap_or(0), + }); + } } + Ok(entries) +} - println!("{}", gettext("Shared Memory:")); - println!("T ID KEY MODE OWNER GROUP"); +#[cfg(target_os = "linux")] +fn read_proc_sem() -> io::Result> { + let mut entries = Vec::new(); + let file = std::fs::File::open("/proc/sysvipc/sem")?; + let reader = io::BufReader::new(file); - for shmid in 0..=maxid { - if unsafe { shmctl(shmid, IPC_STAT, &mut shmbuf) } == -1 { + for (i, line) in reader.lines().enumerate() { + if i == 0 { continue; + } // Skip header + let line = line?; + let fields: Vec<&str> = line.split_whitespace().collect(); + if fields.len() >= 10 { + entries.push(SemInfo { + key: i32::from_str_radix(fields[0], 10).unwrap_or(0), + semid: fields[1].parse().unwrap_or(0), + perms: fields[2].parse().unwrap_or(0), + nsems: fields[3].parse().unwrap_or(0), + uid: fields[4].parse().unwrap_or(0), + gid: fields[5].parse().unwrap_or(0), + cuid: fields[6].parse().unwrap_or(0), + cgid: fields[7].parse().unwrap_or(0), + otime: fields[8].parse().unwrap_or(0), + ctime: fields[9].parse().unwrap_or(0), + }); } + } + Ok(entries) +} - #[cfg(target_os = "macos")] - let key = shmbuf.shm_perm._key; // Check for the correct field name on your system - #[cfg(not(target_os = "macos"))] - let key = shmbuf.shm_perm.__key; // Check for the correct field name on your system - let mode = shmbuf.shm_perm.mode; - let uid = shmbuf.shm_perm.uid; - let gid = shmbuf.shm_perm.gid; +// ============================================================================ +// macOS-specific IPC enumeration +// ============================================================================ - let owner = unsafe { - CStr::from_ptr(libc::getpwuid(uid).as_ref().unwrap().pw_name) - .to_str() - .unwrap() - }; - let group = unsafe { - CStr::from_ptr(libc::getgrgid(gid).as_ref().unwrap().gr_name) - .to_str() - .unwrap() - }; - - let mode_str = format!( - "{}{}{}", - if mode & 0o400 != 0 { "r" } else { "-" }, - if mode & 0o200 != 0 { "w" } else { "-" }, - if mode & 0o100 != 0 { "a" } else { "-" } - ); +// Maximum IPC ID to iterate through on macOS. +// macOS doesn't provide /proc/sysvipc like Linux, so we must iterate. +// 32768 is a reasonable upper bound that balances coverage vs performance. +#[cfg(target_os = "macos")] +const MACOS_MAX_IPC_ID: i32 = 32768; - println!( - "m {:<5} 0x{:08x} {:<10} {:<8} {:<8}", - shmid, key, mode_str, owner, group - ); +#[cfg(target_os = "macos")] +fn read_macos_shm() -> Vec { + use libc::{shmctl, shmid_ds, IPC_STAT}; + + let mut entries = Vec::new(); + let mut shmbuf: shmid_ds = unsafe { std::mem::zeroed() }; + + for shmid in 0..MACOS_MAX_IPC_ID { + if unsafe { shmctl(shmid, IPC_STAT, &mut shmbuf) } == 0 { + entries.push(ShmInfo { + key: shmbuf.shm_perm._key, + shmid, + perms: shmbuf.shm_perm.mode, + size: shmbuf.shm_segsz as u64, + cpid: shmbuf.shm_cpid, + lpid: shmbuf.shm_lpid, + nattch: shmbuf.shm_nattch as u32, + uid: shmbuf.shm_perm.uid, + gid: shmbuf.shm_perm.gid, + cuid: shmbuf.shm_perm.cuid, + cgid: shmbuf.shm_perm.cgid, + atime: shmbuf.shm_atime, + dtime: shmbuf.shm_dtime, + ctime: shmbuf.shm_ctime, + }); + } } + entries } -fn display_semaphores(_args: &Args) { +#[cfg(target_os = "macos")] +fn read_macos_sem() -> Vec { use libc::{semctl, semid_ds, IPC_STAT}; - use std::ffi::CStr; - let mut semid: i32 = 0; - let mut sem_ds: semid_ds = unsafe { std::mem::zeroed() }; + let mut entries = Vec::new(); + let mut sembuf: semid_ds = unsafe { std::mem::zeroed() }; + + for semid in 0..MACOS_MAX_IPC_ID { + if unsafe { semctl(semid, 0, IPC_STAT, &mut sembuf) } == 0 { + entries.push(SemInfo { + key: sembuf.sem_perm._key, + semid, + perms: sembuf.sem_perm.mode, + nsems: sembuf.sem_nsems as u32, + uid: sembuf.sem_perm.uid, + gid: sembuf.sem_perm.gid, + cuid: sembuf.sem_perm.cuid, + cgid: sembuf.sem_perm.cgid, + otime: sembuf.sem_otime, + ctime: sembuf.sem_ctime, + }); + } + } + entries +} + +// ============================================================================ +// IPC info structures +// ============================================================================ + +#[cfg(target_os = "linux")] +#[derive(Debug)] +struct MsgQueueInfo { + key: i32, + msqid: i32, + perms: u16, + cbytes: u64, + qnum: u64, + lspid: i32, + lrpid: i32, + uid: u32, + gid: u32, + cuid: u32, + cgid: u32, + stime: i64, + rtime: i64, + ctime: i64, +} + +#[derive(Debug)] +struct ShmInfo { + key: i32, + shmid: i32, + perms: u16, + size: u64, + cpid: i32, + lpid: i32, + nattch: u32, + uid: u32, + gid: u32, + cuid: u32, + cgid: u32, + atime: i64, + dtime: i64, + ctime: i64, +} + +#[derive(Debug)] +struct SemInfo { + key: i32, + semid: i32, + perms: u16, + nsems: u32, + uid: u32, + gid: u32, + cuid: u32, + cgid: u32, + otime: i64, + ctime: i64, +} + +// ============================================================================ +// Display functions +// ============================================================================ + +fn display_message_queues(_args: &Args) { + println!(); - println!("{}", gettext("Semaphores:")); - println!("T ID KEY MODE OWNER GROUP NSEMS"); + #[cfg(target_os = "linux")] + let args = _args; - loop { - if unsafe { semctl(semid, 0, IPC_STAT, &mut sem_ds) } == -1 { - break; + #[cfg(target_os = "linux")] + { + match read_proc_msg() { + Ok(entries) if !entries.is_empty() => { + // Build header + let mut header = String::from("T ID KEY MODE OWNER GROUP"); + if args.creator { + header.push_str(" CREATOR CGROUP"); + } + if args.outstanding { + header.push_str(" CBYTES QNUM"); + } + if args.max_size { + header.push_str(" QBYTES"); + } + if args.pid { + header.push_str(" LSPID LRPID"); + } + if args.time { + header.push_str(" STIME RTIME CTIME"); + } + println!("{}", header); + println!("{}:", gettext("Message Queues")); + + for entry in entries { + let mode_str = format_mode(entry.perms, 'q', false, false); + let owner = get_username(entry.uid); + let group = get_groupname(entry.gid); + + let mut line = format!( + "q {:>6} 0x{:08x} {:11} {:>8} {:>8}", + entry.msqid, + entry.key as u32, + mode_str, + truncate_to_chars(&owner, 8), + truncate_to_chars(&group, 8) + ); + + if args.creator { + let creator = get_username(entry.cuid); + let cgroup = get_groupname(entry.cgid); + line.push_str(&format!( + " {:>8} {:>8}", + truncate_to_chars(&creator, 8), + truncate_to_chars(&cgroup, 8) + )); + } + if args.outstanding { + line.push_str(&format!(" {:>8} {:>7}", entry.cbytes, entry.qnum)); + } + if args.max_size { + // Note: qbytes is not in /proc, would need sysctl + line.push_str(&format!(" {:>8}", "-")); + } + if args.pid { + line.push_str(&format!(" {:>8} {:>8}", entry.lspid, entry.lrpid)); + } + if args.time { + line.push_str(&format!( + " {:>9} {:>9} {:>9}", + format_time(entry.stime), + format_time(entry.rtime), + format_time(entry.ctime) + )); + } + + println!("{}", line); + } + } + _ => { + println!("{}", gettext("Message Queue facility not in system.")); + } } + } - #[cfg(not(target_os = "macos"))] - let key = sem_ds.sem_perm.__key; // Check for the correct field name on your system - #[cfg(target_os = "macos")] - let key = sem_ds.sem_perm._key; // Check for the correct field name on your system + #[cfg(target_os = "macos")] + { + println!("{}", gettext("Message Queue facility not in system.")); + } +} - let mode = sem_ds.sem_perm.mode; - let uid = sem_ds.sem_perm.uid; - let gid = sem_ds.sem_perm.gid; +fn display_shared_memory(args: &Args) { + println!(); - let owner = unsafe { - CStr::from_ptr(libc::getpwuid(uid).as_ref().unwrap().pw_name) - .to_str() - .unwrap() - }; - let group = unsafe { - CStr::from_ptr(libc::getgrgid(gid).as_ref().unwrap().gr_name) - .to_str() - .unwrap() - }; - - let mode_str = format!( - "{}{}{}", - if mode & 0o400 != 0 { "r" } else { "-" }, - if mode & 0o200 != 0 { "w" } else { "-" }, - if mode & 0o100 != 0 { "a" } else { "-" } - ); + #[cfg(target_os = "linux")] + let entries = read_proc_shm().unwrap_or_default(); + + #[cfg(target_os = "macos")] + let entries = read_macos_shm(); - println!( - "s {:<5} 0x{:08x} {:<10} {:<8} {:<8} {:<5}", - semid, key, mode_str, owner, group, sem_ds.sem_nsems + if entries.is_empty() { + println!("{}", gettext("Shared Memory facility not in system.")); + return; + } + + // Build header + let mut header = String::from("T ID KEY MODE OWNER GROUP"); + if args.creator { + header.push_str(" CREATOR CGROUP"); + } + if args.outstanding { + header.push_str(" NATTCH"); + } + if args.max_size { + header.push_str(" SEGSZ"); + } + if args.pid { + header.push_str(" CPID LPID"); + } + if args.time { + header.push_str(" ATIME DTIME CTIME"); + } + println!("{}", header); + println!("{}:", gettext("Shared Memory")); + + for entry in entries { + let mode_str = format_mode(entry.perms, 'm', false, false); + let owner = get_username(entry.uid); + let group = get_groupname(entry.gid); + + let mut line = format!( + "m {:>6} 0x{:08x} {:11} {:>8} {:>8}", + entry.shmid, + entry.key as u32, + mode_str, + truncate_to_chars(&owner, 8), + truncate_to_chars(&group, 8) ); - semid += 1; + if args.creator { + let creator = get_username(entry.cuid); + let cgroup = get_groupname(entry.cgid); + line.push_str(&format!( + " {:>8} {:>8}", + truncate_to_chars(&creator, 8), + truncate_to_chars(&cgroup, 8) + )); + } + if args.outstanding { + line.push_str(&format!(" {:>8}", entry.nattch)); + } + if args.max_size { + line.push_str(&format!(" {:>10}", entry.size)); + } + if args.pid { + line.push_str(&format!(" {:>8} {:>8}", entry.cpid, entry.lpid)); + } + if args.time { + line.push_str(&format!( + " {:>9} {:>9} {:>9}", + format_time(entry.atime), + format_time(entry.dtime), + format_time(entry.ctime) + )); + } + + println!("{}", line); } } -fn get_current_date() -> String { - // Retrieve the current date and time in a human-readable format - let now = Local::now(); - now.format("%Y-%m-%d %H:%M:%S").to_string() +fn display_semaphores(args: &Args) { + println!(); + + #[cfg(target_os = "linux")] + let entries = read_proc_sem().unwrap_or_default(); + + #[cfg(target_os = "macos")] + let entries = read_macos_sem(); + + if entries.is_empty() { + println!("{}", gettext("Semaphore facility not in system.")); + return; + } + + // Build header + let mut header = String::from("T ID KEY MODE OWNER GROUP"); + if args.creator { + header.push_str(" CREATOR CGROUP"); + } + if args.max_size { + header.push_str(" NSEMS"); + } + if args.time { + header.push_str(" OTIME CTIME"); + } + println!("{}", header); + println!("{}:", gettext("Semaphores")); + + for entry in entries { + let mode_str = format_mode(entry.perms, 's', false, false); + let owner = get_username(entry.uid); + let group = get_groupname(entry.gid); + + let mut line = format!( + "s {:>6} 0x{:08x} {:11} {:>8} {:>8}", + entry.semid, + entry.key as u32, + mode_str, + truncate_to_chars(&owner, 8), + truncate_to_chars(&group, 8) + ); + + if args.creator { + let creator = get_username(entry.cuid); + let cgroup = get_groupname(entry.cgid); + line.push_str(&format!( + " {:>8} {:>8}", + truncate_to_chars(&creator, 8), + truncate_to_chars(&cgroup, 8) + )); + } + if args.max_size { + line.push_str(&format!(" {:>8}", entry.nsems)); + } + if args.time { + line.push_str(&format!( + " {:>9} {:>9}", + format_time(entry.otime), + format_time(entry.ctime) + )); + } + + println!("{}", line); + } } fn display_ipc_status(args: &Args) { - println!( - "{}", - gettext!("IPC status from {} as of {}", "source", get_current_date()) - ); + // POSIX requires: "IPC status from %s as of %s\n", , + #[cfg(target_os = "linux")] + let source = "/proc/sysvipc"; + #[cfg(target_os = "macos")] + let source = "kernel"; + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + let source = "system"; + + println!("IPC status from {} as of {}", source, get_current_date()); if args.message_queues { display_message_queues(args); @@ -265,9 +643,8 @@ fn main() -> Result<(), Box> { let mut args = Args::parse(); - // Validate arguments and determine what to display + // -a enables all print options if args.all { - // Enable all options enable_all_options(&mut args); } diff --git a/sys/tests/ipcrm/mod.rs b/sys/tests/ipcrm/mod.rs new file mode 100644 index 000000000..c6a87e1b0 --- /dev/null +++ b/sys/tests/ipcrm/mod.rs @@ -0,0 +1,107 @@ +// +// Copyright (c) 2024 Jeff Garzik +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +use plib::testing::{run_test_with_checker, TestPlan}; +use std::process::Output; + +fn run_ipcrm_test(args: Vec<&str>, expected_exit_code: i32, check_fn: fn(&TestPlan, &Output)) { + let plan = TestPlan { + cmd: "ipcrm".to_string(), + args: args.iter().map(|&s| s.to_string()).collect(), + stdin_data: String::new(), + expected_out: String::new(), + expected_err: String::new(), + expected_exit_code, + }; + + run_test_with_checker(plan, check_fn); +} + +fn check_empty_stdout(_: &TestPlan, output: &Output) { + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.is_empty(), + "Expected no stdout output, got: {}", + stdout + ); +} + +fn check_error_on_stderr(_: &TestPlan, output: &Output) { + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + !stderr.is_empty(), + "Expected error message on stderr, got none" + ); +} + +#[test] +fn ipcrm_no_args() { + // Running ipcrm without arguments should succeed with no output + // (no IPC objects to remove is not an error) + run_ipcrm_test(vec![], 0, check_empty_stdout); +} + +#[test] +fn ipcrm_invalid_semid() { + // Removing a non-existent semaphore ID should fail + // Using a very high ID that almost certainly doesn't exist + run_ipcrm_test(vec!["-s", "999999"], 1, check_error_on_stderr); +} + +#[test] +fn ipcrm_invalid_shmid() { + // Removing a non-existent shared memory ID should fail + run_ipcrm_test(vec!["-m", "999999"], 1, check_error_on_stderr); +} + +#[cfg(not(target_os = "macos"))] +#[test] +fn ipcrm_invalid_msgid() { + // Removing a non-existent message queue ID should fail (Linux only) + run_ipcrm_test(vec!["-q", "999999"], 1, check_error_on_stderr); +} + +#[test] +fn ipcrm_invalid_semkey() { + // Removing a non-existent semaphore by key should fail + run_ipcrm_test(vec!["-S", "999999"], 1, check_error_on_stderr); +} + +#[test] +fn ipcrm_invalid_shmkey() { + // Removing a non-existent shared memory by key should fail + run_ipcrm_test(vec!["-M", "999999"], 1, check_error_on_stderr); +} + +#[cfg(not(target_os = "macos"))] +#[test] +fn ipcrm_invalid_msgkey() { + // Removing a non-existent message queue by key should fail (Linux only) + run_ipcrm_test(vec!["-Q", "999999"], 1, check_error_on_stderr); +} + +#[test] +fn ipcrm_multiple_invalid_ids() { + // Multiple invalid IDs should all fail + run_ipcrm_test( + vec!["-s", "999998", "-s", "999999"], + 1, + check_error_on_stderr, + ); +} + +#[test] +fn ipcrm_mixed_types() { + // Mix of invalid semaphore and shared memory IDs + run_ipcrm_test( + vec!["-s", "999999", "-m", "999999"], + 1, + check_error_on_stderr, + ); +} diff --git a/sys/tests/ipcs/mod.rs b/sys/tests/ipcs/mod.rs new file mode 100644 index 000000000..736cf607a --- /dev/null +++ b/sys/tests/ipcs/mod.rs @@ -0,0 +1,133 @@ +// +// Copyright (c) 2024 Jeff Garzik +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +use plib::testing::{run_test_with_checker, TestPlan}; +use std::process::Output; + +fn run_ipcs_test(args: Vec<&str>, expected_exit_code: i32, check_fn: fn(&TestPlan, &Output)) { + let plan = TestPlan { + cmd: "ipcs".to_string(), + args: args.iter().map(|&s| s.to_string()).collect(), + stdin_data: String::new(), + expected_out: String::new(), + expected_err: String::new(), + expected_exit_code, + }; + + run_test_with_checker(plan, check_fn); +} + +fn check_output_contains_ipc_status(_: &TestPlan, output: &Output) { + let stdout = String::from_utf8_lossy(&output.stdout); + // POSIX requires the first line to be "IPC status from as of " + assert!( + stdout.contains("IPC status from"), + "Expected 'IPC status from' in output, got: {}", + stdout + ); +} + +fn check_output_contains_queues_section(_: &TestPlan, output: &Output) { + let stdout = String::from_utf8_lossy(&output.stdout); + // Should contain either message queue info or "not in system" message + assert!( + stdout.contains("Message Queue") || stdout.contains("message queue"), + "Expected message queue section in output, got: {}", + stdout + ); +} + +fn check_output_contains_shm_section(_: &TestPlan, output: &Output) { + let stdout = String::from_utf8_lossy(&output.stdout); + // Should contain either shared memory info or "not in system" message + assert!( + stdout.contains("Shared Memory") || stdout.contains("shared memory"), + "Expected shared memory section in output, got: {}", + stdout + ); +} + +fn check_output_contains_sem_section(_: &TestPlan, output: &Output) { + let stdout = String::from_utf8_lossy(&output.stdout); + // Should contain either semaphore info or "not in system" message + assert!( + stdout.contains("Semaphore") || stdout.contains("semaphore"), + "Expected semaphore section in output, got: {}", + stdout + ); +} + +fn check_output_nonempty(_: &TestPlan, output: &Output) { + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(!stdout.is_empty(), "Expected non-empty output"); +} + +#[test] +fn ipcs_no_args() { + // Running ipcs without arguments should show all facilities + run_ipcs_test(vec![], 0, check_output_contains_ipc_status); +} + +#[test] +fn ipcs_message_queues_only() { + run_ipcs_test(vec!["-q"], 0, check_output_contains_queues_section); +} + +#[test] +fn ipcs_shared_memory_only() { + run_ipcs_test(vec!["-m"], 0, check_output_contains_shm_section); +} + +#[test] +fn ipcs_semaphores_only() { + run_ipcs_test(vec!["-s"], 0, check_output_contains_sem_section); +} + +#[test] +fn ipcs_all_options() { + // -a enables all print options (-b, -c, -o, -p, -t) + run_ipcs_test(vec!["-a"], 0, check_output_nonempty); +} + +#[test] +fn ipcs_max_size_option() { + run_ipcs_test(vec!["-b"], 0, check_output_nonempty); +} + +#[test] +fn ipcs_creator_option() { + run_ipcs_test(vec!["-c"], 0, check_output_nonempty); +} + +#[test] +fn ipcs_outstanding_option() { + run_ipcs_test(vec!["-o"], 0, check_output_nonempty); +} + +#[test] +fn ipcs_pid_option() { + run_ipcs_test(vec!["-p"], 0, check_output_nonempty); +} + +#[test] +fn ipcs_time_option() { + run_ipcs_test(vec!["-t"], 0, check_output_nonempty); +} + +#[test] +fn ipcs_combined_facility_options() { + // Combine multiple facility options + run_ipcs_test(vec!["-q", "-m"], 0, check_output_nonempty); +} + +#[test] +fn ipcs_combined_print_options() { + // Combine multiple print options + run_ipcs_test(vec!["-b", "-c", "-t"], 0, check_output_nonempty); +} diff --git a/sys/tests/sys-tests.rs b/sys/tests/sys-tests.rs index e18f07d03..995f4f4d3 100644 --- a/sys/tests/sys-tests.rs +++ b/sys/tests/sys-tests.rs @@ -1 +1,3 @@ mod getconf; +mod ipcrm; +mod ipcs;