xianyun_deploy/registrycache/
process.rs1use std::fs::OpenOptions;
6use std::io;
7use std::os::unix::process::CommandExt;
8use std::process::{Command, Stdio};
9use std::time::{Duration, Instant};
10
11use thiserror::Error;
12
13use super::paths::{self, PathError};
14
15const STOP_TIMEOUT: Duration = Duration::from_secs(5);
18
19#[derive(Debug, Error)]
21pub enum ProcessError {
22 #[error(transparent)]
24 Path(#[from] PathError),
25 #[error("creating registry cache dir: {0}")]
27 CreateDir(io::Error),
28 #[error("opening zot log: {0}")]
30 OpenLog(io::Error),
31 #[error("spawning {0}: {1}")]
33 Spawn(String, io::Error),
34 #[error("writing PID file: {0}")]
36 WritePid(io::Error),
37 #[error("reading PID file: {0}")]
39 ReadPid(io::Error),
40 #[error("parsing PID {0:?}: {1}")]
42 ParsePid(String, std::num::ParseIntError),
43 #[error("removing PID file: {0}")]
45 RemovePid(io::Error),
46 #[error("checking PID {0}: {1}")]
48 CheckPid(i32, io::Error),
49 #[error("sending {0} to {1}: {2}")]
52 Signal(&'static str, i32, io::Error),
53}
54
55pub fn spawn_process(binary: &str, args: &[String]) -> Result<i32, ProcessError> {
65 let log_path = paths::log_path()?;
66 let dir = paths::global_dir()?;
67
68 std::fs::create_dir_all(&dir).map_err(ProcessError::CreateDir)?;
69
70 let log_file = OpenOptions::new()
71 .create(true)
72 .append(true)
73 .open(&log_path)
74 .map_err(ProcessError::OpenLog)?;
75 let log_file_err = log_file.try_clone().map_err(ProcessError::OpenLog)?;
76
77 let mut cmd = Command::new(binary);
78 cmd.args(args)
79 .stdout(Stdio::from(log_file))
80 .stderr(Stdio::from(log_file_err));
81
82 unsafe {
87 cmd.pre_exec(|| {
88 if libc::setsid() == -1 {
89 return Err(io::Error::last_os_error());
90 }
91 Ok(())
92 });
93 }
94
95 let mut child = cmd
96 .spawn()
97 .map_err(|e| ProcessError::Spawn(binary.to_string(), e))?;
98
99 let pid = child.id().cast_signed();
100
101 let pid_path = paths::pid_path()?;
102 std::fs::write(&pid_path, format!("{pid}\n")).map_err(ProcessError::WritePid)?;
103
104 std::thread::spawn(move || {
108 let _ = child.wait();
109 });
110
111 Ok(pid)
112}
113
114pub fn is_running() -> Result<bool, ProcessError> {
117 let pid = match read_pid() {
118 Ok(pid) => pid,
119 Err(ProcessError::ReadPid(e)) if e.kind() == io::ErrorKind::NotFound => return Ok(false),
120 Err(e) => return Err(e),
121 };
122
123 let rc = unsafe { libc::kill(pid, 0) };
127 if rc == 0 {
128 return Ok(true);
129 }
130
131 let err = io::Error::last_os_error();
132 match err.raw_os_error() {
133 Some(e) if e == libc::ESRCH => Ok(false),
134 Some(e) if e == libc::EPERM => Ok(true),
137 _ => Err(ProcessError::CheckPid(pid, err)),
138 }
139}
140
141fn kill_probe_dead(pid: i32) -> bool {
142 let rc = unsafe { libc::kill(pid, 0) };
144 rc != 0 && io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH)
145}
146
147pub fn stop_process() -> Result<(), ProcessError> {
151 let pid = match read_pid() {
152 Ok(pid) => pid,
153 Err(ProcessError::ReadPid(e)) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
154 Err(e) => return Err(e),
155 };
156
157 let rc = unsafe { libc::kill(pid, libc::SIGTERM) };
159 if rc != 0 {
160 let err = io::Error::last_os_error();
161 if err.raw_os_error() != Some(libc::ESRCH) {
162 return Err(ProcessError::Signal("SIGTERM", pid, err));
163 }
164 }
165
166 let deadline = Instant::now() + STOP_TIMEOUT;
167 while Instant::now() < deadline {
168 if kill_probe_dead(pid) {
169 break;
170 }
171
172 std::thread::sleep(Duration::from_millis(100));
173 }
174
175 if !kill_probe_dead(pid) {
176 let rc = unsafe { libc::kill(pid, libc::SIGKILL) };
178 if rc != 0 {
179 let err = io::Error::last_os_error();
180 if err.raw_os_error() != Some(libc::ESRCH) {
181 return Err(ProcessError::Signal("SIGKILL", pid, err));
182 }
183 }
184 }
185
186 let pid_path = paths::pid_path()?;
187 match std::fs::remove_file(&pid_path) {
188 Ok(()) => Ok(()),
189 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
190 Err(e) => Err(ProcessError::RemovePid(e)),
191 }
192}
193
194pub(super) fn read_pid() -> Result<i32, ProcessError> {
195 let pid_path = paths::pid_path()?;
196 let body = std::fs::read_to_string(&pid_path).map_err(ProcessError::ReadPid)?;
197
198 body.trim()
199 .parse::<i32>()
200 .map_err(|e| ProcessError::ParsePid(body, e))
201}
202
203#[cfg(test)]
204mod tests {
205 use std::os::unix::fs::PermissionsExt;
206 use std::path::Path;
207 use std::time::{Duration, Instant};
208
209 use super::*;
210 use crate::registrycache::test_support::with_home;
211
212 fn write_stub_binary() -> tempfile::TempDir {
217 let dir = tempfile::tempdir().unwrap();
218 let bin = dir.path().join("zot-stub");
219
220 let script = format!(
221 "#!/bin/sh\n\
222 trap \"exit 0\" INT TERM\n\
223 echo woof > {}/started\n\
224 i=0; while [ \"$i\" -lt 60 ]; do sleep 1; i=$((i + 1)); done\n",
225 dir.path().display()
226 );
227
228 std::fs::write(&bin, script).unwrap();
229 std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).unwrap();
230
231 dir
232 }
233
234 fn wait_for_process_started(bin: &Path) {
235 let dir = bin.parent().unwrap();
236 let started = dir.join("started");
237
238 let deadline = Instant::now() + Duration::from_secs(5);
239 while Instant::now() < deadline {
240 if started.exists() {
241 return;
242 }
243
244 std::thread::sleep(Duration::from_millis(10));
245 }
246
247 panic!("timed out waiting for stub process to start");
248 }
249
250 fn kill_by_pid(pid: i32) {
251 unsafe {
254 libc::kill(pid, libc::SIGINT);
255 }
256 std::thread::sleep(Duration::from_millis(50));
257 unsafe {
260 libc::kill(pid, libc::SIGKILL);
261 }
262 }
263
264 #[test]
266 fn test_start_writes_pid_and_is_running() {
267 with_home(|_home| {
268 let stub_dir = write_stub_binary();
269 let stub = stub_dir.path().join("zot-stub");
270
271 let pid = spawn_process(stub.to_str().unwrap(), &["--whatever".to_string()]).unwrap();
272 assert!(pid > 0);
273
274 let pid_path = paths::pid_path().unwrap();
275 assert!(pid_path.exists());
276
277 let ok = is_running().unwrap();
278 assert!(ok);
279
280 kill_by_pid(pid);
281 });
282 }
283
284 #[test]
286 fn test_stop_kills_process_and_removes_pid() {
287 with_home(|_home| {
288 let stub_dir = write_stub_binary();
289 let stub = stub_dir.path().join("zot-stub");
290
291 spawn_process(stub.to_str().unwrap(), &[]).unwrap();
292 wait_for_process_started(&stub);
293
294 stop_process().unwrap();
295
296 let ok = is_running().unwrap();
297 assert!(!ok);
298
299 let pid_path = paths::pid_path().unwrap();
300 assert!(!pid_path.exists(), "PID file should be removed after stop");
301 });
302 }
303
304 #[test]
306 fn test_is_running_false_when_pid_file_missing() {
307 with_home(|_home| {
308 let ok = is_running().unwrap();
309 assert!(!ok);
310 });
311 }
312
313 #[test]
315 fn test_is_running_false_when_pid_file_stale() {
316 with_home(|_home| {
317 let pid_path = paths::pid_path().unwrap();
318 std::fs::create_dir_all(pid_path.parent().unwrap()).unwrap();
319 std::fs::write(&pid_path, "9999999\n").unwrap();
320
321 let ok = is_running().unwrap();
322 assert!(!ok);
323 });
324 }
325
326 #[test]
328 fn test_stop_when_not_running_is_noop() {
329 with_home(|_home| {
330 stop_process().unwrap();
331 });
332 }
333}