Skip to main content

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;