use anyhow::{bail, Context, Result};
use futures_util::StreamExt;
use inotify::{Event, Inotify, WatchDescriptor, WatchMask};
use log::*;
use std::ffi::OsString;
use std::path::Path;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::time::{sleep, Duration};
pub struct MonitorHandler {
inotify: Inotify,
monitors: Vec<FileMonitor>,
delay: u64,
}
impl MonitorHandler {
pub fn new(monitor_files: &[PathBuf], delay: u64) -> Result<Self> {
let inotify = Inotify::init().context("Failed to initialize inotify")?;
let mut monitor = MonitorHandler {
inotify,
monitors: vec![],
delay,
};
for path in monitor_files {
monitor.add_watch(path)?;
}
Ok(monitor)
}
pub async fn run(&mut self, status: Arc<AtomicBool>) -> Result<()> {
let mut buffer = [0; 1024];
let mut stream = self.inotify.event_stream(&mut buffer)?;
let res = self.check().await;
status.store(res, Ordering::Relaxed);
crate::metrics::MATCH_STATUS.set(res as i64);
while let Some(event_or_error) = stream.next().await {
match event_or_error {
Ok(event) => {
self.handle_event(event);
let res = self.check().await;
status.store(res, Ordering::Relaxed);
crate::metrics::MATCH_STATUS.set(res as i64);
}
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => continue,
_ => {
panic!("Error while reading inotify events");
}
}
}
Ok(())
}
fn add_watch<P: AsRef<Path>>(&mut self, watch_path: P) -> Result<()> {
info!("Monitoring '{}' for changes", watch_path.as_ref().display());
let mut monitor = FileMonitor::new(&mut self.inotify, watch_path)?;
monitor.update();
self.monitors.push(monitor);
Ok(())
}
async fn check(&self) -> bool {
let res = match self.monitors.first() {
Some(first) => self.monitors.iter().all(|item| item == first),
None => false,
};
if res && self.delay > 0 {
sleep(Duration::from_millis(self.delay)).await;
}
debug!("Monitor status: {}", res);
res
}
fn handle_event(&mut self, event: Event<OsString>) {
debug!("Handling inotify update: {:?}", &event);
if let Some(name) = event.name {
for m in &mut self.monitors {
if m.wd == event.wd && name == m.base_name {
m.update();
}
}
}
}
}
#[derive(Debug)]
struct FileMonitor {
pub base_name: PathBuf,
pub full_name: PathBuf,
pub wd: WatchDescriptor,
pub contents: Option<String>,
}
impl Eq for FileMonitor {}
impl PartialEq for FileMonitor {
fn eq(&self, other: &Self) -> bool {
if self.contents.is_some() && other.contents.is_some() {
self.contents == other.contents
} else {
false
}
}
}
impl FileMonitor {
pub fn new<P: AsRef<Path>>(inotify: &mut Inotify, watch_path: P) -> Result<FileMonitor> {
let watch_dir = parent_directory_to_monitor_from_filename(&watch_path)?;
let base_name = match watch_path.as_ref().file_name() {
Some(path) => PathBuf::from(path),
None => bail!(
"Unable to extract base filename from '{}'",
watch_path.as_ref().display()
),
};
let full_name = watch_dir.join(&base_name);
let wd = inotify.add_watch(
&watch_dir,
WatchMask::CLOSE_WRITE
| WatchMask::DELETE
| WatchMask::MOVED_TO
| WatchMask::MOVED_FROM,
)?;
let mut fm = FileMonitor {
base_name,
full_name,
wd,
contents: None,
};
fm.update();
Ok(fm)
}
pub fn update(&mut self) {
self.contents = std::fs::read_to_string(&self.full_name).ok();
}
}
fn parent_directory_to_monitor_from_filename<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
if path.as_ref().is_dir() {
bail!(
"Path '{}' is a directory, not a file",
path.as_ref().display()
);
}
match path.as_ref().parent() {
Some(p) => Ok(p.canonicalize()?),
None => bail!(format!(
"Unable to find parent of '{}'",
path.as_ref().display()
)),
}
}