use std::{ collections::HashMap, ffi::OsString, io, os::unix::{ffi::OsStringExt, net::UnixStream}, path::{Path, PathBuf}, }; use anyhow::Context as _; use super::{Invocation, WAIT, follows}; use crate::term::Pty; pub(super) type Pid = i32; /// The foreground's group and its leader's executable: an exec keeps the group and swaps the program. pub(super) type Program = (Pid, Option); /// herdr's executable. const HERDR: &str = "herdr"; /// The process group leading the session's foreground. pub(super) fn leader(pty: &Pty) -> io::Result { pty.foreground() } pub(super) fn running(group: Pid) -> io::Result { Ok((group, path(group).ok())) } /// The socket of the herdr server whose terminal client leads process group `group`, if it is one. pub(super) fn client((group, exe): Program) -> anyhow::Result> { let Some(exe) = exe else { log::debug!("herdr: process {group}, another user's or gone, does not read"); return Ok(None); }; if !exe .file_name() .is_some_and(|name| name.eq_ignore_ascii_case(HERDR)) { return Ok(None); } let (args, env) = launch(group).with_context(|| format!("herdr: process {group} does not read"))?; let Some(socket) = follows(&args, &env)? else { return Ok(None); }; if socket.is_absolute() { return Ok(Some(socket)); } // herdr reads a relative socket from its client's folder. let folder = cwd(group).with_context(|| format!("herdr: process {group}'s folder does not read"))?; Ok(Some(folder.join(socket))) } /// herdr's folder without `XDG_CONFIG_HOME`. pub(super) fn home(env: &HashMap) -> anyhow::Result { let home = env.get("HOME").context("herdr: its client has no HOME")?; Ok(PathBuf::from(home).join(".config/herdr")) } /// herdr's socket, which must answer within `WAIT`. pub(super) fn connect(socket: &Path) -> io::Result { let stream = UnixStream::connect(socket)?; stream.set_read_timeout(Some(WAIT))?; stream.set_write_timeout(Some(WAIT))?; Ok(stream) } #[cfg(target_os = "linux")] pub(super) fn path(pid: Pid) -> io::Result { std::fs::read_link(format!("/proc/{pid}/exe")).map(installed) } /// The path of an executable the kernel marks ` (deleted)`, as an update that replaced it leaves it. #[cfg(target_os = "linux")] pub(super) fn installed(path: PathBuf) -> PathBuf { use std::{ffi::OsStr, os::unix::ffi::OsStrExt}; match path.as_os_str().as_bytes().strip_suffix(b" (deleted)") { Some(kept) => PathBuf::from(OsStr::from_bytes(kept)), None => path, } } #[cfg(target_os = "linux")] pub(super) fn launch(pid: Pid) -> io::Result { let read = |name: &str| std::fs::read(format!("/proc/{pid}/{name}")); let (args, env) = (read("cmdline")?, read("environ")?); let args = strings(&args).map(lossy).collect(); let env = strings(&env).filter_map(variable).collect(); Ok((args, env)) } #[cfg(target_os = "linux")] pub(super) fn cwd(pid: Pid) -> io::Result { std::fs::read_link(format!("/proc/{pid}/cwd")) } /// Each NUL-ended string in `bytes`. #[cfg(target_os = "linux")] fn strings(bytes: &[u8]) -> impl Iterator { bytes .strip_suffix(b"\0") .unwrap_or(bytes) .split(|byte| *byte == 0) } #[cfg(target_os = "macos")] pub(super) fn path(pid: Pid) -> io::Result { let mut buffer = vec![0u8; libc::PROC_PIDPATHINFO_MAXSIZE as usize]; // SAFETY: the buffer holds the size given. let length = unsafe { libc::proc_pidpath(pid, buffer.as_mut_ptr().cast(), buffer.len() as u32) }; if length <= 0 { return Err(io::Error::last_os_error()); } buffer.truncate(length as usize); Ok(PathBuf::from(OsString::from_vec(buffer))) } #[cfg(target_os = "macos")] pub(super) fn launch(pid: Pid) -> io::Result { let mut name = [libc::CTL_KERN, libc::KERN_PROCARGS2, pid]; let mut ask = |buffer: &mut Vec| { let mut size = buffer.len(); let into = if buffer.is_empty() { std::ptr::null_mut() } else { buffer.as_mut_ptr().cast() }; // SAFETY: `into` holds `size` bytes, or is null to ask for the size. let done = unsafe { libc::sysctl( name.as_mut_ptr(), 3, into, &mut size, std::ptr::null_mut(), 0, ) }; if done != 0 { return Err(io::Error::last_os_error()); } Ok(size) }; let mut buffer = Vec::new(); let size = ask(&mut buffer)?; buffer.resize(size, 0); let size = ask(&mut buffer)?; buffer.truncate(size); procargs(&buffer) } #[cfg(target_os = "macos")] pub(super) fn cwd(pid: Pid) -> io::Result { // SAFETY: the struct is plain C data, valid zeroed. let mut info: libc::proc_vnodepathinfo = unsafe { std::mem::zeroed() }; let size = std::mem::size_of::() as libc::c_int; // SAFETY: `info` holds the size given. let read = unsafe { libc::proc_pidinfo( pid, libc::PROC_PIDVNODEPATHINFO, 0, (&raw mut info).cast(), size, ) }; if read <= 0 { return Err(io::Error::last_os_error()); } if read != size { return Err(io::Error::other(format!( "proc_pidinfo read {read} of {size} bytes" ))); } let path = info .pvi_cdir .vip_path .iter() .flatten() .map(|byte| *byte as u8); Ok(PathBuf::from(OsString::from_vec( path.take_while(|byte| *byte != 0).collect(), ))) } /// `KERN_PROCARGS2`'s layout: argc, the program's path, NUL padding, the arguments, the variables. #[cfg(target_os = "macos")] pub(super) fn procargs(buffer: &[u8]) -> io::Result { let invalid = |what: &str| io::Error::new(io::ErrorKind::InvalidData, format!("KERN_PROCARGS2 {what}")); let (count, rest) = buffer .split_first_chunk::<4>() .ok_or_else(|| invalid("holds no argument count"))?; let count = usize::try_from(i32::from_ne_bytes(*count)) .map_err(|_| invalid("holds a negative argument count"))?; let mut parts = rest .split(|byte| *byte == 0) .skip(1) .skip_while(|part| part.is_empty()); let args = parts.by_ref().take(count).map(lossy).collect(); let env = parts .take_while(|part| !part.is_empty()) .filter_map(variable) .collect(); Ok((args, env)) } fn lossy(bytes: &[u8]) -> String { String::from_utf8_lossy(bytes).into_owned() } /// A `NAME=value` variable; its value keeps its bytes. fn variable(pair: &[u8]) -> Option<(String, OsString)> { let at = pair.iter().position(|byte| *byte == b'=')?; Some(( lossy(&pair[..at]), OsString::from_vec(pair[at + 1..].to_vec()), )) }