Running as PID 1 in a container, the daemon silently dropped SIGTERM, so `docker stop` always hit the 10s timeout and SIGKILL, and host reboots waited on it. The accept loop now polls a non-blocking listener and a shutdown flag registered via signal-hook. On SIGTERM or SIGINT it stops accepting, gives in-flight requests up to 3s to finish, removes the socket file, and exits with status 0. Log lines mark signal receipt and completion. Adds integration tests that spawn the binary, perform an auth round-trip, signal it, and assert a clean exit and socket removal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kmrgkpp9YBJ6AMaWYPPZuD
131 lines
3.9 KiB
Rust
131 lines
3.9 KiB
Rust
use common::io::read_string;
|
|
use common::request::Request;
|
|
use std::os::unix::fs::FileTypeExt;
|
|
use std::os::unix::net::UnixStream;
|
|
use std::path::PathBuf;
|
|
use std::process::{Child, Command};
|
|
use std::time::{Duration, Instant};
|
|
|
|
const PASSWORD_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../test_passwords");
|
|
|
|
fn temp_socket_path(tag: &str) -> PathBuf {
|
|
let mut path = std::env::temp_dir();
|
|
path.push(format!("saslauthd-{}-{}.sock", tag, std::process::id()));
|
|
let _ = std::fs::remove_file(&path);
|
|
path
|
|
}
|
|
|
|
/// A spawned daemon that is killed if the test panics before stopping it.
|
|
struct Daemon(Child);
|
|
|
|
impl Drop for Daemon {
|
|
fn drop(&mut self) {
|
|
if self.0.try_wait().ok().flatten().is_none() {
|
|
let _ = self.0.kill();
|
|
let _ = self.0.wait();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn is_socket(path: &PathBuf) -> bool {
|
|
std::fs::metadata(path)
|
|
.map(|m| m.file_type().is_socket())
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn spawn_daemon(socket: &PathBuf) -> Daemon {
|
|
let child = Command::new(env!("CARGO_BIN_EXE_saslauthd"))
|
|
.arg("--socket-name")
|
|
.arg(socket)
|
|
.arg("--password-dir")
|
|
.arg(PASSWORD_DIR)
|
|
.spawn()
|
|
.expect("failed to spawn saslauthd");
|
|
|
|
let deadline = Instant::now() + Duration::from_secs(5);
|
|
while !is_socket(socket) {
|
|
assert!(Instant::now() < deadline, "daemon never created {socket:?}");
|
|
std::thread::sleep(Duration::from_millis(10));
|
|
}
|
|
Daemon(child)
|
|
}
|
|
|
|
fn send_signal(daemon: &Daemon, signal: &str) {
|
|
let child = &daemon.0;
|
|
let status = Command::new("kill")
|
|
.arg(format!("-{signal}"))
|
|
.arg(child.id().to_string())
|
|
.status()
|
|
.expect("failed to run kill");
|
|
assert!(status.success(), "kill -{signal} failed");
|
|
}
|
|
|
|
fn wait_for_exit(daemon: &mut Daemon, timeout: Duration) -> std::process::ExitStatus {
|
|
let child = &mut daemon.0;
|
|
let deadline = Instant::now() + timeout;
|
|
loop {
|
|
if let Some(status) = child.try_wait().expect("try_wait failed") {
|
|
return status;
|
|
}
|
|
if Instant::now() >= deadline {
|
|
let _ = child.kill();
|
|
let _ = child.wait();
|
|
panic!("daemon did not exit within {timeout:?}");
|
|
}
|
|
std::thread::sleep(Duration::from_millis(10));
|
|
}
|
|
}
|
|
|
|
fn authenticate(socket: &PathBuf, password: &str) -> String {
|
|
let mut stream = UnixStream::connect(socket).expect("connect to daemon");
|
|
Request::new("bp", password, "imap", "")
|
|
.write_to_stream(&mut stream)
|
|
.expect("write request");
|
|
read_string(&mut stream).expect("read response")
|
|
}
|
|
|
|
fn assert_clean_shutdown(signal: &str) {
|
|
let socket = temp_socket_path(signal);
|
|
let mut daemon = spawn_daemon(&socket);
|
|
|
|
// Make sure the daemon actually works before we shut it down.
|
|
assert_eq!(authenticate(&socket, "some-rad-password"), "OK");
|
|
assert_eq!(authenticate(&socket, "wrong-password"), "NO");
|
|
|
|
let started = Instant::now();
|
|
send_signal(&daemon, signal);
|
|
let status = wait_for_exit(&mut daemon, Duration::from_secs(1));
|
|
|
|
assert!(status.success(), "expected exit status 0 on {signal}, got {status}");
|
|
assert!(
|
|
started.elapsed() < Duration::from_secs(1),
|
|
"shutdown took {:?}",
|
|
started.elapsed()
|
|
);
|
|
assert!(!socket.exists(), "socket file {socket:?} was left behind");
|
|
}
|
|
|
|
#[test]
|
|
fn it_shuts_down_cleanly_on_sigterm() {
|
|
assert_clean_shutdown("TERM");
|
|
}
|
|
|
|
#[test]
|
|
fn it_shuts_down_cleanly_on_sigint() {
|
|
assert_clean_shutdown("INT");
|
|
}
|
|
|
|
#[test]
|
|
fn it_recreates_a_stale_socket_on_startup() {
|
|
let socket = temp_socket_path("stale");
|
|
std::fs::write(&socket, b"stale").expect("create stale socket file");
|
|
|
|
let mut daemon = spawn_daemon(&socket);
|
|
assert_eq!(authenticate(&socket, "some-rad-password"), "OK");
|
|
|
|
send_signal(&daemon, "TERM");
|
|
let status = wait_for_exit(&mut daemon, Duration::from_secs(1));
|
|
assert!(status.success());
|
|
assert!(!socket.exists());
|
|
}
|