conmonrs/attach.rs
1//! The attach sockets of the containers, see the attach socket section of
2//! docs/protocols.md: the stdin of the clients towards the container and the
3//! container output towards the clients.
4
5mod socket;
6
7use self::socket::create_listener;
8use crate::{
9 container_io::Message,
10 fs_util::{FileId, RemoveError},
11 mutex::lock,
12};
13use anyhow::{Context, Result};
14use bytes::Bytes;
15use std::{
16 collections::HashMap,
17 path::{Path, PathBuf},
18 pin::pin,
19 sync::{
20 Arc, Mutex, OnceLock,
21 atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering, fence},
22 },
23};
24use tokio::{
25 select,
26 sync::{
27 Notify,
28 broadcast::{self, Receiver, Sender},
29 },
30 task,
31};
32use tokio_util::sync::CancellationToken;
33use tracing::{debug, trace};
34
35/// Capacity of the stdin channel towards the container.
36const STDIN_CHANNEL_SIZE: usize = 16;
37
38/// Capacity of the output broadcast channel towards the attach clients. A
39/// client which falls further behind than this loses the oldest output,
40/// which gets logged with the number of skipped messages, see
41/// [`Lag`](crate::container_io::Lag). The container output never waits for
42/// an attach client, a slow or stuck client must not block the container or
43/// the other clients, and the server does not buffer output without bound
44/// for it either. About what the stdio channels buffer for exec, as a power
45/// of two, which tokio rounds the capacity of a broadcast channel up to
46/// anyway.
47pub(crate) const OUTPUT_CHANNEL_SIZE: usize = 128;
48
49/// A shared container attach abstraction.
50#[derive(Clone, Debug)]
51pub(crate) struct SharedContainerAttach {
52 // A bounded channel instead of a broadcast, so that a slow container stdin
53 // applies backpressure to the attach clients instead of dropping input.
54 read_half_rx: crate::channel::Receiver<Bytes>,
55 read_half_tx: crate::channel::Sender<Bytes>,
56
57 /// The output towards the attach clients.
58 output: Arc<OutputState>,
59
60 /// Cancelled once a client with `stop_after_stdin_eof` closed its stdin,
61 /// which closes the stdin of the container for good. It is separate from
62 /// the token of the container, which only the exit may cancel.
63 stdin_closed: CancellationToken,
64 stop_after_stdin_eof: Arc<AtomicBool>,
65 terminal: bool,
66
67 /// Cancelled once nothing takes the output anymore, see
68 /// [`SharedContainerAttach::stop_output`].
69 output_stopped: CancellationToken,
70
71 /// The listening attach sockets by path.
72 listeners: Arc<Mutex<HashMap<PathBuf, AttachListener>>>,
73}
74
75/// A listening attach socket and the identity of its file, which tells if
76/// the file got removed or replaced meanwhile. The inode stays in use while
77/// the listener is open, so its number cannot be reused until then.
78#[derive(Debug)]
79struct AttachListener {
80 /// Tells the listeners of a path apart.
81 id: u64,
82 file: FileId,
83
84 /// Stops accepting connections, the accepted ones stay.
85 stop: CancellationToken,
86}
87
88impl AttachListener {
89 fn next_id() -> u64 {
90 static NEXT_ID: AtomicU64 = AtomicU64::new(0);
91 NEXT_ID.fetch_add(1, Ordering::Relaxed)
92 }
93
94 /// Whether the socket file at the path is still the one listened on.
95 fn is_bound_at(&self, path: &Path) -> bool {
96 self.file.is_at(path)
97 }
98}
99
100/// The output towards the attach clients, shared with the registered output
101/// of the container, see [`SharedContainerAttach::output`].
102#[derive(Debug, Default)]
103struct OutputState {
104 /// The sender of the broadcast, created for the first attach client, a
105 /// container without any does not need its buffer. It is never removed,
106 /// so the output reads it without a lock.
107 tx: OnceLock<Sender<Message>>,
108
109 /// Set once the output ended, before the done message gets sent.
110 ended: AtomicBool,
111
112 /// Set once an output got registered, see
113 /// [`SharedContainerAttach::output`].
114 registered: AtomicBool,
115
116 /// The read loops with output on its way into the log, see
117 /// [`SharedContainerAttach::logging`].
118 logging: AtomicUsize,
119
120 /// Notified once no read loop has output on its way into the log.
121 logged: Notify,
122}
123
124impl Default for SharedContainerAttach {
125 fn default() -> Self {
126 Self::new(false)
127 }
128}
129
130impl SharedContainerAttach {
131 /// Create a new shared container attach instance for a container with or
132 /// without a terminal.
133 pub(crate) fn new(terminal: bool) -> Self {
134 let (read_half_tx, read_half_rx) = crate::channel::bounded(STDIN_CHANNEL_SIZE);
135 Self {
136 read_half_rx,
137 read_half_tx,
138 output: Arc::default(),
139 stdin_closed: CancellationToken::new(),
140 stop_after_stdin_eof: Arc::default(),
141 terminal,
142 output_stopped: CancellationToken::new(),
143 listeners: Arc::default(),
144 }
145 }
146
147 /// Add a new attach endpoint to this shared container attach instance. The
148 /// `token` of the container stops it on exit.
149 pub(crate) fn add(
150 &self,
151 socket_path: &Path,
152 token: CancellationToken,
153 stop_after_stdin_eof: bool,
154 ) -> Result<()> {
155 // The socket gets created only by the first attach, so its connections
156 // use the latest setting. CRI-O derives it from the container config,
157 // which makes all attaches of a container agree.
158 self.stop_after_stdin_eof
159 .store(stop_after_stdin_eof, Ordering::Release);
160 create_listener(socket_path, self.clone(), token).context("create attach endpoint")
161 }
162
163 /// Read from all attach endpoints standard input and return the first result.
164 pub(crate) async fn read(&self) -> Result<Bytes> {
165 self.read_half_rx
166 .recv()
167 .await
168 .context("receive attach message")
169 }
170
171 /// Try to read from all attach endpoints standard input and return the first result.
172 pub(crate) fn try_read(&self) -> Result<Bytes> {
173 self.read_half_rx
174 .try_recv()
175 .context("try to receive attach message")
176 }
177
178 /// Send data to the container stdin. This waits while the container does
179 /// not keep up, and drops the data once nothing reads the stdin anymore.
180 pub(crate) async fn send_stdin(&self, data: Bytes) {
181 if self.read_half_tx.send(data).await.is_err() {
182 trace!("Dropping attach stdin because the container stdin is closed");
183 }
184 }
185
186 /// Close the stdin channel once nothing reads it anymore, so that senders
187 /// stop waiting for it. Queued data can still be read.
188 pub(crate) fn close_stdin(&self) {
189 self.read_half_rx.close();
190 }
191
192 /// Write a buffer to all attach endpoints. Sending fails only if the last
193 /// client went away meanwhile, which is no reason to stop the output.
194 pub(crate) fn write(&self, m: Message) {
195 let sent = self.output.tx.get().is_some_and(|tx| tx.send(m).is_ok());
196 if !sent {
197 trace!("No attach clients for the output");
198 }
199 }
200
201 /// Register the output of a container, which ends once the returned value
202 /// and all of its clones got dropped. The attach endpoints get the done
203 /// message only then, otherwise the done of one pipe could cut off the
204 /// remaining output of another one.
205 pub(crate) fn output(&self) -> Arc<AttachOutput> {
206 self.output.registered.store(true, Ordering::SeqCst);
207 Arc::new(AttachOutput {
208 output: self.output.clone(),
209 })
210 }
211
212 /// Whether the container has a terminal.
213 pub(crate) fn is_terminal(&self) -> bool {
214 self.terminal
215 }
216
217 /// Stop reading the output once nothing takes it anymore, like once it
218 /// got drained after the exit of the process. A process left behind
219 /// which holds the pipes or the terminal open would keep the read loops
220 /// going forever otherwise, and writing to the log after the exit file
221 /// told that it is complete. Closing them lets its writes fail, like
222 /// once conmon exited.
223 pub(crate) fn stop_output(&self) {
224 self.output_stopped.cancel();
225 }
226
227 /// Stop the output once the consumer of an exec is done with it and the
228 /// process exited, see [`StopOutput`].
229 pub(crate) fn stop_output_on_exit(&self, exited: CancellationToken) -> StopOutput {
230 StopOutput {
231 attach: self.clone(),
232 exited,
233 }
234 }
235
236 /// Whether an output got registered, which read loops did then.
237 pub(crate) fn has_output(&self) -> bool {
238 self.output.registered.load(Ordering::SeqCst)
239 }
240
241 /// The token cancelled by [`SharedContainerAttach::stop_output`].
242 pub(crate) fn output_stopped(&self) -> &CancellationToken {
243 &self.output_stopped
244 }
245
246 /// The token cancelled once an attach client closed the container stdin.
247 pub(crate) fn stdin_closed(&self) -> &CancellationToken {
248 &self.stdin_closed
249 }
250
251 /// Close the container stdin after an attach client reached EOF. Only
252 /// that, the container token also drives the exit and OOM handling. A
253 /// terminal cannot be closed without ending its output, so it stays open
254 /// and this returns false.
255 pub(crate) fn close_container_stdin(&self) -> bool {
256 if !self.terminal {
257 self.stdin_closed.cancel();
258 }
259 !self.terminal
260 }
261
262 /// Forget a listener which stopped on the exit or an error, and remove
263 /// its socket if it is still at the path, unless another listener
264 /// replaced it meanwhile. The caller keeps the listener open, so that
265 /// the inode number of its socket is not reused yet.
266 fn remove_listener(&self, path: &Path, id: u64) {
267 let mut listeners = lock(&self.listeners);
268 let Some(listener) = listeners.remove(path) else {
269 return;
270 };
271 if listener.id != id {
272 listeners.insert(path.to_path_buf(), listener);
273 return;
274 }
275 match listener.file.remove_at(path) {
276 Ok(()) => {}
277 Err(RemoveError::Stat(e)) => {
278 debug!("Unable to stat attach socket {}: {e}", path.display());
279 }
280 Err(RemoveError::Remove(e)) => {
281 debug!("Unable to remove attach socket {}: {e}", path.display());
282 }
283 }
284 }
285
286 /// Whether an attach client receives the output.
287 #[cfg(test)]
288 pub(crate) fn has_readers(&self) -> bool {
289 self.receiver_count() > 0
290 }
291
292 #[cfg(test)]
293 fn receiver_count(&self) -> usize {
294 self.output.tx.get().map_or(0, Sender::receiver_count)
295 }
296
297 /// Subscribe to the container output sent to the attach endpoints. The
298 /// returned flag is true if the output already ended, then the done
299 /// message may have been sent before subscribing and the receiver only
300 /// gets what is still queued.
301 pub(crate) fn subscribe(&self) -> (Receiver<Message>, bool) {
302 let rx = self
303 .output
304 .tx
305 .get_or_init(|| broadcast::channel(OUTPUT_CHANNEL_SIZE).0)
306 .subscribe();
307 // Check only after subscribing, otherwise the done message could be
308 // sent in between. Pairs with the fence in `AttachOutput::drop`: of
309 // the two fences, the end of the output either passes its fence
310 // first, then this sees the end, or last, then it sees the sender and
311 // sends the done message after this subscribed.
312 fence(Ordering::SeqCst);
313 (rx, self.output.ended.load(Ordering::SeqCst))
314 }
315
316 /// Mark output which a read loop got as on its way into the log, until
317 /// the returned guard gets dropped once the output got broadcast. A slow
318 /// log driver delays the output, which is no pause of it, see
319 /// [`SharedContainerAttach::wait_logged`].
320 pub(crate) fn logging(&self) -> Logging<'_> {
321 self.output.logging.fetch_add(1, Ordering::SeqCst);
322 Logging(&self.output)
323 }
324
325 /// Whether a read loop has output on its way into the log.
326 pub(crate) fn is_logging(&self) -> bool {
327 self.output.logging.load(Ordering::SeqCst) > 0
328 }
329
330 /// Wait until no read loop has output on its way into the log.
331 pub(crate) async fn wait_logged(&self) {
332 loop {
333 let notified = self.output.logged.notified();
334 let mut notified = pin!(notified);
335 // Before checking, so that a notification in between is not
336 // missed.
337 notified.as_mut().enable();
338 if !self.is_logging() {
339 return;
340 }
341 notified.await;
342 }
343 }
344}
345
346/// Stops the output of an exec once dropped by the consumer of its output,
347/// after the process exited, see [`SharedContainerAttach::stop_output`].
348/// Without exit files, the consumer decides when the output is complete: a
349/// slow one blocks the read loops, which a stop after a pause of the output
350/// would cut off with data left in the pipes. A consumer which went away
351/// before the exit takes nothing anymore, the output stops with the exit.
352pub(crate) struct StopOutput {
353 attach: SharedContainerAttach,
354 exited: CancellationToken,
355}
356
357impl Drop for StopOutput {
358 fn drop(&mut self) {
359 if self.attach.output_stopped().is_cancelled() {
360 return;
361 }
362 if self.exited.is_cancelled() {
363 self.attach.stop_output();
364 return;
365 }
366 // Not waiting forever for an exit which got lost, after which the
367 // output got stopped otherwise.
368 let attach = self.attach.clone();
369 let exited = self.exited.clone();
370 task::spawn(async move {
371 select! {
372 () = exited.cancelled() => attach.stop_output(),
373 () = attach.output_stopped().cancelled() => {}
374 }
375 });
376 }
377}
378
379/// Output on its way into the log, see [`SharedContainerAttach::logging`].
380pub(crate) struct Logging<'a>(&'a OutputState);
381
382impl Drop for Logging<'_> {
383 fn drop(&mut self) {
384 if self.0.logging.fetch_sub(1, Ordering::SeqCst) == 1 {
385 self.0.logged.notify_waiters();
386 }
387 }
388}
389
390/// The container output registered by `SharedContainerAttach::output`.
391#[derive(Debug)]
392pub(crate) struct AttachOutput {
393 output: Arc<OutputState>,
394}
395
396impl Drop for AttachOutput {
397 fn drop(&mut self) {
398 // Set before sending, so that an attach client which subscribes
399 // after the done message still sees that the output ended, see
400 // `SharedContainerAttach::subscribe`.
401 self.output.ended.store(true, Ordering::SeqCst);
402 fence(Ordering::SeqCst);
403 // Sending fails only without attach clients, which then need no done
404 // message.
405 if let Some(tx) = self.output.tx.get() {
406 let _ = tx.send(Message::Done);
407 }
408 }
409}
410
411/// Removes the entry of a listener and its socket however its task ends.
412/// Another listener which replaced it took over the path and the entry with
413/// an id of its own, which stay. The listener must still be open, see
414/// [`SharedContainerAttach::remove_listener`].
415struct ListenerCleanup {
416 attach: SharedContainerAttach,
417 path: PathBuf,
418 id: u64,
419}
420
421impl Drop for ListenerCleanup {
422 fn drop(&mut self) {
423 self.attach.remove_listener(&self.path, self.id);
424 }
425}
426
427#[cfg(test)]
428mod tests;