diff --git a/crates/turborepo-filewatch/src/globwatcher.rs b/crates/turborepo-filewatch/src/globwatcher.rs index fb05222813df5..d60d085c36253 100644 --- a/crates/turborepo-filewatch/src/globwatcher.rs +++ b/crates/turborepo-filewatch/src/globwatcher.rs @@ -422,7 +422,9 @@ mod test { setup(&repo_root); let cookie_dir = repo_root.join_component(".git"); - let watcher = FileSystemWatcher::new(&repo_root).await.unwrap(); + let watcher = FileSystemWatcher::new_with_default_cookie_dir(&repo_root) + .await + .unwrap(); let cookie_jar = CookieJar::new(&cookie_dir, Duration::from_secs(2), watcher.subscribe()); let glob_watcher = GlobWatcher::new(&repo_root, cookie_jar, watcher.subscribe()); @@ -500,7 +502,9 @@ mod test { setup(&repo_root); let cookie_dir = repo_root.join_component(".git"); - let watcher = FileSystemWatcher::new(&repo_root).await.unwrap(); + let watcher = FileSystemWatcher::new_with_default_cookie_dir(&repo_root) + .await + .unwrap(); let cookie_jar = CookieJar::new(&cookie_dir, Duration::from_secs(2), watcher.subscribe()); let glob_watcher = GlobWatcher::new(&repo_root, cookie_jar, watcher.subscribe()); @@ -588,7 +592,9 @@ mod test { setup(&repo_root); let cookie_dir = repo_root.join_component(".git"); - let watcher = FileSystemWatcher::new(&repo_root).await.unwrap(); + let watcher = FileSystemWatcher::new_with_default_cookie_dir(&repo_root) + .await + .unwrap(); let cookie_jar = CookieJar::new(&cookie_dir, Duration::from_secs(2), watcher.subscribe()); let glob_watcher = GlobWatcher::new(&repo_root, cookie_jar, watcher.subscribe()); diff --git a/crates/turborepo-filewatch/src/lib.rs b/crates/turborepo-filewatch/src/lib.rs index e88ed35f72939..d4ccfd7e40025 100644 --- a/crates/turborepo-filewatch/src/lib.rs +++ b/crates/turborepo-filewatch/src/lib.rs @@ -10,6 +10,8 @@ use std::{ time::Duration, }; +// windows -> no recursive watch, watch ancestors +// linux -> recursive watch, watch ancestors // macos -> custom watcher impl in fsevents, no recursive watch, no watching ancestors #[cfg(target_os = "macos")] use fsevent::FsEventWatcher; @@ -21,11 +23,7 @@ use notify::{Event, EventHandler, RecursiveMode, Watcher}; use thiserror::Error; use tokio::sync::{broadcast, mpsc}; use tracing::{debug, warn}; -// windows -> no recursive watch, watch ancestors -// linux -> recursive watch, watch ancestors -#[cfg(feature = "watch_ancestors")] -use turbopath::PathRelation; -use turbopath::{AbsoluteSystemPath, AbsoluteSystemPathBuf}; +use turbopath::{AbsoluteSystemPath, AbsoluteSystemPathBuf, PathRelation}; #[cfg(feature = "manual_recursive_watch")] use { notify::{ @@ -86,10 +84,30 @@ pub struct FileSystemWatcher { // dropping the other sender for the broadcast channel, causing all receivers // to be notified of a close. _exit_ch: tokio::sync::oneshot::Sender<()>, + cookie_dir: AbsoluteSystemPathBuf, } impl FileSystemWatcher { - pub async fn new(root: &AbsoluteSystemPath) -> Result { + pub async fn new_with_default_cookie_dir( + root: &AbsoluteSystemPath, + ) -> Result { + // We already store logs in .turbo and recommend it be gitignore'd. + // Watchman uses .git, but we can't guarantee that git is present _or_ + // that the turbo root is the same as the git root. + Self::new(root, &root.join_components(&[".turbo", "cookies"])).await + } + + pub async fn new( + root: &AbsoluteSystemPath, + cookie_dir: &AbsoluteSystemPath, + ) -> Result { + if root.relation_to_path(cookie_dir) != PathRelation::Parent { + return Err(WatchError::Setup(format!( + "Invalid cookie directory: {} does not contain {}", + root, cookie_dir + ))); + } + setup_cookie_dir(cookie_dir)?; let (sender, _) = broadcast::channel(1024); let (send_file_events, mut recv_file_events) = mpsc::channel(1024); let watch_root = root.to_owned(); @@ -99,7 +117,7 @@ impl FileSystemWatcher { let (exit_ch, exit_signal) = tokio::sync::oneshot::channel(); // Ensure we are ready to receive new events, not events for existing state debug!("waiting for initial filesystem cookie"); - wait_for_cookie(root, &mut recv_file_events).await?; + wait_for_cookie(cookie_dir, &mut recv_file_events).await?; tokio::task::spawn(watch_events( watcher, watch_root, @@ -111,12 +129,31 @@ impl FileSystemWatcher { Ok(Self { sender, _exit_ch: exit_ch, + cookie_dir: cookie_dir.to_owned(), }) } pub fn subscribe(&self) -> broadcast::Receiver> { self.sender.subscribe() } + + pub fn cookie_dir(&self) -> &AbsoluteSystemPath { + &self.cookie_dir + } +} + +fn setup_cookie_dir(cookie_dir: &AbsoluteSystemPath) -> Result<(), WatchError> { + // We need to ensure that the cookie directory is cleared out first so + // that we can start over with cookies. + if cookie_dir.exists() { + cookie_dir.remove_dir_all().map_err(|e| { + WatchError::Setup(format!("failed to clear cookie dir {}: {}", cookie_dir, e)) + })?; + } + cookie_dir.create_dir_all().map_err(|e| { + WatchError::Setup(format!("failed to setup cookie dir {}: {}", cookie_dir, e)) + })?; + Ok(()) } #[cfg(not(any(feature = "watch_ancestors", feature = "manual_recursive_watch")))] @@ -333,10 +370,13 @@ fn make_watcher(event_handler: F) -> Result, ) -> Result<(), WatchError> { - let cookie_path = root.join_component(".turbo-cookie"); + // TODO: should this be passed in? Currently the caller guarantees that the + // directory is empty, but it could be the responsibility of the + // filewatcher... + let cookie_path = cookie_dir.join_component(".turbo-cookie"); cookie_path.create_with_contents("cookie").map_err(|e| { WatchError::Setup(format!("failed to write cookie to {}: {}", cookie_path, e)) })?; @@ -441,7 +481,9 @@ mod test { let sibling_path = parent_path.join_component("sibling"); sibling_path.create_dir_all().unwrap(); - let watcher = FileSystemWatcher::new(&repo_root).await.unwrap(); + let watcher = FileSystemWatcher::new_with_default_cookie_dir(&repo_root) + .await + .unwrap(); let mut recv = watcher.subscribe(); expect_watching(&mut recv, &[&repo_root, &parent_path, &child_path]).await; @@ -499,7 +541,9 @@ mod test { let child_path = parent_path.join_component("child"); child_path.create_dir_all().unwrap(); - let watcher = FileSystemWatcher::new(&repo_root).await.unwrap(); + let watcher = FileSystemWatcher::new_with_default_cookie_dir(&repo_root) + .await + .unwrap(); let mut recv = watcher.subscribe(); expect_watching(&mut recv, &[&repo_root, &parent_path, &child_path]).await; @@ -541,7 +585,9 @@ mod test { let child_path = parent_path.join_component("child"); child_path.create_dir_all().unwrap(); - let watcher = FileSystemWatcher::new(&repo_root).await.unwrap(); + let watcher = FileSystemWatcher::new_with_default_cookie_dir(&repo_root) + .await + .unwrap(); let mut recv = watcher.subscribe(); expect_watching(&mut recv, &[&repo_root, &parent_path, &child_path]).await; @@ -570,7 +616,9 @@ mod test { let child_path = parent_path.join_component("child"); child_path.create_dir_all().unwrap(); - let watcher = FileSystemWatcher::new(&repo_root).await.unwrap(); + let watcher = FileSystemWatcher::new_with_default_cookie_dir(&repo_root) + .await + .unwrap(); let mut recv = watcher.subscribe(); expect_watching(&mut recv, &[&repo_root, &parent_path, &child_path]).await; @@ -602,7 +650,9 @@ mod test { let child_path = parent_path.join_component("child"); child_path.create_dir_all().unwrap(); - let watcher = FileSystemWatcher::new(&repo_root).await.unwrap(); + let watcher = FileSystemWatcher::new_with_default_cookie_dir(&repo_root) + .await + .unwrap(); let mut recv = watcher.subscribe(); expect_watching(&mut recv, &[&repo_root, &parent_path, &child_path]).await; @@ -633,7 +683,9 @@ mod test { let child_path = parent_path.join_component("child"); child_path.create_dir_all().unwrap(); - let watcher = FileSystemWatcher::new(&repo_root).await.unwrap(); + let watcher = FileSystemWatcher::new_with_default_cookie_dir(&repo_root) + .await + .unwrap(); let mut recv = watcher.subscribe(); expect_watching(&mut recv, &[&repo_root, &parent_path, &child_path]).await; @@ -674,7 +726,9 @@ mod test { let symlink_path = repo_root.join_component("symlink"); symlink_path.symlink_to_dir(child_path.as_str()).unwrap(); - let watcher = FileSystemWatcher::new(&repo_root).await.unwrap(); + let watcher = FileSystemWatcher::new_with_default_cookie_dir(&repo_root) + .await + .unwrap(); let mut recv = watcher.subscribe(); expect_watching(&mut recv, &[&repo_root, &parent_path, &child_path]).await; @@ -713,7 +767,9 @@ mod test { let symlink_path = repo_root.join_component("symlink"); symlink_path.symlink_to_dir(child_path.as_str()).unwrap(); - let watcher = FileSystemWatcher::new(&repo_root).await.unwrap(); + let watcher = FileSystemWatcher::new_with_default_cookie_dir(&repo_root) + .await + .unwrap(); let mut recv = watcher.subscribe(); expect_watching(&mut recv, &[&repo_root, &parent_path, &child_path]).await; @@ -757,7 +813,9 @@ mod test { let child_path = parent_path.join_component("child"); child_path.create_dir_all().unwrap(); - let watcher = FileSystemWatcher::new(&repo_root).await.unwrap(); + let watcher = FileSystemWatcher::new_with_default_cookie_dir(&repo_root) + .await + .unwrap(); let mut recv = watcher.subscribe(); expect_watching(&mut recv, &[&repo_root, &parent_path, &child_path]).await; @@ -795,7 +853,9 @@ mod test { let child_path = parent_path.join_component("child"); child_path.create_dir_all().unwrap(); - let watcher = FileSystemWatcher::new(&repo_root).await.unwrap(); + let watcher = FileSystemWatcher::new_with_default_cookie_dir(&repo_root) + .await + .unwrap(); let mut recv = watcher.subscribe(); expect_watching(&mut recv, &[&repo_root, &parent_path, &child_path]).await; @@ -816,7 +876,9 @@ mod test { let mut recv = { // create and immediately drop the watcher, which should trigger the exit // channel - let watcher = FileSystemWatcher::new(&repo_root).await.unwrap(); + let watcher = FileSystemWatcher::new_with_default_cookie_dir(&repo_root) + .await + .unwrap(); watcher.subscribe() }; diff --git a/crates/turborepo-lib/src/commands/daemon.rs b/crates/turborepo-lib/src/commands/daemon.rs index dbc605c78cdda..eac4f4abe56d8 100644 --- a/crates/turborepo-lib/src/commands/daemon.rs +++ b/crates/turborepo-lib/src/commands/daemon.rs @@ -174,23 +174,8 @@ pub async fn daemon_server( } CloseReason::Interrupt }); - // We already store logs in .turbo and recommend it be gitignore'd. - // Watchman uses .git, but we can't guarantee that git is present _or_ - // that the turbo root is the same as the git root. - let cookie_dir = base.repo_root.join_components(&[".turbo", "cookies"]); - // We need to ensure that the cookie directory is cleared out first so - // that we can start over with cookies. - if cookie_dir.exists() { - cookie_dir - .remove_dir_all() - .map_err(|e| DaemonError::CookieDir(e, cookie_dir.clone()))?; - } - cookie_dir - .create_dir_all() - .map_err(|e| DaemonError::CookieDir(e, cookie_dir.clone()))?; let reason = crate::daemon::serve( &base.repo_root, - cookie_dir, &daemon_root, log_file, timeout, diff --git a/crates/turborepo-lib/src/daemon/server.rs b/crates/turborepo-lib/src/daemon/server.rs index 17be4752294b3..20341804c6132 100644 --- a/crates/turborepo-lib/src/daemon/server.rs +++ b/crates/turborepo-lib/src/daemon/server.rs @@ -90,11 +90,14 @@ impl From for tonic::Status { async fn start_filewatching( repo_root: AbsoluteSystemPathBuf, - cookie_dir: AbsoluteSystemPathBuf, watcher_tx: watch::Sender>>, ) -> Result<(), WatchError> { - let watcher = FileSystemWatcher::new(&repo_root).await?; - let cookie_jar = CookieJar::new(&cookie_dir, Duration::from_millis(100), watcher.subscribe()); + let watcher = FileSystemWatcher::new_with_default_cookie_dir(&repo_root).await?; + let cookie_jar = CookieJar::new( + watcher.cookie_dir(), + Duration::from_millis(100), + watcher.subscribe(), + ); let glob_watcher = GlobWatcher::new(&repo_root, cookie_jar, watcher.subscribe()); // We can ignore failures here, it means the server is shutting down and // receivers have gone out of scope. @@ -113,7 +116,6 @@ const REQUEST_TIMEOUT: Duration = Duration::from_millis(100); /// to be wired to signal handling. pub async fn serve( repo_root: &AbsoluteSystemPath, - cookie_dir: AbsoluteSystemPathBuf, daemon_root: &AbsoluteSystemPath, log_file: AbsoluteSystemPathBuf, timeout: Duration, @@ -143,7 +145,7 @@ where // all references are dropped. let fw_shutdown = trigger_shutdown.clone(); let fw_handle = tokio::task::spawn(async move { - if let Err(e) = start_filewatching(watcher_repo_root, cookie_dir, watcher_tx).await { + if let Err(e) = start_filewatching(watcher_repo_root, watcher_tx).await { error!("filewatching failed to start: {}", e); let _ = fw_shutdown.send(()).await; } @@ -436,8 +438,6 @@ mod test { .unwrap(); let repo_root = path.join_component("repo"); - let cookie_dir = repo_root.join_component(".git"); - cookie_dir.create_dir_all().unwrap(); let daemon_root = path.join_component("daemon"); let log_file = daemon_root.join_component("log"); tracing::info!("start"); @@ -449,7 +449,6 @@ mod test { let handle = tokio::task::spawn(async move { serve( &repo_root, - cookie_dir, &daemon_root, log_file, Duration::from_secs(60 * 60), @@ -484,8 +483,6 @@ mod test { .unwrap(); let repo_root = path.join_component("repo"); - let cookie_dir = repo_root.join_component(".git"); - cookie_dir.create_dir_all().unwrap(); let daemon_root = path.join_component("daemon"); let log_file = daemon_root.join_component("log"); @@ -496,7 +493,6 @@ mod test { let exit_signal = rx.map(|_result| CloseReason::Interrupt); let close_reason = serve( &repo_root, - cookie_dir, &daemon_root, log_file, Duration::from_millis(5), @@ -526,8 +522,6 @@ mod test { .unwrap(); let repo_root = path.join_component("repo"); - let cookie_dir = repo_root.join_component(".git"); - cookie_dir.create_dir_all().unwrap(); let daemon_root = path.join_component("daemon"); daemon_root.create_dir_all().unwrap(); let log_file = daemon_root.join_component("log"); @@ -540,7 +534,6 @@ mod test { let repo_root = server_repo_root; serve( &repo_root, - cookie_dir, &daemon_root, log_file, Duration::from_secs(60 * 60), diff --git a/crates/turborepo-paths/src/absolute_system_path.rs b/crates/turborepo-paths/src/absolute_system_path.rs index 47aa697dbcd85..0e23e676e55b0 100644 --- a/crates/turborepo-paths/src/absolute_system_path.rs +++ b/crates/turborepo-paths/src/absolute_system_path.rs @@ -122,6 +122,10 @@ impl AbsoluteSystemPath { self.0.as_str().as_bytes() } + pub fn exists(&self) -> bool { + self.0.exists() + } + pub fn ancestors(&self) -> impl Iterator { self.0.ancestors().map(Self::new_unchecked) } diff --git a/crates/turborepo-paths/src/absolute_system_path_buf.rs b/crates/turborepo-paths/src/absolute_system_path_buf.rs index b481b73b8628e..985d3b2cf3652 100644 --- a/crates/turborepo-paths/src/absolute_system_path_buf.rs +++ b/crates/turborepo-paths/src/absolute_system_path_buf.rs @@ -192,10 +192,6 @@ impl AbsoluteSystemPathBuf { self.0.file_name() } - pub fn exists(&self) -> bool { - self.0.exists() - } - pub fn try_exists(&self) -> Result { // try_exists is an experimental API and not yet in fs_err Ok(std::fs::try_exists(&self.0)?)