use super::*;
#[cfg(unix)]
use std::io::Write as _;
#[cfg(unix)]
use std::process::{ChildStdin, ChildStdout};
#[cfg(unix)]
use shep_core::protocol::{
HelloReply, PROTOCOL_VERSION, decode_frame, encode_frame, request::Hello,
};
#[cfg(unix)]
const BRIDGE_DEADLINE: Duration = Duration::from_secs(10);
#[cfg(unix)]
#[test]
fn dial_stdio_with_no_shepherd_exits_5_leaves_stdout_empty_and_creates_no_home() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("no-such-home");
let output = shep(&home).arg("dial-stdio").output().unwrap();
assert_eq!(output.status.code(), Some(5), "{output:?}");
assert!(output.stdout.is_empty(), "stdout: {:?}", output.stdout);
assert!(
!String::from_utf8_lossy(&output.stderr).is_empty(),
"the failure must be explained on stderr"
);
assert!(!home.exists(), "dial-stdio created the home");
}
#[cfg(unix)]
fn dial_a_running_shepherd(home: &TempDir, guard: &mut DaemonGuard) -> Child {
let script = write_test_script(home);
let started = shep(home.path())
.arg("start")
.arg(&script)
.output()
.unwrap();
guard.adopt_home(home.path());
assert_success(&started);
std::process::Command::cargo_bin("shep")
.unwrap()
.arg("--home")
.arg(home.path())
.arg("dial-stdio")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap()
}
#[cfg(unix)]
fn say_hello(stdin: &mut ChildStdin, mut stdout: ChildStdout) -> (HelloReply, ChildStdout) {
let hello = Hello {
client_version: env!("CARGO_PKG_VERSION").to_owned(),
protocol: PROTOCOL_VERSION,
dog_name: None,
};
let payload = encode_frame(&hello).unwrap();
stdin
.write_all(&u32::try_from(payload.len()).unwrap().to_be_bytes())
.unwrap();
stdin.write_all(&payload).unwrap();
stdin.flush().unwrap();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let mut prefix = [0_u8; 4];
let mut frame = Vec::new();
let read = stdout.read_exact(&mut prefix).and_then(|()| {
frame.resize(u32::from_be_bytes(prefix) as usize, 0);
stdout.read_exact(&mut frame)
});
let _ = tx.send(read.map(|()| (frame, stdout)));
});
let (frame, stdout) = rx
.recv_timeout(BRIDGE_DEADLINE)
.expect("no frame came back through dial-stdio in time")
.expect("read the reply frame");
(decode_frame(&frame).unwrap(), stdout)
}
#[cfg(unix)]
#[test]
fn dial_stdio_carries_a_real_handshake_to_a_running_shepherd() {
let home = tempfile::tempdir().unwrap();
let mut guard = DaemonGuard::default();
let mut child = dial_a_running_shepherd(&home, &mut guard);
let mut stdin = child.stdin.take().unwrap();
let (reply, _stdout) = say_hello(&mut stdin, child.stdout.take().unwrap());
let ack = reply.expect("the shepherd accepted this build's Hello");
assert_eq!(ack.daemon_version, env!("CARGO_PKG_VERSION"));
drop(stdin);
let status = common::wait_bounded(&mut child, BRIDGE_DEADLINE, "shep dial-stdio");
assert!(
status.success(),
"closing stdin is an orderly end: {status:?}"
);
graceful_kill(home.path());
}
#[cfg(unix)]
#[test]
fn dial_stdio_exits_promptly_when_the_shepherd_is_killed_with_stdin_still_open() {
let home = tempfile::tempdir().unwrap();
let mut guard = DaemonGuard::default();
let mut child = dial_a_running_shepherd(&home, &mut guard);
let mut stdin = child.stdin.take().unwrap();
let (reply, _stdout) = say_hello(&mut stdin, child.stdout.take().unwrap());
reply.expect("the shepherd accepted this build's Hello");
let daemon = daemon_pid(home.path()).expect("a running shepherd records its pid");
nix::sys::signal::kill(daemon, nix::sys::signal::Signal::SIGKILL).unwrap();
common::wait_bounded(&mut child, BRIDGE_DEADLINE, "shep dial-stdio");
drop(stdin);
}