#![allow(dead_code, unused_imports, unused_qualifications, unreachable_patterns)]
#[cfg(unix)]
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
use std::time::Duration;
#[cfg(unix)]
use super::bin_discovery;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DaemonSpawn {
AlreadyRunning,
Spawned { binary: PathBuf },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DaemonReadyError {
BinaryNotFound { binary_name: String },
SpawnFailed { binary: PathBuf, reason: String },
SocketDirSetupFailed { parent: PathBuf, reason: String },
NotReady {
socket_path: PathBuf,
timeout: Duration,
diagnostic: ReadyDiagnostic,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadyDiagnostic {
pub child_exit_code: Option<i32>,
pub socket_present: bool,
pub connect_error_kind: Option<String>,
pub stderr_excerpt: String,
}
#[cfg(unix)]
const STDERR_DIAGNOSTIC_CAP: usize = 8 * 1024;
impl std::fmt::Display for DaemonReadyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::BinaryNotFound { binary_name } => {
write!(f, "{binary_name} binary not found in known install dirs")
}
Self::SpawnFailed { binary, reason } => {
write!(f, "spawning {}: {reason}", binary.display())
}
Self::SocketDirSetupFailed { parent, reason } => {
write!(
f,
"creating daemon socket dir {}: {reason}",
parent.display()
)
}
Self::NotReady {
socket_path,
timeout,
diagnostic,
} => {
write!(
f,
"daemon did not become ready at {} within {:?}",
socket_path.display(),
timeout
)?;
let exit_part = match diagnostic.child_exit_code {
Some(code) => format!("child exited (status {code})"),
None => "child still running at timeout".to_string(),
};
let socket_part = if diagnostic.socket_present {
if let Some(kind) = &diagnostic.connect_error_kind {
format!("socket present but connect failed ({kind})")
} else {
"socket present".to_string()
}
} else {
"socket never appeared".to_string()
};
write!(f, " [{exit_part}; {socket_part}]")?;
if !diagnostic.stderr_excerpt.is_empty() {
write!(f, "\nchild stderr:\n{}", diagnostic.stderr_excerpt)?;
}
Ok(())
}
}
}
}
impl std::error::Error for DaemonReadyError {}
#[cfg(unix)]
const READINESS_BACKOFF_MS: &[u64] = &[50, 100, 200, 400, 800, 1600, 3000, 4000, 8000, 12000];
#[cfg(unix)]
fn readiness_total_timeout() -> Duration {
Duration::from_millis(READINESS_BACKOFF_MS.iter().sum())
}
#[cfg(unix)]
pub fn ensure_daemon_ready(
binary_name: &str,
app_name: &str,
socket_path: &Path,
) -> Result<DaemonSpawn, DaemonReadyError> {
if is_socket_ready(socket_path) {
return Ok(DaemonSpawn::AlreadyRunning);
}
let binary = bin_discovery::find_trusted_binary(binary_name, app_name).ok_or_else(|| {
DaemonReadyError::BinaryNotFound {
binary_name: binary_name.to_string(),
}
})?;
if let Some(parent) = socket_path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent).map_err(|e| {
DaemonReadyError::SocketDirSetupFailed {
parent: parent.to_path_buf(),
reason: e.to_string(),
}
})?;
}
}
use std::process::Stdio;
let mut child = std::process::Command::new(&binary)
.arg("--socket")
.arg(socket_path)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| DaemonReadyError::SpawnFailed {
binary: binary.clone(),
reason: e.to_string(),
})?;
let stderr_buf = std::sync::Arc::new(std::sync::Mutex::new(Vec::with_capacity(1024)));
if let Some(stderr) = child.stderr.take() {
let buf = std::sync::Arc::clone(&stderr_buf);
std::thread::spawn(move || drain_capped(stderr, buf));
}
for backoff_ms in READINESS_BACKOFF_MS {
std::thread::sleep(Duration::from_millis(*backoff_ms));
if is_socket_ready(socket_path) {
drop(child.try_wait());
return Ok(DaemonSpawn::Spawned { binary });
}
if let Ok(Some(status)) = child.try_wait() {
if status.success() {
if is_socket_ready(socket_path) {
return Ok(DaemonSpawn::Spawned { binary });
}
std::thread::sleep(Duration::from_millis(50));
if is_socket_ready(socket_path) {
return Ok(DaemonSpawn::Spawned { binary });
}
return Err(DaemonReadyError::NotReady {
socket_path: socket_path.to_path_buf(),
timeout: readiness_total_timeout(),
diagnostic: capture_diagnostic(socket_path, Some(status), &stderr_buf),
});
}
return Err(DaemonReadyError::SpawnFailed {
binary: binary.clone(),
reason: format!(
"daemon exited with status {} before becoming ready",
status.code().unwrap_or(-1)
),
});
}
}
let final_status = child.try_wait().ok().flatten();
Err(DaemonReadyError::NotReady {
socket_path: socket_path.to_path_buf(),
timeout: readiness_total_timeout(),
diagnostic: capture_diagnostic(socket_path, final_status, &stderr_buf),
})
}
#[cfg(unix)]
fn drain_capped<R: std::io::Read>(mut reader: R, buf: std::sync::Arc<std::sync::Mutex<Vec<u8>>>) {
let mut tmp = [0_u8; 1024];
let mut truncation_marked = false;
loop {
let n = match reader.read(&mut tmp) {
Ok(0) | Err(_) => return,
Ok(n) => n,
};
let mut guard = match buf.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
let remaining = STDERR_DIAGNOSTIC_CAP.saturating_sub(guard.len());
if remaining > 0 {
let take = remaining.min(n);
guard.extend_from_slice(&tmp[..take]);
} else if !truncation_marked {
guard.extend_from_slice(b"\n... (truncated)");
truncation_marked = true;
}
}
}
#[cfg(unix)]
fn capture_diagnostic(
socket_path: &Path,
child_status: Option<std::process::ExitStatus>,
stderr_buf: &std::sync::Mutex<Vec<u8>>,
) -> ReadyDiagnostic {
let socket_present = socket_path.exists();
let connect_error_kind = if socket_present {
match UnixStream::connect(socket_path) {
Ok(_) => None, Err(e) => Some(format!("{:?}", e.kind())),
}
} else {
None
};
let stderr_excerpt = match stderr_buf.lock() {
Ok(g) => String::from_utf8_lossy(&g).into_owned(),
Err(p) => String::from_utf8_lossy(&p.into_inner()).into_owned(),
};
ReadyDiagnostic {
child_exit_code: child_status.and_then(|s| s.code()),
socket_present,
connect_error_kind,
stderr_excerpt,
}
}
#[cfg(not(unix))]
pub fn ensure_daemon_ready(
_binary_name: &str,
_app_name: &str,
_socket_path: &Path,
) -> Result<DaemonSpawn, DaemonReadyError> {
Ok(DaemonSpawn::AlreadyRunning)
}
#[cfg(unix)]
fn is_socket_ready(socket_path: &Path) -> bool {
socket_path.exists() && UnixStream::connect(socket_path).is_ok()
}
#[cfg(all(test, unix))]
#[allow(clippy::unwrap_used, clippy::panic, clippy::print_stderr)]
mod tests {
use super::*;
use std::os::unix::net::UnixListener;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Mutex, MutexGuard};
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
static HOME_MUTEX: Mutex<()> = Mutex::new(());
fn lock_home() -> MutexGuard<'static, ()> {
HOME_MUTEX.lock().unwrap_or_else(|p| p.into_inner())
}
fn ensure_daemon_ready_etxtbsy_resilient(
binary_name: &str,
app_name: &str,
socket_path: &Path,
) -> Result<DaemonSpawn, DaemonReadyError> {
const ETXTBSY_BACKOFF_MS: &[u64] = &[5, 10, 20, 40, 80];
for backoff_ms in ETXTBSY_BACKOFF_MS {
match ensure_daemon_ready(binary_name, app_name, socket_path) {
Err(DaemonReadyError::SpawnFailed { reason, .. })
if reason.contains("Text file busy") =>
{
std::thread::sleep(Duration::from_millis(*backoff_ms));
}
other => return other,
}
}
ensure_daemon_ready(binary_name, app_name, socket_path)
}
fn unique_socket(tag: &str) -> PathBuf {
let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let dir = PathBuf::from("/tmp").join(format!("eacd-{}-{}-{tag}", std::process::id(), id));
std::fs::create_dir_all(&dir).unwrap();
dir.join("d.sock")
}
#[test]
fn already_running_short_circuits_without_spawning() {
let sock = unique_socket("already-running");
let _unused = std::fs::remove_file(&sock);
let listener = UnixListener::bind(&sock).unwrap();
let got =
ensure_daemon_ready("definitely-not-on-disk", "myapp", &sock).expect("should succeed");
assert_eq!(got, DaemonSpawn::AlreadyRunning);
drop(listener);
let _unused = std::fs::remove_file(&sock);
}
#[test]
fn missing_binary_reports_binary_not_found() {
let sock = unique_socket("missing-binary");
let _unused = std::fs::remove_file(&sock);
let err = ensure_daemon_ready("enclaveapp-definitely-not-a-binary", "myapp", &sock)
.expect_err("should fail");
assert!(matches!(err, DaemonReadyError::BinaryNotFound { .. }));
let _unused = std::fs::remove_file(&sock);
}
struct FakeBinaryHome {
home: PathBuf,
original_home: Option<std::ffi::OsString>,
}
impl Drop for FakeBinaryHome {
fn drop(&mut self) {
#[allow(unsafe_code)]
unsafe {
if let Some(prev) = self.original_home.take() {
std::env::set_var("HOME", prev);
} else {
std::env::remove_var("HOME");
}
}
let _unused = std::fs::remove_dir_all(&self.home);
}
}
fn with_fake_home_bin(name: &str, contents: &[u8]) -> FakeBinaryHome {
use std::os::unix::fs::PermissionsExt;
let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let home =
PathBuf::from("/tmp").join(format!("eacd-home-{}-{}-{name}", std::process::id(), id));
let bindir = home.join(".local").join("bin");
std::fs::create_dir_all(&bindir).unwrap();
let bin_path = bindir.join(name);
std::fs::write(&bin_path, contents).unwrap();
std::fs::set_permissions(&bin_path, std::fs::Permissions::from_mode(0o755)).unwrap();
let original = std::env::var_os("HOME");
#[allow(unsafe_code)]
unsafe {
std::env::set_var("HOME", &home);
}
FakeBinaryHome {
home,
original_home: original,
}
}
#[test]
fn binary_exits_zero_without_socket_returns_not_ready_promptly() {
let _home_guard = lock_home();
let bin_name = format!("eacd-exit0-{}", std::process::id());
let _fake = with_fake_home_bin(&bin_name, b"#!/bin/sh\nexit 0\n");
let sock = unique_socket("exit0");
let _unused = std::fs::remove_file(&sock);
let start = std::time::Instant::now();
let err = ensure_daemon_ready_etxtbsy_resilient(&bin_name, "myapp", &sock)
.expect_err("should fail (no socket)");
let elapsed = start.elapsed();
assert!(
matches!(err, DaemonReadyError::NotReady { .. }),
"expected NotReady; got {err:?}"
);
assert!(
elapsed < Duration::from_millis(2500),
"exit-0 short-circuit took too long: {elapsed:?}",
);
}
#[test]
fn binary_exits_nonzero_returns_spawn_failed_promptly() {
let _home_guard = lock_home();
let bin_name = format!("eacd-exit42-{}", std::process::id());
let _fake = with_fake_home_bin(&bin_name, b"#!/bin/sh\nexit 42\n");
let sock = unique_socket("exit42");
let _unused = std::fs::remove_file(&sock);
let start = std::time::Instant::now();
let err = ensure_daemon_ready_etxtbsy_resilient(&bin_name, "myapp", &sock)
.expect_err("should fail (exit 42)");
let elapsed = start.elapsed();
match err {
DaemonReadyError::SpawnFailed { reason, .. } => {
assert!(
reason.contains("42") || reason.contains("status"),
"SpawnFailed reason should reference the exit status; got: {reason}"
);
}
other => panic!("expected SpawnFailed; got {other:?}"),
}
assert!(
elapsed < Duration::from_millis(2500),
"exit-nonzero short-circuit took too long: {elapsed:?}",
);
}
#[test]
fn fork_style_daemon_exit_zero_after_socket_bound_returns_spawned() {
let nc_check = std::process::Command::new("nc")
.arg("-h")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.output();
let supports_unix = match nc_check {
Ok(out) => {
let h = String::from_utf8_lossy(&out.stderr) + String::from_utf8_lossy(&out.stdout);
h.contains("-U")
}
Err(_) => false,
};
if !supports_unix {
eprintln!("skip: nc -U not supported on this host");
return;
}
let _home_guard = lock_home();
let bin_name = format!("eacd-fork-{}", std::process::id());
let script = b"#!/bin/sh\n\
(nc -lU \"$2\" >/dev/null 2>&1 &)\n\
sleep 0.05\n\
exit 0\n";
let _fake = with_fake_home_bin(&bin_name, script);
let sock = unique_socket("fork-bind");
let _unused = std::fs::remove_file(&sock);
let got = ensure_daemon_ready_etxtbsy_resilient(&bin_name, "myapp", &sock)
.expect("should succeed");
assert!(
matches!(got, DaemonSpawn::Spawned { .. }),
"expected Spawned; got {got:?}"
);
drop(
std::process::Command::new("pkill")
.arg("-f")
.arg(format!("nc -lU.*{}", sock.display()))
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status(),
);
let _unused = std::fs::remove_file(&sock);
}
#[test]
fn not_ready_diagnostic_includes_child_stderr_on_exit_zero_path() {
let _home_guard = lock_home();
let bin_name = format!("eacd-stderr-{}", std::process::id());
let script = b"#!/bin/sh\necho 'TPM init failed: bridge unreachable' >&2\nexit 0\n";
let _fake = with_fake_home_bin(&bin_name, script);
let sock = unique_socket("stderr-exit0");
let _unused = std::fs::remove_file(&sock);
let err = ensure_daemon_ready_etxtbsy_resilient(&bin_name, "myapp", &sock)
.expect_err("should fail (no socket)");
match err {
DaemonReadyError::NotReady { diagnostic, .. } => {
assert_eq!(
diagnostic.child_exit_code,
Some(0),
"exit-0 path should record the actual exit code"
);
assert!(
!diagnostic.socket_present,
"socket should not exist (script never bound)"
);
assert!(
diagnostic
.stderr_excerpt
.contains("TPM init failed: bridge unreachable"),
"stderr_excerpt should contain the script's stderr; got {:?}",
diagnostic.stderr_excerpt
);
}
other => panic!("expected NotReady; got {other:?}"),
}
}
#[test]
fn not_ready_display_embeds_diagnostic() {
let err = DaemonReadyError::NotReady {
socket_path: PathBuf::from("/tmp/x.sock"),
timeout: Duration::from_secs(30),
diagnostic: ReadyDiagnostic {
child_exit_code: None,
socket_present: true,
connect_error_kind: Some("ConnectionRefused".to_string()),
stderr_excerpt: "thread 'main' panicked at 'no key found'".to_string(),
},
};
let s = format!("{err}");
assert!(s.contains("/tmp/x.sock"));
assert!(s.contains("child still running"));
assert!(s.contains("socket present but connect failed"));
assert!(s.contains("ConnectionRefused"));
assert!(s.contains("no key found"));
}
#[test]
fn not_ready_diagnostic_caps_stderr_at_8kib() {
let _home_guard = lock_home();
let bin_name = format!("eacd-cap-{}", std::process::id());
let script = b"#!/bin/sh\nyes 'aaaaaaaa' | head -c 16384 >&2\nexit 0\n";
let _fake = with_fake_home_bin(&bin_name, script);
let sock = unique_socket("stderr-cap");
let _unused = std::fs::remove_file(&sock);
let err = ensure_daemon_ready_etxtbsy_resilient(&bin_name, "myapp", &sock)
.expect_err("should fail (no socket)");
match err {
DaemonReadyError::NotReady { diagnostic, .. } => {
assert!(
diagnostic.stderr_excerpt.len() <= STDERR_DIAGNOSTIC_CAP + 32,
"excerpt should be capped near {STDERR_DIAGNOSTIC_CAP}, got {}",
diagnostic.stderr_excerpt.len()
);
assert!(
diagnostic.stderr_excerpt.contains("(truncated)"),
"excerpt should carry the truncation marker"
);
}
other => panic!("expected NotReady; got {other:?}"),
}
}
}