Skip to main content

tale_ndjson/multiplexed/
watcher.rs

1//! Multi-file watching using notify and async-watcher.
2//!
3//! This module handles file system event monitoring for multiple files,
4//! coordinating with the file state manager and batch processor.
5
6use std::path::{Path, PathBuf};
7use std::time::Duration;
8
9use miette::Result;
10use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
11use tokio::sync::mpsc;
12use tokio::task::JoinHandle;
13
14use super::file_state::FileStateManager;
15use crate::errors::*;
16
17/// Events that can occur during file watching
18#[derive(Debug, Clone)]
19pub enum WatchEvent {
20    /// A file was modified (new content available)
21    FileModified(PathBuf),
22    /// A file was created
23    FileCreated(PathBuf),
24    /// A file was deleted
25    FileDeleted(PathBuf),
26    /// A file was moved/renamed
27    FileMoved { from: PathBuf, to: PathBuf },
28    /// An error occurred while watching
29    Error(String),
30}
31
32/// Configuration for the file watcher
33#[derive(Debug, Clone)]
34pub struct WatcherConfig {
35    /// How long to wait between file system checks
36    pub poll_interval: Duration,
37    /// Maximum number of events to buffer
38    pub event_buffer_size: usize,
39    /// Whether to use polling fallback for unreliable filesystems
40    pub use_polling: bool,
41}
42
43impl Default for WatcherConfig {
44    fn default() -> Self {
45        Self {
46            poll_interval: Duration::from_millis(100),
47            event_buffer_size: 1000,
48            use_polling: false,
49        }
50    }
51}
52
53/// Multi-file watcher using notify and async coordination
54pub struct MultiFileWatcher {
55    /// Configuration for this watcher
56    _config: WatcherConfig,
57    /// File state manager
58    file_manager: FileStateManager,
59    /// Channel for sending watch events
60    event_sender: Option<mpsc::UnboundedSender<WatchEvent>>,
61    /// The notify watcher instance
62    _watcher: Option<RecommendedWatcher>,
63    /// Handle to the event processing task
64    _task_handle: Option<JoinHandle<()>>,
65}
66
67impl MultiFileWatcher {
68    /// Create a new MultiFileWatcher
69    pub fn new(_config: WatcherConfig) -> Self {
70        Self {
71            _config,
72            file_manager: FileStateManager::new(),
73            event_sender: None,
74            _watcher: None,
75            _task_handle: None,
76        }
77    }
78
79    /// Add files to be watched
80    pub async fn add_files<I, P>(&mut self, paths: I) -> Result<(), TaleError>
81    where
82        I: IntoIterator<Item = P>,
83        P: AsRef<Path>,
84    {
85        for path in paths {
86            self.file_manager.add_file_for_tailing(path)?;
87        }
88
89        Ok(())
90    }
91
92    /// Start watching files and return a stream of events
93    pub async fn watch(&mut self) -> Result<mpsc::UnboundedReceiver<WatchEvent>, TaleError> {
94        let (event_sender, event_receiver) = mpsc::unbounded_channel();
95
96        // Create a channel for notify events
97        let (notify_sender, notify_receiver) = std::sync::mpsc::channel();
98
99        // Create the notify watcher
100        let mut watcher = notify::recommended_watcher(move |result: notify::Result<Event>| {
101            if let Err(e) = notify_sender.send(result) {
102                eprintln!("Failed to send notify event: {e}");
103            }
104        })?;
105
106        // Watch all tracked files
107        for file_path in self.file_manager.tracked_files() {
108            watcher
109                .watch(file_path, RecursiveMode::NonRecursive)
110                .map_err(TaleError::NotifyError)?;
111        }
112
113        // Spawn a task to process notify events and convert them to WatchEvents
114        let event_sender_clone = event_sender.clone();
115        let task_handle = tokio::spawn(async move {
116            while let Ok(result) = notify_receiver.recv() {
117                match result {
118                    Ok(event) => {
119                        if let Some(watch_event) = Self::convert_notify_event(event)
120                            && event_sender_clone.send(watch_event).is_err()
121                        {
122                            break; // Receiver dropped
123                        }
124                    }
125                    Err(e) => {
126                        let error_event = WatchEvent::Error(format!("Notify error: {e}"));
127                        if event_sender_clone.send(error_event).is_err() {
128                            break; // Receiver dropped
129                        }
130                    }
131                }
132            }
133        });
134
135        self.event_sender = Some(event_sender);
136        self._watcher = Some(watcher);
137        self._task_handle = Some(task_handle);
138
139        Ok(event_receiver)
140    }
141
142    /// Stop watching all files
143    pub async fn stop(&mut self) -> Result<(), TaleError> {
144        // Drop the watcher to stop file watching
145        self._watcher = None;
146
147        // Close the event sender to signal the task to exit
148        self.event_sender = None;
149
150        // Wait for the task to complete
151        if let Some(handle) = self._task_handle.take() {
152            let _ = handle.await;
153        }
154
155        Ok(())
156    }
157
158    /// Convert a notify Event to our WatchEvent
159    fn convert_notify_event(event: Event) -> Option<WatchEvent> {
160        match event.kind {
161            EventKind::Modify(_) => {
162                // File was modified
163                event.paths.first().map(|path| WatchEvent::FileModified(path.clone()))
164            }
165            EventKind::Create(_) => {
166                // File was created
167                event.paths.first().map(|path| WatchEvent::FileCreated(path.clone()))
168            }
169            EventKind::Remove(_) => {
170                // File was deleted
171                event.paths.first().map(|path| WatchEvent::FileDeleted(path.clone()))
172            }
173            _ => {
174                // Other event types we don't handle yet
175                None
176            }
177        }
178    }
179
180    /// Get the current file state manager
181    pub fn file_manager(&self) -> &FileStateManager {
182        &self.file_manager
183    }
184
185    /// Get mutable access to the file state manager
186    pub fn file_manager_mut(&mut self) -> &mut FileStateManager {
187        &mut self.file_manager
188    }
189}
190
191/// Create a new multi-file watcher with default configuration
192pub fn create_watcher() -> MultiFileWatcher {
193    MultiFileWatcher::new(WatcherConfig::default())
194}
195
196/// Create a new multi-file watcher with custom configuration
197pub fn create_watcher_with_config(config: WatcherConfig) -> MultiFileWatcher {
198    MultiFileWatcher::new(config)
199}