aquarium_control/utilities/
publish_pid.rs

1/* Copyright 2024 Uwe Martin
2
3Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
5The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
7THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8*/
9
10use crate::utilities::publish_pid_config::PublishPidConfig;
11use std::num::ParseIntError;
12use std::{fs, process};
13use thiserror::Error;
14
15/// Contains the error definition for PublishPid
16#[derive(Debug, Error)]
17pub enum PublishPidError {
18    #[error("[{location}] Unable to read PID file ({file}).")]
19    UnableToReadPidFile {
20        location: String,
21        file: String,
22
23        #[source]
24        source: std::io::Error,
25    },
26
27    #[error("[{location}] PID file ({file}) contains invalid content.")]
28    PidFileParseError {
29        location: String,
30        file: String,
31
32        #[source]
33        source: ParseIntError,
34    },
35
36    #[error("[{location}] PID file ({file}) already contains published PID: {pid}")]
37    PidFileContainsPid {
38        location: String,
39        file: String,
40        pid: u32,
41    },
42
43    #[error("[{location}] Unable to write PID file ({file}).")]
44    UnableToWritePidFile {
45        location: String,
46        file: String,
47
48        #[source]
49        source: std::io::Error,
50    },
51}
52/// Contains the struct for configuration data and provides implementation for
53/// publication of process id (Pid).
54#[derive(Clone)]
55pub struct PublishPid {
56    config: PublishPidConfig,
57}
58
59impl PublishPid {
60    /// Creates a new `PublishPid` instance and publishes the current process ID (PID) to a file.
61    ///
62    /// This constructor performs a critical step of publishing the application's PID to a
63    /// designated file. This is often used to prevent multiple instances of the application
64    /// from running concurrently.
65    ///
66    /// The process involves:
67    /// 1.  Checking for the existence of the PID file.
68    /// 2.  If the file exists and contains a valid, positive PID, the function returns an error,
69    ///     indicating that another instance of the application is likely already running.
70    /// 3.  If the file does not exist, is empty, or contains "0", the function
71    ///     proceeds to write the current process's PID to the file, creating it if necessary.
72    ///
73    /// # Arguments
74    /// * `config` - Configuration data for publishing the PID, primarily containing
75    ///   the `pid_filename`.
76    ///
77    /// # Returns
78    /// A `Result` containing a new `PublishPid` struct on success.
79    ///
80    /// # Errors
81    /// Returns a `PublishPidError` if the PID cannot be published:
82    /// - `PublishPidError::UnableToReadPidFile`: If the specified PID file exists but cannot
83    ///   be read due to, for example, insufficient permissions.
84    /// - `PublishPidError::PidFileParseError`: If the PID file contains content that cannot
85    ///   be parsed into a `u32` integer.
86    /// - `PublishPidError::PidFileContainsPid`: If the PID file already contains a valid,
87    ///   positive PID, indicating a potential duplicate instance is running.
88    /// - `PublishPidError::UnableToWritePidFile`: If writing the current process's PID to
89    ///   the file fails, for example due to insufficient write permissions or an invalid path.
90    pub fn new(config: PublishPidConfig) -> Result<PublishPid, PublishPidError> {
91        match fs::read_to_string(&config.pid_filename) {
92            Ok(file_data) => {
93                // File exists, check its content.
94                let file_data_trimmed = file_data.trim();
95                if !file_data_trimmed.is_empty() {
96                    let process_id_read: u32 = file_data_trimmed.parse().map_err(|e| {
97                        PublishPidError::PidFileParseError {
98                            location: module_path!().to_string(),
99                            file: config.pid_filename.clone(),
100                            source: e,
101                        }
102                    })?;
103
104                    if process_id_read > 0 {
105                        // A common extension here is to check if a process with this PID is
106                        // actually running, but for now, checking the file is a robust start.
107                        return Err(PublishPidError::PidFileContainsPid {
108                            location: module_path!().to_string(),
109                            file: config.pid_filename.clone(),
110                            pid: process_id_read,
111                        });
112                    }
113                }
114                // If we are here, the file was empty or contained "0". We can proceed to write.
115            }
116            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
117                // File doesn't exist, which is a valid state. We will create it.
118            }
119            Err(e) => {
120                // Any other read error is fatal.
121                return Err(PublishPidError::UnableToReadPidFile {
122                    location: module_path!().to_string(),
123                    file: config.pid_filename.clone(),
124                    source: e,
125                });
126            }
127        }
128
129        // If all checks have passed, it's safe to write our PID.
130        let process_id_numeric: u32 = process::id();
131        let process_id_string = format!("{process_id_numeric}\n");
132        fs::write(&config.pid_filename, process_id_string).map_err(|e| {
133            PublishPidError::UnableToWritePidFile {
134                location: module_path!().to_string(),
135                file: config.pid_filename.clone(),
136                source: e,
137            }
138        })?;
139
140        Ok(PublishPid { config })
141    }
142
143    /// Erases the PID from the file by writing "0" to it.
144    ///
145    /// This is typically called during a graceful shutdown to release the application "lock"
146    /// and allow a new instance to start up without conflicts.
147    ///
148    /// # Returns
149    /// An empty `Result` (`Ok(())`) on a successful write.
150    ///
151    /// # Errors
152    /// Returns a `PublishPidError::UnableToWritePidFile` if the PID file cannot be
153    /// written to. This could be due to a permissions issue, an invalid path, or if
154    /// the file was unexpectedly deleted.
155    pub fn erase_pid(&self) -> Result<(), PublishPidError> {
156        fs::write(&self.config.pid_filename, "0\n").map_err(|e| {
157            PublishPidError::UnableToWritePidFile {
158                location: module_path!().to_string(),
159                file: self.config.pid_filename.clone(),
160                source: e,
161            }
162        })
163    }
164}
165
166#[cfg(test)]
167pub mod tests {
168    use std::fs;
169    use std::process;
170
171    use crate::utilities::config::{read_config_file, ConfigData};
172    use crate::utilities::publish_pid::{PublishPid, PublishPidError};
173
174    #[test]
175    pub fn test_publish_pid() {
176        let config: ConfigData =
177            read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
178        let pid_filename = config.publish_pid.pid_filename.clone();
179        fs::write(pid_filename.clone(), "0".to_string())
180            .expect("test_publish_PID: Could not erase PID.");
181
182        // create the test object and run code to be tested
183        _ = PublishPid::new(config.publish_pid);
184
185        let process_id_numeric: u32 = process::id();
186        let process_id_string: String = process_id_numeric.to_string() + "\n";
187
188        let file_data =
189            fs::read_to_string(pid_filename).expect("test_publish_PID: Unable to read file");
190
191        assert_eq!(file_data, process_id_string);
192        println!(
193            "Data read from file ({}) equals data generated ({}).",
194            { file_data.trim() },
195            { process_id_string.trim() }
196        );
197    }
198
199    #[test]
200    pub fn test_publish_pid_invalid_content_pid_file() {
201        let mut config: ConfigData =
202            read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
203        config.publish_pid.pid_filename =
204            "/var/local/aquarium-ctrl-invalid-content.pid".to_string();
205        let result = PublishPid::new(config.publish_pid);
206        assert!(matches!(
207            result,
208            Err(PublishPidError::PidFileParseError { .. })
209        ));
210    }
211
212    #[test]
213    pub fn test_publish_pid_file_already_contains_pid() {
214        let mut config: ConfigData =
215            read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
216        config.publish_pid.pid_filename = "/var/local/aquarium-ctrl-test.pid".to_string();
217        let result = PublishPid::new(config.publish_pid);
218        assert!(matches!(
219            result,
220            Err(PublishPidError::PidFileContainsPid { .. })
221        ));
222    }
223}