// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. use crate::colors; use core::task::{Context, Poll}; use deno_core::error::AnyError; use deno_core::futures::stream::{Stream, StreamExt}; use deno_core::futures::{Future, FutureExt}; use notify::event::Event as NotifyEvent; use notify::event::EventKind; use notify::Config; use notify::Error as NotifyError; use notify::RecommendedWatcher; use notify::RecursiveMode; use notify::Watcher; use std::path::PathBuf; use std::pin::Pin; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; use tokio::select; use tokio::time::{delay_for, Delay}; const DEBOUNCE_INTERVAL_MS: Duration = Duration::from_millis(200); type FileWatcherFuture = Pin>>>; struct Debounce { delay: Delay, event_detected: Arc, } impl Debounce { fn new() -> Self { Self { delay: delay_for(DEBOUNCE_INTERVAL_MS), event_detected: Arc::new(AtomicBool::new(false)), } } } impl Stream for Debounce { type Item = (); /// Note that this never returns `Poll::Ready(None)`, which means that file watcher will be alive /// until the Deno process is terminated. fn poll_next( self: Pin<&mut Self>, cx: &mut Context, ) -> Poll> { let inner = self.get_mut(); if inner.event_detected.load(Ordering::Relaxed) { inner.event_detected.store(false, Ordering::Relaxed); Poll::Ready(Some(())) } else { match inner.delay.poll_unpin(cx) { Poll::Ready(_) => { inner.delay = delay_for(DEBOUNCE_INTERVAL_MS); Poll::Pending } Poll::Pending => Poll::Pending, } } } } async fn error_handler(watch_future: FileWatcherFuture<()>) { let result = watch_future.await; if let Err(err) = result { let msg = format!("{}: {}", colors::red_bold("error"), err.to_string(),); eprintln!("{}", msg); } } /// This function adds watcher functionality to subcommands like `fmt` or `lint`. /// The difference from [`watch_func_with_module_resolution`] is that this doesn't depend on /// [`ModuleGraph`]. /// /// - `target_resolver` is used for resolving file paths to be watched at every restarting of the watcher. The /// return value of this closure will then be passed to `operation` as an argument. /// /// - `operation` is the actual operation we want to run every time the watcher detects file /// changes. For example, in the case where we would like to apply `fmt`, then `operation` would /// have the logic for it like calling `format_source_files`. /// /// - `job_name` is just used for printing watcher status to terminal. /// /// Note that the watcher will stop working if `target_resolver` fails at some point. /// /// [`ModuleGraph`]: crate::module_graph::Graph pub async fn watch_func( target_resolver: F, operation: G, job_name: &str, ) -> Result<(), AnyError> where F: Fn() -> Result, AnyError>, G: Fn(Vec) -> FileWatcherFuture<()>, { let mut debounce = Debounce::new(); loop { let paths = target_resolver()?; let _watcher = new_watcher(&paths, &debounce)?; let func = error_handler(operation(paths)); let mut is_file_changed = false; select! { _ = debounce.next() => { is_file_changed = true; info!( "{} File change detected! Restarting!", colors::intense_blue("Watcher"), ); }, _ = func => {}, }; if !is_file_changed { info!( "{} {} finished! Restarting on file change...", colors::intense_blue("Watcher"), job_name, ); debounce.next().await; info!( "{} File change detected! Restarting!", colors::intense_blue("Watcher"), ); } } } /// This function adds watcher functionality to subcommands like `run` or `bundle`. /// The difference from [`watch_func`] is that this does depend on [`ModuleGraph`]. /// /// - `module_resolver` is used for both resolving file paths to be watched at every restarting /// of the watcher and building [`ModuleGraph`] or [`ModuleSpecifier`] which will then be passed /// to `operation`. /// /// - `operation` is the actual operation we want to run every time the watcher detects file /// changes. For example, in the case where we would like to bundle, then `operation` would /// have the logic for it like doing bundle with the help of [`ModuleGraph`]. /// /// - `job_name` is just used for printing watcher status to terminal. /// /// Note that the watcher will try to continue watching files using the previously resolved /// data if `module_resolver` fails at some point, which means the watcher won't work at all /// if `module_resolver` fails at the first attempt. /// /// [`ModuleGraph`]: crate::module_graph::Graph /// [`ModuleSpecifier`]: deno_core::ModuleSpecifier pub async fn watch_func_with_module_resolution( module_resolver: F, operation: G, job_name: &str, ) -> Result<(), AnyError> where F: Fn() -> FileWatcherFuture<(Vec, T)>, G: Fn(T) -> FileWatcherFuture<()>, T: Clone, { let mut debounce = Debounce::new(); // Store previous data. If module resolution fails at some point, the watcher will try to // continue watching files using these data. let mut paths = None; let mut module = None; loop { match module_resolver().await { Ok((next_paths, next_module)) => { paths = Some(next_paths); module = Some(next_module); } Err(e) => { // If at least one of `paths` and `module` is `None`, the watcher cannot decide which files // should be watched. So return the error immediately without watching anything. if paths.is_none() || module.is_none() { return Err(e); } } } // These `unwrap`s never cause panic since `None` is already checked above. let cur_paths = paths.clone().unwrap(); let cur_module = module.clone().unwrap(); let _watcher = new_watcher(&cur_paths, &debounce)?; let func = error_handler(operation(cur_module)); let mut is_file_changed = false; select! { _ = debounce.next() => { is_file_changed = true; info!( "{} File change detected! Restarting!", colors::intense_blue("Watcher"), ); }, _ = func => {}, }; if !is_file_changed { info!( "{} {} finished! Restarting on file change...", colors::intense_blue("Watcher"), job_name, ); debounce.next().await; info!( "{} File change detected! Restarting!", colors::intense_blue("Watcher"), ); } } } fn new_watcher( paths: &[PathBuf], debounce: &Debounce, ) -> Result { let event_detected = Arc::clone(&debounce.event_detected); let mut watcher: RecommendedWatcher = Watcher::new_immediate( move |res: Result| { if let Ok(event) = res { if matches!(event.kind, EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)) { event_detected.store(true, Ordering::Relaxed); } } }, )?; watcher.configure(Config::PreciseEvents(true)).unwrap(); for path in paths { // Ignore any error e.g. `PathNotFound` let _ = watcher.watch(path, RecursiveMode::NonRecursive); } Ok(watcher) }