aquarium_control/watchmen/
watchdog.rs1use crate::utilities::proc_ext_req::ProcessExternalRequestTrait;
10use crate::watchmen::petting::PettingTrait;
11use crate::watchmen::watchdog_channels::WatchDogChannels;
12use log::error;
13use spin_sleep::SpinSleeper;
14use std::time::Duration;
15
16#[cfg(all(target_os = "linux", not(test)))]
17use log::info;
18
19use crate::utilities::acknowledge_signal_handler::AcknowledgeSignalHandlerTrait;
20use crate::watchmen::watchdog_config::WatchdogConfig;
21#[cfg(all(target_os = "linux", not(test)))]
22use nix::unistd::gettid;
23use thiserror::Error;
24
25#[derive(Error, Debug)]
27pub enum WatchdogError {
28 #[error("[{0}] Duration between two watchdog heartbeats is zero - please assign a value > 0")]
30 DurationBetweenTwoHeartbeatsIsZero(String),
31
32 #[error("[{0}] Watchdog filename is empty - please assign a value")]
34 FilenameIsEmpty(String),
35
36 #[error("[{0}] Watchdog heartbeat message is empty - please assign a value")]
38 HeartbeatMessageIsEmpty(String),
39
40 #[error("[{0}] Watchdog deactivation message is empty - please assign a value")]
42 DeactivationMessageIsEmpty(String),
43
44 #[error("[{0}] watchdog heartbeat and deactivation message are identical ({1}) - please assign different values")]
46 HeartbeatAndDeactivationMessagesAreIdentical(String, String),
47}
48
49pub struct Watchdog {
69 config: WatchdogConfig,
70}
71
72impl Watchdog {
73 pub fn new(config: WatchdogConfig) -> Result<Watchdog, WatchdogError> {
97 if config.heartbeat_duration_millis == 0 {
98 return Err(WatchdogError::DurationBetweenTwoHeartbeatsIsZero(
99 module_path!().to_string(),
100 ));
101 }
102 if config.watchdog_filename.is_empty() {
103 return Err(WatchdogError::FilenameIsEmpty(module_path!().to_string()));
104 }
105 if config.watchdog_heartbeat.is_empty() {
106 return Err(WatchdogError::HeartbeatMessageIsEmpty(
107 module_path!().to_string(),
108 ));
109 }
110 if config.watchdog_deactivation.is_empty() {
111 return Err(WatchdogError::DeactivationMessageIsEmpty(
112 module_path!().to_string(),
113 ));
114 }
115 if config.watchdog_deactivation == config.watchdog_heartbeat {
116 return Err(WatchdogError::HeartbeatAndDeactivationMessagesAreIdentical(
117 module_path!().to_string(),
118 config.watchdog_deactivation,
119 ));
120 }
121
122 Ok(Watchdog { config })
123 }
124
125 pub fn execute(
148 &mut self,
149 watchdog_channels: &mut WatchDogChannels,
150 petting: &mut impl PettingTrait,
151 ) {
152 #[cfg(all(target_os = "linux", not(test)))]
153 info!(target: module_path!(), "Thread started with TID: {}", gettid());
154
155 let spin_sleeper = SpinSleeper::default();
156 let sleep_duration = Duration::from_millis(self.config.heartbeat_duration_millis);
157 let mut quit_command_received: bool; let mut stop_command_received: bool; let mut start_command_received: bool; let mut inhibited: bool = false;
161
162 loop {
163 if self.config.active && !inhibited {
164 petting.pet(
166 &self.config.watchdog_filename,
167 &self.config.watchdog_heartbeat,
168 );
169 }
170 spin_sleeper.sleep(sleep_duration);
171 (
172 quit_command_received,
173 start_command_received,
174 stop_command_received,
175 ) = self.process_external_request(
176 &mut watchdog_channels.rx_watchdog_from_signal_handler,
177 watchdog_channels.rx_watchdog_from_messaging_opt.as_mut(),
178 );
179
180 if quit_command_received {
181 if self.config.active {
182 petting.pet(
184 &self.config.watchdog_filename,
185 &self.config.watchdog_deactivation,
186 );
187 }
188 break;
189 }
190
191 if start_command_received {
194 inhibited = false;
195 }
196 if stop_command_received {
197 inhibited = true;
198 }
199 }
200
201 watchdog_channels.acknowledge_signal_handler();
202 }
203}
204
205#[cfg(test)]
206pub mod tests {
207 use crate::launch::channels::Channels;
208 use crate::mocks::mock_petting::tests::MockPetting;
209 use crate::utilities::channel_content::InternalCommand;
210 use crate::utilities::config::{read_config_file, ConfigData};
211 use crate::watchmen::petting::Petting;
212 use crate::watchmen::watchdog::{Watchdog, WatchdogError};
213 use spin_sleep::SpinSleeper;
214 use std::fs::OpenOptions;
215 use std::io::Read;
216 use std::thread::scope;
217 use std::time::Duration;
218
219 #[test]
220 fn test_new_fails_with_empty_filename() {
222 let mut config: ConfigData =
224 read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
225 config.watchdog.watchdog_filename = "".to_string();
226
227 let result = Watchdog::new(config.watchdog);
229
230 assert!(matches!(result, Err(WatchdogError::FilenameIsEmpty(_))));
232 }
233
234 #[test]
235 fn test_new_fails_with_empty_heartbeat() {
237 let mut config: ConfigData =
239 read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
240 config.watchdog.watchdog_heartbeat = "".to_string();
241
242 let result = Watchdog::new(config.watchdog);
244
245 assert!(matches!(
247 result,
248 Err(WatchdogError::HeartbeatMessageIsEmpty(_))
249 ));
250 }
251
252 #[test]
253 fn test_new_fails_with_empty_deactivation_message() {
255 let mut config: ConfigData =
257 read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
258 config.watchdog.watchdog_deactivation = "".to_string();
259
260 let result = Watchdog::new(config.watchdog);
262
263 assert!(matches!(
265 result,
266 Err(WatchdogError::DeactivationMessageIsEmpty(_))
267 ));
268 }
269
270 #[test]
271 fn test_new_fails_with_identical_messages() {
273 let mut config: ConfigData =
275 read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
276 config.watchdog.watchdog_deactivation = "T".to_string();
277 config.watchdog.watchdog_heartbeat = "T".to_string();
278
279 let result = Watchdog::new(config.watchdog);
281
282 assert!(matches!(
284 result,
285 Err(WatchdogError::HeartbeatAndDeactivationMessagesAreIdentical(
286 _,
287 _
288 ))
289 ));
290 }
291
292 #[test]
293 fn test_new_fails_with_zero_heartbeat_duration() {
295 let mut config: ConfigData =
297 read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
298 config.watchdog.heartbeat_duration_millis = 0;
299
300 let result = Watchdog::new(config.watchdog);
302
303 assert!(matches!(
305 result,
306 Err(WatchdogError::DurationBetweenTwoHeartbeatsIsZero(_))
307 ));
308 }
309
310 #[test]
311 fn test_watchdog_execute_petting() {
313 let config: ConfigData =
314 read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
315
316 let mut channels = Channels::new_for_test();
317
318 let min_duration = Duration::from_millis(config.watchdog.heartbeat_duration_millis - 50);
319 let max_duration = Duration::from_millis(config.watchdog.heartbeat_duration_millis + 50);
320
321 let reference_heartbeat_signal = config.watchdog.watchdog_heartbeat.clone();
322 let reference_deactivation_signal = config.watchdog.watchdog_deactivation.clone();
323
324 let mut watchdog = Watchdog::new(config.watchdog).unwrap();
325 let mut mock_petting = MockPetting::new();
326
327 scope(|scope| {
328 scope.spawn(move || {
330 let sleep_duration_test_environment = Duration::from_millis(3100);
331 let spin_sleeper_test_environment = SpinSleeper::default();
332
333 spin_sleeper_test_environment.sleep(sleep_duration_test_environment);
334 channels
335 .signal_handler
336 .send_to_watchdog(InternalCommand::Terminate)
337 .unwrap();
338 channels.signal_handler.receive_from_watchdog().unwrap();
339 });
340
341 scope.spawn(move || {
343 watchdog.execute(
344 &mut channels.watchdog,
345 &mut mock_petting,
346 );
347
348 assert_eq!(mock_petting.petting_recorder.len(), 5);
349
350 for i in 1..mock_petting.petting_recorder.len() {
353 let previous_ts = mock_petting.petting_recorder[i - 1];
354 let current_ts = mock_petting.petting_recorder[i];
355 let delta_duration = current_ts.duration_since(previous_ts);
356
357 assert!(
359 delta_duration >= min_duration && delta_duration <= max_duration,
360 "Heartbeat delta {:?} at index {} is outside the allowed range [{:?}, {:?}]",
361 delta_duration,
362 i - 1,
363 min_duration,
364 max_duration,
365 );
366 }
367
368 for i in 0..4 {
370 assert_eq!(
371 mock_petting.signal_recorder[i],
372 reference_heartbeat_signal,
373 "Signal at index {} was expected to be '{}', but was '{}'",
374 i,
375 reference_deactivation_signal,
376 mock_petting.signal_recorder[i]
377 );
378 }
379
380 assert_eq!(
381 mock_petting.signal_recorder[4],
382 reference_deactivation_signal,
383 "Signal was expected to be '{}', but was '{}'",
384 reference_heartbeat_signal,
385 mock_petting.signal_recorder.last().unwrap()
386 );
387 });
388 });
389 }
390
391 #[test]
392 fn test_watchdog_check_heartbeat_message() {
394 let mut config: ConfigData =
395 read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
396
397 config.watchdog.watchdog_filename = "/var/log/watchdog.log".to_string();
398
399 let mut channels = Channels::new_for_test();
400
401 let reference_heartbeat_signal = config.watchdog.watchdog_heartbeat.clone();
402 let reference_deactivation_signal = config.watchdog.watchdog_deactivation.clone();
403
404 let file = OpenOptions::new()
406 .write(true)
407 .open(config.watchdog.watchdog_filename.clone())
408 .unwrap();
409 _ = file.set_len(0); let watchdog_filename_for_assert = config.watchdog.watchdog_filename.clone();
412 let mut watchdog = Watchdog::new(config.watchdog).unwrap();
413 let mut petting = Petting::new();
414
415 scope(|scope| {
416 scope.spawn(move || {
418 let sleep_duration_test_environment = Duration::from_millis(3100);
419 let spin_sleeper_test_environment = SpinSleeper::default();
420
421 spin_sleeper_test_environment.sleep(sleep_duration_test_environment);
422 channels
423 .signal_handler
424 .send_to_watchdog(InternalCommand::Terminate)
425 .unwrap();
426 channels.signal_handler.receive_from_watchdog().unwrap();
427 });
428
429 scope.spawn(move || {
431 let sleep_duration_assert1 = Duration::from_millis(500);
432 let sleep_duration_assert2 = Duration::from_millis(4500);
433 let spin_sleeper_asserts = SpinSleeper::default();
434
435 spin_sleeper_asserts.sleep(sleep_duration_assert1);
437
438 let mut file = OpenOptions::new()
440 .read(true)
441 .open(watchdog_filename_for_assert.clone())
442 .unwrap();
443 let mut content = String::new();
444 file.read_to_string(&mut content).unwrap();
445 assert_eq!(content, reference_heartbeat_signal);
446
447 spin_sleeper_asserts.sleep(sleep_duration_assert2);
449
450 let mut file = OpenOptions::new()
452 .read(true)
453 .open(watchdog_filename_for_assert)
454 .unwrap();
455 let mut content = String::new();
456 file.read_to_string(&mut content).unwrap();
457 assert_eq!(content, reference_deactivation_signal);
458 });
459
460 scope.spawn(move || {
462 watchdog.execute(&mut channels.watchdog, &mut petting);
463 });
464 });
465 }
466}