use std::time::Duration;
use miette::miette;
use miette::IntoDiagnostic;
use notify_debouncer_full::notify;
use notify_debouncer_full::notify::PollWatcher;
use notify_debouncer_full::notify::RecommendedWatcher;
use notify_debouncer_full::notify::RecursiveMode;
use notify_debouncer_full::DebounceEventHandler;
use notify_debouncer_full::DebounceEventResult;
use notify_debouncer_full::Debouncer;
use notify_debouncer_full::FileIdMap;
use tokio::runtime::Handle;
use tokio::sync::mpsc;
use tokio::task::block_in_place;
use tracing::instrument;
use crate::cli::Opts;
use crate::event_filter::file_events_from_action;
use crate::ghci::manager::GhciEvent;
use crate::normal_path::NormalPath;
use crate::shutdown::ShutdownHandle;
pub struct WatcherOpts {
pub watch: Vec<NormalPath>,
pub debounce: Duration,
pub poll: Option<Duration>,
}
impl WatcherOpts {
pub fn from_cli(opts: &Opts) -> Self {
Self {
watch: opts.watch.paths.clone(),
debounce: opts.watch.debounce,
poll: opts.watch.poll,
}
}
}
#[instrument(level = "debug", skip_all)]
pub async fn run_watcher(
handle: ShutdownHandle,
ghci_sender: mpsc::Sender<GhciEvent>,
opts: WatcherOpts,
) -> miette::Result<()> {
if opts.poll.is_some() {
run_debouncer::<PollWatcher>(handle, ghci_sender, opts).await
} else {
run_debouncer::<RecommendedWatcher>(handle, ghci_sender, opts).await
}
}
async fn run_debouncer<T: notify::Watcher>(
mut handle: ShutdownHandle,
ghci_sender: mpsc::Sender<GhciEvent>,
opts: WatcherOpts,
) -> miette::Result<()> {
let mut config = notify::Config::default();
if let Some(interval) = opts.poll {
config = config.with_poll_interval(interval);
}
let event_handler = EventHandler {
handle: Handle::current(),
ghci_sender,
shutdown: handle.clone(),
};
let cache = FileIdMap::new();
let tick_rate = None;
let mut debouncer: Debouncer<T, FileIdMap> = notify_debouncer_full::new_debouncer_opt(
opts.debounce,
tick_rate,
event_handler,
cache,
config,
)
.into_diagnostic()?;
{
let watcher = debouncer.watcher();
for path in &opts.watch {
watcher
.watch(path.as_std_path(), RecursiveMode::Recursive)
.into_diagnostic()?;
}
let mut cache = debouncer.cache();
for path in &opts.watch {
cache.add_root(path.as_std_path(), RecursiveMode::Recursive);
}
}
tracing::debug!("notify watcher started");
let _ = handle.on_shutdown_requested().await;
block_in_place(|| debouncer.stop());
Ok(())
}
struct EventHandler {
handle: Handle,
ghci_sender: mpsc::Sender<GhciEvent>,
shutdown: ShutdownHandle,
}
impl EventHandler {
async fn handle_event_async(&self, event: DebounceEventResult) {
if let Err(err) = self.handle_event_inner(event).await {
tracing::error!("{err:?}");
let _ = self.shutdown.request_shutdown();
}
}
#[instrument(skip_all, level = "debug")]
async fn handle_event_inner(&self, event: DebounceEventResult) -> miette::Result<()> {
let events = match event {
Ok(events) => events,
Err(errors) => {
for err in errors {
tracing::error!("{err}");
}
return Err(miette!("Watching files failed"));
}
};
tracing::trace!(?events, "Got events");
let events = file_events_from_action(events)?;
if events.is_empty() {
tracing::debug!("No relevant file events");
} else {
tracing::trace!(?events, "Processed events");
self.ghci_sender
.send(GhciEvent::Reload { events })
.await
.into_diagnostic()?;
}
Ok(())
}
}
impl DebounceEventHandler for EventHandler {
fn handle_event(&mut self, event: DebounceEventResult) {
self.handle.block_on(self.handle_event_async(event))
}
}