use std::io::{Read, Write};
use std::process::{Command, Stdio};
use std::sync::mpsc::{channel, Receiver};
use std::thread;
use super::{serve_session, spawn_contained_session};
use crate::broker::protocol_v2::{session_frame, SessionExit, SessionFrame};
use crate::broker::session_codec::{
encode_session_frame, try_decode_session_frame, DecodedSessionFrame,
};
use crate::broker::session_pump::run_child_session;
use crate::containment::ContainedProcessGroup;
mod mempipe {
use std::collections::VecDeque;
use std::io::{self, Read, Write};
use std::sync::{Arc, Condvar, Mutex};
struct Shared {
buf: VecDeque<u8>,
writer_open: bool,
}
type Chan = Arc<(Mutex<Shared>, Condvar)>;
pub struct Reader(Chan);
pub struct Writer(Chan);
pub fn pipe() -> (Reader, Writer) {
let chan = Arc::new((
Mutex::new(Shared {
buf: VecDeque::new(),
writer_open: true,
}),
Condvar::new(),
));
(Reader(chan.clone()), Writer(chan))
}
impl Read for Reader {
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
let (lock, cv) = &*self.0;
let mut guard = lock.lock().unwrap();
loop {
if !guard.buf.is_empty() {
let n = out.len().min(guard.buf.len());
for slot in out.iter_mut().take(n) {
*slot = guard.buf.pop_front().unwrap();
}
return Ok(n);
}
if !guard.writer_open {
return Ok(0);
}
guard = cv.wait(guard).unwrap();
}
}
}
impl Write for Writer {
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
let (lock, cv) = &*self.0;
lock.lock().unwrap().buf.extend(data.iter().copied());
cv.notify_all();
Ok(data.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Drop for Writer {
fn drop(&mut self) {
let (lock, cv) = &*self.0;
lock.lock().unwrap().writer_open = false;
cv.notify_all();
}
}
}
#[derive(Debug, PartialEq, Eq)]
struct Reassembled {
stdout: Vec<u8>,
stderr: Vec<u8>,
exit: (i32, i32),
}
fn fixture() -> Command {
let exe = std::env::current_exe().expect("test executable path");
let profile_dir = exe
.parent()
.and_then(std::path::Path::parent)
.expect("test binary should live in <profile>/deps/");
let path = profile_dir.join(format!(
"testbin-stdio-scripted{}",
std::env::consts::EXE_SUFFIX
));
assert!(
path.is_file(),
"test fixture is missing at {} — run `soldr cargo build -p testbins` first",
path.display()
);
Command::new(path)
}
fn frame(kind: session_frame::Kind) -> SessionFrame {
SessionFrame { kind: Some(kind) }
}
fn run_direct(directives: &[&str], stdin: &[u8]) -> Reassembled {
let mut child = fixture()
.args(directives)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn fixture directly");
child.stdin.take().unwrap().write_all(stdin).unwrap();
let output = child.wait_with_output().unwrap();
Reassembled {
stdout: output.stdout,
stderr: output.stderr,
exit: (output.status.code().unwrap_or(-1), 0),
}
}
fn collect(out_rx: Receiver<SessionFrame>, exit: SessionExit) -> Reassembled {
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let mut exit_frame = None;
for frame in out_rx {
match frame.kind {
Some(session_frame::Kind::Stdout(b)) => stdout.extend_from_slice(&b),
Some(session_frame::Kind::Stderr(b)) => stderr.extend_from_slice(&b),
Some(session_frame::Kind::Exit(e)) => exit_frame = Some((e.code, e.signal)),
_ => panic!("unexpected inbound-only frame on the outbound lane"),
}
}
let exit_frame = exit_frame.expect("a terminal Exit frame is always sent");
assert_eq!(exit_frame, (exit.code, exit.signal));
Reassembled {
stdout,
stderr,
exit: exit_frame,
}
}
fn run_contained_through_pump(directives: &[&str], stdin: &[u8]) -> Reassembled {
let group = ContainedProcessGroup::new().expect("contained group");
let child = spawn_contained_session(&group, fixture().args(directives))
.expect("contained spawn with piped stdio");
let (stdin_tx, stdin_rx) = channel();
if !stdin.is_empty() {
stdin_tx
.send(frame(session_frame::Kind::Stdin(stdin.to_vec())))
.unwrap();
}
stdin_tx
.send(frame(session_frame::Kind::StdinEof(true)))
.unwrap();
drop(stdin_tx);
let (out_tx, out_rx) = channel();
let exit = run_child_session(child, out_tx, stdin_rx).expect("pump reaps contained child");
collect(out_rx, exit)
}
fn run_over_transport(directives: &[&str], stdin: &[u8]) -> Reassembled {
let group = ContainedProcessGroup::new().expect("contained group");
let child = spawn_contained_session(&group, fixture().args(directives))
.expect("contained spawn with piped stdio");
let (server_in, client_in) = mempipe::pipe(); let (client_out, server_out) = mempipe::pipe();
let stdin_vec = stdin.to_vec();
let client = thread::spawn(move || -> Reassembled {
let mut client_in = client_in;
let mut seq = 0u64;
if !stdin_vec.is_empty() {
let wire =
encode_session_frame(&frame(session_frame::Kind::Stdin(stdin_vec)), seq).unwrap();
seq += 1;
client_in.write_all(&wire).unwrap();
}
let eof = encode_session_frame(&frame(session_frame::Kind::StdinEof(true)), seq).unwrap();
client_in.write_all(&eof).unwrap();
drop(client_in);
let mut client_out = client_out;
let mut buf: Vec<u8> = Vec::new();
let mut chunk = [0u8; 8192];
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let mut exit_frame = None;
loop {
while let Some(DecodedSessionFrame { frame, consumed }) =
try_decode_session_frame(&buf).expect("decode ok")
{
match frame.kind {
Some(session_frame::Kind::Stdout(b)) => stdout.extend_from_slice(&b),
Some(session_frame::Kind::Stderr(b)) => stderr.extend_from_slice(&b),
Some(session_frame::Kind::Exit(e)) => exit_frame = Some((e.code, e.signal)),
_ => panic!("unexpected frame on outbound stream"),
}
buf.drain(..consumed);
}
let n = client_out.read(&mut chunk).unwrap();
if n == 0 {
break;
}
buf.extend_from_slice(&chunk[..n]);
}
Reassembled {
stdout,
stderr,
exit: exit_frame.expect("a terminal Exit frame is always sent"),
}
});
let exit = serve_session(child, server_in, server_out).expect("serve_session reaps child");
let reassembled = client.join().expect("client thread");
assert_eq!(
reassembled.exit,
(exit.code, exit.signal),
"the client-visible Exit must match serve_session's return"
);
reassembled
}
#[test]
fn contained_child_through_pump_matches_oracle() {
let script = &["out:ARTIFACT", "err:DIAGNOSTIC", "out:MORE", "exit:5"];
assert_eq!(
run_contained_through_pump(script, b""),
run_direct(script, b"")
);
}
#[test]
fn contained_child_is_byte_transparent_for_raw_non_utf8() {
let script = &["outhex:00ff80", "errhex:8081ff"];
let got = run_contained_through_pump(script, b"");
assert_eq!(got.stdout, vec![0x00, 0xff, 0x80]);
assert_eq!(got.stderr, vec![0x80, 0x81, 0xff]);
assert_eq!(got, run_direct(script, b""));
}
#[test]
fn serve_session_over_transport_matches_oracle() {
let script = &["out:ARTIFACT", "err:DIAGNOSTIC", "out:MORE", "exit:5"];
assert_eq!(run_over_transport(script, b""), run_direct(script, b""));
}
#[test]
fn serve_session_is_byte_transparent_for_raw_non_utf8() {
let script = &["outhex:00ff80", "errhex:8081ff"];
let got = run_over_transport(script, b"");
assert_eq!(got.stdout, vec![0x00, 0xff, 0x80]);
assert_eq!(got.stderr, vec![0x80, 0x81, 0xff]);
assert_eq!(got, run_direct(script, b""));
}
#[test]
fn serve_session_delivers_full_byte_range_stdin() {
let payload: Vec<u8> = (0u8..=255).collect();
let got = run_over_transport(&["echo"], &payload);
assert_eq!(got.stdout, payload);
assert_eq!(got, run_direct(&["echo"], &payload));
}
#[test]
fn serve_session_reports_nonzero_exit() {
let got = run_over_transport(&["err:boom", "exit:7"], b"");
assert_eq!(got.exit, (7, 0));
assert_eq!(got.stderr, b"boom");
}