use std::{
fmt::Write,
io::Read,
os::unix::{
io::{AsRawFd, FromRawFd},
net::UnixListener,
process::CommandExt,
},
path,
process::{Command, Stdio},
time,
};
use anyhow::{anyhow, Context};
use nix::{
sys::signal::{self, Signal},
unistd::{ForkResult, Pid},
};
use ntest::timeout;
use regex::Regex;
mod support;
#[test]
#[timeout(30000)]
fn start() -> anyhow::Result<()> {
support::dump_err(|| {
let tmp_dir = tempfile::Builder::new()
.prefix("shpool-test")
.rand_bytes(20)
.tempdir()
.context("creating tmp dir")?;
let mut child = Command::new(support::shpool_bin()?)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.arg("--socket")
.arg(tmp_dir.path().join("shpool.socket"))
.arg("daemon")
.spawn()
.context("spawning daemon process")?;
std::thread::sleep(time::Duration::from_millis(500));
child.kill().context("killing child")?;
let mut stdout = child.stdout.take().context("missing stdout")?;
let mut stdout_str = String::from("");
stdout.read_to_string(&mut stdout_str).context("slurping stdout")?;
if stdout_str != "" {
println!("{}", stdout_str);
return Err(anyhow!("unexpected stdout output"));
}
let mut stderr = child.stderr.take().context("missing stderr")?;
let mut stderr_str = String::from("");
stderr.read_to_string(&mut stderr_str).context("slurping stderr")?;
assert!(stderr_str.contains("STARTING DAEMON"));
Ok(())
})
}
#[test]
#[timeout(30000)]
fn systemd_activation() -> anyhow::Result<()> {
support::dump_err(|| {
let tmp_dir = tempfile::Builder::new()
.prefix("shpool-test")
.rand_bytes(20)
.tempdir()
.context("creating tmp dir")?;
let sock_path = tmp_dir.path().join("shpool.socket");
let activation_sock = UnixListener::bind(&sock_path)?;
let (parent_stderr, child_stderr) =
nix::unistd::pipe().context("creating pipe to collect stderr")?;
let child_stderr_pipe = unsafe { Stdio::from_raw_fd(child_stderr) };
let mut cmd = Command::new(support::shpool_bin()?);
cmd.stdout(Stdio::piped())
.stderr(child_stderr_pipe)
.env("LISTEN_FDS", "1")
.env("LISTEN_FDNAMES", sock_path)
.arg("daemon");
let mut pid_buf = String::with_capacity(128);
let child_pid = match unsafe { nix::unistd::fork() } {
Ok(ForkResult::Parent { child, .. }) => child,
Ok(ForkResult::Child) => {
let fdarg = match nix::unistd::dup2(activation_sock.as_raw_fd(), 3) {
Ok(newfd) => newfd,
Err(e) => {
eprintln!("dup err: {}", e);
std::process::exit(1)
}
};
let fdflags = nix::fcntl::fcntl(fdarg, nix::fcntl::FcntlArg::F_GETFD)
.expect("getfd flags to work");
let mut newflags = nix::fcntl::FdFlag::from_bits(fdflags).unwrap();
newflags.remove(nix::fcntl::FdFlag::FD_CLOEXEC);
nix::fcntl::fcntl(fdarg, nix::fcntl::FcntlArg::F_SETFD(newflags))
.expect("FD_CLOEXEC to be unset");
write!(&mut pid_buf, "{}", std::process::id())
.expect("to be able to format the pid");
cmd.env("LISTEN_PID", pid_buf);
let err = cmd.exec();
eprintln!("exec err: {:?}", err);
std::process::exit(1);
}
Err(e) => {
return Err(e).context("forking daemon proc");
}
};
std::thread::sleep(time::Duration::from_millis(500));
nix::sys::signal::kill(child_pid, Some(nix::sys::signal::Signal::SIGKILL))
.context("killing daemon")?;
nix::sys::wait::waitpid(child_pid, None).context("reaping daemon")?;
let mut stderr_buf: Vec<u8> = vec![0; 1024 * 8];
let len =
nix::unistd::read(parent_stderr, &mut stderr_buf[..]).context("reading stderr")?;
let stderr = String::from_utf8_lossy(&stderr_buf[..len]);
assert!(stderr.contains("using systemd activation socket"));
Ok(())
})
}
#[test]
#[timeout(30000)]
fn config() -> anyhow::Result<()> {
support::dump_err(|| {
let tmp_dir = tempfile::Builder::new()
.prefix("shpool-test")
.rand_bytes(20)
.tempdir()
.context("creating tmp dir")?;
let mut child = Command::new(support::shpool_bin()?)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.arg("--socket")
.arg(tmp_dir.path().join("shpool.socket"))
.arg("--config-file")
.arg(support::testdata_file("empty.toml"))
.arg("daemon")
.spawn()
.context("spawning daemon process")?;
std::thread::sleep(time::Duration::from_millis(500));
child.kill().context("killing child")?;
let mut stdout = child.stdout.take().context("missing stdout")?;
let mut stdout_str = String::from("");
stdout.read_to_string(&mut stdout_str).context("slurping stdout")?;
if stdout_str != "" {
println!("{}", stdout_str);
return Err(anyhow!("unexpected stdout output"));
}
let mut stderr = child.stderr.take().context("missing stderr")?;
let mut stderr_str = String::from("");
stderr.read_to_string(&mut stderr_str).context("slurping stderr")?;
assert!(stderr_str.contains("STARTING DAEMON"));
Ok(())
})
}
#[test]
#[timeout(30000)]
fn hooks() -> anyhow::Result<()> {
support::dump_err(|| {
let mut daemon_proc =
support::daemon::Proc::new_instrumented("norc.toml").context("starting daemon proc")?;
let sh1_detached_re = Regex::new("sh1.*disconnected")?;
{
let mut sh1_proc =
daemon_proc.attach("sh1", Default::default()).context("starting attach proc")?;
let mut sh1_matcher = sh1_proc.line_matcher()?;
sh1_proc.run_cmd("echo hi")?;
sh1_matcher.match_re("hi$")?;
let mut busy_proc =
daemon_proc.attach("sh1", Default::default()).context("starting attach proc")?;
busy_proc.proc.wait()?;
}
daemon_proc.wait_until_list_matches(|listout| sh1_detached_re.is_match(listout))?;
let mut sh1_proc =
daemon_proc.attach("sh1", Default::default()).context("starting attach proc")?;
sh1_proc.run_cmd("exit")?;
support::wait_until(|| {
let hook_records = daemon_proc.hook_records.as_ref().unwrap().lock().unwrap();
Ok(hook_records.shell_disconnects.len() > 0)
})?;
let hook_records = daemon_proc.hook_records.as_ref().unwrap().lock().unwrap();
assert_eq!(hook_records.new_sessions[0], "sh1");
assert_eq!(hook_records.reattaches[0], "sh1");
assert_eq!(hook_records.busys[0], "sh1");
assert_eq!(hook_records.client_disconnects[0], "sh1");
assert_eq!(hook_records.shell_disconnects[0], "sh1");
Ok(())
})
}
#[test]
#[timeout(30000)]
fn cleanup_socket() -> anyhow::Result<()> {
support::dump_err(|| {
let mut daemon_proc =
support::daemon::Proc::new("norc.toml", false).context("starting daemon proc")?;
signal::kill(
Pid::from_raw(daemon_proc.proc.as_ref().unwrap().id() as i32),
Signal::SIGINT,
)?;
daemon_proc.proc_wait()?;
assert!(!path::Path::new(&daemon_proc.socket_path).exists());
Ok(())
})
}