aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-03-27 22:43:29 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-03-27 22:43:29 +0300
commit9aa50f04a9d5724cda773d609cf9a9154a01f256 (patch)
tree90e024492f66cdfd176bd6072665708395d0eb79
parenta57b97285c3308fbdcc167ab3da9ae1dd78767b6 (diff)
fix: use KERN_PROCARGS2 for macOS process name resolution
proc_name() fails on macOS Sequoia due to undersized buffer (MAXCOMLEN+1 = 17 bytes). Even with a larger buffer, it returns the kernel comm name ('node') rather than the effective process title ('pi') set via Node.js process.title. Replace with sysctl(KERN_PROCARGS2) which reads argv[0] and reflects runtime title changes, matching what ps(1) displays. Falls back to pbi_comm from proc_pidinfo if sysctl fails.
-rw-r--r--src/platform/macos.rs161
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.