Skip to main content

conmonrs/container_io/
stdio.rs

1//! The stdio channels and the collector of the output of an exec.
2
3use super::{ContainerIO, Drain, Message, STDERR_TAIL_SIZE};
4use crate::{
5    channel::{Receiver, RecvError, Sender},
6    mutex::lock,
7    tail::Tail,
8    util::with_stderr,
9};
10use anyhow::{Result, bail};
11use std::sync::{
12    Arc,
13    atomic::{AtomicBool, Ordering},
14};
15use tokio::{
16    select,
17    task::{self, JoinHandle},
18};
19use tokio_util::sync::CancellationToken;
20use tracing::{debug, error};
21
22/// The sending half of a stdio channel, which a read loop forwards the
23/// output of its pipe to.
24///
25/// Until a consumer attached, see [`ContainerIO::consume_stdio`], the channel
26/// keeps the latest chunks only and drops the oldest one when it is full. The
27/// output of a container must never wait for a consumer which may not exist,
28/// only the stderr of a failed runtime is of interest. Once one attached, a
29/// full channel makes the read loop wait, so that a slow consumer gets all
30/// output and the process waits for it instead. An exec gets its consumer
31/// before its runtime starts, see [`ContainerIO::collect_stdio`]. The
32/// consumer closes the channel when it is done, the read loop drops the
33/// sender then and keeps reading the pipe.
34#[derive(Debug)]
35pub(crate) struct StdioSender {
36    pub(super) tx: Sender<Message>,
37    consumed: Arc<AtomicBool>,
38}
39
40impl StdioSender {
41    /// A sender on `tx`, whose consumer sets `consumed` once it attached.
42    pub(crate) fn new(tx: Sender<Message>, consumed: &Arc<AtomicBool>) -> Self {
43        Self {
44            tx,
45            consumed: consumed.clone(),
46        }
47    }
48
49    /// Whether a consumer reads the channel, see
50    /// [`ContainerIO::consume_stdio`].
51    pub(super) fn consumed(&self) -> bool {
52        self.consumed.load(Ordering::Acquire)
53    }
54
55    /// Send a message, false if the channel got closed or all receivers are
56    /// gone.
57    pub(super) async fn send(&self, message: Message) -> bool {
58        if self.consumed() {
59            self.tx.send(message).await.is_ok()
60        } else {
61            self.tx.force_send(message).is_ok()
62        }
63    }
64}
65
66/// Closes a stdio channel on drop, also if the consumer future gets dropped.
67struct CloseOnDrop<'a>(&'a Receiver<Message>);
68
69impl Drop for CloseOnDrop<'_> {
70    fn drop(&mut self) {
71        self.0.close();
72    }
73}
74
75/// A stdio channel of a pipe, see [`StdioSender`].
76#[derive(Debug)]
77pub(super) struct StdioChannel {
78    /// Until the channel got closed, consumers read clones of it.
79    rx: Option<Receiver<Message>>,
80
81    /// Until the read loop of the pipe took it, once the process got
82    /// connected.
83    pub(super) tx: Option<StdioSender>,
84}
85
86impl StdioChannel {
87    pub(super) fn new(capacity: usize, consumed: &Arc<AtomicBool>) -> Self {
88        let (tx, rx) = crate::channel::bounded(capacity);
89        Self {
90            rx: Some(rx),
91            tx: Some(StdioSender::new(tx, consumed)),
92        }
93    }
94
95    /// Close the channel and drop what it buffers right away, a quiet
96    /// container would keep the buffered chunks otherwise. The read loop
97    /// drops its sender once it notices, which frees the channel.
98    fn close(&mut self) {
99        self.tx = None;
100        if let Some(rx) = self.rx.take() {
101            rx.close();
102            while rx.try_recv().is_ok() {}
103        }
104    }
105}
106
107/// The output which an exec wrote while its runtime ran, per pipe.
108#[derive(Debug, Default)]
109pub(crate) struct EarlyStdio {
110    pub(crate) stdout: EarlyStream,
111    pub(crate) stderr: EarlyStream,
112}
113
114/// The output of a pipe up to [`ContainerIO::MAX_STDIO_STREAM_SIZE`].
115#[derive(Debug, Default)]
116pub(crate) struct EarlyStream {
117    pub(crate) data: Vec<u8>,
118
119    /// Whether the output ended.
120    pub(crate) done: bool,
121
122    /// Whether output got dropped because of the size limit. Nothing may be
123    /// appended then, it would leave a gap.
124    pub(crate) truncated: bool,
125
126    /// The end of the output which got dropped, where the error of a runtime
127    /// which failed is, up to [`STDERR_TAIL_SIZE`].
128    pub(super) tail: Tail<STDERR_TAIL_SIZE>,
129}
130
131impl EarlyStream {
132    pub(super) fn push(&mut self, data: &[u8]) {
133        let len = self.data.len() + data.len();
134        if self.truncated || len >= ContainerIO::MAX_STDIO_STREAM_SIZE {
135            self.truncated = true;
136            self.tail.push(data);
137            return;
138        }
139        // Grow like a vector would, but not beyond the limit.
140        if len > self.data.capacity() {
141            let capacity = len
142                .max(2 * self.data.capacity())
143                .min(ContainerIO::MAX_STDIO_STREAM_SIZE);
144            self.data.reserve_exact(capacity - self.data.len());
145        }
146        self.data.extend_from_slice(data);
147    }
148
149    /// The last [`STDERR_TAIL_SIZE`] bytes of the output, also of the
150    /// part which got dropped. The dropped part continues the kept data, a
151    /// short one gets completed by the end of the data.
152    pub(super) fn end(&self) -> Vec<u8> {
153        let tail = if self.truncated { self.tail.end() } else { &[] };
154        let from_data = STDERR_TAIL_SIZE.saturating_sub(tail.len());
155        let data = &self.data[self.data.len().saturating_sub(from_data)..];
156        [data, tail].concat()
157    }
158}
159
160#[derive(Debug)]
161pub(super) struct StdioCollector {
162    stop: CancellationToken,
163    task: JoinHandle<EarlyStdio>,
164}
165
166impl Drop for StdioCollector {
167    /// The collector must not outlive its exec, which may have failed before
168    /// the output got taken.
169    fn drop(&mut self) {
170        self.stop.cancel();
171        self.task.abort();
172    }
173}
174
175impl ContainerIO {
176    /// Retrieve clones of the stdout and stderr channels.
177    ///
178    /// Fails if the channels have been dropped via [`ContainerIO::drop_stdio`].
179    pub(crate) fn stdio(&self) -> Result<(Receiver<Message>, Receiver<Message>)> {
180        let (stdout, stderr) = self.receivers()?;
181        Ok((stdout.clone(), stderr.clone()))
182    }
183
184    /// The stdout and stderr receivers, see [`ContainerIO::stdio`].
185    fn receivers(&self) -> Result<(&Receiver<Message>, &Receiver<Message>)> {
186        match (&self.stdout.rx, &self.stderr.rx) {
187            (Some(stdout), Some(stderr)) => Ok((stdout, stderr)),
188            _ => bail!("stdio channels have been dropped"),
189        }
190    }
191
192    /// Drop the stdout and stderr channels.
193    ///
194    /// The channels buffer the last chunks per pipe for consumers like
195    /// `exec_sync`, which may start reading after the process already produced
196    /// output, and for reporting the runtime's stderr if a create fails. Once
197    /// a container is created nothing consumes them anymore, attach sessions
198    /// use the attach broadcast, so they get dropped, see
199    /// [`StdioChannel::close`].
200    pub(crate) fn drop_stdio(&mut self) {
201        self.stdout.close();
202        self.stderr.close();
203    }
204
205    /// Read the output of an exec from now on, until it gets taken with
206    /// [`ContainerIO::take_early_stdio`] once its runtime finished. The
207    /// process may write a lot before, which the channels alone would drop,
208    /// see [`StdioSender`]. A runtime without a terminal shares the pipes,
209    /// so the reads must not wait for anything: they apply backpressure,
210    /// but take everything up to [`ContainerIO::MAX_STDIO_STREAM_SIZE`] per
211    /// pipe and drop what follows.
212    ///
213    /// The output of a pipe which is not of interest gets read and dropped.
214    pub(crate) fn collect_stdio(&self, stdout_enabled: bool, stderr_enabled: bool) -> Result<()> {
215        let (stdout, stderr) = self.stdio()?;
216        self.consume_stdio();
217        let stop = CancellationToken::new();
218        let collect = {
219            let stop = stop.clone();
220            async move {
221                let (stdout, stderr) = tokio::join!(
222                    Self::collect(&stdout, &stop, stdout_enabled),
223                    Self::collect(&stderr, &stop, stderr_enabled),
224                );
225                EarlyStdio { stdout, stderr }
226            }
227        };
228        *lock(&self.collector) = Some(StdioCollector {
229            stop,
230            task: task::spawn(collect),
231        });
232        Ok(())
233    }
234
235    /// Collect the output of a channel until `stop`, the remaining messages
236    /// stay queued for the consumer which comes next. Beyond the size limit
237    /// the output gets read and dropped.
238    pub(super) async fn collect(
239        receiver: &Receiver<Message>,
240        stop: &CancellationToken,
241        enabled: bool,
242    ) -> EarlyStream {
243        let mut stream = EarlyStream::default();
244        loop {
245            let message = select! {
246                biased;
247                () = stop.cancelled() => return stream,
248                message = receiver.recv() => message,
249            };
250            match message {
251                Ok(Message::Data(data, _)) if enabled => stream.push(&data),
252                Ok(Message::Data(..)) => {}
253                Ok(Message::Done) => {
254                    stream.done = true;
255                    return stream;
256                }
257                Err(RecvError) => return stream,
258            }
259        }
260    }
261
262    /// The output collected since [`ContainerIO::collect_stdio`], empty
263    /// without that.
264    pub(crate) async fn take_early_stdio(&self) -> EarlyStdio {
265        let Some(mut collector) = lock(&self.collector).take() else {
266            return EarlyStdio::default();
267        };
268        collector.stop.cancel();
269        (&mut collector.task).await.unwrap_or_else(|e| {
270            error!("Unable to collect the exec output: {e}");
271            EarlyStdio::default()
272        })
273    }
274
275    /// Apply backpressure to the read loops from now on, see
276    /// [`StdioSender`].
277    fn consume_stdio(&self) {
278        self.consumed.store(true, Ordering::Release);
279    }
280
281    /// Attach the stderr output of a runtime which failed to its error. With
282    /// a terminal, the error carries it already. The read loops get stopped
283    /// afterwards, a process which the runtime left behind, like a daemon of
284    /// the process it started, may keep the pipes open, see
285    /// [`SharedContainerAttach::stop_output`].
286    ///
287    /// [`SharedContainerAttach::stop_output`]: crate::attach::SharedContainerAttach::stop_output
288    pub(crate) async fn runtime_error(&self, e: anyhow::Error) -> anyhow::Error {
289        let stderr = match self.terminal {
290            None => {
291                // The runtime exited, only its remaining output is of
292                // interest. Its error is at the end, also of output which
293                // exceeded the size limit.
294                self.consume_stdio();
295                let early = self.take_early_stdio().await;
296                if early.stderr.truncated {
297                    early.stderr.end()
298                } else {
299                    let exited = CancellationToken::new();
300                    exited.cancel();
301                    match self.read_all_with_early(&exited, early).await {
302                        // Only the end, like of truncated output, the error
303                        // must not carry up to the size limit.
304                        Ok((_, mut stderr)) => {
305                            stderr.drain(..stderr.len().saturating_sub(STDERR_TAIL_SIZE));
306                            stderr
307                        }
308                        Err(read_err) => {
309                            debug!("Unable to read runtime output: {read_err:#}");
310                            vec![]
311                        }
312                    }
313                }
314            }
315            Some(_) => vec![],
316        };
317        self.attach.stop_output();
318        with_stderr(e, &stderr)
319    }
320
321    /// Read the output until it ended. Once `exited` got cancelled, the
322    /// output still in flight is taken as long as the [`Drain`] does. A
323    /// background process which holds the pipes open must not keep this
324    /// waiting. The channels get closed afterwards, so that the read loops
325    /// discard what follows. There is no deadline of its own: the watcher of
326    /// an exec cancels `exited` also when it kills the process at its
327    /// timeout, and a deadline which raced that one would end the output of
328    /// a process which exited right at it.
329    pub(crate) async fn read_all(&self, exited: &CancellationToken) -> Result<(Vec<u8>, Vec<u8>)> {
330        self.consume_stdio();
331        let early = self.take_early_stdio().await;
332        self.read_all_with_early(exited, early).await
333    }
334
335    /// Read the output after the one collected before, see
336    /// [`ContainerIO::read_all`].
337    async fn read_all_with_early(
338        &self,
339        exited: &CancellationToken,
340        early: EarlyStdio,
341    ) -> Result<(Vec<u8>, Vec<u8>)> {
342        let (stdout, stderr) = self.receivers()?;
343        Ok(tokio::join!(
344            Self::read_stream(exited, stdout, early.stdout),
345            Self::read_stream(exited, stderr, early.stderr),
346        ))
347    }
348
349    /// Read a stream, after the `early` output collected before.
350    pub(super) async fn read_stream(
351        exited: &CancellationToken,
352        receiver: &Receiver<Message>,
353        early: EarlyStream,
354    ) -> Vec<u8> {
355        // Also after the size limit, a process which keeps writing must not
356        // wait for this consumer.
357        let _close = CloseOnDrop(receiver);
358
359        // The collector kept below the limit. Without it, start with 4 KiB,
360        // which handles most exec_sync outputs, doubling handles growth.
361        let mut stdio = early.data;
362        if early.done || early.truncated {
363            return stdio;
364        }
365        stdio.reserve(4096);
366        let mut drain = None;
367        loop {
368            let res = select! {
369                // Once the process exited, the drain bounds the reading,
370                // output after it gave up is dropped.
371                biased;
372                () = exited.cancelled(), if drain.is_none() => {
373                    drain = Some(Drain::default());
374                    continue;
375                }
376                res = Self::next_message(receiver, drain.as_mut()) => res,
377            };
378            match res {
379                Some(Message::Data(data, _)) => match stdio.len().checked_add(data.len()) {
380                    Some(len) if len < Self::MAX_STDIO_STREAM_SIZE => {
381                        stdio.extend_from_slice(&data);
382                    }
383                    _ => break,
384                },
385                Some(Message::Done) | None => break,
386            }
387        }
388        stdio
389    }
390
391    /// The next message, bounded by the drain once the process exited. None
392    /// once the output ended or the drain gave up, a closed channel ends the
393    /// output as well.
394    async fn next_message(
395        receiver: &Receiver<Message>,
396        drain: Option<&mut Drain>,
397    ) -> Option<Message> {
398        match drain {
399            Some(drain) => drain.recv_stdio(receiver).await,
400            None => receiver.recv().await.ok(),
401        }
402    }
403}