src/tail/engine/inotify.rs

//! Linux-specific tailing engine that uses inotify via the `notify` crate.
//!
//! This module is only compiled on Linux hosts. For other platforms see
//! [`crate::tail::engine::kqueue`] or the polling fallback.

#![cfg(target_os = "linux")]

use std::path::PathBuf;

use anyhow::{Context, Result};
use notify::event::{ModifyKind, RemoveKind};
use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use tokio::sync::mpsc;

use super::Event;

pub async fn run(paths: Vec<PathBuf>, tx: mpsc::Sender<Event>) -> Result<()> {
    let (raw_tx, mut raw_rx) = mpsc::unbounded_channel();
    let mut watcher = RecommendedWatcher::new(
        move |res: notify::Result<notify::Event>| {
            if let Ok(ev) = res {
                let _ = raw_tx.send(ev);
            }
        },
        notify::Config::default(),
    )
    .context("creating inotify watcher")?;

    for p in &paths {
        watcher
            .watch(p, RecursiveMode::NonRecursive)
            .with_context(|| format!("watching {}", p.display()))?;
    }

    while let Some(event) = raw_rx.recv().await {
        for path in event.paths.iter().cloned() {
            let translated = translate(&event.kind, path);
            if let Some(e) = translated {
                if tx.send(e).await.is_err() {
                    return Ok(());
                }
            }
        }
    }
    Ok(())
}

fn translate(kind: &EventKind, path: PathBuf) -> Option<Event> {
    match kind {
        EventKind::Modify(ModifyKind::Data(_)) => Some(Event::Grew(path)),
        EventKind::Modify(ModifyKind::Name(_)) => Some(Event::Rotated(path)),
        EventKind::Modify(ModifyKind::Metadata(_)) => None,
        EventKind::Remove(RemoveKind::File)
        | EventKind::Remove(RemoveKind::Folder)
        | EventKind::Remove(RemoveKind::Any) => Some(Event::Gone(path)),
        EventKind::Create(_) => Some(Event::Rotated(path)),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::Path;

    #[test]
    fn translate_data_modify_is_grew() {
        let ev = translate(
            &EventKind::Modify(ModifyKind::Data(notify::event::DataChange::Content)),
            Path::new("/tmp/foo.log").to_path_buf(),
        );
        assert!(matches!(ev, Some(Event::Grew(_))));
    }

    #[test]
    fn translate_remove_is_gone() {
        let ev = translate(
            &EventKind::Remove(RemoveKind::File),
            Path::new("/tmp/foo.log").to_path_buf(),
        );
        assert!(matches!(ev, Some(Event::Gone(_))));
    }
}