diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/platform/macos.rs | 161 |
1 files changed, 143 insertions, 18 deletions
diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 218d00e9..f8482bba 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -1,26 +1,46 @@ use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; /// Get the foreground process name for a given child PID. /// /// Uses `proc_pidinfo(PROC_PIDTBSDINFO)` to read `e_tpgid` — the foreground -/// process group ID of the child's controlling terminal. Then `proc_name()` -/// to resolve that PID to a short command name. +/// process group ID of the child's controlling terminal — then resolves the +/// group leader to a human-visible name via `KERN_PROCARGS2`. /// -/// This is the macOS equivalent of Linux's `/proc/{pid}/stat` → tpgid → `/proc/{tpgid}/comm`. +/// `KERN_PROCARGS2` reads the process's `argv[0]`, which reflects runtime +/// title changes (e.g. Node.js `process.title = "pi"`). This matches what +/// `ps -o comm` displays and is the only reliable way on macOS to see the +/// effective command name for scripting runtimes. +/// +/// Falls back to `pbi_comm` from `proc_pidinfo` if `KERN_PROCARGS2` fails. pub fn foreground_process_name(child_pid: u32) -> Option<String> { if child_pid == 0 { return None; } - // Get BSD info for the child process — contains e_tpgid + let fg_pgid = foreground_pgid(child_pid)?; + + // Primary: argv[0] via KERN_PROCARGS2 (reflects process.title changes) + // Note: we use the PGID as a PID — works because the group leader's PID + // equals the PGID. Same assumption Linux makes with /proc/{tpgid}/comm. + if let Some(name) = process_argv0_name(fg_pgid) { + return Some(name); + } + + // Fallback: kernel comm name from proc_pidinfo + process_comm_name(fg_pgid) +} + +/// 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( - child_pid as libc::c_int, + pid as libc::c_int, libc::PROC_PIDTBSDINFO, 0, &mut info as *mut _ as *mut libc::c_void, @@ -32,28 +52,133 @@ pub fn foreground_process_name(child_pid: u32) -> Option<String> { return None; } - let fg_pid = info.e_tpgid; - if fg_pid == 0 { + 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; } - // Resolve the foreground PID to a process name - let mut buf = [0u8; libc::MAXCOMLEN + 1]; - let len = unsafe { - libc::proc_name( - fg_pid as libc::c_int, + 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, - buf.len() as u32, + &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> { + 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 len <= 0 { + if ret != size { + return None; + } + + let end = info + .pbi_comm + .iter() + .position(|&b| b == 0) + .unwrap_or(info.pbi_comm.len()); + if end == 0 { return None; } - let len = len as usize; - let end = buf[..len].iter().position(|&b| b == 0).unwrap_or(len); - Some(String::from_utf8_lossy(&buf[..end]).into_owned()) + let bytes: Vec<u8> = info.pbi_comm[..end].iter().map(|&b| b as u8).collect(); + String::from_utf8(bytes).ok() } /// Get the current working directory of a process. |
