use std::io::{self, Read, Write};
use std::process::{Child, Command, Stdio};
use std::sync::mpsc::{sync_channel, Receiver, RecvTimeoutError};
use std::thread::{self, JoinHandle};
use std::time::Duration;
use brazen::{
envelope_request, Bytes, CanonicalError, Envelope, ErrorKind, ExecSpec, TransportResponse,
WireRequest,
};
pub(super) fn send_exec(
spec: &ExecSpec,
wire: &WireRequest,
) -> Result<TransportResponse, CanonicalError> {
match spec.envelope {
Envelope::Body => Ok(TransportResponse {
status: 200,
body: Box::new(spawn(spec, wire, wire.body.clone())?),
retry_after: None,
}),
Envelope::Http => super::relay::respond(spawn(spec, wire, envelope_request(wire))?),
}
}
fn spawn(
spec: &ExecSpec,
wire: &WireRequest,
stdin_bytes: Vec<u8>,
) -> Result<ExecBody, CanonicalError> {
let mut command = Command::new(&spec.program);
command
.args(&spec.args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if spec.envelope == Envelope::Http {
let t = wire.timeouts;
for (name, secs) in [
("BZ_TRANSPORT_CONNECT_TIMEOUT", t.connect),
("BZ_TRANSPORT_RESPONSE_TIMEOUT", t.response),
("BZ_TRANSPORT_IDLE_TIMEOUT", t.idle),
] {
if let Some(secs) = secs {
command.env(name, secs.to_string());
}
}
}
let mut child = command
.spawn()
.map_err(|e| spawn_error(&spec.program, &e))?;
let body = stdin_bytes;
if let Some(mut stdin) = child.stdin.take() {
thread::spawn(move || {
let _ = stdin.write_all(&body);
});
}
let stderr = child.stderr.take();
let stderr_thread = thread::spawn(move || {
let mut buf = String::new();
if let Some(mut pipe) = stderr {
let _ = pipe.read_to_string(&mut buf);
}
buf
});
let stdout = child.stdout.take();
let (tx, rx) = sync_channel::<io::Result<Bytes>>(4);
thread::spawn(move || {
let Some(mut pipe) = stdout else { return };
loop {
let mut buf = vec![0u8; 8192];
match pipe.read(&mut buf) {
Ok(0) => return,
Ok(n) => {
buf.truncate(n);
if tx.send(Ok(buf)).is_err() {
return;
}
}
Err(e) => {
let _ = tx.send(Err(e));
return;
}
}
}
});
Ok(ExecBody {
rx,
child,
stderr_thread: Some(stderr_thread),
idle: wire.timeouts.idle.map(Duration::from_secs),
done: false,
})
}
pub(super) struct ExecBody {
rx: Receiver<io::Result<Bytes>>,
child: Child,
stderr_thread: Option<JoinHandle<String>>,
idle: Option<Duration>,
done: bool,
}
impl ExecBody {
fn reap(&mut self, kill: bool) -> (Option<i32>, String) {
self.done = true;
if kill {
let _ = self.child.kill();
self.stderr_thread.take();
return (self.child.wait().ok().and_then(|s| s.code()), String::new());
}
let code = self.child.wait().ok().and_then(|s| s.code());
let stderr = self
.stderr_thread
.take()
.and_then(|h| h.join().ok())
.unwrap_or_default();
(code, stderr)
}
}
impl Iterator for ExecBody {
type Item = io::Result<Bytes>;
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
let received = match self.idle {
Some(budget) => self.rx.recv_timeout(budget),
None => self.rx.recv().map_err(|_| RecvTimeoutError::Disconnected),
};
match received {
Ok(Ok(bytes)) => Some(Ok(bytes)),
Ok(Err(e)) => {
self.reap(true);
Some(Err(e))
}
Err(RecvTimeoutError::Timeout) => {
let (_, _) = self.reap(true);
Some(Err(io::Error::new(
io::ErrorKind::TimedOut,
"child produced no output within the silence budget; killed",
)))
}
Err(RecvTimeoutError::Disconnected) => {
let (code, stderr) = self.reap(false);
let failed = code != Some(0);
let text = stderr.trim();
if failed && !text.is_empty() {
Some(Err(io::Error::other(format!(
"child exited with status {}: {text}",
code.map_or_else(|| "signal".to_owned(), |c| c.to_string()),
))))
} else {
None
}
}
}
}
}
impl Drop for ExecBody {
fn drop(&mut self) {
if !self.done {
self.reap(true);
}
}
}
fn spawn_error(program: &str, e: &io::Error) -> CanonicalError {
CanonicalError {
kind: ErrorKind::Transport,
message: format!("exec transport: failed to spawn `{program}`: {e}"),
provider_detail: None,
retry_after_seconds: None,
}
}