use std::{
fmt::Write,
io::Read,
os::unix::{io::AsRawFd, net::UnixListener, process::CommandExt as _},
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;
use crate::support::daemon::{AttachArgs, DaemonArgs};
#[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.is_empty() {
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 = Stdio::from(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.as_raw_fd(), &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.is_empty() {
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",
AttachArgs { cmd: Some(String::from("/bin/bash")), ..Default::default() },
)
.context("starting attach proc")?;
let mut sh1_matcher = sh1_proc.line_matcher()?;
sh1_proc.run_cmd("echo hi")?;
sh1_matcher.scan_until_re("hi$")?;
let mut busy_proc = daemon_proc
.attach(
"sh1",
AttachArgs { cmd: Some(String::from("/bin/bash")), ..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",
AttachArgs { cmd: Some(String::from("/bin/bash")), ..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.is_empty())
})?;
let hook_records = daemon_proc.hook_records.as_ref().unwrap().lock().unwrap();
eprintln!("hook_records: {:?}", hook_records);
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",
DaemonArgs { listen_events: false, ..DaemonArgs::default() },
)
.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(())
})
}
#[test]
#[timeout(30000)]
fn echo_sentinel() -> anyhow::Result<()> {
support::dump_err(|| {
let output = Command::new(support::shpool_bin()?)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.env("SHPOOL__INTERNAL__PRINT_SENTINEL", "prompt")
.arg("daemon")
.output()?;
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("SHPOOL_PROMPT_SETUP_SENTINEL"));
Ok(())
})
}
#[test]
#[timeout(30000)]
fn allows_dynamic_log_adjustments() -> anyhow::Result<()> {
support::dump_err(|| {
let mut daemon_proc = support::daemon::Proc::new(
"norc.toml",
DaemonArgs { verbosity: 0, ..Default::default() },
)
.context("starting daemon proc")?;
daemon_proc.set_log_level("trace")?;
loop {
let mut sh1 = daemon_proc.attach("sh1", AttachArgs::default())?;
sh1.run_cmd("echo hi")?;
let log_data = std::fs::read_to_string(&daemon_proc.log_file)?;
if log_data.contains("echo hi") {
break;
}
std::thread::sleep(time::Duration::from_millis(300));
}
Ok(())
})
}