use std::path::Path;
pub const UPGRADE_BIN_ENV: &str = "RAG_RAT_UPGRADE_BIN";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ProcInfo {
pub pid: i32,
pub exe_inode: u64,
pub is_self: bool,
pub eligible: bool,
}
pub(crate) fn select_targets(procs: &[ProcInfo], installed_inode: u64) -> Vec<i32> {
let mut others: Vec<i32> = Vec::new();
let mut own: Option<i32> = None;
for proc in procs {
if !proc.eligible || proc.exe_inode == installed_inode {
continue;
}
if proc.is_self {
own = Some(proc.pid);
} else {
others.push(proc.pid);
}
}
others.sort_unstable();
others.extend(own); others
}
#[cfg(target_os = "linux")]
pub fn trigger(install_path: &Path) {
let Some(installed_inode) = linux::inode(install_path) else {
return;
};
let Some(bin_name) = install_path.file_name() else {
return;
};
let procs = linux::scan_proc(bin_name);
for pid in select_targets(&procs, installed_inode) {
linux::send_sigusr1(pid);
}
}
#[cfg(not(target_os = "linux"))]
pub fn trigger(_install_path: &Path) {}
#[cfg(target_os = "linux")]
mod linux {
use std::{
ffi::OsStr,
fs,
os::unix::{ffi::OsStrExt, fs::MetadataExt},
path::Path,
};
use super::{ProcInfo, UPGRADE_BIN_ENV};
pub(super) fn inode(path: &Path) -> Option<u64> {
fs::metadata(path).ok().map(|meta| meta.ino())
}
pub(super) fn scan_proc(bin_name: &OsStr) -> Vec<ProcInfo> {
let self_pid = std::process::id() as i32;
let Ok(entries) = fs::read_dir("/proc") else {
return Vec::new();
};
entries
.flatten()
.filter_map(|entry| {
let pid: i32 = entry.file_name().to_str()?.parse().ok()?;
let proc_dir = entry.path();
let exe_inode = fs::metadata(proc_dir.join("exe")).ok()?.ino();
Some(ProcInfo {
pid,
exe_inode,
is_self: pid == self_pid,
eligible: is_eligible(&proc_dir, bin_name),
})
})
.collect()
}
fn is_eligible(proc_dir: &Path, bin_name: &OsStr) -> bool {
runs_our_mcp(proc_dir, bin_name) && has_upgrade_env(proc_dir)
}
fn runs_our_mcp(proc_dir: &Path, bin_name: &OsStr) -> bool {
let Ok(cmdline) = fs::read(proc_dir.join("cmdline")) else {
return false;
};
let mut args = cmdline.split(|&byte| byte == 0).filter(|arg| !arg.is_empty());
let Some(argv0) = args.next() else {
return false;
};
let argv0_name = Path::new(OsStr::from_bytes(argv0)).file_name();
let is_our_binary = argv0_name == Some(bin_name);
let runs_mcp = args.any(|arg| arg == b"mcp");
is_our_binary && runs_mcp
}
fn has_upgrade_env(proc_dir: &Path) -> bool {
let Ok(environ) = fs::read(proc_dir.join("environ")) else {
return false; };
let needle = format!("{UPGRADE_BIN_ENV}=");
environ.split(|&byte| byte == 0).any(|entry| entry.starts_with(needle.as_bytes()))
}
pub(super) fn send_sigusr1(pid: i32) {
unsafe {
libc::kill(pid, libc::SIGUSR1);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn proc(pid: i32, exe_inode: u64, is_self: bool, eligible: bool) -> ProcInfo {
ProcInfo { pid, exe_inode, is_self, eligible }
}
#[test]
fn selects_only_outdated_eligible_processes() {
let installed = 100;
let procs = vec![
proc(10, 99, false, true), proc(11, 100, false, true), proc(12, 99, false, false), proc(13, 42, false, true), ];
assert_eq!(select_targets(&procs, installed), vec![10, 13]);
}
#[test]
fn self_is_signaled_last() {
let installed = 100;
let procs = vec![
proc(7, 1, true, true), proc(30, 1, false, true), proc(20, 1, false, true), ];
assert_eq!(select_targets(&procs, installed), vec![20, 30, 7]);
}
#[test]
fn empty_when_nothing_outdated_or_eligible() {
let installed = 5;
let procs = vec![proc(1, 5, false, true), proc(2, 4, false, false)];
assert!(select_targets(&procs, installed).is_empty());
}
}