Skip to main content

conmonrs/
container_io.rs

1//! The stdio of the containers and execs, pipes or a terminal, which the
2//! server reads into the log drivers, the attach clients and the output of the
3//! execs.
4
5use crate::{attach::SharedContainerAttach, container_log::SharedContainerLog, terminal::Terminal};
6use anyhow::{Context, Result, bail};
7use bytes::Bytes;
8use std::{
9    fmt,
10    sync::{Arc, Mutex, atomic::AtomicBool},
11};
12
13mod drain;
14mod read_loop;
15mod stdio;
16
17#[cfg(test)]
18pub(crate) use drain::DRAIN_TIMEOUT;
19pub(crate) use drain::{DRAIN_MAX_LOG_WAIT, DRAIN_MAX_WAIT, Drain, Lag};
20pub(crate) use stdio::{EarlyStdio, StdioSender};
21use stdio::{StdioChannel, StdioCollector};
22
23/// How many read chunks the stdio and terminal channels buffer per pipe.
24pub(crate) const STDIO_CHANNEL_SIZE: usize = 100;
25
26/// The most a read of a container I/O stream takes at once. The read loops
27/// read into the scratch buffer of their thread and copy each chunk out of
28/// it, see [`with_scratch`](crate::util::with_scratch), so an idle container
29/// holds no read buffer. 8 KiB halves the reads and log writes of 4 KiB for
30/// bulk output, the log files start the buffer of each chunk with as much,
31/// see `LogFile::BUF_CAPACITY`.
32pub(crate) const READ_BUFFER_SIZE: usize = 8 * 1024;
33
34/// How much of the end of a stderr gets kept for the error of a runtime
35/// which failed, with pipes of its own or on the stderr of the process. The
36/// last lines hold the reason, a runtime with debug logging writes far more.
37pub(crate) const STDERR_TAIL_SIZE: usize = 64 * 1024;
38
39/// A container IO shared by its users, like the watcher, the attach
40/// endpoints and the consumer of the output of an exec.
41pub(crate) type SharedContainerIO = Arc<ContainerIO>;
42
43/// The stdio of a container or exec, a terminal or pipes, and the log
44/// drivers and attach endpoints its output goes to.
45#[derive(Debug)]
46pub(crate) struct ContainerIO {
47    /// None for a process with pipes.
48    terminal: Option<Terminal>,
49    logger: SharedContainerLog,
50    attach: SharedContainerAttach,
51
52    /// The stdio channels, see [`StdioSender`]. A terminal merges stderr
53    /// into stdout, its stderr channel ends right away.
54    stdout: StdioChannel,
55    stderr: StdioChannel,
56
57    /// Whether a consumer reads the stdio channels, see [`StdioSender`].
58    consumed: Arc<AtomicBool>,
59
60    /// Collects the output of an exec while its runtime runs, see
61    /// [`ContainerIO::collect_stdio`]. Taken by the consumer of the output,
62    /// which shares the IO with others already.
63    collector: Mutex<Option<StdioCollector>>,
64}
65
66impl ContainerIO {
67    /// The terminal of the process, none with pipes.
68    pub(crate) fn terminal(&self) -> Option<&Terminal> {
69        self.terminal.as_ref()
70    }
71    /// The log drivers which get the output.
72    pub(crate) fn logger(&self) -> &SharedContainerLog {
73        &self.logger
74    }
75    /// The attach endpoints of the process.
76    pub(crate) fn attach(&self) -> &SharedContainerAttach {
77        &self.attach
78    }
79
80    /// Resize the terminal. Errors in case of no terminal containers.
81    pub(crate) fn resize(&self, width: u16, height: u16) -> Result<()> {
82        if !self.try_resize(width, height)? {
83            bail!("container has no terminal")
84        }
85        Ok(())
86    }
87
88    /// Resize the terminal if the container has one and return whether it
89    /// does. One which the runtime did not connect yet gets the size once
90    /// it does, see [`TerminalResizer::resize`](crate::terminal::TerminalResizer::resize).
91    pub(crate) fn try_resize(&self, width: u16, height: u16) -> Result<bool> {
92        let Some(terminal) = &self.terminal else {
93            return Ok(false);
94        };
95        terminal.resize(width, height).context("resize terminal")?;
96        Ok(true)
97    }
98
99    /// The end of file character of the terminal, none without one.
100    pub(crate) fn terminal_eof(&self) -> Option<u8> {
101        self.terminal.as_ref().map(Terminal::eof_char)
102    }
103}
104
105/// A message to be sent through the ContainerIO.
106#[derive(Clone, Debug, PartialEq, Eq)]
107pub(crate) enum Message {
108    Data(Bytes, Pipe),
109    Done,
110}
111
112/// Available pipe types.
113#[derive(Clone, Copy, Debug, PartialEq, Eq)]
114pub(crate) enum Pipe {
115    /// Standard output.
116    StdOut,
117
118    /// Standard error.
119    StdErr,
120}
121
122impl Pipe {
123    /// The name of the pipe as used in the logs.
124    pub(crate) fn as_str(self) -> &'static str {
125        match self {
126            Self::StdOut => "stdout",
127            Self::StdErr => "stderr",
128        }
129    }
130
131    /// The first byte of a packet to an attach client which carries output
132    /// of the pipe, see the attach socket section of docs/protocols.md.
133    pub(crate) const fn attach_packet_byte(self) -> u8 {
134        match self {
135            Self::StdOut => 2,
136            Self::StdErr => 3,
137        }
138    }
139
140    /// The channel of the pipe in the remote command protocols of the
141    /// streaming server, the first byte of a WebSocket message.
142    pub(crate) const fn remotecommand_channel(self) -> u8 {
143        match self {
144            Self::StdOut => 1,
145            Self::StdErr => 2,
146        }
147    }
148}
149
150impl fmt::Display for Pipe {
151    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
152        f.write_str(self.as_str())
153    }
154}
155
156impl ContainerIO {
157    /// The most output of a pipe an exec collects while its runtime runs,
158    /// see [`ContainerIO::collect_stdio`], and the most an `exec_sync`
159    /// reads of each pipe.
160    pub(crate) const MAX_STDIO_STREAM_SIZE: usize = 16 * 1024 * 1024;
161
162    /// Create a new container IO instance.
163    pub(crate) fn new(terminal: bool, logger: SharedContainerLog) -> Result<Self> {
164        let terminal = terminal
165            .then(Terminal::new)
166            .transpose()
167            .context("create new terminal")?;
168        let consumed = Arc::default();
169        let stdout = StdioChannel::new(STDIO_CHANNEL_SIZE, &consumed);
170        // With a terminal, stderr carries nothing: no read loop takes its
171        // sender, so it goes right away and the consumers see the end. A
172        // channel which never got a message holds no buffer, whatever its
173        // capacity. An exec keeps the receiver as long as the IO, only its
174        // consumer closes it.
175        let mut stderr = StdioChannel::new(STDIO_CHANNEL_SIZE, &consumed);
176        if terminal.is_some() {
177            stderr.tx = None;
178        }
179        Ok(Self {
180            attach: SharedContainerAttach::new(terminal.is_some()),
181            terminal,
182            logger,
183            stdout,
184            stderr,
185            consumed,
186            collector: Mutex::default(),
187        })
188    }
189}
190
191#[cfg(test)]
192mod tests;