pidfile_rs/
lib.rs

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