diff options
Diffstat (limited to 'src/platform')
| -rw-r--r-- | src/platform/fallback.rs | 4 | ||||
| -rw-r--r-- | src/platform/linux.rs | 74 | ||||
| -rw-r--r-- | src/platform/macos.rs | 107 | ||||
| -rw-r--r-- | src/platform/mod.rs | 14 |
4 files changed, 169 insertions, 30 deletions
diff --git a/src/platform/fallback.rs b/src/platform/fallback.rs index 735f7c11..7bcd23c5 100644 --- a/src/platform/fallback.rs +++ b/src/platform/fallback.rs @@ -1,7 +1,9 @@ use std::path::PathBuf; +use super::ForegroundJob; + /// Unsupported platform stub. -pub fn foreground_process_name(_child_pid: u32) -> Option<String> { +pub fn foreground_job(_child_pid: u32) -> Option<ForegroundJob> { None } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 001ef03e..10695e92 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,8 +1,51 @@ use std::path::PathBuf; -/// Get the foreground process name for a given child PID. -/// Uses /proc/<pid>/stat to find the tpgid, then /proc/<tpgid>/comm. -pub fn foreground_process_name(child_pid: u32) -> Option<String> { +use super::{ForegroundJob, ForegroundProcess}; + +/// Collect the foreground terminal job for a given child PID. +pub fn foreground_job(child_pid: u32) -> Option<ForegroundJob> { + let tpgid = foreground_pgid(child_pid)? as u32; + let mut processes = Vec::new(); + + for entry in std::fs::read_dir("/proc").ok()? { + let entry = entry.ok()?; + let file_name = entry.file_name(); + let pid_str = file_name.to_str()?; + if !pid_str.bytes().all(|b| b.is_ascii_digit()) { + continue; + } + + let pid: u32 = match pid_str.parse() { + Ok(pid) => pid, + Err(_) => continue, + }; + + let Some((pgrp, name)) = process_pgrp_and_comm(pid) else { + continue; + }; + if pgrp as u32 != tpgid { + continue; + } + + processes.push(ForegroundProcess { + pid, + name, + argv0: None, + cmdline: process_cmdline(pid), + }); + } + + if processes.is_empty() { + return None; + } + + Some(ForegroundJob { + process_group_id: tpgid, + processes, + }) +} + +fn foreground_pgid(child_pid: u32) -> Option<i32> { // /proc/<pid>/stat format: "pid (comm) state ppid pgrp session tty_nr tpgid ..." // The (comm) field can contain spaces and parens, so we find the last ')' first. let stat = std::fs::read_to_string(format!("/proc/{child_pid}/stat")).ok()?; @@ -10,11 +53,30 @@ pub fn foreground_process_name(child_pid: u32) -> Option<String> { let fields: Vec<&str> = rest.split_whitespace().collect(); // After (comm): state(0) ppid(1) pgrp(2) session(3) tty_nr(4) tpgid(5) let tpgid: i32 = fields.get(5)?.parse().ok()?; - if tpgid <= 0 { + (tpgid > 0).then_some(tpgid) +} + +fn process_pgrp_and_comm(pid: u32) -> Option<(i32, String)> { + let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?; + let close = stat.rfind(')')?; + let comm = stat.get(1 + stat.find('(')?..close)?.to_string(); + let rest = stat.get(close + 2..)?; + let fields: Vec<&str> = rest.split_whitespace().collect(); + let pgrp: i32 = fields.get(2)?.parse().ok()?; + Some((pgrp, comm)) +} + +fn process_cmdline(pid: u32) -> Option<String> { + let bytes = std::fs::read(format!("/proc/{pid}/cmdline")).ok()?; + if bytes.is_empty() { return None; } - let comm = std::fs::read_to_string(format!("/proc/{tpgid}/comm")).ok()?; - Some(comm.trim().to_string()) + let parts: Vec<String> = bytes + .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. diff --git a/src/platform/macos.rs b/src/platform/macos.rs index f8482bba..524896d4 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -2,34 +2,60 @@ use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; 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 resolves the -/// group leader to a human-visible name via `KERN_PROCARGS2`. -/// -/// `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> { +use super::{ForegroundJob, ForegroundProcess}; + +/// 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; + } - // 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); + 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; } - // Fallback: kernel comm name from proc_pidinfo - process_comm_name(fg_pgid) + Some(ForegroundJob { + process_group_id: fg_pgid, + processes, + }) } /// Read `e_tpgid` (foreground process group of the controlling terminal) @@ -151,6 +177,10 @@ fn kern_procargs2(pid: u32) -> Option<Vec<u8>> { /// 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)) +} + +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; @@ -164,10 +194,10 @@ fn process_comm_name(pid: u32) -> Option<String> { ) }; - if ret != size { - return None; - } + (ret == size).then_some(info) +} +fn comm_from_bsdinfo(info: &libc::proc_bsdinfo) -> Option<String> { let end = info .pbi_comm .iter() @@ -181,6 +211,37 @@ fn process_comm_name(pid: u32) -> Option<String> { String::from_utf8(bytes).ok() } +fn process_cmdline(pid: u32) -> Option<String> { + 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<String> = 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`. diff --git a/src/platform/mod.rs b/src/platform/mod.rs index df8869c7..ade86249 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -3,6 +3,20 @@ //! Centralizes OS-dependent behavior behind a clean boundary so core //! modules don't scatter `#[cfg]` branches through product logic. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ForegroundProcess { + pub pid: u32, + pub name: String, + pub argv0: Option<String>, + pub cmdline: Option<String>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ForegroundJob { + pub process_group_id: u32, + pub processes: Vec<ForegroundProcess>, +} + #[cfg(target_os = "linux")] mod linux; #[cfg(target_os = "linux")] |
