Skip to main content

oino_tools/env/
mod.rs

1//! Execution environment: the file system and process boundary every tool goes through.
2//!
3//! Tools can swap their `operations` (for SSH, containers…); Oino generalises that into
4//! one [`ExecutionEnv`] so ACP clients can host the file system and terminals (modes §6.4).
5//! [`LocalEnv`] is the default. Errors carry Node's `code`/`syscall`/`path` so the texts the
6//! model sees match the host's (`ENOENT: no such file or directory, open '/x'`).
7
8mod error;
9mod local;
10
11use futures::future::BoxFuture;
12use tokio::sync::mpsc;
13
14pub use error::{FsError, SpawnError};
15pub use local::LocalEnv;
16
17/// Accessibility check of [`ExecutionEnv::access`] (Node `fs.access` modes).
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum AccessMode {
20    /// `F_OK`: the path exists.
21    Exists,
22    /// `R_OK`: the path is readable.
23    Read,
24    /// `R_OK | W_OK`: the path is readable and writable.
25    ReadWrite,
26}
27
28/// What [`ExecutionEnv::stat`] reports (symlinks followed).
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub struct FileStat {
31    /// The path is a directory.
32    pub is_dir: bool,
33    /// The path is a regular file.
34    pub is_file: bool,
35    /// Size in bytes.
36    pub size: u64,
37}
38
39/// A process to start with [`ExecutionEnv::spawn`].
40#[derive(Clone, Debug, PartialEq, Eq)]
41pub struct SpawnRequest {
42    /// Executable.
43    pub program: String,
44    /// Arguments.
45    pub args: Vec<String>,
46    /// Working directory.
47    pub cwd: String,
48    /// The complete environment (the parent environment is not inherited).
49    pub env: Vec<(String, String)>,
50    /// Bytes written to stdin before it is closed; `None` connects stdin to the null device.
51    pub stdin: Option<Vec<u8>>,
52}
53
54/// How a process ended.
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub struct ExitStatus {
57    /// Exit code, when the process exited normally.
58    pub code: Option<i32>,
59    /// Terminating signal number, when the process was killed by a signal.
60    pub signal: Option<i32>,
61}
62
63impl ExitStatus {
64    /// Exit-code rule: the code, else `128 + signal`, else `1`.
65    #[must_use]
66    pub fn shell_exit_code(self) -> i32 {
67        match (self.code, self.signal) {
68            (Some(code), _) => code,
69            (None, Some(signal)) => 128 + signal,
70            (None, None) => 1,
71        }
72    }
73}
74
75/// Control half of a running process.
76pub trait ProcessControl: Send {
77    /// OS process id, when known.
78    fn pid(&self) -> Option<u32>;
79
80    /// Kills the process and all its descendants (`killProcessTree`: `killpg(SIGKILL)` on
81    /// Unix, `taskkill /F /T` on Windows). Never fails; a dead process is ignored.
82    fn kill_tree(&self);
83
84    /// Waits for the process to exit. Cancel-safe.
85    fn wait(&mut self) -> BoxFuture<'_, std::io::Result<ExitStatus>>;
86
87    /// Stops reading output (pipes inherited by detached descendants may never close).
88    fn close_output(&mut self);
89}
90
91/// A started process: stdout and stderr interleaved in arrival order, plus its control.
92pub struct SpawnedProcess {
93    /// Output chunks; closed when both pipes reached end of file or output was closed.
94    pub output: mpsc::UnboundedReceiver<Vec<u8>>,
95    /// Kill / wait handle.
96    pub control: Box<dyn ProcessControl>,
97}
98
99impl std::fmt::Debug for SpawnedProcess {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        f.debug_struct("SpawnedProcess")
102            .field("pid", &self.control.pid())
103            .finish_non_exhaustive()
104    }
105}
106
107/// The file system and process host used by the tools.
108pub trait ExecutionEnv: Send + Sync {
109    /// Node `fs.access(path, mode)`.
110    fn access<'a>(&'a self, path: &'a str, mode: AccessMode) -> BoxFuture<'a, Result<(), FsError>>;
111
112    /// Node `fs.readFile(path)`.
113    fn read_file<'a>(&'a self, path: &'a str) -> BoxFuture<'a, Result<Vec<u8>, FsError>>;
114
115    /// Reads at most `max` bytes from the start of the file (image type sniffing).
116    fn read_prefix<'a>(
117        &'a self,
118        path: &'a str,
119        max: usize,
120    ) -> BoxFuture<'a, Result<Vec<u8>, FsError>>;
121
122    /// Node `fs.writeFile(path, data)`: creates or truncates.
123    fn write_file<'a>(
124        &'a self,
125        path: &'a str,
126        data: &'a [u8],
127    ) -> BoxFuture<'a, Result<(), FsError>>;
128
129    /// Node `fs.mkdir(path, {recursive: true})`.
130    fn mkdir_p<'a>(&'a self, path: &'a str) -> BoxFuture<'a, Result<(), FsError>>;
131
132    /// Node `fs.stat(path)`.
133    fn stat<'a>(&'a self, path: &'a str) -> BoxFuture<'a, Result<FileStat, FsError>>;
134
135    /// Node `fs.readdir(path)`: entry names in directory order.
136    fn read_dir<'a>(&'a self, path: &'a str) -> BoxFuture<'a, Result<Vec<String>, FsError>>;
137
138    /// Node `fs.realpath(path)`.
139    fn realpath<'a>(&'a self, path: &'a str) -> BoxFuture<'a, Result<String, FsError>>;
140
141    /// Starts a process in its own process group with piped stdout/stderr.
142    fn spawn(&self, request: SpawnRequest) -> BoxFuture<'_, Result<SpawnedProcess, SpawnError>>;
143
144    /// Whether paths refer to this machine's file system. `grep` and `find` walk the local file
145    /// system directly and refuse to run otherwise.
146    fn is_local(&self) -> bool;
147}
148
149/// `access(F_OK)` succeeds.
150pub async fn path_exists(env: &dyn ExecutionEnv, path: &str) -> bool {
151    env.access(path, AccessMode::Exists).await.is_ok()
152}