1use std::fs;
2use std::io;
3use std::path::PathBuf;
4use std::process;
5
6#[cfg(unix)]
7use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt};
8
9fn pid_dir() -> PathBuf {
14 let base = if let Ok(xdg) = std::env::var("XDG_RUNTIME_DIR") {
15 PathBuf::from(xdg)
16 } else {
17 let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
18 PathBuf::from(home).join(".local").join("state")
19 };
20 base.join("semantic-diff")
21}
22
23fn ensure_pid_dir() -> io::Result<PathBuf> {
25 let dir = pid_dir();
26 if !dir.exists() {
27 #[cfg(unix)]
28 {
29 fs::DirBuilder::new()
30 .recursive(true)
31 .mode(0o700)
32 .create(&dir)?;
33 }
34 #[cfg(not(unix))]
35 {
36 fs::create_dir_all(&dir)?;
37 }
38 }
39 Ok(dir)
40}
41
42pub fn pid_file_path() -> PathBuf {
44 pid_dir().join("semantic-diff.pid")
45}
46
47pub fn log_file_path() -> PathBuf {
49 pid_dir().join("semantic-diff.log")
50}
51
52pub fn write_pid_file() -> io::Result<()> {
57 let dir = ensure_pid_dir()?;
58 let pid_path = dir.join("semantic-diff.pid");
59 let tmp_path = dir.join(".semantic-diff.pid.tmp");
60
61 let _ = fs::remove_file(&tmp_path);
63
64 {
66 let mut opts = fs::OpenOptions::new();
67 opts.write(true).create_new(true);
68 #[cfg(unix)]
69 opts.mode(0o600);
70 let mut file = opts.open(&tmp_path)?;
71 io::Write::write_all(&mut file, process::id().to_string().as_bytes())?;
72 }
73
74 fs::rename(&tmp_path, &pid_path)?;
76
77 Ok(())
78}
79
80pub fn remove_pid_file() {
82 let _ = fs::remove_file(pid_file_path());
83}
84
85fn validate_pid_ownership(pid: u32) -> bool {
91 if pid == 0 {
92 return false;
93 }
94
95 #[cfg(target_os = "macos")]
96 {
97 let output = std::process::Command::new("ps")
98 .args(["-p", &pid.to_string(), "-o", "comm="])
99 .output();
100 match output {
101 Ok(out) => {
102 let comm = String::from_utf8_lossy(&out.stdout);
103 comm.contains("semantic-diff")
104 }
105 Err(_) => false,
106 }
107 }
108
109 #[cfg(target_os = "linux")]
110 {
111 let comm_path = format!("/proc/{}/comm", pid);
112 match fs::read_to_string(&comm_path) {
113 Ok(comm) => comm.trim().contains("semantic-diff"),
114 Err(_) => false,
115 }
116 }
117
118 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
119 {
120 true
121 }
122}
123
124#[allow(dead_code)]
128pub fn read_pid() -> Option<u32> {
129 let pid_path = pid_file_path();
130 let pid: u32 = fs::read_to_string(pid_path).ok()?.trim().parse().ok()?;
131
132 if validate_pid_ownership(pid) {
133 Some(pid)
134 } else {
135 None
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use std::env;
143
144 #[test]
145 fn pid_dir_uses_xdg_runtime_dir_when_set() {
146 let test_dir = "/tmp/test-xdg-signal";
147 env::set_var("XDG_RUNTIME_DIR", test_dir);
148 let dir = pid_dir();
149 env::remove_var("XDG_RUNTIME_DIR");
150 assert!(
151 dir.starts_with(test_dir),
152 "pid_dir() should start with XDG_RUNTIME_DIR, got {:?}",
153 dir
154 );
155 assert!(
156 dir.ends_with("semantic-diff"),
157 "pid_dir() should end with 'semantic-diff', got {:?}",
158 dir
159 );
160 }
161
162 #[test]
163 fn pid_dir_falls_back_to_home_local_state() {
164 let saved = env::var("XDG_RUNTIME_DIR").ok();
167 env::remove_var("XDG_RUNTIME_DIR");
168 let dir = pid_dir();
169 if let Some(v) = saved {
170 env::set_var("XDG_RUNTIME_DIR", v);
171 }
172 assert!(
175 dir.ends_with("semantic-diff"),
176 "pid_dir() fallback should end with 'semantic-diff', got {:?}",
177 dir
178 );
179 }
180
181 #[test]
182 fn write_pid_file_creates_file_with_correct_pid() {
183 let test_dir = tempfile::tempdir().unwrap();
185 let dir = test_dir.path().join("semantic-diff");
186 fs::create_dir_all(&dir).unwrap();
187 let pid_path = dir.join("semantic-diff.pid");
188 let tmp_path = dir.join(".semantic-diff.pid.tmp");
189 let _ = fs::remove_file(&tmp_path);
190 {
191 let mut file = fs::OpenOptions::new()
192 .write(true)
193 .create_new(true)
194 .open(&tmp_path)
195 .unwrap();
196 io::Write::write_all(&mut file, process::id().to_string().as_bytes()).unwrap();
197 }
198 fs::rename(&tmp_path, &pid_path).unwrap();
199 let content = fs::read_to_string(&pid_path).unwrap();
200 assert_eq!(
201 content.trim(),
202 process::id().to_string(),
203 "PID file should contain current PID"
204 );
205 }
206
207 #[test]
208 fn read_pid_returns_none_for_nonexistent_file() {
209 let test_dir = tempfile::tempdir().unwrap();
210 env::set_var("XDG_RUNTIME_DIR", test_dir.path());
211 let result = read_pid();
212 env::remove_var("XDG_RUNTIME_DIR");
213 assert_eq!(result, None, "read_pid should return None when file doesn't exist");
214 }
215
216 #[test]
217 fn validate_pid_ownership_returns_false_for_invalid_pids() {
218 assert!(!validate_pid_ownership(0), "PID 0 should be invalid");
219 assert!(
220 !validate_pid_ownership(999_999_999),
221 "Very large PID should be invalid (process unlikely to exist)"
222 );
223 }
224
225 #[test]
226 fn atomic_write_creates_file_after_write() {
227 let test_dir = tempfile::tempdir().unwrap();
229 let dir = test_dir.path().join("semantic-diff");
230 fs::create_dir_all(&dir).unwrap();
231 let pid_path = dir.join("semantic-diff.pid");
232 let tmp_path = dir.join(".semantic-diff.pid.tmp");
233 let _ = fs::remove_file(&tmp_path);
234 fs::write(&tmp_path, "12345").unwrap();
235 fs::rename(&tmp_path, &pid_path).unwrap();
236 assert!(pid_path.exists(), "PID file should exist after atomic write");
237 assert!(!tmp_path.exists(), "Temp file should not exist after atomic write");
238 }
239}