Skip to main content

conmonrs/container_io/
read_loop.rs

1//! The read loops of the output and the stdin of the process.
2
3use super::{ContainerIO, Message, Pipe, READ_BUFFER_SIZE, StdioSender};
4use crate::{
5    attach::{AttachOutput, SharedContainerAttach},
6    container_log::SharedContainerLog,
7    util::{sleep_until, with_scratch},
8};
9use anyhow::{Context, Result};
10use bytes::Bytes;
11use nix::errno::Errno;
12use std::{
13    future,
14    io::{self, ErrorKind},
15    pin::Pin,
16    sync::Arc,
17    task::{Poll, ready},
18};
19use tokio::{
20    io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadBuf},
21    process::{ChildStderr, ChildStdin, ChildStdout},
22    select, task,
23    time::{Duration, Instant},
24};
25use tokio_util::sync::CancellationToken;
26use tracing::{Instrument, debug, debug_span, error, info, trace};
27
28/// Logs a failing log driver once until it works again. A full disk would
29/// otherwise log an error for every chunk of output.
30#[derive(Debug, Default)]
31pub(super) struct LogErrors {
32    failing: bool,
33}
34
35impl LogErrors {
36    fn check(&mut self, pipe: Pipe, res: Result<()>) {
37        match res {
38            Ok(()) if self.failing => {
39                self.failing = false;
40                info!("Writing the log of {pipe} works again");
41            }
42            Ok(()) => {}
43            Err(e) if !self.failing => {
44                self.failing = true;
45                error!("Unable to write the log of {pipe}, dropping output until it works: {e:#}");
46            }
47            Err(e) => trace!("Unable to write the log of {pipe}: {e:#}"),
48        }
49    }
50}
51
52impl ContainerIO {
53    /// Read the pipes of the runtime, which the process it starts shares.
54    pub(crate) fn connect_streams(
55        &mut self,
56        stdin: Option<ChildStdin>,
57        stdout: Option<ChildStdout>,
58        stderr: Option<ChildStderr>,
59        token: CancellationToken,
60    ) {
61        debug!("Start reading from IO streams");
62        // Shared by both read loops, the output ends once the last one ended.
63        let output = self.attach.output();
64        self.spawn_stdin_loop(stdin, token);
65        if let Some(stdout) = stdout {
66            self.spawn_read_loop(stdout, Pipe::StdOut, output.clone());
67        }
68        if let Some(stderr) = stderr {
69            self.spawn_read_loop(stderr, Pipe::StdErr, output);
70        }
71    }
72
73    /// Wait until the runtime connected the terminal and read it. A wait
74    /// which gets dropped leaves the connection for the next one.
75    pub(crate) async fn connect_terminal(
76        &mut self,
77        stdin: bool,
78        token: CancellationToken,
79    ) -> Result<()> {
80        let terminal = self
81            .terminal
82            .as_mut()
83            .context("container has no terminal")?;
84        let tty = match terminal.connect().await {
85            Ok(tty) => tty,
86            Err(e) => {
87                // Nothing will ever read the stdin, so attach clients must
88                // not wait for it.
89                self.attach.close_stdin();
90                return Err(e);
91            }
92        };
93        // A terminal reports EIO once no process has it open, which ends its
94        // output. The server keeps it open itself until the exit, so that a
95        // process may close and open it again, like an init system or a getty
96        // does. Its output ends once the server drops it at the exit.
97        match tty.open_peer() {
98            Ok(peer) => {
99                let exited = token.clone();
100                task::spawn(async move {
101                    exited.cancelled().await;
102                    drop(peer);
103                });
104            }
105            Err(e) => debug!("Unable to keep the terminal open until the exit: {e}"),
106        }
107        let output = self.attach.output();
108        self.spawn_read_loop(tty.clone(), Pipe::StdOut, output);
109        self.spawn_stdin_loop(stdin.then_some(tty), token);
110        Ok(())
111    }
112
113    /// Spawn the [`ContainerIO::read_loop`] of a pipe, which takes the
114    /// sender of its stdio channel.
115    pub(super) fn spawn_read_loop<T>(&mut self, reader: T, pipe: Pipe, output: Arc<AttachOutput>)
116    where
117        T: AsyncRead + Unpin + Send + 'static,
118    {
119        let channel = match pipe {
120            Pipe::StdOut => &mut self.stdout,
121            Pipe::StdErr => &mut self.stderr,
122        };
123        let read_loop = Self::read_loop(
124            reader,
125            pipe,
126            self.logger.clone(),
127            channel.tx.take(),
128            self.attach.clone(),
129            output,
130        );
131        task::spawn(
132            async move {
133                if let Err(e) = read_loop.await {
134                    error!("Read loop of {pipe} failed: {e:#}");
135                }
136            }
137            .instrument(debug_span!("read_loop", %pipe))
138            .in_current_span(),
139        );
140    }
141
142    /// Spawn the [`ContainerIO::read_loop_stdin`] of the process. Without a
143    /// stdin, attach clients must not wait for one which nothing ever reads.
144    fn spawn_stdin_loop<T>(&self, writer: Option<T>, token: CancellationToken)
145    where
146        T: AsyncWrite + Unpin + Send + 'static,
147    {
148        let Some(writer) = writer else {
149            self.attach.close_stdin();
150            return;
151        };
152        let attach = self.attach.clone();
153        task::spawn(
154            async move {
155                if let Err(e) = Self::read_loop_stdin(writer, attach, token).await {
156                    error!("Stdin read loop failure: {e:#}");
157                }
158            }
159            .instrument(debug_span!("read_loop_stdin"))
160            .in_current_span(),
161        );
162    }
163
164    /// How long a line without its ending waits before it is written as a
165    /// partial one, so that prompts and progress output show up in the logs.
166    pub(super) const IDLE_FLUSH: Duration = Duration::from_millis(500);
167
168    /// Read the pipe until it ends and let the log drivers write what they
169    /// still buffer, however the loop ended.
170    pub(crate) async fn read_loop<T>(
171        reader: T,
172        pipe: Pipe,
173        logger: SharedContainerLog,
174        message_tx: Option<StdioSender>,
175        attach: SharedContainerAttach,
176        attach_output: Arc<AttachOutput>,
177    ) -> Result<()>
178    where
179        T: AsyncRead + Unpin,
180    {
181        let log_has_drivers = logger.lock().await.has_drivers();
182        let res =
183            Self::read_pipe(reader, pipe, &logger, log_has_drivers, message_tx, &attach).await;
184        if log_has_drivers {
185            // The line which waits for its ending is on its way into the log
186            // like a chunk, see `SharedContainerAttach::logging`.
187            let _logging = attach.logging();
188            if let Err(e) = logger.lock().await.finish(pipe).await {
189                error!("Unable to finish the log of {pipe}: {e:#}");
190            }
191        }
192        // The output ends for the attach endpoints and the exit files only
193        // once the last line is in the log.
194        drop(attach_output);
195        res
196    }
197
198    async fn read_pipe<T>(
199        mut reader: T,
200        pipe: Pipe,
201        logger: &SharedContainerLog,
202        log_has_drivers: bool,
203        mut message_tx: Option<StdioSender>,
204        attach: &SharedContainerAttach,
205    ) -> Result<()>
206    where
207        T: AsyncRead + Unpin,
208    {
209        let mut log_errors = LogErrors::default();
210        let stopped = attach.output_stopped().clone();
211
212        // When the line which waits for its ending in the log gets written as
213        // a partial one, known from the last write, so that the logger is not
214        // locked for it on every read. Set once the line started, a line
215        // which trickles in must not wait for a pause of the output.
216        let mut flush_at = None;
217
218        loop {
219            // Once stopped, nothing gets read anymore, also if more is there.
220            let read = select! {
221                biased;
222                () = stopped.cancelled() => {
223                    debug!("Stopping read loop, nothing takes the output anymore");
224                    Self::forward(&mut message_tx, Message::Done).await;
225                    return Ok(());
226                }
227                read = Self::read_chunk(&mut reader) => read,
228                // The drivers decide whether they buffer lines at all.
229                () = sleep_until(flush_at) => {
230                    let _logging = attach.logging();
231                    let res = logger.lock().await.flush(pipe).await;
232                    log_errors.check(pipe, res.context("flush paused log line"));
233                    flush_at = None;
234                    continue;
235                }
236                // The sender keeps the buffer of a closed channel, see
237                // `ContainerIO::drop_stdio`, a pipe which stays quiet would
238                // never send to notice it. Only watched without a consumer,
239                // the exec which has one ends soon after it closed.
240                () = Self::closed(message_tx.as_ref()),
241                    if message_tx.as_ref().is_some_and(|tx| !tx.consumed()) =>
242                {
243                    trace!("Stdio channel closed, dropping sender");
244                    message_tx = None;
245                    continue;
246                }
247            };
248
249            match read {
250                Ok(data) if data.is_empty() => {
251                    debug!("Nothing more to read");
252
253                    Self::forward(&mut message_tx, Message::Done).await;
254
255                    return Ok(());
256                }
257
258                Ok(data) => {
259                    trace!("Read {} bytes", data.len());
260
261                    // Until it got broadcast below, the output is on its way,
262                    // see `SharedContainerAttach::logging`.
263                    let logging = log_has_drivers.then(|| attach.logging());
264
265                    // Write to logger if drivers are configured
266                    // (exec_sync uses an empty logger, so skip the overhead).
267                    // A failing driver drops the chunk, the output must keep
268                    // flowing to the other drivers and the attach clients.
269                    if log_has_drivers {
270                        let mut locked_logger = logger.lock().await;
271                        let res = locked_logger.write(pipe, &data).await;
272                        let pending = locked_logger.has_pending(pipe);
273                        drop(locked_logger);
274                        log_errors.check(pipe, res.context("write to log file"));
275
276                        // A newline in the chunk ended the line which waited
277                        // before, the one waiting now started in this chunk.
278                        flush_at = match flush_at {
279                            _ if !pending => None,
280                            Some(at) if memchr::memchr(b'\n', &data).is_none() => Some(at),
281                            _ => Some(Instant::now() + Self::IDLE_FLUSH),
282                        };
283                    }
284
285                    // Bytes::clone() is just a refcount increment, no data
286                    // copy. Without clients the send fails cheaply, a check
287                    // before it would miss a client which subscribes in
288                    // between.
289                    attach.write(Message::Data(data.clone(), pipe));
290                    drop(logging);
291
292                    Self::forward(&mut message_tx, Message::Data(data, pipe)).await;
293                }
294
295                // A terminal reports EIO once nothing has it open: at the
296                // exit, or once the process closed it if the server could not
297                // keep it open, see `connect_terminal`.
298                Err(e) if e.raw_os_error() == Some(Errno::EIO as i32) => {
299                    debug!("Stopping read loop");
300                    Self::forward(&mut message_tx, Message::Done).await;
301
302                    return Ok(());
303                }
304                Err(e) => return Err(e).context("read from container file descriptor"),
305            }
306        }
307    }
308
309    /// Read a chunk of up to [`READ_BUFFER_SIZE`] into the scratch buffer of
310    /// the thread, see [`with_scratch`], and copy it out. Empty once the
311    /// output ended.
312    async fn read_chunk<T>(reader: &mut T) -> io::Result<Bytes>
313    where
314        T: AsyncRead + Unpin,
315    {
316        future::poll_fn(|cx| {
317            with_scratch(READ_BUFFER_SIZE, |scratch| {
318                let mut buf = ReadBuf::new(scratch);
319                ready!(Pin::new(&mut *reader).poll_read(cx, &mut buf))?;
320                Poll::Ready(Ok(Bytes::copy_from_slice(buf.filled())))
321            })
322        })
323        .await
324    }
325
326    /// Complete once the stdio channel got closed, never without one.
327    async fn closed(message_tx: Option<&StdioSender>) {
328        match message_tx {
329            Some(tx) => tx.tx.closed().await,
330            None => future::pending().await,
331        }
332    }
333
334    /// Forward a message to the stdio channel, see [`StdioSender`]. If the
335    /// channel got closed or all receivers are gone (see
336    /// [`ContainerIO::drop_stdio`]), the sender is dropped as well so that the
337    /// channel buffer gets freed and no further sends are attempted.
338    async fn forward(message_tx: &mut Option<StdioSender>, message: Message) {
339        if let Some(tx) = message_tx
340            && !tx.send(message).await
341        {
342            trace!("Stdio channel closed, dropping sender");
343            *message_tx = None;
344        }
345    }
346
347    /// Write the stdin of the attach endpoints to `writer` until they closed
348    /// it or the process exited, which cancels `token`.
349    pub(crate) async fn read_loop_stdin(
350        mut writer: impl AsyncWrite + Unpin,
351        attach: SharedContainerAttach,
352        token: CancellationToken,
353    ) -> Result<()> {
354        // The exit of the container cancels the token. Without it the loop would
355        // wait for attach input forever, which a container may never get.
356        // Nothing gets written after the exit either, which could block on
357        // descendants holding the pipe open.
358        let res = select! {
359            res = Self::forward_stdin(&mut writer, &attach) => res,
360            () = token.cancelled() => Ok(()),
361        };
362
363        // Nothing reads the stdin anymore, so let the senders drop their data
364        // instead of waiting for the channel forever.
365        attach.close_stdin();
366        res
367    }
368
369    async fn forward_stdin(
370        mut writer: impl AsyncWrite + Unpin,
371        attach: &SharedContainerAttach,
372    ) -> Result<()> {
373        let stdin_closed = attach.stdin_closed().clone();
374        loop {
375            select! {
376                res = attach.read() => {
377                    let data = res.context("read from stdin attach endpoints")?;
378                    if !Self::handle_stdin_data(&data, &mut writer).await? {
379                        return Ok(());
380                    }
381                }
382                () = stdin_closed.cancelled() => break,
383            }
384        }
385
386        // An attach which closes stdin right after sending data may race with
387        // the cancellation, so write everything that was sent before.
388        attach.close_stdin();
389        while let Ok(data) = attach.try_read() {
390            if !Self::handle_stdin_data(&data, &mut writer).await? {
391                break;
392            }
393        }
394        Ok(())
395    }
396
397    /// Write attach input to the stdin of the process, false once that got
398    /// closed, which ends the stdin like its EOF. A pipe reports EPIPE once
399    /// the process exited or closed its stdin. A terminal reports EIO only
400    /// once nothing has it open, see `connect_terminal`, otherwise its writes
401    /// block once its buffer is full, until the exit ends the stdin, see
402    /// `read_loop_stdin`.
403    async fn handle_stdin_data(data: &[u8], mut writer: impl AsyncWrite + Unpin) -> Result<bool> {
404        debug!("Got {} attach bytes", data.len());
405
406        match writer.write_all(data).await {
407            Ok(()) => Ok(true),
408            Err(e)
409                if e.kind() == ErrorKind::BrokenPipe
410                    || e.raw_os_error() == Some(Errno::EIO as i32) =>
411            {
412                debug!("Stopping stdin, the process closed it: {e}");
413                Ok(false)
414            }
415            Err(e) => Err(e).context("write attach stdin to stream"),
416        }
417    }
418}