129 lines
4.9 KiB
Rust
129 lines
4.9 KiB
Rust
//! The three pipes of one container: its arguments go in on standard input, its output and errors
|
|
//! come back, each on its own thread so no pipe can block another. The threads start with
|
|
//! `Builder`, which returns an error instead of panicking, and the output is collected within a
|
|
//! grace period so a pipe held open by something outside the container cannot hold `brokerd` (M3b
|
|
//! review findings 3 and 4).
|
|
|
|
use std::io::{Read, Write};
|
|
use std::process::Child;
|
|
use std::sync::mpsc::{self, Receiver, RecvTimeoutError};
|
|
use std::thread::Builder;
|
|
use std::time::{Duration, Instant};
|
|
|
|
/// How long, after the container has ended, its output may take to reach its end.
|
|
pub const GRACE: Duration = Duration::from_secs(2);
|
|
|
|
/// What came back.
|
|
pub(crate) struct Finished {
|
|
pub out: Vec<u8>,
|
|
pub truncated: bool,
|
|
pub err: String,
|
|
/// A pipe had not ended by the grace deadline: something outside the container holds it.
|
|
pub open: bool,
|
|
}
|
|
|
|
pub(crate) struct Io {
|
|
stdout: Option<Receiver<(Vec<u8>, bool)>>,
|
|
stderr: Option<Receiver<(Vec<u8>, bool)>>,
|
|
}
|
|
|
|
impl Io {
|
|
/// Take `child`'s three pipes (`child.stdin.take()` and so on) and start one thread for each:
|
|
/// write `input` to standard input and then drop it (its handle is not kept, it is never
|
|
/// joined); `read_capped` standard output with `cap`; `read_capped` standard error with
|
|
/// `err_cap`. The reader threads are detached, keeping only the channel receiver. A pipe that
|
|
/// is `None` gets no thread; a thread that cannot be started is the returned error.
|
|
pub(crate) fn start(
|
|
child: &mut Child,
|
|
input: String,
|
|
cap: usize,
|
|
err_cap: usize,
|
|
) -> std::io::Result<Io> {
|
|
if let Some(mut stdin) = child.stdin.take() {
|
|
Builder::new().spawn(move || {
|
|
let _ = stdin.write_all(input.as_bytes());
|
|
})?;
|
|
}
|
|
let stdout = if let Some(stdout) = child.stdout.take() {
|
|
Some(reader(stdout, cap)?)
|
|
} else {
|
|
None
|
|
};
|
|
let stderr = if let Some(stderr) = child.stderr.take() {
|
|
Some(reader(stderr, err_cap)?)
|
|
} else {
|
|
None
|
|
};
|
|
Ok(Io { stdout, stderr })
|
|
}
|
|
|
|
/// Collect the output within `grace`: one deadline for both readers, after which a pipe that has
|
|
/// not ended counts as open (something outside the container holds it) and is abandoned.
|
|
pub(crate) fn finish(self, grace: Duration) -> Finished {
|
|
let until = Instant::now() + grace;
|
|
let (out, truncated, stdout_open) = collect(self.stdout, until);
|
|
let (err_bytes, _, stderr_open) = collect(self.stderr, until);
|
|
let err = String::from_utf8_lossy(&err_bytes).into_owned();
|
|
Finished {
|
|
out,
|
|
truncated,
|
|
err,
|
|
open: stdout_open || stderr_open,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One detached reader: `read_capped` `pipe` and hand the result back on a channel. The handle is
|
|
/// kept only long enough to start the thread; a spawn error is returned, a send error ignored.
|
|
fn reader(
|
|
pipe: impl Read + Send + 'static,
|
|
cap: usize,
|
|
) -> std::io::Result<Receiver<(Vec<u8>, bool)>> {
|
|
let (tx, rx) = mpsc::channel();
|
|
Builder::new().spawn(move || {
|
|
let _ = tx.send(read_capped(pipe, cap));
|
|
})?;
|
|
Ok(rx)
|
|
}
|
|
|
|
/// Read from `rx` until `until`, classifying the outcome: `None` (no pipe) is empty and closed, a
|
|
/// timeout is empty and open, and a disconnected channel (the reader panicked) is empty and closed.
|
|
fn collect(rx: Option<Receiver<(Vec<u8>, bool)>>, until: Instant) -> (Vec<u8>, bool, bool) {
|
|
match rx {
|
|
None => (Vec::new(), false, false),
|
|
Some(rx) => match rx.recv_timeout(until.saturating_duration_since(Instant::now())) {
|
|
Ok((bytes, truncated)) => (bytes, truncated, false),
|
|
Err(RecvTimeoutError::Timeout) => (Vec::new(), false, true),
|
|
Err(RecvTimeoutError::Disconnected) => (Vec::new(), false, false),
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Everything `from` gives, keeping the first `cap` bytes; `true` if there was more. Reads on past
|
|
/// the cap, so the writer is never blocked or broken by a closed pipe. No indexing and no
|
|
/// subtraction that could wrap.
|
|
pub(crate) fn read_capped(from: impl Read, cap: usize) -> (Vec<u8>, bool) {
|
|
let mut from = from;
|
|
let mut kept: Vec<u8> = Vec::new();
|
|
let mut truncated = false;
|
|
let mut chunk = [0u8; 8192];
|
|
loop {
|
|
let n = match from.read(&mut chunk) {
|
|
Ok(0) => break,
|
|
Ok(n) => n,
|
|
Err(_) => break,
|
|
};
|
|
let remaining = cap.saturating_sub(kept.len());
|
|
if remaining == 0 {
|
|
truncated = true;
|
|
} else {
|
|
let take = n.min(remaining);
|
|
kept.extend_from_slice(chunk.get(..take).unwrap_or_default());
|
|
if take < n {
|
|
truncated = true;
|
|
}
|
|
}
|
|
}
|
|
(kept, truncated)
|
|
}
|