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}