opencode_cloud_core/singleton/
mod.rs1use std::fs::{self, File};
8use std::io::{Read, Write};
9use std::path::PathBuf;
10
11use thiserror::Error;
12
13#[derive(Error, Debug)]
15pub enum SingletonError {
16 #[error("Another instance is already running (PID: {0})")]
18 AlreadyRunning(u32),
19
20 #[error("Failed to create lock directory: {0}")]
22 CreateDirFailed(String),
23
24 #[error("Failed to create lock file: {0}")]
26 LockFailed(String),
27
28 #[error("Invalid lock file path")]
30 InvalidPath,
31}
32
33pub struct InstanceLock {
38 pid_path: PathBuf,
39}
40
41impl InstanceLock {
42 pub fn acquire(pid_path: PathBuf) -> Result<Self, SingletonError> {
53 if let Some(parent) = pid_path.parent() {
55 fs::create_dir_all(parent)
56 .map_err(|e| SingletonError::CreateDirFailed(e.to_string()))?;
57 }
58
59 if pid_path.exists() {
61 let mut file =
63 File::open(&pid_path).map_err(|e| SingletonError::LockFailed(e.to_string()))?;
64 let mut contents = String::new();
65 file.read_to_string(&mut contents)
66 .map_err(|e| SingletonError::LockFailed(e.to_string()))?;
67
68 if let Ok(pid) = contents.trim().parse::<u32>() {
69 if is_process_running(pid) {
71 return Err(SingletonError::AlreadyRunning(pid));
72 }
73 tracing::info!("Removing stale PID file (PID {} not running)", pid);
75 }
76 fs::remove_file(&pid_path).map_err(|e| SingletonError::LockFailed(e.to_string()))?;
78 }
79
80 let mut file =
82 File::create(&pid_path).map_err(|e| SingletonError::LockFailed(e.to_string()))?;
83 write!(file, "{}", std::process::id())
84 .map_err(|e| SingletonError::LockFailed(e.to_string()))?;
85
86 tracing::debug!("Acquired singleton lock at: {}", pid_path.display());
87
88 Ok(Self { pid_path })
89 }
90
91 pub fn release(self) {
96 }
98
99 pub fn pid_path(&self) -> &PathBuf {
101 &self.pid_path
102 }
103}
104
105impl Drop for InstanceLock {
106 fn drop(&mut self) {
107 if let Err(e) = fs::remove_file(&self.pid_path) {
108 tracing::warn!("Failed to remove PID file on drop: {}", e);
109 } else {
110 tracing::debug!("Released singleton lock: {}", self.pid_path.display());
111 }
112 }
113}
114
115fn is_process_running(pid: u32) -> bool {
121 #[cfg(unix)]
122 {
123 match std::process::Command::new("kill")
126 .args(["-0", &pid.to_string()])
127 .output()
128 {
129 Ok(output) => output.status.success(),
130 Err(_) => {
131 #[cfg(target_os = "linux")]
133 {
134 std::path::Path::new(&format!("/proc/{pid}")).exists()
135 }
136 #[cfg(not(target_os = "linux"))]
137 {
138 false
140 }
141 }
142 }
143 }
144
145 #[cfg(windows)]
146 {
147 false
150 }
151
152 #[cfg(not(any(unix, windows)))]
153 {
154 false
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use tempfile::TempDir;
163
164 #[test]
165 fn test_acquire_creates_pid_file() {
166 let temp_dir = TempDir::new().unwrap();
167 let pid_path = temp_dir.path().join("test.pid");
168
169 let lock = InstanceLock::acquire(pid_path.clone()).unwrap();
170
171 assert!(pid_path.exists());
173
174 let contents = std::fs::read_to_string(&pid_path).unwrap();
176 let written_pid: u32 = contents.trim().parse().unwrap();
177 assert_eq!(written_pid, std::process::id());
178
179 drop(lock);
181
182 assert!(!pid_path.exists());
184 }
185
186 #[test]
187 fn test_acquire_fails_when_already_locked() {
188 let temp_dir = TempDir::new().unwrap();
189 let pid_path = temp_dir.path().join("test.pid");
190
191 let _lock1 = InstanceLock::acquire(pid_path.clone()).unwrap();
193
194 let result = InstanceLock::acquire(pid_path.clone());
196 assert!(matches!(result, Err(SingletonError::AlreadyRunning(_))));
197 }
198
199 #[test]
200 fn test_stale_lock_cleanup() {
201 let temp_dir = TempDir::new().unwrap();
202 let pid_path = temp_dir.path().join("test.pid");
203
204 std::fs::write(&pid_path, "999999").unwrap();
207
208 let lock = InstanceLock::acquire(pid_path.clone());
210
211 if lock.is_ok() {
215 assert!(pid_path.exists());
216 let contents = std::fs::read_to_string(&pid_path).unwrap();
217 let written_pid: u32 = contents.trim().parse().unwrap();
218 assert_eq!(written_pid, std::process::id());
219 }
220 }
221
222 #[test]
223 fn test_is_process_running_with_current_process() {
224 let current_pid = std::process::id();
225 assert!(is_process_running(current_pid));
226 }
227
228 #[test]
229 fn test_is_process_running_with_invalid_pid() {
230 let unlikely_pid = 4_000_000_000;
232 assert!(!is_process_running(unlikely_pid));
233 }
234
235 #[test]
236 fn test_creates_parent_directories() {
237 let temp_dir = TempDir::new().unwrap();
238 let pid_path = temp_dir
239 .path()
240 .join("deep")
241 .join("nested")
242 .join("dir")
243 .join("test.pid");
244
245 let lock = InstanceLock::acquire(pid_path.clone()).unwrap();
246 assert!(pid_path.exists());
247 drop(lock);
248 }
249}