Files
agent-manager/src/process.rs
T
bruno d911ce5d92
release / release (push) Successful in 13m38s
release / macos (push) Canceled after 0s
fix: robustesse v1.1.6 — timeout runner/doctor, anti-deadlock, securite migrate, win32 is_running, websocket RFC6455, keyring JSON
- runner.rs: timeout 30s sur les commandes capturees (doctor ne pend plus) + pipes stdout/stderr draines dans des threads (anti-deadlock) ; run_untimed() pour les installations longues (npm/cargo/go) — sh() des installateurs l'utilise, les sondes gardent le timeout

- migrate_cmd.rs: rejet des chemins unsafe (path traversal) dans import_bundle

- process.rs: is_running Windows via OpenProcess/GetExitCodeProcess (fiable vs tasklist)

- probe.rs: drain des pipes en threads dans detect_external_version

- web.rs: boucle de traduction i18n (curseur, plus de boucle infinie si la traduction contient {{)

- serve.rs: frames WebSocket RFC 6455 avec vraies longueurs > 125 octets (126/127)

- secrets.rs: index keyring serialise en JSON (cles contenant des virgules)

- sandbox.rs: tests sans fuite de TempDir (plus de mem::forget)

- version 1.1.6, man pages et ROADMAP maj
2026-08-26 10:17:17 -04:00

252 lines
7.3 KiB
Rust

//! Process management: detached background start, PID bookkeeping, stop
//! with a graceful signal followed by a forceful kill after a timeout.
use crate::app::App;
use anyhow::{bail, Context, Result};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
/// Log file for a background agent.
pub fn agent_log_path(app: &App, agent_name: &str) -> PathBuf {
app.log.log_dir().join(format!("{agent_name}.log"))
}
/// Is a process with this PID alive?
pub fn is_running(pid: u32) -> bool {
#[cfg(windows)]
{
use windows_sys::Win32::Foundation::{CloseHandle, FALSE, STILL_ACTIVE};
use windows_sys::Win32::System::Threading::{
GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
};
unsafe {
let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if handle == 0 {
return false;
}
let mut code: u32 = 0;
let ok = GetExitCodeProcess(handle, &mut code);
CloseHandle(handle);
ok != 0 && code == STILL_ACTIVE as u32
}
}
#[cfg(not(windows))]
{
Command::new("kill")
.args(["-0", &pid.to_string()])
.status()
.map(|s| s.success())
.unwrap_or(false)
}
}
/// Send SIGTERM/SIGKILL (or taskkill / taskkill /F on Windows).
fn signal(pid: u32, force: bool) -> bool {
#[cfg(windows)]
{
let mut cmd = Command::new("taskkill");
cmd.arg("/PID").arg(pid.to_string());
if force {
cmd.arg("/T").arg("/F");
}
cmd.status().map(|s| s.success()).unwrap_or(false)
}
#[cfg(not(windows))]
{
let sig = if force { "-KILL" } else { "-TERM" };
Command::new("kill")
.arg(sig)
.arg(pid.to_string())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
}
/// Stop a process: TERM first, then KILL after the timeout. Returns whether
/// the process was running when the call started.
pub fn stop_pid(pid: u32, force: bool, timeout_secs: u64) -> bool {
if !is_running(pid) {
return false;
}
if force {
signal(pid, true);
} else {
signal(pid, false);
let deadline = Instant::now() + Duration::from_secs(timeout_secs);
while Instant::now() < deadline {
if !is_running(pid) {
return true;
}
std::thread::sleep(Duration::from_millis(200));
}
if is_running(pid) {
signal(pid, true);
}
}
for _ in 0..50 {
if !is_running(pid) {
return true;
}
std::thread::sleep(Duration::from_millis(100));
}
!is_running(pid)
}
/// Kill a process and its whole tree (process group on unix, taskkill /T on
/// Windows). Used by the plugin runner and the lab benchmark to stop
/// runaway scripts and timed-out task runs.
pub fn kill_tree(pid: u32) {
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
let _ = Command::new("kill")
.arg("-KILL")
.arg(format!("-{pid}")) // negative pid = the process group
.status();
let _ = Command::new("kill").arg("-KILL").arg(pid.to_string()).status();
}
#[cfg(windows)]
{
let _ = Command::new("taskkill")
.args(["/PID", &pid.to_string(), "/T", "/F"])
.status();
}
}
/// Start a program detached: stdout/stderr are redirected to the agent log
/// file. Returns the PID. Fails when the process exits immediately.
pub fn spawn_background(
app: &App,
agent_name: &str,
program: &Path,
args: &[String],
env: &BTreeMap<String, String>,
cwd: Option<&Path>,
) -> Result<u32> {
let log = agent_log_path(app, agent_name);
if app.dry_run() {
app.log.dry(format!(
"would start {} (detached, log: {})",
program.display(),
log.display()
));
return Ok(0);
}
if let Some(parent) = log.parent() {
std::fs::create_dir_all(parent)?;
}
let file = std::fs::File::create(&log)
.with_context(|| format!("cannot create log file {}", log.display()))?;
let (prog, prefix) = crate::runner::resolve_program(&program.display().to_string());
let mut full_args = prefix;
full_args.extend(args.iter().cloned());
let mut cmd = Command::new(&prog);
cmd.args(&full_args)
.envs(env)
.stdout(Stdio::from(file.try_clone()?))
.stderr(Stdio::from(file));
if let Some(dir) = cwd {
cmd.current_dir(dir);
}
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
cmd.process_group(0);
}
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
cmd.creation_flags(CREATE_NEW_PROCESS_GROUP);
}
app.log
.cmd(&format!("{} {}", program.display(), args.join(" ")));
let mut child = cmd
.spawn()
.with_context(|| format!("failed to start {}", program.display()))?;
let pid = child.id();
// Give the process a moment: some agents crash at startup.
std::thread::sleep(Duration::from_millis(400));
match child.try_wait() {
Ok(Some(status)) => {
let tail = last_lines(&log, 8);
bail!(
"agent '{agent_name}' exited immediately after start (status {status}); log: {}
{}",
log.display(),
tail
);
}
Ok(None) => {}
Err(_) => {}
}
Ok(pid)
}
/// Tail of a text file (last n lines).
pub fn last_lines(path: &Path, n: usize) -> String {
let Ok(text) = std::fs::read_to_string(path) else {
return String::new();
};
let lines: Vec<&str> = text.lines().collect();
let start = lines.len().saturating_sub(n);
lines[start..].join("
")
}
/// Best-effort desktop notification once an agent has started.
pub fn desktop_notify(title: &str, body: &str) {
#[cfg(target_os = "linux")]
{
let _ = Command::new("notify-send")
.args(["-a", "agent-manager", title, body])
.spawn();
}
#[cfg(target_os = "macos")]
{
let _ = Command::new("osascript")
.args([
"-e",
&format!("display notification \"{body}\" with title \"{title}\""),
])
.spawn();
}
#[cfg(windows)]
{
// No built-in non-blocking toast without extra tooling; log instead.
let _ = (title, body);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sleep_cmd() -> (&'static str, Vec<String>) {
if cfg!(windows) {
("ping", vec!["-n".to_string(), "30".to_string(), "127.0.0.1".to_string()])
} else {
("sleep", vec!["30".to_string()])
}
}
#[test]
fn pid_lifecycle() {
let (prog, args) = sleep_cmd();
let mut child = Command::new(prog).args(&args).spawn().unwrap();
let pid = child.id();
assert!(is_running(pid));
assert!(stop_pid(pid, true, 5));
assert!(!is_running(pid));
let _ = child.wait();
}
#[test]
fn stop_non_running_is_false() {
assert!(!stop_pid(u32::MAX - 1, true, 1));
}
}