Skip to main content

ferroday_cage/
observer.rs

1//! Live output streaming to a caller-supplied observer, and the collected
2//! whole-run form built on it.
3
4use crate::status::ExitStatus;
5
6/// A milestone in a sandbox launch, reported to [`Observer::progress`].
7///
8/// A launch passes through these stages in order, whether it is a direct
9/// [`Cage::spawn_with`] or a held [`Cage::spawn_pending_with`] released with
10/// [`Pending::proceed`]. They carry no payload and are the library's own
11/// progress, distinct from the command's captured output: an observer keys a
12/// launch log or progress display off the variant.
13///
14/// The enum is `#[non_exhaustive]`, so finer milestones can be added without
15/// breaking existing observers.
16///
17/// [`Cage::spawn_with`]: crate::Cage::spawn_with
18/// [`Cage::spawn_pending_with`]: crate::Cage::spawn_pending_with
19/// [`Pending::proceed`]: crate::Pending::proceed
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21#[non_exhaustive]
22pub enum Progress {
23    /// The launch has begun: the sandbox's channels are created and its
24    /// launch stage is forked.
25    Launching,
26    /// The sandbox's namespaces exist and its supervisor is published. For a
27    /// direct launch the command is about to be executed; for a pending
28    /// launch this is the gate, where a userspace network stack can attach
29    /// before the command runs.
30    Supervised,
31    /// The command has been executed and is now running.
32    Executing,
33}
34
35/// A sink for the sandboxed command's output.
36///
37/// An observer is borrowed by [`Cage::run_with`] or [`Cage::spawn_with`],
38/// which capture the command's standard output and standard error into
39/// pipes. The library delivers the captured bytes by calling the observer
40/// on the caller's own thread, from inside the blocking calls —
41/// [`Cage::run_with`] itself, or [`Running::wait`] and its deadline
42/// variants. No internal thread is spawned, and output flows only while one
43/// of those calls is pumping.
44///
45/// Chunks are delivered exactly as they are read from the pipes: they are
46/// not lines, they are not UTF-8, and their boundaries carry no meaning. An
47/// observer that needs lines assembles them itself.
48///
49/// Alongside the command's output, an observer receives the library's own
50/// launch [`Progress`] milestones through [`progress`](Self::progress).
51///
52/// Every method has an empty default body, so an observer implements only
53/// what it consumes, and new callbacks can be added without breaking
54/// existing implementations.
55///
56/// # Example
57///
58/// ```
59/// use ferroday_cage::Observer;
60///
61/// #[derive(Default)]
62/// struct Collect {
63///     stdout: Vec<u8>,
64///     stderr: Vec<u8>,
65/// }
66///
67/// impl Observer for Collect {
68///     fn stdout(&mut self, chunk: &[u8]) {
69///         self.stdout.extend_from_slice(chunk);
70///     }
71///     fn stderr(&mut self, chunk: &[u8]) {
72///         self.stderr.extend_from_slice(chunk);
73///     }
74/// }
75/// ```
76///
77/// [`Cage::run_with`]: crate::Cage::run_with
78/// [`Cage::spawn_with`]: crate::Cage::spawn_with
79/// [`Running::wait`]: crate::Running::wait
80pub trait Observer {
81    /// Receives a chunk of the command's standard output.
82    fn stdout(&mut self, chunk: &[u8]) {
83        let _ = chunk;
84    }
85
86    /// Receives a chunk of the command's standard error.
87    fn stderr(&mut self, chunk: &[u8]) {
88        let _ = chunk;
89    }
90
91    /// Receives a launch [`Progress`] milestone.
92    ///
93    /// Called on the calling thread as the launch advances, before any
94    /// captured output flows. Unlike [`stdout`](Self::stdout) and
95    /// [`stderr`](Self::stderr), a milestone is the library's own progress,
96    /// not the command's output, so an observer can drive a launch log or a
97    /// progress display without parsing the command's streams.
98    fn progress(&mut self, event: Progress) {
99        let _ = event;
100    }
101}
102
103/// A closure is an observer that watches the launch's milestones and ignores
104/// the command's output.
105///
106/// The same blanket impl [`ProvisionObserver`] carries, so a caller that wants
107/// one line per milestone writes the same thing on either side:
108///
109/// ```no_run
110/// # #[cfg(feature = "tarball")]
111/// # fn main() -> Result<(), ferroday_cage::Error> {
112/// use ferroday_cage::{Cage, Progress};
113///
114/// # let rootfs = "/var/cache/myapp/rootfs";
115/// let mut cage = Cage::builder().command("/bin/true").rootfs(rootfs).build()?;
116/// cage.run_with(&mut |event: Progress| eprintln!("{event:?}"))?;
117/// # Ok(())
118/// # }
119/// # #[cfg(not(feature = "tarball"))]
120/// # fn main() {}
121/// ```
122///
123/// A closure cannot receive the command's output, because it would have no way
124/// to say which stream a chunk came from. An observer that wants the output
125/// implements the trait.
126///
127/// [`ProvisionObserver`]: crate::provision::ProvisionObserver
128impl<F: FnMut(Progress)> Observer for F {
129    fn progress(&mut self, event: Progress) {
130        self(event)
131    }
132}
133
134/// A completed command's exit status and its captured output.
135///
136/// Produced by [`Cage::output`], the run-it-and-give-me-everything shape of a
137/// launch. [`Observer`] remains the way to consume output as it is produced, to
138/// bound what is kept, or to act on it before the command finishes.
139///
140/// [`Cage::output`]: crate::Cage::output
141// The restriction fallback is feature-gated, so the sentence naming it — and
142// its intra-doc link — is gated with it, per the crate-level convention.
143#[cfg_attr(
144    feature = "hardening",
145    doc = "
146[`Restriction::output`](crate::Restriction::output) produces the same type for a
147restriction.
148"
149)]
150#[derive(Debug, Clone, PartialEq, Eq)]
151#[non_exhaustive]
152pub struct Output {
153    /// The command's exit status. As everywhere in this library, a non-zero
154    /// exit is data rather than an error.
155    pub status: ExitStatus,
156    /// Everything the command wrote to standard output, concatenated in
157    /// order. Raw bytes: not lines, and not guaranteed to be UTF-8.
158    pub stdout: Vec<u8>,
159    /// Everything the command wrote to standard error, concatenated in order.
160    pub stderr: Vec<u8>,
161}
162
163/// The observer behind [`Output`]: accumulates both streams whole.
164///
165/// Unbounded by construction, which is why it is internal — a caller running
166/// untrusted code writes an [`Observer`] that caps what it keeps rather than
167/// reaching for this.
168#[derive(Default)]
169pub(crate) struct Collect {
170    stdout: Vec<u8>,
171    stderr: Vec<u8>,
172}
173
174impl Collect {
175    /// Pairs what was collected with the command's outcome.
176    pub(crate) fn into_output(self, status: ExitStatus) -> Output {
177        Output {
178            status,
179            stdout: self.stdout,
180            stderr: self.stderr,
181        }
182    }
183}
184
185impl Observer for Collect {
186    fn stdout(&mut self, chunk: &[u8]) {
187        self.stdout.extend_from_slice(chunk);
188    }
189
190    fn stderr(&mut self, chunk: &[u8]) {
191        self.stderr.extend_from_slice(chunk);
192    }
193}