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}