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;