use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; use super::{ForegroundJob, ForegroundProcess, Signal}; /// Collect the foreground terminal job for a given child PID. pub fn foreground_job(child_pid: u32) -> Option { if child_pid == 0 { return None; } let fg_pgid = foreground_pgid(child_pid)?; let mut pids = vec![0i32; 4096]; let bytes = unsafe { libc::proc_listallpids( pids.as_mut_ptr() as *mut libc::c_void, (pids.len() * std::mem::size_of::()) as libc::c_int, ) }; if bytes <= 0 { return None; } let count = (bytes as usize) / std::mem::size_of::(); let mut processes = Vec::new(); for raw_pid in pids.into_iter().take(count) { if raw_pid <= 0 { continue; } let pid = raw_pid as u32; let Some(info) = process_bsdinfo(pid) else { continue; }; if info.pbi_pgid as u32 != fg_pgid { continue; } let Some(name) = comm_from_bsdinfo(&info) else { continue; }; processes.push(ForegroundProcess { pid, name, argv0: process_argv0_name(pid), cmdline: process_cmdline(pid), }); } if processes.is_empty() { return None; } Some(ForegroundJob { process_group_id: fg_pgid, processes, }) } /// Read `e_tpgid` (foreground process group of the controlling terminal) /// for the given PID. fn foreground_pgid(pid: u32) -> Option { let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() }; let size = std::mem::size_of::() as libc::c_int; let ret = unsafe { libc::proc_pidinfo( pid as libc::c_int, libc::PROC_PIDTBSDINFO, 0, &mut info as *mut _ as *mut libc::c_void, size, ) }; if ret != size { return None; } let fg = info.e_tpgid; if fg == 0 { None } else { Some(fg) } } /// Get the effective process name from `argv[0]` via `sysctl(KERN_PROCARGS2)`. /// /// This is the macOS equivalent of reading `/proc/{pid}/cmdline` on Linux. /// It reflects runtime title changes like Node.js `process.title = "pi"`. fn process_argv0_name(pid: u32) -> Option { let buf = kern_procargs2(pid)?; // Layout: [argc: i32] [exec_path\0] [padding\0...] [argv[0]\0] [argv[1]\0] ... if buf.len() < 4 { return None; } let argc = i32::from_ne_bytes([buf[0], buf[1], buf[2], buf[3]]); if argc < 1 { return None; } // Skip past exec_path and null padding to reach argv[0] let rest = &buf[4..]; let exec_end = rest.iter().position(|&b| b == 0)?; let mut pos = exec_end; while pos < rest.len() && rest[pos] == 0 { pos += 1; } if pos >= rest.len() { return None; } // Read argv[0] let argv0_end = rest[pos..] .iter() .position(|&b| b == 0) .unwrap_or(rest.len() - pos); let argv0 = std::str::from_utf8(&rest[pos..pos + argv0_end]).ok()?; if argv0.is_empty() { return None; } // Return basename (argv[0] may be a full path like "/usr/bin/node") let basename = Path::new(argv0).file_name()?.to_str()?; // Strip leading dash (login shells show as "-zsh") let name = basename.strip_prefix('-').unwrap_or(basename); if name.is_empty() { return None; } Some(name.to_string()) } /// Raw `sysctl(KERN_PROCARGS2)` call. Returns the full buffer. fn kern_procargs2(pid: u32) -> Option> { unsafe { let mut mib = [libc::CTL_KERN, libc::KERN_PROCARGS2, pid as libc::c_int]; // First call: query required buffer size let mut size: libc::size_t = 0; let ret = libc::sysctl( mib.as_mut_ptr(), 3, std::ptr::null_mut(), &mut size, std::ptr::null_mut(), 0, ); if ret != 0 || size == 0 { return None; } // Second call: read data let mut buf = vec![0u8; size]; let ret = libc::sysctl( mib.as_mut_ptr(), 3, buf.as_mut_ptr() as *mut libc::c_void, &mut size, std::ptr::null_mut(), 0, ); if ret != 0 { return None; } buf.truncate(size); Some(buf) } } /// Fallback: read `pbi_comm` from `proc_pidinfo(PROC_PIDTBSDINFO)`. /// /// This is the kernel-level short command name (like `node`, `zsh`). /// It does NOT reflect `process.title` changes. fn process_comm_name(pid: u32) -> Option { process_bsdinfo(pid).and_then(|info| comm_from_bsdinfo(&info)) } fn process_bsdinfo(pid: u32) -> Option { let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() }; let size = std::mem::size_of::() as libc::c_int; let ret = unsafe { libc::proc_pidinfo( pid as libc::c_int, libc::PROC_PIDTBSDINFO, 0, &mut info as *mut _ as *mut libc::c_void, size, ) }; (ret == size).then_some(info) } fn comm_from_bsdinfo(info: &libc::proc_bsdinfo) -> Option { let end = info .pbi_comm .iter() .position(|&b| b == 0) .unwrap_or(info.pbi_comm.len()); if end == 0 { return None; } let bytes: Vec = info.pbi_comm[..end].iter().map(|&b| b as u8).collect(); String::from_utf8(bytes).ok() } fn process_cmdline(pid: u32) -> Option { let buf = kern_procargs2(pid)?; if buf.len() < 4 { return None; } let argc = i32::from_ne_bytes([buf[0], buf[1], buf[2], buf[3]]); if argc < 1 { return None; } let rest = &buf[4..]; let mut start = 0usize; while start < rest.len() && rest[start] != 0 { start += 1; } while start < rest.len() && rest[start] == 0 { start += 1; } if start >= rest.len() { return None; } let parts: Vec = rest[start..] .split(|&b| b == 0) .filter(|part| !part.is_empty()) .map(|part| String::from_utf8_lossy(part).into_owned()) .collect(); (!parts.is_empty()).then(|| parts.join(" ")) } /// Get the current working directory of a process. /// /// Uses `proc_pidinfo(PROC_PIDVNODEPATHINFO)` to read `pvi_cdir.vip_path`. pub fn process_cwd(pid: u32) -> Option { if pid == 0 { return None; } let mut pathinfo: libc::proc_vnodepathinfo = unsafe { std::mem::zeroed() }; let size = std::mem::size_of::() as libc::c_int; let ret = unsafe { libc::proc_pidinfo( pid as libc::c_int, libc::PROC_PIDVNODEPATHINFO, 0, &mut pathinfo as *mut _ as *mut libc::c_void, size, ) }; if ret != size { return None; } // vip_path is [[c_char; 32]; 32] in libc (workaround for old Rust const generics). // Reinterpret as flat bytes (total MAXPATHLEN = 1024). let vip_path = unsafe { std::slice::from_raw_parts( pathinfo.pvi_cdir.vip_path.as_ptr() as *const u8, libc::MAXPATHLEN as usize, ) }; let nul = vip_path.iter().position(|&b| b == 0)?; if nul == 0 { return None; } Some(PathBuf::from(OsStr::from_bytes(&vip_path[..nul]))) } pub fn session_processes(child_pid: u32) -> Vec { if child_pid == 0 { return Vec::new(); } let target_session = unsafe { libc::getsid(child_pid as libc::c_int) }; if target_session <= 0 { return Vec::new(); } let mut pids = vec![0i32; 4096]; let bytes = unsafe { libc::proc_listallpids( pids.as_mut_ptr() as *mut libc::c_void, (pids.len() * std::mem::size_of::()) as libc::c_int, ) }; if bytes <= 0 { return Vec::new(); } let count = (bytes as usize) / std::mem::size_of::(); pids.into_iter() .take(count) .filter(|pid| *pid > 0) .filter(|pid| unsafe { libc::getsid(*pid) } == target_session) .map(|pid| pid as u32) .collect() } pub fn signal_processes(pids: &[u32], signal: Signal) { let sig = match signal { Signal::Hangup => libc::SIGHUP, Signal::Terminate => libc::SIGTERM, Signal::Kill => libc::SIGKILL, }; for &pid in pids { if pid == 0 { continue; } unsafe { libc::kill(pid as libc::c_int, sig); } } } pub fn process_exists(pid: u32) -> bool { if pid == 0 { return false; } let result = unsafe { libc::kill(pid as libc::c_int, 0) }; if result == 0 { true } else { std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM) } }