mirror of
https://github.com/herdrdev/herdr.git
synced 2026-09-22 08:01:06 +00:00
442 lines
12 KiB
Rust
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"));
|
|
}
|
|
}
|