//! 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, 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, bool)>>, stderr: Option, 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 { 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, 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, bool)>>, until: Instant) -> (Vec, 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, bool) { let mut from = from; let mut kept: Vec = 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) }