#![allow(clippy::module_name_repetitions)]
use std::path::{Path, PathBuf};
use std::time::Duration;
pub mod protocol;
pub mod service_install;
pub mod metrics;
#[cfg(unix)]
pub mod server;
#[cfg(not(unix))]
pub mod server {
#[derive(Clone, Debug)]
pub struct DaemonSearchRenderings {
pub response: serde_json::Value,
pub performance: Option<serde_json::Value>,
pub human: String,
pub reuse_contract: serde_json::Value,
pub timing: serde_json::Value,
}
}
pub const DAEMON_REQUEST_SCHEMA_V1: &str = "ee.daemon.request.v1";
pub const DAEMON_RESPONSE_SCHEMA_V1: &str = "ee.daemon.response.v1";
pub const DAEMON_REQUEST_MAX_BYTES: usize = 4 * 1024 * 1024;
pub const DAEMON_RESPONSE_MAX_BYTES: usize = 4 * 1024 * 1024;
pub const DAEMON_DEFAULT_RPC_TIMEOUT: Duration = Duration::from_secs(30);
pub const DAEMON_SOCKET_UNAVAILABLE_CODE: &str = "daemon_socket_unavailable";
pub const DAEMON_OVERLOADED_CODE: &str = "daemon_overloaded";
pub const DAEMON_MAX_INFLIGHT: usize = 32;
pub const DAEMON_PEER_UNAUTHORIZED_CODE: &str = "daemon_peer_unauthorized";
pub const DAEMON_METHOD_UNAUTHORIZED_CODE: &str = "daemon_method_unauthorized";
pub const DAEMON_SETSOCKOPT_FAILED_CODE: &str = "daemon_setsockopt_failed";
pub const DAEMON_SHUTTING_DOWN_CODE: &str = "daemon_shutting_down";
#[must_use]
pub fn default_daemon_socket_path() -> PathBuf {
default_daemon_socket_path_with(|key| std::env::var_os(key), current_euid())
}
#[must_use]
pub fn workspace_daemon_socket_path(workspace: &Path) -> PathBuf {
let canonical = crate::config::workspace::canonical_workspace_root_or_lexical(workspace);
let digest = blake3::hash(canonical.as_os_str().as_encoded_bytes()).to_hex();
let name = format!("d-{}.sock", &digest[..24]);
let mut socket = default_daemon_socket_path().with_file_name(&name);
if socket.as_os_str().as_encoded_bytes().len() > 65 {
socket = PathBuf::from("/tmp")
.join(format!("ee-{}", current_euid()))
.join(name);
}
socket
}
fn default_daemon_socket_path_with(
mut env_var: impl FnMut(&str) -> Option<std::ffi::OsString>,
uid: u32,
) -> PathBuf {
let tmp = env_var("TMPDIR").unwrap_or_else(|| "/tmp".into());
if let Some(runtime_dir) = env_var("XDG_RUNTIME_DIR") {
let runtime = Path::new(&runtime_dir);
if !runtime.as_os_str().is_empty() && !runtime_dir_is_shared_tmp_root(runtime) {
return runtime.join("ee").join("daemon.sock");
}
}
Path::new(&tmp)
.join(format!("ee-{uid}"))
.join("daemon.sock")
}
fn runtime_dir_is_shared_tmp_root(path: &Path) -> bool {
path == Path::new("/tmp") || path == Path::new("/var/tmp") || path == Path::new("/private/tmp")
}
#[cfg(unix)]
#[must_use]
pub fn current_euid() -> u32 {
rustix::process::geteuid().as_raw()
}
#[cfg(not(unix))]
#[must_use]
pub fn current_euid() -> u32 {
0
}
#[derive(Debug)]
pub enum DaemonStartError {
PlatformUnsupported,
SocketDirCreate {
path: PathBuf,
source: std::io::Error,
},
InsecureSocketParent { path: PathBuf, reason: String },
SocketPathOccupied { path: PathBuf },
AlreadyRunning { path: PathBuf },
Bind {
path: PathBuf,
source: std::io::Error,
},
}
impl std::fmt::Display for DaemonStartError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PlatformUnsupported => formatter.write_str(
"ee daemon UDS RPC is only supported on Unix targets; \
the in-process CLI path remains available on Windows.",
),
Self::SocketDirCreate { path, source } => write!(
formatter,
"Failed to create daemon socket parent directory {}: {source}",
path.display()
),
Self::InsecureSocketParent { path, reason } => write!(
formatter,
"Refusing to publish daemon socket under insecure parent {}: {reason}",
path.display()
),
Self::SocketPathOccupied { path } => write!(
formatter,
"Daemon socket path {} is occupied by a non-socket file; \
remove it manually before retrying `ee daemon start`.",
path.display()
),
Self::AlreadyRunning { path } => write!(
formatter,
"A live daemon socket already exists at {}; stop the running daemon or choose a \
different --socket path before retrying `ee daemon start`.",
path.display()
),
Self::Bind { path, source } => write!(
formatter,
"Failed to bind daemon socket at {}: {source}",
path.display()
),
}
}
}
impl std::error::Error for DaemonStartError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::PlatformUnsupported
| Self::InsecureSocketParent { .. }
| Self::SocketPathOccupied { .. }
| Self::AlreadyRunning { .. } => None,
Self::SocketDirCreate { source, .. } | Self::Bind { source, .. } => Some(source),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::OsString;
#[test]
fn workspace_sockets_are_short_distinct_and_canonical() -> Result<(), std::io::Error> {
let temp = tempfile::tempdir()?;
let first = temp.path().join("first");
let second = temp.path().join("second");
std::fs::create_dir_all(first.join("child"))?;
std::fs::create_dir_all(&second)?;
let socket = workspace_daemon_socket_path(&first);
assert_eq!(
socket,
workspace_daemon_socket_path(&first.join("child/.."))
);
assert_ne!(socket, workspace_daemon_socket_path(&second));
assert!(socket.as_os_str().as_encoded_bytes().len() + 37 < 104);
assert_eq!(
socket.parent(),
workspace_daemon_socket_path(&second).parent()
);
Ok(())
}
fn daemon_socket_path_for_env(vars: &[(&str, &str)], uid: u32) -> PathBuf {
default_daemon_socket_path_with(
|name| {
vars.iter()
.find(|(candidate, _)| *candidate == name)
.map(|(_, value)| OsString::from(value))
},
uid,
)
}
#[test]
fn default_socket_path_uses_xdg_runtime_dir_when_set() {
assert_eq!(
daemon_socket_path_for_env(
&[
("XDG_RUNTIME_DIR", "/run/user/1000"),
("TMPDIR", "/ignored")
],
501,
),
Path::new("/run/user/1000/ee/daemon.sock"),
);
}
#[test]
fn default_socket_path_falls_back_to_tmpdir_when_xdg_is_unset_or_empty() {
assert_eq!(
daemon_socket_path_for_env(&[("TMPDIR", "/var/tmp")], 1000),
Path::new("/var/tmp/ee-1000/daemon.sock"),
);
assert_eq!(
daemon_socket_path_for_env(&[("XDG_RUNTIME_DIR", ""), ("TMPDIR", "/var/tmp")], 1000),
Path::new("/var/tmp/ee-1000/daemon.sock"),
);
}
#[test]
fn default_socket_path_ignores_shared_tmp_xdg_runtime_dir() {
assert_eq!(
daemon_socket_path_for_env(&[("XDG_RUNTIME_DIR", "/tmp"), ("TMPDIR", "/tmp")], 1000),
Path::new("/tmp/ee-1000/daemon.sock"),
);
assert_eq!(
daemon_socket_path_for_env(
&[("XDG_RUNTIME_DIR", "/var/tmp"), ("TMPDIR", "/var/tmp")],
1000,
),
Path::new("/var/tmp/ee-1000/daemon.sock"),
);
assert_eq!(
daemon_socket_path_for_env(
&[
("XDG_RUNTIME_DIR", "/private/tmp"),
("TMPDIR", "/private/tmp"),
],
501,
),
Path::new("/private/tmp/ee-501/daemon.sock"),
);
}
#[test]
fn default_socket_path_collapses_xdg_tmp_and_unset_xdg_to_same_path() {
let with_tmp_xdg =
daemon_socket_path_for_env(&[("XDG_RUNTIME_DIR", "/tmp"), ("TMPDIR", "/tmp")], 1000);
let without_xdg = daemon_socket_path_for_env(&[("TMPDIR", "/tmp")], 1000);
assert_eq!(with_tmp_xdg, without_xdg);
}
#[test]
fn default_socket_path_falls_back_to_per_uid_tmpdir_default() {
assert_eq!(
daemon_socket_path_for_env(&[], 1000),
Path::new("/tmp/ee-1000/daemon.sock"),
);
}
#[test]
fn schema_constants_match_docs_filenames() {
assert_eq!(DAEMON_REQUEST_SCHEMA_V1, "ee.daemon.request.v1");
assert_eq!(DAEMON_RESPONSE_SCHEMA_V1, "ee.daemon.response.v1");
}
#[test]
fn request_and_response_caps_are_symmetric() {
assert_eq!(DAEMON_REQUEST_MAX_BYTES, DAEMON_RESPONSE_MAX_BYTES);
assert_eq!(DAEMON_REQUEST_MAX_BYTES, 4 * 1024 * 1024);
}
#[test]
fn default_rpc_timeout_is_generous_for_skeleton_methods() {
assert_eq!(DAEMON_DEFAULT_RPC_TIMEOUT, Duration::from_secs(30));
}
}