#![cfg(target_os = "linux")]
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
fn unique_stem() -> String {
format!(
"cordis-cli-shutdown-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|elapsed| elapsed.as_nanos() as u64)
.unwrap_or(0)
)
}
#[allow(clippy::zombie_processes)]
fn spawn_ready_daemon(stem: &str) -> (Child, PathBuf) {
let dir = std::env::temp_dir().join(stem);
std::fs::create_dir_all(&dir).unwrap();
let config = dir.join("cordis.yml");
std::fs::write(&config, "entries: []\n").unwrap();
let mut child = Command::new(env!("CARGO_BIN_EXE_cordis"))
.arg("run")
.arg(&config)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.expect("spawn cordis daemon");
let stderr = child.stderr.take().expect("piped stderr");
let seen = Arc::new(Mutex::new(String::new()));
let log = Arc::clone(&seen);
std::thread::spawn(move || {
for line in BufReader::new(stderr).lines() {
let Ok(line) = line else { break };
log.lock().unwrap().push_str(&line);
log.lock().unwrap().push('\n');
}
});
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
if seen.lock().unwrap().contains("worker ready") {
return (child, config);
}
std::thread::sleep(Duration::from_millis(50));
}
let _ = child.kill();
panic!(
"worker never reported ready; daemon stderr:\n{}",
seen.lock().unwrap()
);
}
fn processes_referencing(stem: &str) -> usize {
let mut count = 0;
let entries = std::fs::read_dir("/proc").unwrap();
for entry in entries.flatten() {
let Ok(cmdline) = std::fs::read_to_string(entry.path().join("cmdline")) else {
continue;
};
if cmdline.contains(stem) {
count += 1;
}
}
count
}
#[cfg(target_os = "linux")]
#[test]
fn sigterm_to_daemon_only_takes_the_worker_down() {
let stem = unique_stem();
let (mut daemon, _config) = spawn_ready_daemon(&stem);
let pid = daemon.id() as i32;
unsafe {
libc::kill(pid, libc::SIGTERM);
}
let deadline = Instant::now() + Duration::from_secs(5);
loop {
match daemon.try_wait().expect("daemon is waitable") {
Some(status) => {
assert_eq!(status.code(), Some(0), "daemon exit code after SIGTERM");
break;
}
None if Instant::now() < deadline => {
std::thread::sleep(Duration::from_millis(50));
}
None => panic!("daemon did not exit within 5s of SIGTERM"),
}
}
let deadline = Instant::now() + Duration::from_secs(5);
loop {
if processes_referencing(&stem) == 0 {
break;
}
assert!(
Instant::now() < deadline,
"worker processes survived the daemon shutdown"
);
std::thread::sleep(Duration::from_millis(50));
}
let _ = std::fs::remove_dir_all(std::env::temp_dir().join(&stem));
}