use std::io::{self, Read, Write};
use std::os::fd::{AsRawFd, OwnedFd};
use serde::{Deserialize, Serialize};
#[derive(Debug)]
pub struct PtyStream {
fd: OwnedFd,
}
impl PtyStream {
pub const fn new(fd: OwnedFd) -> Self {
Self { fd }
}
pub const fn as_fd(&self) -> &OwnedFd {
&self.fd
}
}
impl Read for PtyStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
nix::unistd::read(self.fd.as_raw_fd(), buf).map_err(io::Error::from)
}
}
impl Write for PtyStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
nix::unistd::write(&self.fd, buf).map_err(io::Error::from)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl expectrl::process::NonBlocking for PtyStream {
fn set_blocking(&mut self, on: bool) -> io::Result<()> {
let raw_fd = self.fd.as_raw_fd();
let flags =
nix::fcntl::fcntl(raw_fd, nix::fcntl::FcntlArg::F_GETFL).map_err(io::Error::from)?;
let mut oflags = nix::fcntl::OFlag::from_bits_truncate(flags);
if on {
oflags.remove(nix::fcntl::OFlag::O_NONBLOCK);
} else {
oflags.insert(nix::fcntl::OFlag::O_NONBLOCK);
}
let _rc = nix::fcntl::fcntl(raw_fd, nix::fcntl::FcntlArg::F_SETFL(oflags))
.map_err(io::Error::from)?;
Ok(())
}
}
#[derive(Debug)]
pub struct StubProcess;
impl expectrl::process::Healthcheck for StubProcess {
type Status = bool;
fn get_status(&self) -> io::Result<Self::Status> {
Ok(true)
}
fn is_alive(&self) -> io::Result<bool> {
Ok(true)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum CaseTag {
#[serde(rename = "ok")]
Ok,
#[serde(rename = "fail")]
Fail,
#[serde(rename = "continue")]
Continue,
#[serde(rename = "next")]
Next,
#[serde(rename = "needs_user")]
NeedsUser,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExpectCase {
pub pattern: String,
pub tag: CaseTag,
#[serde(default)]
pub respond: Option<String>,
#[serde(default)]
pub label: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
#[non_exhaustive]
pub enum BatchStep {
#[serde(rename = "send")]
Send {
input: String,
},
#[serde(rename = "expect")]
Expect {
pattern: String,
#[serde(default)]
timeout_secs: Option<u64>,
},
#[serde(rename = "expect_cases")]
ExpectCases {
cases: Vec<ExpectCase>,
#[serde(default)]
timeout_secs: Option<u64>,
},
#[serde(rename = "signal")]
Signal {
signal: String,
},
}
#[derive(Debug, Clone, Serialize)]
pub struct BatchStepResult {
pub index: usize,
pub step_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub captures: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub matched_case: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tag: Option<CaseTag>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
pub fn expand_captures(template: &str, captures: &[String]) -> String {
let mut result = template.to_string();
for (i, cap) in captures.iter().enumerate() {
let placeholder = format!("${i}");
result = result.replace(&placeholder, cap);
}
result
}
pub fn extract_matches(captures: &expectrl::Captures) -> Vec<String> {
let mut result = Vec::new();
for m in captures.matches() {
result.push(String::from_utf8_lossy(m).into_owned());
}
result
}
pub fn session_from_fd(fd: OwnedFd) -> io::Result<expectrl::Session<StubProcess, PtyStream>> {
let stream = PtyStream::new(fd);
expectrl::Session::new(StubProcess, stream)
}