Files
herdr/src/platform/macos.rs
T

442 lines
12 KiB
Rust

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<ForegroundJob> {
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::<i32>()) as libc::c_int,
)
};
if bytes <= 0 {
return None;
}
let count = (bytes as usize) / std::mem::size_of::<i32>();
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<u32> {
let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() };
let size = std::mem::size_of::<libc::proc_bsdinfo>() 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<String> {
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<Vec<u8>> {
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<String> {
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<libc::proc_bsdinfo> {
let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() };
let size = std::mem::size_of::<libc::proc_bsdinfo>() 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<String> {
let end = info
.pbi_comm
.iter()
.position(|&b| b == 0)
.unwrap_or(info.pbi_comm.len());
if end == 0 {
return None;
}
let bytes: Vec<u8> = info.pbi_comm[..end].iter().map(|&b| b as u8).collect();
String::from_utf8(bytes).ok()
}
fn process_cmdline(pid: u32) -> Option<String> {
let buf = kern_procargs2(pid)?;
let argv = procargs2_argv(&buf)?;
Some(argv.join(" "))
}
fn procargs2_argv(buf: &[u8]) -> Option<Vec<String>> {
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<PathBuf> {
if pid == 0 {
return None;
}
let mut pathinfo: libc::proc_vnodepathinfo = unsafe { std::mem::zeroed() };
let size = std::mem::size_of::<libc::proc_vnodepathinfo>() 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<u32> {
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::<i32>()) as libc::c_int,
)
};
if bytes <= 0 {
return Vec::new();
}
let count = (bytes as usize) / std::mem::size_of::<i32>();
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<u8> {
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"));
}
}