nullnet_libconfmon/watcher/impl.rs
1use super::{
2 types::{FileData, FileInfo, Snapshot},
3 utils::{get_mtime, make_error_mapper},
4};
5use crate::{Detector, Error, ErrorKind, Platform, State};
6use std::{path::PathBuf, time::Duration};
7
8/// A simple file watcher that monitors changes in a list of files and triggers appropriate handlers.
9#[allow(async_fn_in_trait)]
10pub trait WatcherHandler {
11 /// Defines how the snapshot should be uploaded or processed.
12 ///
13 /// # Parameters
14 /// - `snapshot`: A snapshot of the monitored files, containing file metadata and content.
15 ///
16 /// # Returns
17 /// - `Ok(())` if processing is successful.
18 /// - `Err(Error)` if an error occurs while processing the snapshot.
19 async fn on_snapshot(&self, snapshot: Snapshot, state: State) -> Result<(), Error>;
20
21 /// Handles errors that occur during file monitoring.
22 ///
23 /// # Parameters
24 /// - `error`: The error encountered during monitoring.
25 async fn on_error(&self, error: Error);
26}
27
28/// A file watcher that monitors specified files for changes and notifies a handler when updates occur.
29pub struct Watcher<H: WatcherHandler> {
30 /// List of monitored files and their metadata.
31 files: Vec<FileInfo>,
32 /// Polling interval (in milliseconds) for checking file modifications.
33 poll_interval: u64,
34 /// Handler for processing snapshots and handling errors.
35 handler: H,
36 /// Target platform
37 platform: Platform,
38}
39
40impl<H: WatcherHandler> Watcher<H> {
41 /// Creates a new `Watcher` instance that monitors configuration files for changes.
42 ///
43 /// # Parameters
44 /// - `platform`: The target platform for which the configuration state should be monitored.
45 /// - `poll_interval`: Time interval (in milliseconds) to check for file changes.
46 /// - `handler`: An instance implementing `WatcherHandler` for handling snapshots and errors.
47 ///
48 /// # Returns
49 /// - `Ok(Self)`: A properly initialized `Watcher` instance.
50 /// - `Err(Error)`: If any file metadata cannot be retrieved.
51 pub async fn new(platform: Platform, poll_interval: u64, handler: H) -> Result<Self, Error> {
52 let mut files = Vec::new();
53
54 for path in get_files_to_monitor(platform) {
55 let mtime = get_mtime(&path)
56 .await
57 .map_err(make_error_mapper(ErrorKind::ErrorInitializingWatcher))?;
58
59 files.push(FileInfo { path, mtime });
60 }
61
62 Ok(Self {
63 files,
64 poll_interval,
65 handler,
66 platform,
67 })
68 }
69
70 /// Starts monitoring the files and system state for changes.
71 ///
72 /// This function continuously checks the monitored files for modifications
73 /// and observes system state transitions. When a file modification or a
74 /// relevant state transition (from `Draft` to `Applied`) is detected, it
75 /// triggers the `on_snapshot` method of the handler.
76 ///
77 /// # Returns
78 /// - `Ok(())` if the monitoring process runs smoothly.
79 /// - `Err(Error)` if an unrecoverable error occurs.
80 pub async fn watch(&mut self) {
81 let mut last_state = Detector::check(self.platform).await;
82
83 loop {
84 let mut should_upload = self.check_files_for_changes().await;
85
86 let current_state = Detector::check(self.platform).await;
87
88 if last_state == State::Draft && current_state == State::Applied {
89 should_upload = true;
90 }
91
92 last_state = current_state;
93
94 if should_upload {
95 self.handle_snapshot().await;
96 }
97
98 tokio::time::sleep(Duration::from_millis(self.poll_interval)).await;
99 }
100 }
101
102 /// Checks the monitored files for modifications.
103 pub async fn check_files_for_changes(&mut self) -> bool {
104 let mut should_upload = false;
105
106 for file in &mut self.files {
107 match get_mtime(&file.path).await {
108 Ok(current) if current > file.mtime => {
109 file.mtime = current;
110 should_upload = true;
111 }
112 Err(err) => {
113 self.handler.on_error(err).await;
114 }
115 _ => {}
116 }
117 }
118
119 should_upload
120 }
121
122 /// Captures and processes a snapshot of the monitored files and system state.
123 pub async fn handle_snapshot(&mut self) {
124 match self.snapshot().await {
125 Ok(snapshot) => {
126 let state = Detector::check(self.platform).await;
127 if let Err(err) = self.handler.on_snapshot(snapshot, state).await {
128 self.handler.on_error(err).await;
129 }
130 }
131 Err(err) => {
132 self.handler.on_error(err).await;
133 }
134 }
135 }
136
137 /// Generates a snapshot of the current state of the monitored files.
138 ///
139 /// # Returns
140 /// - `Ok(Snapshot)`: A snapshot containing the contents and metadata of monitored files.
141 /// - `Err(Error)`: If a file cannot be read.
142 pub async fn snapshot(&self) -> Result<Snapshot, Error> {
143 let mut snapshot = Snapshot::new();
144
145 for file in &self.files {
146 let content = tokio::fs::read(&file.path)
147 .await
148 .map_err(make_error_mapper(ErrorKind::ErrorReadingFile))?;
149
150 let filename = file
151 .path
152 .file_name()
153 .unwrap_or(file.path.as_os_str())
154 .to_string_lossy()
155 .into_owned();
156
157 snapshot.push(FileData { filename, content });
158 }
159
160 Ok(snapshot)
161 }
162
163 /// Forces the capture of a snapshot and dispatches it to the handler.
164 ///
165 /// # Returns
166 /// - `Ok(())` if the snapshot was successfully processed by the handler.
167 /// - `Err(Error)`: If an error occurs during snapshot creation or handling.
168 pub async fn force_capture_and_dispatch(&self) -> Result<(), Error> {
169 let snapshot = self.snapshot().await?;
170 let state = Detector::check(self.platform).await;
171
172 let result = self.handler.on_snapshot(snapshot, state).await;
173
174 if result.is_err() {
175 self.handler
176 .on_error(result.as_ref().unwrap_err().clone())
177 .await
178 }
179
180 result
181 }
182}
183
184/// Returns a list of files that should be monitored based on the given platform.
185///
186/// # Parameters
187/// - `platform`: The target platform for which files need to be monitored.
188///
189/// # Returns
190/// - `Vec<PathBuf>`: A vector containing paths to the configuration files that need monitoring.
191fn get_files_to_monitor(platform: Platform) -> Vec<PathBuf> {
192 match platform {
193 Platform::PfSense | Platform::OPNsense => vec![PathBuf::from("/conf/config.xml")],
194 }
195}