// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. use crate::colors; use deno_core::error::AnyError; use deno_core::futures::ready; use deno_core::futures::stream::{Stream, StreamExt}; use deno_core::futures::Future; 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 pin_project::pin_project; use std::path::PathBuf; use std::pin::Pin; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::task::Context; use std::task::Poll; use std::time::Duration; use tokio::pin; use tokio::select; use tokio::time::sleep; use tokio::time::Instant; use tokio::time::Sleep; const DEBOUNCE_INTERVAL: Duration = Duration::from_millis(200); type FileWatcherFuture = Pin>>; #[pin_project(project = DebounceProjection)] struct Debounce { #[pin] timer: Sleep, event_detected: Arc, } impl Debounce { fn new() -> Self { Self { timer: sleep(DEBOUNCE_INTERVAL), event_detected: Arc::new(AtomicBool::new(false)), } } } impl Stream for Debounce { type Item = (); /// Note that this never returns `Poll::Ready(None)`, which means that the /// file watcher will be alive until the Deno process is terminated. fn poll_next( self: Pin<&mut Self>, cx: &mut Context, ) -> Poll> { if self.event_detected.swap(false, Ordering::Relaxed) { Poll::Ready(Some(())) } else { let mut timer = self.project().timer; ready!(timer.as_mut().poll(cx)); timer.reset(Instant::now() + DEBOUNCE_INTERVAL); 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 debounce = Debounce::new(); pin!(debounce); 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"), ); } } } pub enum ModuleResolutionResult { Success { paths_to_watch: Vec, module_info: T, }, Fail { source_path: PathBuf, error: AnyError, }, } /// 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>, G: Fn(T) -> FileWatcherFuture>, T: Clone, { let debounce = Debounce::new(); pin!(debounce); // Store previous data. If module resolution fails at some point, the watcher will try to // continue watching files using these data. let mut paths = Vec::new(); let mut module = None; loop { match module_resolver().await { ModuleResolutionResult::Success { paths_to_watch, module_info, } => { paths = paths_to_watch; module = Some(module_info); } ModuleResolutionResult::Fail { source_path, error } => { if paths.is_empty() { paths = vec![source_path]; } if module.is_none() { eprintln!("{}: {}", colors::red_bold("error"), error); } } } let _watcher = new_watcher(&paths, &debounce)?; if let Some(module) = &module { let func = error_handler(operation(module.clone())); 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"), ); } } else { info!( "{} {} failed! 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) }