Skip to main content

oxdock_process/
contract.rs

1use std::collections::HashMap;
2
3use anyhow::{Result, bail};
4use oxdock_fs::{CargoScratch, GuardedPath, PolicyPath};
5#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
6use std::process::ExitStatus;
7
8use std::sync::Arc;
9
10// Shared-IO handles and take-once OS pipe halves live in `oxdock-pipe`
11// (leaf crate, no cycle); re-exported here so every existing
12// `oxdock_process::` path keeps resolving. The OS items keep their
13// `not(miri)` gates: kernel pipes are compiled out under Miri isolation.
14#[cfg(not(miri))]
15pub use oxdock_pipe::{OsPipeReader, OsPipeWriter, create_os_pipe};
16pub use oxdock_pipe::{SharedInput, SharedOutput};
17
18/// Context passed to process managers describing the current execution
19/// environment. Clones are cheap and explicit so background handles can own
20/// their working roots without juggling lifetimes.
21#[derive(Clone, Debug)]
22pub struct CommandContext {
23    cwd: PolicyPath,
24    envs: Arc<HashMap<String, String>>,
25    cargo_target_dir: CargoScratch,
26    workspace_root: GuardedPath,
27    build_context: GuardedPath,
28}
29
30impl CommandContext {
31    pub fn new(
32        cwd: &PolicyPath,
33        envs: Arc<HashMap<String, String>>,
34        cargo_target_dir: &CargoScratch,
35        workspace_root: &GuardedPath,
36        build_context: &GuardedPath,
37    ) -> Self {
38        Self {
39            cwd: cwd.clone(),
40            envs,
41            cargo_target_dir: cargo_target_dir.clone(),
42            workspace_root: workspace_root.clone(),
43            build_context: build_context.clone(),
44        }
45    }
46
47    /// Convenience constructor cloning a plain map into a fresh `Arc`.
48    pub fn from_map(
49        cwd: &PolicyPath,
50        envs: &HashMap<String, String>,
51        cargo_target_dir: &CargoScratch,
52        workspace_root: &GuardedPath,
53        build_context: &GuardedPath,
54    ) -> Self {
55        Self::new(
56            cwd,
57            Arc::new(envs.clone()),
58            cargo_target_dir,
59            workspace_root,
60            build_context,
61        )
62    }
63
64    pub fn cwd(&self) -> &PolicyPath {
65        &self.cwd
66    }
67
68    pub fn envs(&self) -> &Arc<HashMap<String, String>> {
69        &self.envs
70    }
71
72    pub fn cargo_target_dir(&self) -> &CargoScratch {
73        &self.cargo_target_dir
74    }
75
76    pub fn workspace_root(&self) -> &GuardedPath {
77        &self.workspace_root
78    }
79
80    pub fn build_context(&self) -> &GuardedPath {
81        &self.build_context
82    }
83}
84
85/// Handle for background processes spawned by a [`ProcessManager`].
86pub trait BackgroundHandle: Send {
87    fn try_wait(&mut self) -> Result<Option<ExitStatus>>;
88    fn kill(&mut self) -> Result<()>;
89    fn wait(&mut self) -> Result<ExitStatus>;
90}
91
92#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
93pub enum CommandMode {
94    #[default]
95    Foreground,
96    Background,
97}
98
99#[derive(Clone, Default)]
100pub enum CommandStdout {
101    #[default]
102    Inherit,
103    Stream(SharedOutput),
104    Capture,
105    /// Direct OS kernel pipe writer for concurrent pipelines. Single use:
106    /// the handle is taken on spawn and the parent retains no copy, so the
107    /// reader observes EOF once the producer exits. Only valid with
108    /// concurrently spawned consumers (`ASYNC`); never for sequential steps.
109    #[cfg(not(miri))]
110    OsPipe(OsPipeWriter),
111}
112
113#[derive(Clone, Default)]
114pub enum CommandStdin {
115    /// Isolated null stdin. Preserves the previous `None` behavior.
116    #[default]
117    Null,
118    Inherit,
119    Stream(SharedInput),
120    /// Direct OS kernel pipe reader for concurrent pipelines. See
121    /// [`CommandStdout::OsPipe`] for the single use contract.
122    #[cfg(not(miri))]
123    OsPipe(OsPipeReader),
124}
125
126impl From<Option<SharedInput>> for CommandStdin {
127    fn from(stdin: Option<SharedInput>) -> Self {
128        match stdin {
129            Some(reader) => CommandStdin::Stream(reader),
130            None => CommandStdin::Null,
131        }
132    }
133}
134
135#[derive(Clone, Default)]
136pub enum CommandStderr {
137    #[default]
138    Inherit,
139    Stream(SharedOutput),
140    /// Direct OS kernel pipe writer, mirroring [`CommandStdout::OsPipe`].
141    /// Merging stdout and stderr into one live name takes the same slot
142    /// twice, so the second take bails; merge in shell via `2>&1` instead.
143    #[cfg(not(miri))]
144    OsPipe(OsPipeWriter),
145}
146
147#[derive(Clone, Default)]
148pub struct CommandOptions {
149    pub mode: CommandMode,
150    pub stdin: CommandStdin,
151    pub stdout: CommandStdout,
152    pub stderr: CommandStderr,
153}
154
155impl CommandOptions {
156    pub fn foreground() -> Self {
157        Self::default()
158    }
159
160    pub fn background() -> Self {
161        Self {
162            mode: CommandMode::Background,
163            ..Self::default()
164        }
165    }
166}
167
168pub enum CommandResult<H> {
169    Completed,
170    Captured(Vec<u8>),
171    Background(H),
172}
173
174/// Host environment variable that forces spawned children to inherit the
175/// parent's stdout/stderr instead of using the executor's stream routing.
176/// Recognized values are `"1"` and case-insensitive `"true"`. Set on the
177/// script environment (an `ENV` step or host inherit), not the process
178/// environment: the executor reads it from [`CommandContext::envs`].
179pub const INHERIT_STDOUT_ENV_VAR: &str = "OXDOCK_INHERIT_STDOUT";
180
181/// Host process-environment variable enabling `eprintln!` diagnostics for
182/// every spawned command (program plus argv/script). Read from the process
183/// environment at spawn time; any value (including empty) enables it.
184pub const PROCESS_DEBUG_ENV_VAR: &str = "OXBOOK_DEBUG";
185
186/// Abstraction for running shell commands both in the foreground and
187/// background. `oxdock-core` relies on this trait to decouple the executor
188/// from `std::process::Command`, which in turn enables Miri-friendly test
189/// doubles.
190pub trait ProcessManager: Clone + Send + 'static {
191    type Handle: BackgroundHandle + Clone + Send + 'static;
192
193    fn run_command(
194        &mut self,
195        ctx: &CommandContext,
196        script: &str,
197        options: CommandOptions,
198    ) -> Result<CommandResult<Self::Handle>>;
199
200    /// Spawn a command without waiting for completion. Returns a background
201    /// handle that can be polled or waited on later. The default implementation
202    /// delegates to `run_command` with `CommandMode::Background`.
203    fn spawn_command(
204        &mut self,
205        ctx: &CommandContext,
206        script: &str,
207        options: CommandOptions,
208    ) -> Result<CommandResult<Self::Handle>> {
209        self.run_command(ctx, script, options)
210    }
211
212    /// Run an executable directly with an argument vector (no shell).
213    /// Backs the `RUN ["exe", "arg", ...]` exec form. The default
214    /// implementation bails so existing out-of-tree managers keep
215    /// compiling; in-tree managers override this.
216    fn run_argv(
217        &mut self,
218        _ctx: &CommandContext,
219        argv: &[String],
220        _options: CommandOptions,
221    ) -> Result<CommandResult<Self::Handle>> {
222        bail!("run_argv not implemented for argv {argv:?}")
223    }
224
225    /// Spawn an argv command without waiting for completion. The default
226    /// implementation delegates to `run_argv`, mirroring `spawn_command`.
227    fn spawn_argv(
228        &mut self,
229        ctx: &CommandContext,
230        argv: &[String],
231        options: CommandOptions,
232    ) -> Result<CommandResult<Self::Handle>> {
233        self.run_argv(ctx, argv, options)
234    }
235}