use std::ffi::OsStr; use std::io::Write; use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use super::{ClipboardCommand, 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)) } pub fn write_clipboard(bytes: &[u8]) -> bool { run_clipboard_command( &ClipboardCommand { program: "pbcopy", args: &[], }, bytes, ) } fn run_clipboard_command(command: &ClipboardCommand, bytes: &[u8]) -> bool { let mut child = match Command::new(command.program) .args(command.args) .stdin(Stdio::piped()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() { Ok(child) => child, Err(_) => return false, }; let Some(mut stdin) = child.stdin.take() else { let _ = child.kill(); let _ = child.wait(); return false; }; if stdin.write_all(bytes).is_err() { let _ = child.kill(); let _ = child.wait(); return false; } drop(stdin); child.wait().map(|status| status.success()).unwrap_or(false) } 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)?; let argv = procargs2_argv(&buf)?; Some(argv.join(" ")) } fn procargs2_argv(buf: &[u8]) -> Option> { 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; } // Layout: [argc: i32] [exec_path\0] [padding\0...] [argv[0]\0] [argv[1]\0] ... [env\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; } let mut argv = Vec::with_capacity(argc as usize); let mut current = pos; for _ in 0..argc { if current >= rest.len() { return None; } let end = rest[current..] .iter() .position(|&b| b == 0) .map(|offset| current + offset) .unwrap_or(rest.len()); if end == current { return None; } argv.push(String::from_utf8_lossy(&rest[current..end]).into_owned()); current = end + 1; } Some(argv) } /// 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) } } #[cfg(test)] mod tests { use super::*; fn build_procargs2(exec_path: &str, argv: &[&str], env: &[&str]) -> Vec { let mut buf = Vec::new(); buf.extend_from_slice(&(argv.len() as i32).to_ne_bytes()); buf.extend_from_slice(exec_path.as_bytes()); buf.push(0); buf.push(0); for arg in argv { buf.extend_from_slice(arg.as_bytes()); buf.push(0); } for entry in env { buf.extend_from_slice(entry.as_bytes()); buf.push(0); } buf } #[test] fn procargs2_argv_excludes_environment_entries() { let buf = build_procargs2( "/usr/bin/node", &["node", "/Users/can/.local/bin/pi"], &[ "PATH=/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin", "TERM=tmux-256color", ], ); let argv = procargs2_argv(&buf).expect("expected argv"); assert_eq!(argv, vec!["node", "/Users/can/.local/bin/pi"]); assert_eq!(argv.join(" "), "node /Users/can/.local/bin/pi"); assert!(!argv.join(" ").contains("codex.system")); } }