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}