use clap::Parser;
use crate::daemon_core::core::NormalizedPath;
fn daemon_max_blocking_threads() -> usize {
let parallelism = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(8);
parallelism.saturating_mul(8).clamp(128, 512)
}
const DAEMON_PROFILE_ENV: &str = "ZCCACHE_DAEMON_PROFILE";
const TOKIO_CONSOLE_PROFILE: &str = "tokio-console";
const TOKIO_CONSOLE_BIND_ENV: &str = "TOKIO_CONSOLE_BIND";
const TOKIO_CONSOLE_DEFAULT_BIND: &str = "127.0.0.1:6669";
#[derive(Debug, Parser)]
#[command(name = "zccache-daemon", version, about)]
pub struct Args {
#[arg(long)]
config: Option<NormalizedPath>,
#[arg(long, default_value = "info")]
log_level: String,
#[arg(long)]
foreground: bool,
#[arg(long)]
endpoint: Option<String>,
#[arg(
long,
default_value_t = crate::daemon_core::core::config::DEFAULT_IDLE_TIMEOUT_SECS,
env = "ZCCACHE_IDLE_TIMEOUT_SECS"
)]
idle_timeout: u64,
#[arg(long)]
no_depgraph_cache: bool,
#[arg(long)]
log_file: Option<NormalizedPath>,
}
pub fn run() {
let args = Args::parse();
run_with(args);
}
pub fn run_from<I, T>(argv: I)
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
let args = Args::parse_from(argv);
run_with(args);
}
pub fn run_with(args: Args) {
if args.foreground {
match args.log_file.as_deref() {
Some(path) => crate::daemon_core::daemon::trampoline::redirect_stdio_to_log(path),
None => crate::daemon_core::daemon::trampoline::detach_stdio(),
}
init_tracing(&args.log_level);
crate::daemon_core::daemon::trampoline::unlock_exe();
crate::daemon_core::daemon::trampoline::release_cwd();
run_server(args);
} else {
print_status(&args);
}
}
fn print_status(args: &Args) {
let endpoint = args
.endpoint
.clone()
.unwrap_or_else(crate::daemon_core::ipc::default_endpoint);
println!("zccache-daemon v{}", env!("CARGO_PKG_VERSION"));
println!();
println!(" endpoint: {endpoint}");
println!(
" namespace: {}",
crate::daemon_core::core::config::daemon_namespace_label()
);
println!(" lock file: {}", crate::daemon_core::ipc::lock_file_path().display());
println!();
#[expect(
clippy::expect_used,
reason = "current-thread runtime construction with enable_all only fails on OS resource exhaustion; daemon-status query cannot proceed without it"
)]
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("failed to create tokio runtime");
match rt.block_on(query_daemon_status(&endpoint)) {
Ok(status) => {
println!(" status: running");
println!(" daemon ns: {}", status.daemon_namespace);
println!(" daemon ep: {}", status.endpoint);
println!(
" private: {}",
if status.private_daemon.enabled {
"yes"
} else {
"no"
}
);
println!(" uptime: {}s", status.uptime_secs);
println!(" artifacts: {}", status.artifact_count);
println!(" cache size: {} bytes", status.cache_size_bytes);
println!(" metadata: {} entries", status.metadata_entries);
println!(
" hits/miss: {} / {}",
status.cache_hits, status.cache_misses
);
}
Err(_) => {
println!(" status: not running");
println!();
println!("Start with: zccache-daemon --foreground");
}
}
}
async fn query_daemon_status(
endpoint: &str,
) -> Result<crate::daemon_core::protocol::DaemonStatus, Box<dyn std::error::Error>> {
let resp = crate::daemon_core::ipc::daemon_control_roundtrip(
endpoint,
crate::daemon_core::ipc::DaemonControlRequest::Status,
None,
)
.await?;
match resp {
Some(crate::daemon_core::protocol::Response::Status(s)) => Ok(s),
Some(other) => Err(format!("unexpected response: {other:?}").into()),
None => Err("connection closed".into()),
}
}
fn run_server(args: Args) {
let endpoint = args.endpoint.unwrap_or_else(crate::daemon_core::ipc::default_endpoint);
let idle_timeout = args.idle_timeout;
let _crash_guard = crate::daemon_core::core::crash::install("zccache-daemon");
crate::daemon_core::core::crash::check_previous_crashes();
tracing::info!(%endpoint, idle_timeout, "zccache-daemon starting");
{
#[expect(
clippy::expect_used,
reason = "current-thread probe runtime construction with enable_all only fails on OS resource exhaustion; daemon-defer probe cannot proceed without it"
)]
let probe_rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("failed to create probe runtime");
let existing_daemon = probe_rt.block_on(crate::daemon_core::ipc::probe_existing_daemon(
&endpoint,
std::time::Duration::from_millis(500),
));
if existing_daemon {
tracing::info!(
%endpoint,
"active daemon already serving — deferring"
);
std::process::exit(0);
}
}
let cache_root = crate::daemon_core::core::config::default_cache_dir();
let daemon_state_root = crate::daemon_core::core::config::daemon_state_dir();
tracing::info!(
daemon_namespace = %crate::daemon_core::core::config::daemon_namespace_label(),
mutable_state_dir = %daemon_state_root.display(),
"resolved daemon cache state"
);
crate::daemon_core::core::defender::maybe_emit_first_run_banner(cache_root.as_path());
#[expect(
clippy::expect_used,
reason = "multi-thread runtime construction with enable_all only fails on OS resource exhaustion; daemon cannot proceed without it"
)]
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.max_blocking_threads(daemon_max_blocking_threads())
.build()
.expect("failed to create tokio runtime");
let no_depgraph_cache = args.no_depgraph_cache;
let manifest_cache_root = cache_root.clone();
rt.block_on(async move {
let bind_endpoint = endpoint.clone();
let bind_result =
tokio::task::spawn_blocking(move || crate::daemon_core::daemon::DaemonServer::bind(&bind_endpoint))
.await;
let server = match bind_result {
Ok(Ok(s)) => s,
Ok(Err(e)) => {
let is_pipe_in_use = matches!(
&e,
crate::daemon_core::ipc::IpcError::Io(io_err) if matches!(
io_err.kind(),
std::io::ErrorKind::PermissionDenied
| std::io::ErrorKind::AddrInUse
| std::io::ErrorKind::AlreadyExists
)
);
if is_pipe_in_use {
tracing::info!(
%endpoint,
error = %e,
"another daemon already owns endpoint — deferring"
);
std::process::exit(0);
}
tracing::error!("failed to bind {endpoint}: {e}");
crate::daemon_core::ipc::remove_lock_file();
std::process::exit(1);
}
Err(e) => {
tracing::error!("failed to join daemon bind worker for {endpoint}: {e}");
crate::daemon_core::ipc::remove_lock_file();
std::process::exit(1);
}
};
let spawn_meta = crate::daemon_core::daemon::lifecycle::client_meta(env!("CARGO_PKG_VERSION"));
crate::daemon_core::daemon::lifecycle::write_event(
crate::daemon_core::daemon::lifecycle::EVENT_SPAWN,
serde_json::json!({
"endpoint": &endpoint,
"daemon_namespace": crate::daemon_core::core::config::daemon_namespace_label(),
"idle_timeout": idle_timeout,
"version": env!("CARGO_PKG_VERSION"),
"client_version": spawn_meta["client_version"],
"client_binary_path": spawn_meta["client_binary_path"],
}),
);
let pid = std::process::id();
if let Err(e) = crate::daemon_core::ipc::write_lock_file(pid) {
tracing::warn!("failed to write lock file: {e}");
}
if let Err(e) = crate::daemon_core::core::config::write_last_version_marker() {
tracing::debug!("failed to write last-version marker: {e}");
}
if let Err(e) = crate::daemon_core::ipc::write_backend_identity(&server.backend_identity()) {
tracing::warn!("failed to write running-process backend identity: {e}");
}
if let Some(path) = crate::daemon_core::ipc::publish_manifest(&manifest_cache_root) {
tracing::debug!(manifest = %path.display(), "published running-process cache manifest");
}
if let Ok(binary) = std::env::current_exe() {
let _ = crate::daemon_core::ipc::publish_service_definition(&binary);
}
server.mark_dep_graph_load_pending();
let setter = server.dep_graph_setter();
let depgraph_path = crate::daemon_core::depgraph::depgraph_file_path();
let load_handle = tokio::task::spawn_blocking(move || {
if no_depgraph_cache {
let _ = std::fs::remove_file(&depgraph_path);
tracing::info!("depgraph cache disabled — starting with empty graph");
setter.install(None, None);
return;
}
let start = std::time::Instant::now();
let outcome = crate::daemon_core::depgraph::classify_load(&depgraph_path);
let warning = outcome.warning(&depgraph_path);
match outcome {
crate::daemon_core::depgraph::DepGraphLoadOutcome::Loaded { graph } => {
let stats = graph.stats();
let (cold_ctxs, warm_ctxs, stale_ctxs) = graph.state_breakdown();
let ctxs_with_key = graph.contexts_with_artifact_key();
tracing::info!(
contexts = stats.context_count,
files = stats.file_count,
cold = cold_ctxs,
warm = warm_ctxs,
stale = stale_ctxs,
with_artifact_key = ctxs_with_key,
elapsed_ms = start.elapsed().as_millis() as u64,
"loaded depgraph from disk (background)"
);
setter.install(Some(graph), None);
}
crate::daemon_core::depgraph::DepGraphLoadOutcome::Missing => {
setter.install(None, None);
}
crate::daemon_core::depgraph::DepGraphLoadOutcome::VersionMismatch {
file_version,
expected_version,
} => {
tracing::warn!(
file_version,
expected_version,
"depgraph version mismatch — starting with empty graph"
);
if let Some(ref w) = warning {
eprintln!("{w}");
}
setter.install(None, warning);
}
crate::daemon_core::depgraph::DepGraphLoadOutcome::Corrupt { ref message }
| crate::daemon_core::depgraph::DepGraphLoadOutcome::IoError { ref message } => {
tracing::warn!("depgraph load failed: {message} — starting with empty graph");
if let Some(ref w) = warning {
eprintln!("{w}");
}
setter.install(None, warning);
}
}
});
let compiler_hash_loader = server.compiler_hash_cache_loader();
tokio::task::spawn_blocking(move || {
compiler_hash_loader.load_and_install();
});
let metadata_loader = server.metadata_cache_loader();
tokio::task::spawn_blocking(move || {
metadata_loader.load_and_install();
});
let system_includes_loader = server.system_includes_loader();
tokio::task::spawn_blocking(move || {
system_includes_loader.load_and_install();
});
let artifact_store_loader = server.artifact_store_loader();
tokio::task::spawn_blocking(move || {
artifact_store_loader.load_and_install();
});
let shutdown = server.shutdown_handle();
tokio::spawn(async move {
if let Ok(()) = tokio::signal::ctrl_c().await {
tracing::info!("received Ctrl+C — shutting down");
shutdown.notify_waiters();
}
});
tracing::info!(%endpoint, "listening for connections");
let mut server = server;
if let Err(e) = server.run(idle_timeout).await {
tracing::error!("server error: {e}");
crate::daemon_core::ipc::remove_lock_file();
load_handle.abort();
std::process::exit(1);
}
tracing::info!("daemon exiting cleanly");
crate::daemon_core::ipc::remove_lock_file();
load_handle.abort();
});
}
fn init_tracing(level: &str) {
use tracing_subscriber::EnvFilter;
let mut filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level));
for target in [
"crate::artifact",
"crate::compiler",
"crate::core",
"crate::depgraph",
"crate::download",
"crate::fingerprint",
"crate::fscache",
"crate::gha",
"crate::hash",
"crate::ipc",
"crate::protocol",
"crate::symbols",
"crate::watcher",
] {
if let Ok(d) = format!("{target}=info").parse() {
filter = filter.add_directive(d);
}
}
let profile = std::env::var(DAEMON_PROFILE_ENV).ok();
let tokio_console_enabled = profile.as_deref() == Some(TOKIO_CONSOLE_PROFILE);
if tokio_console_enabled {
let bind = std::env::var(TOKIO_CONSOLE_BIND_ENV)
.unwrap_or_else(|_| TOKIO_CONSOLE_DEFAULT_BIND.to_string());
init_tokio_console_tracing(filter, &bind);
return;
}
init_fmt_tracing(filter);
if let Some(profile) = profile {
if profile != TOKIO_CONSOLE_PROFILE {
tracing::warn!(
profile,
"unknown daemon profile requested; running without extra profiling"
);
}
}
}
fn init_fmt_tracing(filter: tracing_subscriber::EnvFilter) {
use tracing_subscriber::prelude::*;
tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer()
.with_target(true)
.with_thread_ids(true)
.with_filter(filter),
)
.init();
}
#[cfg(feature = "tokio-console")]
fn init_tokio_console_tracing(filter: tracing_subscriber::EnvFilter, bind: &str) {
use tracing_subscriber::prelude::*;
let console_layer = std::panic::catch_unwind(console_subscriber::spawn);
match console_layer {
Ok(console_layer) => {
tracing_subscriber::registry()
.with(console_layer)
.with(
tracing_subscriber::fmt::layer()
.with_target(true)
.with_thread_ids(true)
.with_filter(filter),
)
.init();
tracing::info!(
bind,
"tokio-console daemon profile enabled; connect with `tokio-console {bind}`"
);
}
Err(_) => {
init_fmt_tracing(filter);
tracing::warn!(
bind,
"tokio-console daemon profile requested but unavailable; \
rebuild with RUSTFLAGS=\"--cfg tokio_unstable\""
);
}
}
}
#[cfg(not(feature = "tokio-console"))]
fn init_tokio_console_tracing(filter: tracing_subscriber::EnvFilter, bind: &str) {
init_fmt_tracing(filter);
tracing::warn!(
bind,
"tokio-console daemon profile requested but zccache was built without \
the `tokio-console` feature"
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn args_default_is_background_status_mode() {
let args = Args::parse_from(["zccache-daemon"]);
assert!(!args.foreground);
assert_eq!(args.log_level, "info");
assert!(args.endpoint.is_none());
}
#[test]
fn args_foreground_and_endpoint_parse() {
let args = Args::parse_from([
"zccache-daemon",
"--foreground",
"--endpoint",
"test-endpoint",
"--idle-timeout",
"0",
]);
assert!(args.foreground);
assert_eq!(args.endpoint.as_deref(), Some("test-endpoint"));
assert_eq!(args.idle_timeout, 0);
}
}