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}