Skip to main content

agent_can/
process.rs

1use std::fs::OpenOptions;
2use std::io::Write;
3use std::path::{Path, PathBuf};
4use std::time::{Duration, SystemTime};
5use tokio::time::sleep;
6
7const LOCK_POLL_INTERVAL: Duration = Duration::from_millis(50);
8const STALE_INVALID_LOCK_AGE: Duration = Duration::from_secs(1);
9
10#[cfg(unix)]
11pub fn kill_pid(pid: u32) -> std::io::Result<()> {
12    let result = unsafe { libc::kill(pid as i32, libc::SIGKILL) };
13    if result == 0 {
14        Ok(())
15    } else {
16        Err(std::io::Error::last_os_error())
17    }
18}
19
20#[cfg(unix)]
21pub fn pid_exists(pid: u32) -> std::io::Result<bool> {
22    let result = unsafe { libc::kill(pid as i32, 0) };
23    if result == 0 {
24        return Ok(true);
25    }
26
27    let err = std::io::Error::last_os_error();
28    match err.raw_os_error() {
29        Some(code) if code == libc::ESRCH => Ok(false),
30        Some(code) if code == libc::EPERM => Ok(true),
31        _ => Err(err),
32    }
33}
34
35#[cfg(windows)]
36pub fn kill_pid(pid: u32) -> std::io::Result<()> {
37    use windows_sys::Win32::Foundation::CloseHandle;
38    use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_TERMINATE, TerminateProcess};
39
40    let handle = unsafe { OpenProcess(PROCESS_TERMINATE, 0, pid) };
41    if handle.is_null() {
42        return Err(std::io::Error::last_os_error());
43    }
44
45    let terminated = unsafe { TerminateProcess(handle, 1) };
46    let terminate_error = if terminated == 0 {
47        Some(std::io::Error::last_os_error())
48    } else {
49        None
50    };
51    unsafe {
52        CloseHandle(handle);
53    }
54
55    if let Some(err) = terminate_error {
56        Err(err)
57    } else {
58        Ok(())
59    }
60}
61
62#[cfg(windows)]
63pub fn pid_exists(pid: u32) -> std::io::Result<bool> {
64    use windows_sys::Win32::Foundation::{CloseHandle, ERROR_ACCESS_DENIED};
65    use windows_sys::Win32::Storage::FileSystem::SYNCHRONIZE;
66    use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION};
67
68    let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, 0, pid) };
69    if handle.is_null() {
70        let err = std::io::Error::last_os_error();
71        return match err.raw_os_error() {
72            Some(code) if code == ERROR_ACCESS_DENIED as i32 => Ok(true),
73            Some(_) => Ok(false),
74            None => Err(err),
75        };
76    }
77
78    unsafe {
79        CloseHandle(handle);
80    }
81    Ok(true)
82}
83
84#[cfg(not(any(unix, windows)))]
85compile_error!("process helpers are only implemented for Unix and Windows targets");
86
87pub struct StartupLock {
88    path: PathBuf,
89}
90
91impl StartupLock {
92    pub async fn acquire(path: PathBuf, timeout: Duration) -> std::io::Result<Self> {
93        if let Some(parent) = path.parent() {
94            std::fs::create_dir_all(parent)?;
95        }
96
97        let deadline = std::time::Instant::now() + timeout;
98        loop {
99            match try_create_lock(&path) {
100                Ok(()) => return Ok(Self { path }),
101                Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
102                    if clear_stale_lock(&path)? {
103                        continue;
104                    }
105                    if std::time::Instant::now() >= deadline {
106                        return Err(std::io::Error::new(
107                            std::io::ErrorKind::TimedOut,
108                            format!("timed out waiting for startup lock '{}'", path.display()),
109                        ));
110                    }
111                    sleep(LOCK_POLL_INTERVAL).await;
112                }
113                Err(err) => return Err(err),
114            }
115        }
116    }
117}
118
119impl Drop for StartupLock {
120    fn drop(&mut self) {
121        let _ = std::fs::remove_file(&self.path);
122    }
123}
124
125fn try_create_lock(path: &Path) -> std::io::Result<()> {
126    let mut file = OpenOptions::new().write(true).create_new(true).open(path)?;
127    writeln!(file, "{}", std::process::id())?;
128    file.sync_all()?;
129    Ok(())
130}
131
132fn clear_stale_lock(path: &Path) -> std::io::Result<bool> {
133    let Ok(contents) = std::fs::read_to_string(path) else {
134        return Ok(false);
135    };
136
137    if let Ok(owner_pid) = contents.trim().parse::<u32>() {
138        if !pid_exists(owner_pid)? {
139            std::fs::remove_file(path)?;
140            return Ok(true);
141        }
142        return Ok(false);
143    }
144
145    let modified_at = path
146        .metadata()?
147        .modified()
148        .unwrap_or(SystemTime::UNIX_EPOCH);
149    let age = SystemTime::now()
150        .duration_since(modified_at)
151        .unwrap_or(Duration::ZERO);
152    if age >= STALE_INVALID_LOCK_AGE {
153        std::fs::remove_file(path)?;
154        return Ok(true);
155    }
156
157    Ok(false)
158}