cc_switch/daemon/
pidfile.rs1use anyhow::{Context, Result};
9use std::io::{ErrorKind, Write};
10use std::path::PathBuf;
11
12pub struct Pidfile {
13 path: PathBuf,
14}
15
16impl Pidfile {
17 pub fn new(path: PathBuf) -> Self {
18 Self { path }
19 }
20
21 pub fn acquire(&self) -> Result<()> {
24 let mut options = std::fs::OpenOptions::new();
25 options.write(true).create_new(true);
26
27 #[cfg(unix)]
28 {
29 use std::os::unix::fs::OpenOptionsExt;
30 options.mode(0o600);
31 }
32
33 let mut file = options
34 .open(&self.path)
35 .with_context(|| format!("failed to create pidfile at {}", self.path.display()))?;
36 let pid = std::process::id();
37 file.write_all(format!("{pid}\n").as_bytes())
38 .with_context(|| format!("failed to write pid to {}", self.path.display()))?;
39 file.sync_all()
40 .with_context(|| format!("failed to fsync pidfile {}", self.path.display()))?;
41 Ok(())
42 }
43
44 pub fn release(&self) -> Result<()> {
46 match std::fs::remove_file(&self.path) {
47 Ok(()) => Ok(()),
48 Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
49 Err(err) => Err(err)
50 .with_context(|| format!("failed to remove pidfile {}", self.path.display())),
51 }
52 }
53
54 pub fn read(&self) -> Result<Option<u32>> {
57 let raw = match std::fs::read_to_string(&self.path) {
58 Ok(text) => text,
59 Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None),
60 Err(err) => {
61 return Err(err)
62 .with_context(|| format!("failed to read pidfile {}", self.path.display()));
63 }
64 };
65 let trimmed = raw.trim();
66 trimmed.parse::<u32>().map(Some).map_err(|err| {
67 anyhow::anyhow!(
68 "pidfile {} contains unparseable content {:?}: {err}",
69 self.path.display(),
70 trimmed
71 )
72 })
73 }
74}
75
76#[cfg(unix)]
85pub fn process_alive(pid: u32) -> Result<bool> {
86 let ret = unsafe { libc::kill(pid as libc::pid_t, 0) };
89 if ret == 0 {
90 return Ok(true);
91 }
92 let err = std::io::Error::last_os_error();
93 match err.raw_os_error() {
94 Some(libc::ESRCH) => Ok(false),
95 Some(libc::EPERM) => Ok(true),
96 _ => Err(err).with_context(|| format!("kill({pid}, 0) failed")),
97 }
98}
99
100#[cfg(not(unix))]
101pub fn process_alive(_pid: u32) -> Result<bool> {
102 Ok(false)
103}
104
105#[cfg(target_os = "linux")]
115pub fn process_name(pid: u32) -> Option<String> {
116 let raw = std::fs::read_to_string(format!("/proc/{pid}/comm")).ok()?;
117 let trimmed = raw.trim();
118 if trimmed.is_empty() {
119 None
120 } else {
121 Some(trimmed.to_string())
122 }
123}
124
125#[cfg(all(unix, not(target_os = "linux")))]
126pub fn process_name(pid: u32) -> Option<String> {
127 use std::process::Command;
128 let output = Command::new("ps")
129 .args(["-p", &pid.to_string(), "-o", "comm="])
130 .output()
131 .ok()?;
132 if !output.status.success() {
133 return None;
134 }
135 let stdout = String::from_utf8(output.stdout).ok()?;
136 let trimmed = stdout.trim();
137 if trimmed.is_empty() {
138 return None;
139 }
140 let basename = std::path::Path::new(trimmed)
141 .file_name()
142 .and_then(|name| name.to_str())
143 .unwrap_or(trimmed)
144 .to_string();
145 Some(basename)
146}
147
148#[cfg(not(unix))]
149pub fn process_name(_pid: u32) -> Option<String> {
150 None
151}
152
153#[cfg(test)]
154mod tests {
155 use super::{Pidfile, process_alive};
156 use tempfile::TempDir;
157
158 fn make_path() -> (TempDir, std::path::PathBuf) {
159 let dir = TempDir::new().expect("tempdir");
160 let path = dir.path().join("ccs-proxy.pid");
161 (dir, path)
162 }
163
164 #[test]
165 fn acquire_writes_pid_to_file() {
166 let (_dir, path) = make_path();
167 let pidfile = Pidfile::new(path.clone());
168 pidfile.acquire().expect("acquire");
169
170 let raw = std::fs::read_to_string(&path).expect("read pidfile");
171 let parsed: u32 = raw.trim().parse().expect("parse pid");
172 assert_eq!(parsed, std::process::id());
173 }
174
175 #[test]
176 fn acquire_errors_when_file_already_exists() {
177 let (_dir, path) = make_path();
178 let pidfile = Pidfile::new(path);
179 pidfile.acquire().expect("first acquire");
180 let second = pidfile.acquire();
181 assert!(second.is_err(), "second acquire must fail");
182 }
183
184 #[test]
185 fn release_removes_file() {
186 let (_dir, path) = make_path();
187 let pidfile = Pidfile::new(path.clone());
188 pidfile.acquire().expect("acquire");
189 pidfile.release().expect("release");
190 assert!(!path.exists(), "pidfile should be gone after release");
191 pidfile.release().expect("release is idempotent");
192 }
193
194 #[test]
195 fn read_missing_file_returns_none() {
196 let (_dir, path) = make_path();
197 let pidfile = Pidfile::new(path);
198 assert!(pidfile.read().expect("read").is_none());
199 }
200
201 #[test]
202 fn read_unparseable_returns_err() {
203 let (_dir, path) = make_path();
204 std::fs::write(&path, "hello\n").expect("write garbage");
205 let pidfile = Pidfile::new(path);
206 assert!(pidfile.read().is_err());
207 }
208
209 #[test]
210 fn read_returns_pid_for_valid_file() {
211 let (_dir, path) = make_path();
212 let pidfile = Pidfile::new(path);
213 pidfile.acquire().expect("acquire");
214 let pid = pidfile.read().expect("read");
215 assert_eq!(pid, Some(std::process::id()));
216 }
217
218 #[cfg(unix)]
219 #[test]
220 fn process_alive_for_self() {
221 let alive = process_alive(std::process::id()).expect("query self");
222 assert!(alive);
223 }
224
225 #[cfg(unix)]
226 #[test]
227 fn process_alive_for_pid_1() {
228 let alive = process_alive(1).expect("query pid 1");
232 assert!(alive, "PID 1 must be reported alive on Unix");
233 }
234
235 #[test]
236 fn process_alive_for_high_unused_pid() {
237 let alive = process_alive(0xFFFF_FFFE).expect("query unused pid");
238 assert!(!alive);
239 }
240
241 #[cfg(unix)]
242 #[test]
243 fn acquire_sets_unix_0600_perms() {
244 use std::os::unix::fs::PermissionsExt;
245 let (_dir, path) = make_path();
246 let pidfile = Pidfile::new(path.clone());
247 pidfile.acquire().expect("acquire");
248 let meta = std::fs::metadata(&path).expect("stat");
249 let mode = meta.permissions().mode() & 0o777;
250 assert_eq!(mode, 0o600, "pidfile must be 0600, got {mode:o}");
251 }
252}