Skip to main content

synwire_sandbox/
output.rs

1//! Output capture for non-interactive sandbox processes.
2//!
3//! [`CapturedOutput`] stores stdout and stderr in a temporary directory that is
4//! automatically removed when the last reference is dropped, giving Go-`defer`
5//! lifecycle semantics.
6
7use std::path::PathBuf;
8use std::sync::Arc;
9
10// ── OutputMode ───────────────────────────────────────────────────────────────
11
12/// How stdout and stderr are captured for non-interactive processes.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum OutputMode {
16    /// stdout and stderr go to separate files (`stdout`, `stderr`).
17    Separate,
18    /// stdout and stderr interleave into a single file (`output`).
19    Combined,
20}
21
22// ── CapturedOutput ───────────────────────────────────────────────────────────
23
24/// Captured stdout/stderr from a non-interactive sandbox process.
25///
26/// Wraps a [`tempfile::TempDir`] that is automatically removed when the last
27/// `Arc<CapturedOutput>` is dropped (either from the [`ProcessCapture`] handle
28/// or from the [`ProcessRecord`](crate::ProcessRecord) in the registry).
29#[derive(Debug)]
30pub struct CapturedOutput {
31    /// Temporary directory that owns the output files.
32    dir: tempfile::TempDir,
33    /// How stdout and stderr are stored.
34    mode: OutputMode,
35}
36
37impl CapturedOutput {
38    /// Allocate a new output capture directory.
39    ///
40    /// # Errors
41    ///
42    /// Returns an [`std::io::Error`] if the directory cannot be created.
43    pub fn new(mode: OutputMode) -> std::io::Result<Self> {
44        Ok(Self {
45            dir: tempfile::TempDir::with_prefix("synwire-")?,
46            mode,
47        })
48    }
49
50    /// Path to the stdout output file (or combined output file).
51    #[must_use]
52    pub fn stdout_path(&self) -> PathBuf {
53        match self.mode {
54            OutputMode::Combined => self.dir.path().join("output"),
55            OutputMode::Separate => self.dir.path().join("stdout"),
56        }
57    }
58
59    /// Path to the stderr output file, or `None` when streams are combined.
60    #[must_use]
61    pub fn stderr_path(&self) -> Option<PathBuf> {
62        match self.mode {
63            OutputMode::Separate => Some(self.dir.path().join("stderr")),
64            OutputMode::Combined => None,
65        }
66    }
67
68    /// Read captured stdout (or combined output) as a UTF-8 string.
69    ///
70    /// Returns an empty string if the file does not yet exist (process has not
71    /// produced any output).
72    ///
73    /// # Errors
74    ///
75    /// Returns an [`std::io::Error`] if the file exists but cannot be read.
76    pub fn read_stdout(&self) -> std::io::Result<String> {
77        let path = self.stdout_path();
78        if path.exists() {
79            std::fs::read_to_string(path)
80        } else {
81            Ok(String::new())
82        }
83    }
84
85    /// Read captured stderr as a UTF-8 string.
86    ///
87    /// Returns `None` when using [`OutputMode::Combined`] (use
88    /// [`read_stdout`](Self::read_stdout) instead). Returns an empty string if
89    /// the stderr file does not yet exist.
90    ///
91    /// # Errors
92    ///
93    /// Returns an [`std::io::Error`] if the file exists but cannot be read.
94    pub fn read_stderr(&self) -> std::io::Result<Option<String>> {
95        match self.stderr_path() {
96            Some(p) if p.exists() => std::fs::read_to_string(p).map(Some),
97            Some(_) => Ok(Some(String::new())),
98            None => Ok(None),
99        }
100    }
101
102    /// The capture mode.
103    #[must_use]
104    pub const fn mode(&self) -> OutputMode {
105        self.mode
106    }
107}
108
109// ── ProcessCapture ───────────────────────────────────────────────────────────
110
111/// Handle returned by [`NamespaceContainer::spawn_captured`](crate::platform::linux::namespace::NamespaceContainer::spawn_captured).
112///
113/// `output` is an [`Arc`]-wrapped [`CapturedOutput`]; the temp directory lives
114/// as long as this handle or any [`ProcessRecord`](crate::ProcessRecord)
115/// referring to the same `Arc` is alive. Pass `child` to
116/// [`monitor_child`](crate::process_registry::monitor_child) for automatic
117/// registry status updates when the process exits.
118#[derive(Debug)]
119pub struct ProcessCapture {
120    /// Shared reference to the captured output directory.
121    pub output: Arc<CapturedOutput>,
122    /// The running child process.
123    pub child: tokio::process::Child,
124    /// OCI bundle directory — kept alive while the container runs.
125    /// `None` for non-container processes.
126    pub(crate) _bundle: Option<tempfile::TempDir>,
127}