use anyhow::Result;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ProcessIdentity {
pub start_marker: u64,
pub executable: String,
}
pub fn run_with_timeout(
mut cmd: std::process::Command,
timeout: std::time::Duration,
) -> Option<std::process::Output> {
use std::process::Stdio;
use std::time::Instant;
let mut child = cmd
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.ok()?;
let start = Instant::now();
loop {
match child.try_wait() {
Ok(Some(_)) => return child.wait_with_output().ok(),
Ok(None) => {
if start.elapsed() >= timeout {
let _ = child.kill();
let _ = child.wait();
return None;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
Err(_) => return None,
}
}
}
pub use super::detached::{DetachedChild, DetachedStderr, spawn_detached};
pub use lean_ctx_fs::process::is_alive;
pub fn identity(pid: u32) -> Option<ProcessIdentity> {
#[cfg(target_os = "macos")]
{
macos_process_identity(pid)
}
#[cfg(target_os = "linux")]
{
linux_process_identity(pid)
}
#[cfg(windows)]
{
windows_process_identity(pid)
}
#[cfg(target_os = "freebsd")]
{
freebsd_process_identity(pid)
}
#[cfg(not(any(
target_os = "macos",
target_os = "linux",
target_os = "freebsd",
windows
)))]
{
let _ = pid;
None
}
}
#[cfg(target_os = "freebsd")]
fn freebsd_process_identity(pid: u32) -> Option<ProcessIdentity> {
let info = freebsd_kinfo(pid)?;
let pid = info.ki_pid;
let start_marker = u64::try_from(info.ki_start.tv_sec)
.ok()?
.checked_mul(1_000_000)?
.checked_add(u64::try_from(info.ki_start.tv_usec).ok()?)?;
freebsd_identity_with_path(pid, start_marker)
}
#[cfg(target_os = "freebsd")]
fn freebsd_kinfo(pid: u32) -> Option<libc::kinfo_proc> {
let pid = libc::c_int::try_from(pid).ok()?;
let mut info = std::mem::MaybeUninit::<libc::kinfo_proc>::zeroed();
let mut info_len = std::mem::size_of::<libc::kinfo_proc>();
let mib = [libc::CTL_KERN, libc::KERN_PROC, libc::KERN_PROC_PID, pid];
let rc = unsafe {
libc::sysctl(
mib.as_ptr(),
4,
info.as_mut_ptr().cast(),
&mut info_len,
std::ptr::null(),
0,
)
};
if rc != 0 || info_len != std::mem::size_of::<libc::kinfo_proc>() {
return None;
}
let info = unsafe { info.assume_init() };
(info.ki_pid == pid).then_some(info)
}
#[cfg(target_os = "freebsd")]
fn freebsd_identity_with_path(pid: libc::c_int, start_marker: u64) -> Option<ProcessIdentity> {
let mut path = [0_u8; libc::PATH_MAX as usize];
let mut path_len = path.len();
let mib = [
libc::CTL_KERN,
libc::KERN_PROC,
libc::KERN_PROC_PATHNAME,
pid,
];
let rc = unsafe {
libc::sysctl(
mib.as_ptr(),
4,
path.as_mut_ptr().cast(),
&mut path_len,
std::ptr::null(),
0,
)
};
if rc != 0 || path_len == 0 {
return None;
}
let bytes = path.get(..path_len)?;
let bytes = bytes.strip_suffix(&[0]).unwrap_or(bytes);
if bytes.is_empty() {
return None;
}
Some(ProcessIdentity {
start_marker,
executable: String::from_utf8_lossy(bytes).into_owned(),
})
}
pub fn matches_identity(pid: u32, expected: &ProcessIdentity) -> bool {
identity(pid).as_ref() == Some(expected)
}
#[must_use]
pub fn installed_executable_path(path: &str) -> String {
let path = path.strip_suffix(" (deleted)").unwrap_or(path);
let name_at = path.rfind(['/', '\\']).map_or(0, |i| i + 1);
let (dir, name) = path.split_at(name_at);
let lower = name.to_ascii_lowercase();
let sidecar = lower
.strip_prefix("lean-ctx.old")
.and_then(|rest| rest.strip_suffix(".exe"))
.is_some_and(is_parked_sidecar_suffix);
if sidecar {
format!("{dir}lean-ctx.exe")
} else {
path.to_string()
}
}
fn is_parked_sidecar_suffix(middle: &str) -> bool {
if middle.is_empty() {
return true;
}
let Some(rest) = middle.strip_prefix('-') else {
return false;
};
let mut parts = rest.split('-');
let digits =
|p: Option<&str>| p.is_some_and(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()));
digits(parts.next()) && digits(parts.next()) && parts.next().is_none()
}
#[must_use]
pub fn same_process(expected: &ProcessIdentity, live: &ProcessIdentity) -> bool {
if expected.start_marker != live.start_marker {
return false;
}
let a = installed_executable_path(&expected.executable);
let b = installed_executable_path(&live.executable);
a == b || (cfg!(windows) && a.eq_ignore_ascii_case(&b))
}
#[must_use]
pub fn is_same_process(pid: u32, expected: &ProcessIdentity) -> bool {
identity(pid).is_some_and(|live| same_process(expected, &live))
}
#[must_use]
pub fn owned_by_current_user(pid: u32) -> bool {
#[cfg(target_os = "macos")]
{
let me = unsafe { libc::geteuid() };
macos_bsd_info(pid).is_some_and(|info| info.pbi_uid == me)
}
#[cfg(target_os = "linux")]
{
let me = unsafe { libc::geteuid() };
std::fs::read_to_string(format!("/proc/{pid}/status"))
.ok()
.and_then(|status| {
let line = status.lines().find(|l| l.starts_with("Uid:"))?;
line.split_whitespace().nth(2)?.parse::<u32>().ok()
})
.is_some_and(|euid| euid == me)
}
#[cfg(target_os = "freebsd")]
{
let me = unsafe { libc::geteuid() };
freebsd_kinfo(pid).is_some_and(|info| info.ki_uid == me)
}
#[cfg(windows)]
{
windows_owned_by_current_user(pid)
}
#[cfg(not(any(
target_os = "macos",
target_os = "linux",
target_os = "freebsd",
windows
)))]
{
let _ = pid;
false
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StopSignal {
Terminate,
Kill,
}
pub fn signal_same_process(pid: u32, expected: &ProcessIdentity, signal: StopSignal) -> bool {
if pid == 0 || pid == std::process::id() {
return false;
}
#[cfg(windows)]
{
let _ = signal;
windows_terminate_same_process(pid, expected)
}
#[cfg(unix)]
{
let sig = match signal {
StopSignal::Terminate => libc::SIGTERM,
StopSignal::Kill => libc::SIGKILL,
};
let Ok(raw_pid) = libc::pid_t::try_from(pid) else {
return false;
};
#[cfg(target_os = "linux")]
if let Some(delivered) = linux_pidfd_signal(raw_pid, expected, sig) {
return delivered;
}
is_same_process(pid, expected) && unsafe { libc::kill(raw_pid, sig) } == 0
}
#[cfg(not(any(unix, windows)))]
{
let _ = (expected, signal);
false
}
}
#[cfg(target_os = "linux")]
fn linux_pidfd_signal(
pid: libc::pid_t,
expected: &ProcessIdentity,
sig: libc::c_int,
) -> Option<bool> {
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) };
if fd < 0 {
return match std::io::Error::last_os_error().raw_os_error() {
Some(libc::ESRCH) => Some(false),
_ => None,
};
}
let fd = libc::c_int::try_from(fd).ok()?;
let pidfd = unsafe { OwnedFd::from_raw_fd(fd) };
if !is_same_process(u32::try_from(pid).ok()?, expected) {
return Some(false);
}
let rc = unsafe {
libc::syscall(
libc::SYS_pidfd_send_signal,
pidfd.as_raw_fd(),
sig,
std::ptr::null::<libc::siginfo_t>(),
0,
)
};
Some(rc == 0)
}
#[cfg(target_os = "macos")]
#[repr(C)]
struct ProcBsdInfo {
pbi_flags: u32,
pbi_status: u32,
pbi_xstatus: u32,
pbi_pid: u32,
pbi_ppid: u32,
pbi_uid: u32,
pbi_gid: u32,
pbi_ruid: u32,
pbi_rgid: u32,
pbi_svuid: u32,
pbi_svgid: u32,
rfu_1: u32,
pbi_comm: [std::ffi::c_char; 16],
pbi_name: [std::ffi::c_char; 32],
pbi_nfiles: u32,
pbi_pgid: u32,
pbi_pjobc: u32,
e_tdev: u32,
e_tpgid: u32,
pbi_nice: i32,
pbi_start_tvsec: u64,
pbi_start_tvusec: u64,
}
#[cfg(target_os = "macos")]
const PROC_PIDTBSDINFO: std::ffi::c_int = 3;
#[cfg(target_os = "macos")]
#[link(name = "proc")]
unsafe extern "C" {
fn proc_pidinfo(
pid: std::ffi::c_int,
flavor: std::ffi::c_int,
arg: u64,
buffer: *mut std::ffi::c_void,
buffersize: std::ffi::c_int,
) -> std::ffi::c_int;
fn proc_pidpath(
pid: std::ffi::c_int,
buffer: *mut std::ffi::c_void,
buffersize: u32,
) -> std::ffi::c_int;
}
#[cfg(target_os = "macos")]
fn macos_bsd_info(pid: u32) -> Option<ProcBsdInfo> {
let mut info = std::mem::MaybeUninit::<ProcBsdInfo>::zeroed();
let size = i32::try_from(std::mem::size_of::<ProcBsdInfo>()).ok()?;
let read = unsafe {
proc_pidinfo(
i32::try_from(pid).ok()?,
PROC_PIDTBSDINFO,
0,
info.as_mut_ptr().cast(),
size,
)
};
if read != size {
return None;
}
Some(unsafe { info.assume_init() })
}
#[cfg(target_os = "macos")]
fn macos_process_identity(pid: u32) -> Option<ProcessIdentity> {
let info = macos_bsd_info(pid)?;
let mut path = [0_u8; libc::PATH_MAX as usize];
let path_len = unsafe {
proc_pidpath(
i32::try_from(pid).ok()?,
path.as_mut_ptr().cast(),
u32::try_from(path.len()).ok()?,
)
};
if path_len <= 0 {
return None;
}
let path_len = usize::try_from(path_len).ok()?;
let executable = String::from_utf8_lossy(path.get(..path_len)?).into_owned();
let start_marker = info
.pbi_start_tvsec
.checked_mul(1_000_000)?
.checked_add(info.pbi_start_tvusec)?;
Some(ProcessIdentity {
start_marker,
executable,
})
}
#[cfg(target_os = "linux")]
fn linux_process_identity(pid: u32) -> Option<ProcessIdentity> {
let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
let (_, fields) = stat.rsplit_once(')')?;
let start_marker = fields.split_whitespace().nth(19)?.parse().ok()?;
let executable = std::fs::read_link(format!("/proc/{pid}/exe"))
.ok()?
.to_string_lossy()
.into_owned();
Some(ProcessIdentity {
start_marker,
executable,
})
}
#[cfg(windows)]
struct ProcessHandle(windows_sys::Win32::Foundation::HANDLE);
#[cfg(windows)]
impl ProcessHandle {
fn open(pid: u32, access: u32) -> Option<Self> {
use windows_sys::Win32::System::Threading::OpenProcess;
let handle = unsafe { OpenProcess(access, 0, pid) };
(!handle.is_null()).then_some(Self(handle))
}
}
#[cfg(windows)]
impl Drop for ProcessHandle {
fn drop(&mut self) {
unsafe { windows_sys::Win32::Foundation::CloseHandle(self.0) };
}
}
#[cfg(windows)]
fn windows_process_identity(pid: u32) -> Option<ProcessIdentity> {
use windows_sys::Win32::System::Threading::PROCESS_QUERY_LIMITED_INFORMATION;
let handle = ProcessHandle::open(pid, PROCESS_QUERY_LIMITED_INFORMATION)?;
windows_identity_of(&handle)
}
#[cfg(windows)]
fn windows_terminate_same_process(pid: u32, expected: &ProcessIdentity) -> bool {
use windows_sys::Win32::System::Threading::{
PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_TERMINATE, TerminateProcess,
};
let Some(handle) =
ProcessHandle::open(pid, PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_TERMINATE)
else {
return false;
};
if !windows_identity_of(&handle).is_some_and(|live| same_process(expected, &live)) {
return false;
}
unsafe { TerminateProcess(handle.0, 1) != 0 }
}
#[cfg(windows)]
pub(crate) fn windows_token_user_sid(
process: windows_sys::Win32::Foundation::HANDLE,
) -> Option<Vec<u8>> {
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::Security::{
GetLengthSid, GetTokenInformation, IsValidSid, TOKEN_QUERY, TOKEN_USER, TokenUser,
};
use windows_sys::Win32::System::Threading::OpenProcessToken;
let mut token = std::ptr::null_mut();
if unsafe { OpenProcessToken(process, TOKEN_QUERY, &raw mut token) } == 0 {
return None;
}
let sid = (|| {
let mut needed = 0_u32;
unsafe { GetTokenInformation(token, TokenUser, std::ptr::null_mut(), 0, &raw mut needed) };
if needed < u32::try_from(std::mem::size_of::<TOKEN_USER>()).ok()? || needed > 4096 {
return None;
}
let mut buf = vec![0_u64; (needed as usize).div_ceil(8)];
let buf_bytes = buf.len() * 8;
let ok = unsafe {
GetTokenInformation(
token,
TokenUser,
buf.as_mut_ptr().cast(),
needed,
&raw mut needed,
)
};
if ok == 0 {
return None;
}
let sid = unsafe { &*buf.as_ptr().cast::<TOKEN_USER>() }.User.Sid;
if sid.is_null() || unsafe { IsValidSid(sid) } == 0 {
return None;
}
let len = unsafe { GetLengthSid(sid) } as usize;
let (start, base) = (sid as usize, buf.as_ptr() as usize);
if start < base || start + len > base + buf_bytes {
return None;
}
Some(unsafe { std::slice::from_raw_parts(sid.cast::<u8>(), len) }.to_vec())
})();
unsafe { CloseHandle(token) };
sid
}
#[cfg(windows)]
fn windows_owned_by_current_user(pid: u32) -> bool {
use windows_sys::Win32::System::Threading::{
GetCurrentProcess, PROCESS_QUERY_LIMITED_INFORMATION,
};
let Some(handle) = ProcessHandle::open(pid, PROCESS_QUERY_LIMITED_INFORMATION) else {
return false;
};
let mine = windows_token_user_sid(unsafe { GetCurrentProcess() });
let theirs = windows_token_user_sid(handle.0);
mine.is_some() && mine == theirs
}
#[cfg(windows)]
fn windows_identity_of(handle: &ProcessHandle) -> Option<ProcessIdentity> {
use std::os::windows::ffi::OsStringExt;
use windows_sys::Win32::Foundation::FILETIME;
use windows_sys::Win32::System::Threading::{GetProcessTimes, QueryFullProcessImageNameW};
let handle = handle.0;
let mut created = FILETIME::default();
let mut exited = FILETIME::default();
let mut kernel = FILETIME::default();
let mut user = FILETIME::default();
let times_ok = unsafe {
GetProcessTimes(
handle,
&raw mut created,
&raw mut exited,
&raw mut kernel,
&raw mut user,
)
} != 0;
let mut path = vec![0_u16; 32_768];
let mut path_len = u32::try_from(path.len()).ok()?;
let path_ok =
unsafe { QueryFullProcessImageNameW(handle, 0, path.as_mut_ptr(), &raw mut path_len) != 0 };
if !times_ok || !path_ok {
return None;
}
let executable = std::ffi::OsString::from_wide(path.get(..usize::try_from(path_len).ok()?)?)
.to_string_lossy()
.into_owned();
let start_marker = (u64::from(created.dwHighDateTime) << 32) | u64::from(created.dwLowDateTime);
Some(ProcessIdentity {
start_marker,
executable,
})
}
pub fn terminate_gracefully(pid: u32) -> Result<()> {
#[cfg(unix)]
{
let ret = unsafe { libc::kill(pid as libc::pid_t, libc::SIGTERM) };
if ret != 0 {
anyhow::bail!(
"Failed to send SIGTERM to PID {pid}: {}",
std::io::Error::last_os_error()
);
}
Ok(())
}
#[cfg(windows)]
{
force_kill(pid)
}
}
pub fn force_kill(pid: u32) -> Result<()> {
#[cfg(unix)]
{
let ret = unsafe { libc::kill(pid as libc::pid_t, libc::SIGKILL) };
if ret != 0 {
anyhow::bail!(
"Failed to send SIGKILL to PID {pid}: {}",
std::io::Error::last_os_error()
);
}
Ok(())
}
#[cfg(windows)]
{
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::Threading::{
OpenProcess, PROCESS_TERMINATE, TerminateProcess,
};
unsafe {
let handle = OpenProcess(PROCESS_TERMINATE, 0, pid);
if handle.is_null() {
anyhow::bail!(
"Failed to open PID {pid} for termination: {}",
std::io::Error::last_os_error()
);
}
let ok = TerminateProcess(handle, 1);
CloseHandle(handle);
if ok == 0 {
anyhow::bail!(
"Failed to terminate PID {pid}: {}",
std::io::Error::last_os_error()
);
}
Ok(())
}
}
}
fn protected_self_pids() -> std::collections::HashSet<u32> {
let mut protected = std::collections::HashSet::new();
protected.insert(std::process::id());
#[cfg(unix)]
{
let mut pid = std::process::id();
for _ in 0..16 {
let Ok(output) = std::process::Command::new("ps")
.args(["-o", "ppid=", "-p", &pid.to_string()])
.output()
else {
break;
};
let Ok(ppid) = String::from_utf8_lossy(&output.stdout)
.trim()
.parse::<u32>()
else {
break;
};
if ppid <= 1 || !protected.insert(ppid) {
break;
}
pid = ppid;
}
let own_pgid = unsafe { libc::getpgrp() };
if own_pgid > 0 {
protected.extend(process_group_pids(own_pgid));
}
}
protected
}
#[cfg(unix)]
fn process_group_pids(pgid: libc::pid_t) -> Vec<u32> {
std::process::Command::new("pgrep")
.args(["-g", &pgid.to_string()])
.output()
.ok()
.map(|output| {
String::from_utf8_lossy(&output.stdout)
.lines()
.filter_map(|line| line.trim().parse::<u32>().ok())
.collect()
})
.unwrap_or_default()
}
pub fn find_pids_by_name(name: &str) -> Vec<u32> {
let protected = protected_self_pids();
let mut pids = Vec::new();
#[cfg(unix)]
{
let uid = unsafe { libc::getuid() }.to_string();
if let Ok(output) = std::process::Command::new("pgrep")
.args(["-U", &uid, "-x", name])
.output()
{
collect_pids(&output.stdout, &protected, &mut pids);
}
if let Ok(output) = std::process::Command::new("pgrep")
.args(["-U", &uid])
.arg("-f")
.arg(format!("/{name}(\\s|$)"))
.output()
{
collect_pids(&output.stdout, &protected, &mut pids);
}
pids.sort_unstable();
pids.dedup();
}
#[cfg(windows)]
{
if let Ok(output) = std::process::Command::new("tasklist")
.args([
"/FI",
&format!("IMAGENAME eq {name}.exe"),
"/FO",
"CSV",
"/NH",
])
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
let parts: Vec<&str> = line.split(',').collect();
if parts.len() >= 2 {
let pid_str = parts[1].trim().trim_matches('"');
if let Ok(pid) = pid_str.parse::<u32>() {
if !protected.contains(&pid) {
pids.push(pid);
}
}
}
}
}
}
pids
}
#[cfg(unix)]
fn collect_pids(stdout: &[u8], protected: &std::collections::HashSet<u32>, out: &mut Vec<u32>) {
let text = String::from_utf8_lossy(stdout);
for line in text.lines() {
if let Ok(pid) = line.trim().parse::<u32>()
&& !protected.contains(&pid)
{
out.push(pid);
}
}
}
mod services;
pub use services::*;
#[cfg(test)]
mod tests;