#![cfg(unix)]
use std::{
path::{Path, PathBuf},
process::{Child, Command, Stdio},
time::{Duration, Instant},
};
use tempfile::TempDir;
fn find_sqryd_binary() -> Option<PathBuf> {
if let Ok(path) = std::env::var("CARGO_BIN_EXE_sqryd") {
let p = PathBuf::from(path);
if p.is_file() {
return Some(p);
}
}
let binary_name = format!("sqryd{}", std::env::consts::EXE_SUFFIX);
let exe = std::env::current_exe().ok()?;
let parent = exe.parent()?;
let candidate = parent.join(&binary_name);
if candidate.is_file() {
return Some(candidate);
}
let grandparent = parent.parent()?;
let candidate = grandparent.join(&binary_name);
if candidate.is_file() {
return Some(candidate);
}
None
}
struct TestContext {
runtime_dir: TempDir,
config_path: PathBuf,
}
impl TestContext {
fn new() -> Self {
let runtime_dir = TempDir::new().expect("create test runtime_dir");
let config_path = runtime_dir.path().join("test-daemon.toml");
std::fs::write(
&config_path,
"# sqryd test isolation config\nipc_shutdown_drain_secs = 2\n",
)
.expect("write test daemon config");
Self {
runtime_dir,
config_path,
}
}
fn socket_path(&self) -> PathBuf {
self.runtime_dir.path().join("sqry").join("sqryd.sock")
}
fn spawn_foreground(&self, sqryd: &Path) -> Child {
Command::new(sqryd)
.arg("foreground")
.env_remove("SQRY_DAEMON_CONFIG")
.env_remove("SQRY_DAEMON_MEMORY_MB")
.env_remove("SQRY_DAEMON_SOCKET")
.env_remove("SQRY_DAEMON_PIPE")
.env_remove("SQRY_DAEMON_LOG_LEVEL")
.env_remove("SQRY_DAEMON_LOG_FILE")
.env_remove("SQRY_DAEMON_STALE_MAX_AGE_HOURS")
.env_remove("SQRY_DAEMON_TOOL_TIMEOUT_SECS")
.env_remove("SQRY_DAEMON_MAX_SHIM_CONNECTIONS")
.env_remove("SQRY_DAEMON_AUTO_START_READY_TIMEOUT_SECS")
.env_remove("SQRY_DAEMON_LOG_KEEP_ROTATIONS")
.env("XDG_RUNTIME_DIR", self.runtime_dir.path())
.env("SQRY_DAEMON_SOCKET", self.socket_path())
.env("SQRY_DAEMON_CONFIG", &self.config_path)
.env("SQRY_DAEMON_LOG_LEVEL", "warn")
.env_remove("TMPDIR")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.unwrap_or_else(|e| panic!("failed to spawn sqryd: {e}"))
}
}
fn wait_for_daemon_up(ctx: &TestContext, timeout: Duration) -> bool {
let deadline = Instant::now() + timeout;
loop {
let ready_sentinel = ctx.runtime_dir.path().join("sqry").join("sqryd.ready");
if ready_sentinel.exists() || ctx.socket_path().exists() {
return true;
}
if Instant::now() >= deadline {
return false;
}
std::thread::sleep(Duration::from_millis(20));
}
}
fn wait_for_exit(child: &mut Child, timeout: Duration) -> Option<std::process::ExitStatus> {
let deadline = Instant::now() + timeout;
loop {
if let Some(status) = child.try_wait().expect("try_wait") {
return Some(status);
}
if Instant::now() >= deadline {
return None;
}
std::thread::sleep(Duration::from_millis(25));
}
}
fn send_signal(pid: libc::pid_t, signal: libc::c_int) {
let rc = unsafe { libc::kill(pid, signal) };
if rc < 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() != Some(libc::ESRCH) {
eprintln!(
"warn: kill({pid}, {signal}) failed: {err} (process may have already exited)"
);
}
}
}
fn kill_and_reap(child: &mut Child, timeout: Duration) {
let _ = child.kill(); if wait_for_exit(child, timeout).is_none() {
let _ = child.try_wait();
}
}
#[test]
fn first_sqryd_binds_second_rejected_with_ex_tempfail() {
let sqryd = match find_sqryd_binary() {
Some(p) => p,
None => {
eprintln!("SKIP: sqryd binary not found (run `cargo build -p sqry-daemon` first)");
return;
}
};
let ctx = TestContext::new();
let mut first = ctx.spawn_foreground(&sqryd);
let first_pid = first.id();
let up = wait_for_daemon_up(&ctx, Duration::from_secs(10));
if !up {
kill_and_reap(&mut first, Duration::from_secs(3));
panic!(
"first sqryd instance did not become ready within 10 s \
(socket: {}, runtime_dir: {})",
ctx.socket_path().display(),
ctx.runtime_dir.path().display()
);
}
let mut second = ctx.spawn_foreground(&sqryd);
let second_status = wait_for_exit(&mut second, Duration::from_secs(5));
send_signal(first_pid as libc::pid_t, libc::SIGTERM);
if wait_for_exit(&mut first, Duration::from_secs(5)).is_none() {
kill_and_reap(&mut first, Duration::from_secs(2));
}
let second_status = second_status.unwrap_or_else(|| {
kill_and_reap(&mut second, Duration::from_secs(2));
panic!("second sqryd instance did not exit within 5 s");
});
assert_eq!(
second_status.code(),
Some(75),
"second sqryd instance must exit 75 (EX_TEMPFAIL / AlreadyRunning), \
got: {second_status:?}"
);
}
#[test]
fn sigterm_triggers_graceful_shutdown_within_drain_deadline() {
let sqryd = match find_sqryd_binary() {
Some(p) => p,
None => {
eprintln!("SKIP: sqryd binary not found (run `cargo build -p sqry-daemon` first)");
return;
}
};
let ctx = TestContext::new();
let mut daemon = ctx.spawn_foreground(&sqryd);
let daemon_pid = daemon.id();
let up = wait_for_daemon_up(&ctx, Duration::from_secs(10));
if !up {
kill_and_reap(&mut daemon, Duration::from_secs(3));
panic!(
"sqryd did not become ready within 10 s \
(socket: {}, runtime_dir: {})",
ctx.socket_path().display(),
ctx.runtime_dir.path().display()
);
}
send_signal(daemon_pid as libc::pid_t, libc::SIGTERM);
let deadline_secs = 2u64 + 2;
let status = wait_for_exit(&mut daemon, Duration::from_secs(deadline_secs));
let status = status.unwrap_or_else(|| {
kill_and_reap(&mut daemon, Duration::from_secs(2));
panic!("sqryd did not exit within {deadline_secs} s after SIGTERM (pid={daemon_pid})")
});
assert_eq!(
status.code(),
Some(0),
"sqryd must exit 0 after SIGTERM, got: {status:?}"
);
}
#[test]
fn sigkill_leaves_stale_pidfile_but_next_start_reclaims() {
let sqryd = match find_sqryd_binary() {
Some(p) => p,
None => {
eprintln!("SKIP: sqryd binary not found (run `cargo build -p sqry-daemon` first)");
return;
}
};
let ctx = TestContext::new();
let mut first = ctx.spawn_foreground(&sqryd);
let first_pid = first.id();
let up = wait_for_daemon_up(&ctx, Duration::from_secs(10));
if !up {
kill_and_reap(&mut first, Duration::from_secs(3));
panic!(
"first sqryd instance did not become ready within 10 s \
(socket: {}, runtime_dir: {})",
ctx.socket_path().display(),
ctx.runtime_dir.path().display()
);
}
let pidfile_path = ctx.runtime_dir.path().join("sqry").join("sqryd.pid");
send_signal(first_pid as libc::pid_t, libc::SIGKILL);
if wait_for_exit(&mut first, Duration::from_secs(5)).is_none() {
let still_alive = first
.try_wait()
.expect("try_wait on first after SIGKILL")
.is_none();
if still_alive {
panic!(
"first sqryd instance (pid={first_pid}) was not reaped within 5 s after \
SIGKILL; test environment is broken \
(runtime_dir: {})",
ctx.runtime_dir.path().display()
);
}
}
assert!(
pidfile_path.exists(),
"pidfile must survive SIGKILL (Drop did not run): {}",
pidfile_path.display()
);
if ctx.socket_path().exists() {
let _ = std::fs::remove_file(ctx.socket_path());
}
let ready_sentinel = ctx.runtime_dir.path().join("sqry").join("sqryd.ready");
if ready_sentinel.exists() {
let _ = std::fs::remove_file(&ready_sentinel);
}
let mut second = ctx.spawn_foreground(&sqryd);
let second_pid = second.id();
let up = wait_for_daemon_up(&ctx, Duration::from_secs(10));
send_signal(second_pid as libc::pid_t, libc::SIGTERM);
if wait_for_exit(&mut second, Duration::from_secs(5)).is_none() {
kill_and_reap(&mut second, Duration::from_secs(2));
}
assert!(
up,
"second sqryd instance must reclaim the stale pidfile and bind the socket, \
but it did not become ready within 10 s \
(socket: {}, runtime_dir: {})",
ctx.socket_path().display(),
ctx.runtime_dir.path().display()
);
}