pidfile_rs/
lib.rs

1// Copyright (C) 2020—2024 Andrej Shadura
2// SPDX-License-Identifier: MIT
3use libc::{c_char, c_int, mode_t, pid_t};
4use log::warn;
5use std::ffi::{CString, NulError};
6use std::fs::Permissions;
7use std::io;
8use std::os::unix::{ffi::OsStrExt, fs::PermissionsExt};
9use std::path::Path;
10use thiserror::Error;
11
12#[cfg(any(target_os = "dragonfly", target_os = "freebsd"))]
13use libc::{pidfh, pidfile_close, pidfile_open, pidfile_remove, pidfile_write};
14
15#[cfg(not(any(target_os = "dragonfly", target_os = "freebsd")))]
16extern "C" {
17    fn pidfile_open(path: *const c_char, mode: mode_t, pidptr: *mut pid_t) -> *mut pidfh;
18    fn pidfile_write(pfh: *mut pidfh) -> c_int;
19    fn pidfile_close(pfh: *mut pidfh) -> c_int;
20    fn pidfile_remove(pfh: *mut pidfh) -> c_int;
21}
22
23#[cfg(not(any(target_os = "dragonfly", target_os = "freebsd")))]
24#[allow(non_camel_case_types)]
25enum pidfh {}
26
27/// A PID file protected with a lock.
28///
29/// An instance of `Pidfile` can be used to manage a PID file: create it,
30/// lock it, detect already running daemons. It is backed by [`pidfile`][]
31/// functions of `libbsd`/`libutil` which use `flopen` to lock the PID
32/// file.
33///
34/// When a PID file is created, the process ID of the current process is
35/// *not* written there, making it possible to lock the PID file before
36/// forking and only write the ID of the forked process when it is ready.
37///
38/// The PID file is deleted automatically when the `Pidfile` comes out of
39/// the scope. To close the PID file without deleting it, for example, in
40/// the parent process of a forked daemon, call `close()`.
41///
42/// # Example
43///
44/// When the parent process exits without calling destructors, e.g. by
45/// using [`exit`][] or when forking with [`daemon`(3)], `Pidfile` can
46/// be used in the following way:
47/// ```rust
48/// # use std::error::Error;
49/// # use std::fs::Permissions;
50/// # use std::os::unix::fs::PermissionsExt;
51/// # use tempfile::tempdir;
52/// // This example uses daemon(3) wrapped by nix to daemonise:
53/// use nix::unistd::daemon;
54/// use pidfile_rs::Pidfile;
55///
56/// // ...
57///
58/// # let dir = tempdir()?;
59/// # let mut pidfile_path = dir.path().to_owned();
60/// # pidfile_path.push("file.pid");
61/// # println!("pidfile_path = {:?}", pidfile_path);
62/// let pidfile = Some(Pidfile::new(
63///     &pidfile_path,
64///     Permissions::from_mode(0o600)
65/// )?);
66///
67/// // do some pre-fork preparations
68/// // ...
69///
70/// // in the parent process, the PID file is closed without deleting it
71/// daemon(false, true)?;
72///
73/// pidfile.unwrap().write();
74///
75/// // do some work
76/// println!("The daemon’s work is done, now it’s time to go home.");
77///
78/// // the PID file will be deleted when this function returns
79///
80/// # Ok::<(), Box<dyn Error>>(())
81/// ```
82///
83/// [`exit`]: https://doc.rust-lang.org/std/process/fn.exit.html
84/// [`pidfile`]: https://linux.die.net/man/3/pidfile
85/// [`daemon`(3)]: https://linux.die.net/man/3/daemon
86#[derive(Debug)]
87pub struct Pidfile {
88    pidfh: *mut pidfh,
89}
90
91#[derive(Error, Debug)]
92pub enum PidfileError {
93    /// The file cannot be locked. The `pid` field contains the PID of the
94    /// already running process or `None` in case it did not write
95    /// its PID yet.
96    #[error("daemon already running with {}", match .pid {
97        Some(pid) => format!("PID {pid}"),
98        None => "unknown PID".into()
99    })]
100    AlreadyRunning { pid: Option<pid_t> },
101    /// An I/O error has occurred.
102    #[error(transparent)]
103    Io(#[from] io::Error),
104    /// An interior NUL byte was found in the path.
105    #[error(transparent)]
106    NulError(#[from] NulError),
107}
108
109impl Pidfile {
110    /// Creates a new PID file and locks it.
111    ///
112    /// If the PID file cannot be locked, returns `PidfileError::AlreadyRunning` with
113    /// a PID of the already running process, or `None` if no PID has been written to
114    /// the PID file yet.
115    pub fn new(path: &Path, permissions: Permissions) -> Result<Pidfile, PidfileError> {
116        let c_path = CString::new(path.as_os_str().as_bytes()).map_err(PidfileError::NulError)?;
117        let mut old_pid: pid_t = -1;
118        let pidfh =
119            unsafe { pidfile_open(c_path.as_ptr(), permissions.mode() as mode_t, &mut old_pid) };
120        if !pidfh.is_null() {
121            Ok(Pidfile { pidfh })
122        } else {
123            let err = io::Error::last_os_error();
124            if err.kind() == io::ErrorKind::AlreadyExists {
125                Err(PidfileError::AlreadyRunning {
126                    pid: if old_pid != -1 { Some(old_pid) } else { None },
127                })
128            } else {
129                Err(PidfileError::Io(err))
130            }
131        }
132    }
133
134    /// Writes the current process ID to the PID file.
135    ///
136    /// The file is truncated before writing.
137    pub fn write(&mut self) -> Result<(), PidfileError> {
138        if unsafe { pidfile_write(self.pidfh) == 0 } {
139            Ok(())
140        } else {
141            Err(PidfileError::Io(io::Error::last_os_error()))
142        }
143    }
144
145    /// Closes the PID file without removing it.
146    ///
147    /// This function consumes the object, making it impossible
148    /// to manipulated with the PID file after this function has
149    /// been called.
150    pub fn close(mut self) {
151        if unsafe { pidfile_close(self.pidfh) != 0 } {
152            let err = io::Error::last_os_error();
153            warn!("Failed to close the PID file: {err}");
154        }
155        self.pidfh = std::ptr::null_mut();
156    }
157}
158
159impl Drop for Pidfile {
160    /// Closes the PID file and removes it.
161    fn drop(&mut self) {
162        if unsafe { pidfile_remove(self.pidfh) != 0 } {
163            let err = io::Error::last_os_error();
164            warn!("Failed to remove the PID file: {err}");
165        }
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use crate::*;
172    use std::fs::{read_to_string, Permissions};
173    use std::io;
174    use std::os::unix::fs::PermissionsExt;
175    use std::process;
176    use tempfile::tempdir;
177
178    #[test]
179    fn create_file() {
180        let dir = tempdir().unwrap();
181        let mut pidfile_path = dir.path().to_owned();
182        pidfile_path.push("file.pid");
183        let my_pid = process::id().to_string();
184        {
185            let mut pidfile = Pidfile::new(&pidfile_path, Permissions::from_mode(0o600))
186                .expect("Failed to create PID file");
187            println!("pidfile_path = {pidfile_path:?}");
188            assert!(pidfile_path.is_file());
189            pidfile.write().expect("Failed to write PID file");
190
191            let contents = read_to_string(pidfile_path.as_path()).expect("Can’t read PID file");
192            assert_eq!(my_pid, contents);
193        }
194
195        assert!(!pidfile_path.is_file(), "PID file should have disappeared");
196    }
197
198    #[test]
199    fn close_file() {
200        let dir = tempdir().unwrap();
201        let mut pidfile_path = dir.path().to_owned();
202        pidfile_path.push("file.pid");
203        let my_pid = process::id().to_string();
204        {
205            let mut pidfile = Pidfile::new(&pidfile_path, Permissions::from_mode(0o600))
206                .expect("Failed to create PID file");
207            println!("pidfile_path = {pidfile_path:?}");
208            assert!(pidfile_path.is_file());
209            pidfile.write().expect("Failed to write PID file");
210
211            let contents = read_to_string(pidfile_path.as_path()).expect("Can’t read PID file");
212            assert_eq!(my_pid, contents);
213
214            pidfile.close();
215        }
216
217        assert!(
218            pidfile_path.is_file(),
219            "PID file should have not disappeared"
220        );
221    }
222
223    #[test]
224    fn invalid_path() {
225        let dir = tempdir().unwrap();
226        let mut pidfile_path = dir.path().to_owned();
227        pidfile_path.push("<<non-existing>>");
228        pidfile_path.push("file.pid");
229        let error = Pidfile::new(&pidfile_path, Permissions::from_mode(0o600))
230            .expect_err("PID file shouldn’t exist, but it does");
231        println!("pidfile_path = {pidfile_path:?}");
232        assert!(!pidfile_path.is_file());
233        if let PidfileError::Io(error) = error {
234            assert_eq!(error.kind(), io::ErrorKind::NotFound);
235        } else {
236            panic!("unexpected error: {:?}", error)
237        }
238
239        pidfile_path.push("\0");
240        let error = Pidfile::new(&pidfile_path, Permissions::from_mode(0o600))
241            .expect_err("NULs should not have been accepted, but they were");
242        if let PidfileError::NulError(error) = error {
243            println!("expected error: {error}");
244        } else {
245            panic!("unexpected error: {:?}", error)
246        }
247    }
248
249    #[test]
250    fn concurrent() {
251        let dir = tempdir().unwrap();
252        let mut pidfile_path = dir.path().to_owned();
253        pidfile_path.push("file.pid");
254        let my_pid = process::id().to_string();
255        let mut pidfile = Pidfile::new(&pidfile_path, Permissions::from_mode(0o600))
256            .expect("Failed to create PID file");
257        println!("pidfile_path = {pidfile_path:?}");
258        assert!(pidfile_path.is_file(), "PID file not created?");
259        pidfile.write().expect("Failed to write PID file");
260
261        let contents = read_to_string(pidfile_path.as_path()).expect("Can’t read PID file");
262        assert_eq!(my_pid, contents);
263
264        let error = Pidfile::new(&pidfile_path, Permissions::from_mode(0o600))
265            .expect_err("Expected error, but got");
266        assert_eq!(
267            error.to_string(),
268            format!("daemon already running with PID {my_pid}")
269        );
270        if let PidfileError::AlreadyRunning { pid } = error {
271            assert_eq!(
272                my_pid,
273                pid.expect("No PID written?").to_string(),
274                "PID different?!"
275            );
276        } else {
277            panic!("unexpected error: {:?}", error)
278        }
279    }
280}