pidfile/
lib.rs

1//! Simple PID lock file management.
2//!
3//! This is a very basic library for creating and using PID files
4//! to coordinate among processes.
5//!
6//! A PID file is a file that contains the PID of a process. It can be used
7//! as a crude form of locking to prevent multiple instances of a process
8//! from running at the same time, or to provide a lock for a resource which
9//! should only be accessed by one process at a time.
10//!
11//! This library provides a simple API for creating and using PID files. PID
12//! files are created at a given path, and are automatically removed when the
13//! PID file object is dropped.
14//!
15//! # Example
16//!
17//! ```rust
18//! use pidfile::PidFile;
19//!
20//! fn main() -> Result<(), Box<dyn std::error::Error>> {
21//!    let pidfile = PidFile::new("/tmp/myapp.pid")?;
22//!   // Do stuff
23//!
24//!   Ok(())
25//! }
26//! ```
27
28use std::io;
29use std::path::{Path, PathBuf};
30
31/// A PID file is a file that contains the PID of a process. It is used to
32/// prevent multiple instances of a process from running at the same time,
33/// or to provide a lock for a resource which should only be accessed by one
34/// process at a time.
35#[derive(Debug)]
36pub struct PidFile {
37    path: PathBuf,
38}
39
40/// Check if a PID file is in use.
41///
42/// If the PID file corresponds to a currently unused PID, the file
43/// will be removed by this function.
44fn pid_file_in_use(path: &Path) -> Result<bool, io::Error> {
45    match std::fs::read_to_string(path) {
46        Ok(info) => {
47            let pid: libc::pid_t = info.trim().parse().map_err(|error| {
48                tracing::debug!(path=%path.display(), "Unable to parse PID file {path}: {error}", path = path.display());
49                io::Error::new(io::ErrorKind::InvalidData, "expected a PID")
50            })?;
51
52            // SAFETY: I dunno? Libc is probably fine.
53            #[allow(unsafe_code)]
54            let errno = unsafe { libc::kill(pid, 0) };
55
56            if errno == 0 {
57                tracing::debug!(%pid, "PID {pid} is still running", pid = pid);
58                // This PID still exists, so the pid file is valid.
59                return Ok(true);
60            }
61
62            if errno == -1 {
63                tracing::debug!(%pid, "Unkonwn error checking PID file: {errno}");
64                return Ok(false);
65            };
66
67            let error = io::Error::from_raw_os_error(errno);
68            match error.kind() {
69                io::ErrorKind::NotFound => Ok(false),
70                _ => Err(error),
71            }
72        }
73        Err(error) => match error.kind() {
74            io::ErrorKind::NotFound => Ok(false),
75            _ => Err(error),
76        },
77    }
78}
79
80impl PidFile {
81    /// Create a new PID file at the given path for this process.
82    ///
83    /// If the PID file already exists, this function will check if the
84    /// PID file is still in use. If the PID file is in use, this function
85    /// will return Err(io::ErrorKind::AddrInUse). If the PID file is not
86    /// in use, it will be removed and a new PID file will be created.
87    pub fn new(path: impl Into<PathBuf>) -> Result<Self, io::Error> {
88        let path = path.into();
89        if path.exists() {
90            match pid_file_in_use(&path) {
91                Ok(true) => {
92                    tracing::error!(path=%path.display(), "PID File {path} is already in use", path = path.display());
93                    return Err(io::Error::new(
94                        io::ErrorKind::AddrInUse,
95                        format!("PID File {path} is already in use", path = path.display()),
96                    ));
97                }
98                Ok(false) => {
99                    tracing::debug!(path=%path.display(), "Removing stale PID file at {path}", path = path.display());
100                    let _ = std::fs::remove_file(&path);
101                }
102                Err(error) if error.kind() == io::ErrorKind::InvalidData => {
103                    tracing::warn!(path=%path.display(), "Removing invalid PID file at {path}", path = path.display());
104                    let _ = std::fs::remove_file(&path);
105                }
106                Err(error) => {
107                    tracing::error!(path=%path.display(), "Unable to check PID file {path}: {error}", path = path.display());
108                    return Err(error);
109                }
110            }
111        }
112
113        // SAFETY: What could go wrong?
114        #[allow(unsafe_code)]
115        let pid = unsafe { libc::getpid() };
116
117        if pid <= 0 {
118            tracing::error!("libc::getpid() returned a negative PID: {pid}");
119            return Err(io::Error::new(io::ErrorKind::Other, "negative PID"));
120        }
121
122        std::fs::write(&path, format!("{}", pid))?;
123        tracing::trace!(%pid, path=%path.display(), "Locked PID file at {path}", path = path.display());
124
125        Ok(Self { path })
126    }
127
128    /// Check if a PID file is in use at this path.
129    ///
130    /// If this function returns an error, it indicates that either the PID file
131    /// could not be accessed, or when accessed, it contained data which did not look like a PID.
132    pub fn is_locked(path: &Path) -> Result<bool, io::Error> {
133        match pid_file_in_use(path) {
134            Ok(true) => Ok(true),
135            Ok(false) => Ok(false),
136            Err(error) if error.kind() == io::ErrorKind::InvalidData => {
137                tracing::warn!(path=%path.display(), "Invalid PID file at {path}", path = path.display());
138                Ok(false)
139            }
140            Err(error) => {
141                tracing::error!(path=%path.display(), "Unable to check PID file {path}: {error}", path=path.display());
142                Err(error)
143            }
144        }
145    }
146}
147
148impl Drop for PidFile {
149    fn drop(&mut self) {
150        match std::fs::remove_file(&self.path) {
151            Ok(_) => {}
152            Err(error) => eprintln!(
153                "Encountered an error removing the PID file at {}: {}",
154                self.path.display(),
155                error
156            ),
157        }
158    }
159}
160
161#[cfg(test)]
162mod test {
163    use super::*;
164
165    #[test]
166    fn test_pid_file() {
167        let tmp = tempfile::tempdir().unwrap();
168        let path = tmp.path().join("pidfile-test.pid");
169        let pid_file = PidFile::new(path.clone()).unwrap();
170        assert!(PidFile::is_locked(&path).unwrap());
171        drop(pid_file);
172        assert!(!PidFile::is_locked(&path).unwrap());
173    }
174
175    #[test]
176    fn test_invalid_file() {
177        let path = Path::new("/tmp/pidfile-test.pid");
178        std::fs::write(path, "not a pid").unwrap();
179        tracing::subscriber::with_default(tracing::subscriber::NoSubscriber::new(), || {
180            assert!(
181                !PidFile::is_locked(path).unwrap(),
182                "Invalid file should not be locked."
183            )
184        });
185        assert!(
186            path.exists(),
187            "Invalid file should exist after checking for locks."
188        );
189
190        let pid_file =
191            tracing::subscriber::with_default(tracing::subscriber::NoSubscriber::new(), || {
192                PidFile::new(path).unwrap()
193            });
194        assert!(
195            PidFile::is_locked(path).unwrap(),
196            "PID file should be locked after creation."
197        );
198        drop(pid_file);
199        assert!(
200            !PidFile::is_locked(path).unwrap(),
201            "PID file should not be locked after drop."
202        );
203    }
204}