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}