Skip to content

Commit 4aa6fdd

Browse files
committed
feat: Remove daemon from watch mode, use in-process file watching
Replace the daemon gRPC IPC in turbo watch with in-process file watching infrastructure. All watcher components (FileSystemWatcher, CookieWriter, GlobWatcher, PackageWatcher, HashWatcher, PackageChangesWatcher) are instantiated directly in WatchClient, eliminating the daemon dependency for watch mode. Key changes: - Replace DaemonClient with OutputWatcher trait in RunCache/TaskCache - Create InProcessOutputWatcher wrapping GlobWatcher - Build full watcher stack in WatchClient::new() - Pre-populate hash baselines at startup to prevent spurious rebuilds - Wait for active runs to complete before processing new change events - Make TUI start_task resilient to concurrent/out-of-order events - Handle SIGINT as clean exit (exit code 0, no error message) The daemon is still used by other consumers (turbo daemon CLI, LSP, turbo info, diagnostics). This change only decouples watch mode.
1 parent 0efbe30 commit 4aa6fdd

File tree

9 files changed

+683
-266
lines changed

9 files changed

+683
-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)