aboutsummaryrefslogtreecommitdiff
path: root/src/platform
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-03-28 00:20:12 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-03-28 00:20:12 +0300
commit787786c62009f6c27a0091b61017ae5ad5c95c0e (patch)
treee0c21aec008cae69e79514c6c6a25c9d4f411fdf /src/platform
parent0403775fd2a9458417c5349d5441125ab9b2553c (diff)
fix wrapped foreground job agent detection
Diffstat (limited to 'src/platform')
-rw-r--r--src/platform/fallback.rs4
-rw-r--r--src/platform/linux.rs74
-rw-r--r--src/platform/macos.rs107
-rw-r--r--src/platform/mod.rs14
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")]