use std::io::{self, Read, Write};
use std::sync::{Arc, Mutex, PoisonError};
use std::thread::{self, JoinHandle};
use std::time::Duration;
use conpty_oxide::blocking::{Child, Command, OwnedReadHalf, OwnedWriteHalf};
use conpty_oxide::{ExitStatus, PtyController, SessionOptions};
use super::{lock, strip_escapes, wait_until};
pub struct OutputCollector {
buffer: Arc<Mutex<Vec<u8>>>,
reader: JoinHandle<io::Result<()>>,
}
impl OutputCollector {
#[must_use]
pub fn spawn(mut half: OwnedReadHalf) -> Self {
let buffer = Arc::new(Mutex::new(Vec::new()));
let sink = Arc::clone(&buffer);
let reader = thread::Builder::new()
.name("conpty-oxide-test-reader".into())
.spawn(move || {
let mut chunk = [0_u8; 4096];
loop {
let read = half.read(&mut chunk)?;
if read == 0 {
return Ok(());
}
lock(&sink).extend_from_slice(&chunk[..read]);
}
})
.expect("spawning the collector thread must succeed");
Self { buffer, reader }
}
#[must_use]
pub fn text(&self) -> String {
String::from_utf8_lossy(&lock(&self.buffer)).into_owned()
}
pub fn wait_for(&self, needle: &str, limit: Duration) {
let found = wait_until(limit, || strip_escapes(&self.text()).contains(needle));
assert!(
found,
"{needle:?} never appeared in the output: {:?}",
strip_escapes(&self.text())
);
}
pub fn join(self) -> Vec<u8> {
let Self { buffer, reader } = self;
reader
.join()
.expect("the collector thread must not panic")
.expect("reading to end-of-file must succeed");
Arc::try_unwrap(buffer)
.expect("the collector thread holds the only other reference")
.into_inner()
.unwrap_or_else(PoisonError::into_inner)
}
#[must_use]
pub fn join_text(self) -> String {
String::from_utf8_lossy(&self.join()).into_owned()
}
}
pub struct Session {
pub child: Child,
pub output: OutputCollector,
pub writer: OwnedWriteHalf,
pub controller: PtyController,
}
impl Session {
pub fn start(command: &mut Command) -> Self {
Self::start_with(command, SessionOptions::default())
}
pub fn start_with(command: &mut Command, options: SessionOptions) -> Self {
let parts = command
.spawn_with(options)
.expect("spawning must succeed")
.into_parts();
Self {
child: parts.child,
output: OutputCollector::spawn(parts.output),
writer: parts.input,
controller: parts.controller,
}
}
pub fn write_line(&mut self, line: &str) {
self.writer
.write_all(line.as_bytes())
.expect("writing console input must succeed");
self.writer
.write_all(b"\r\n")
.expect("writing console input must succeed");
self.writer.flush().expect("flush must succeed");
}
#[must_use]
pub fn finish(self) -> (String, ExitStatus) {
let (bytes, status) = self.finish_raw();
(strip_escapes(&String::from_utf8_lossy(&bytes)), status)
}
#[must_use]
pub fn finish_raw(self) -> (Vec<u8>, ExitStatus) {
let Self {
mut child,
output,
writer,
controller,
} = self;
let status = child.wait().expect("waiting must succeed");
let bytes = output.join();
drop(writer);
drop(controller);
(bytes, status)
}
}