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}