Skip to content

Commit 67ff6d2

Browse files
authored
feat: Remove daemon from watch mode (#12175)
## Summary Replaces the daemon gRPC IPC in `turbo watch` with in-process file watching. All watcher components are instantiated directly in `WatchClient`, eliminating the daemon as a dependency for watch mode. - Introduces `OutputWatcher` trait to abstract output change tracking, replacing `DaemonClient` in `RunCache`/`TaskCache` - Builds the full watcher stack in-process: `FileSystemWatcher` → `CookieWriter` → `GlobWatcher` + `PackageWatcher` → `HashWatcher` → `PackageChangesWatcher` - Pre-populates hash baselines at startup so build output writes don't trigger spurious rebuilds - Waits for active runs to complete before processing new change events, preventing concurrent builds of the same package - Makes TUI `start_task` resilient to out-of-order events from concurrent runs - Handles SIGINT as a clean exit (code 0) instead of propagating an error The daemon is still used by other consumers (`turbo daemon` CLI, LSP, `turbo info`). ## Testing Manually verified on the turborepo monorepo (24 packages): - Initial build settles without spurious rebuilds - File edits during builds are not lost - Rapid successive edits are debounced - Same-content writes don't trigger rebuilds - Ctrl+C exits cleanly - TUI stays alive across rebuilds Automated: 11 E2E tests, 18 watch unit tests, 8 OutputWatcher trait tests, 6 GlobWatcher delegation tests, 2 TUI start_task resilience tests — all passing.
1 parent 5e8fe42 commit 67ff6d2

File tree

9 files changed

+687
-266
lines changed

9 files changed

+687
-266
lines changed

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/turborepo-lib/src/cli/mod.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1727,12 +1727,18 @@ pub async fn run(
17271727
let mut client =
17281728
WatchClient::new(base, *experimental_write_cache, event, query_server.clone())
17291729
.await?;
1730-
if let Err(e) = client.start().await {
1731-
client.shutdown().await;
1732-
return Err(e.into());
1730+
match client.start().await {
1731+
Ok(()) => {}
1732+
Err(crate::run::watch::Error::SignalInterrupt) => {
1733+
// Normal shutdown via Ctrl+C — not an error.
1734+
}
1735+
Err(e) => {
1736+
client.shutdown().await;
1737+
return Err(e.into());
1738+
}
17331739
}
1734-
// We only exit if we get a signal, so we return a non-zero exit code
1735-
return Ok(1);
1740+
client.shutdown().await;
1741+
return Ok(0);
17361742
}
17371743
Command::Prune {
17381744
scope,

crates/turborepo-lib/src/package_changes_watcher.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,9 +441,27 @@ impl Subscriber {
441441
else {
442442
return;
443443
};
444-
// We store the hash of the package's files. If the hash is already
445-
// in here, we don't need to recompute it
444+
// Pre-populate hash baselines for all known packages. Without
445+
// this, the first file change for each package would always be
446+
// treated as "new" (no old hash to compare against), causing
447+
// spurious rebuilds from build output writes on the initial run.
446448
let mut package_file_hashes = HashMap::new();
449+
for (name, info) in repo_state.pkg_dep_graph.packages() {
450+
let pkg = WorkspacePackage {
451+
name: name.clone(),
452+
path: info.package_path().to_owned(),
453+
};
454+
if let Ok(hash) = self
455+
.hash_watcher
456+
.get_file_hashes(HashSpec {
457+
package_path: pkg.path.clone(),
458+
inputs: InputGlobs::Default,
459+
})
460+
.await
461+
{
462+
package_file_hashes.insert(pkg.path, hash);
463+
}
464+
}
447465

448466
let mut change_mapper = match repo_state.get_change_mapper() {
449467
Some(change_mapper) => change_mapper,

crates/turborepo-lib/src/run/builder.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ use turbopath::{AbsoluteSystemPath, AbsoluteSystemPathBuf};
1111
use turborepo_analytics::{start_analytics, AnalyticsHandle};
1212
use turborepo_api_client::{APIAuth, APIClient};
1313
use turborepo_cache::AsyncCache;
14-
use turborepo_daemon::{DaemonClient, DaemonConnector};
1514
use turborepo_env::EnvironmentVariableMap;
1615
use turborepo_errors::Spanned;
1716
use turborepo_process::ProcessManager;
@@ -61,11 +60,11 @@ pub struct RunBuilder {
6160
should_validate_engine: bool,
6261
// If true, we will add all tasks to the graph, even if they are not specified
6362
add_all_tasks: bool,
64-
// When running under `turbo watch`, a daemon client is needed so that
63+
// When running under `turbo watch`, an output watcher is needed so that
6564
// the run cache can register output globs and skip restoring outputs
6665
// that are already on disk. Without this, cache restores write files
6766
// that trigger the file watcher, causing an infinite rebuild loop.
68-
daemon_client: Option<DaemonClient<DaemonConnector>>,
67+
output_watcher: Option<Arc<dyn turborepo_run_cache::OutputWatcher>>,
6968
query_server: Option<Arc<dyn turborepo_query_api::QueryServer>>,
7069
}
7170

@@ -108,7 +107,7 @@ impl RunBuilder {
108107
should_print_prelude_override: None,
109108
should_validate_engine: true,
110109
add_all_tasks: false,
111-
daemon_client: None,
110+
output_watcher: None,
112111
query_server: None,
113112
})
114113
}
@@ -118,8 +117,11 @@ impl RunBuilder {
118117
self
119118
}
120119

121-
pub fn with_daemon_client(mut self, client: DaemonClient<DaemonConnector>) -> Self {
122-
self.daemon_client = Some(client);
120+
pub fn with_output_watcher(
121+
mut self,
122+
watcher: Arc<dyn turborepo_run_cache::OutputWatcher>,
123+
) -> Self {
124+
self.output_watcher = Some(watcher);
123125
self
124126
}
125127

@@ -471,7 +473,7 @@ impl RunBuilder {
471473
&self.repo_root,
472474
self.opts.runcache_opts,
473475
&self.opts.cache_opts,
474-
self.daemon_client,
476+
self.output_watcher,
475477
self.color_config,
476478
self.opts.run_opts.dry_run.is_some(),
477479
));

0 commit comments

Comments
 (0)