bash_interop/rig/mod.rs
1//! Run bash under instrumentation, and hear what it says.
2//!
3//! ```bash
4//! declare -- BC_SAY__ARG_LABEL=DEPLOY
5//! BC_SAY REC compiled "$target" # ship an arglist and carry on
6//!
7//! declare -- BC_ASK__ARG_LABEL=DEPLOY # ask: block, then run the
8//! declare -a BC_ASK__ARGS=(which target) # answer here, in this frame
9//! BC_ASK
10//! ```
11//!
12//! A [`Rig`] is a **description**: the bash it gives the subject, where the
13//! session's files go, and how to build a reaction once a shell is there. The
14//! reaction is [`Reacting`], and it is made **per shell**, at the moment that
15//! shell announces itself — so which bash it is, how it was started and what
16//! it had switched on are members from construction, never parameters. Every
17//! shell has a pipe of its own and a task of its own, so serving many shells
18//! is many straight-line loops that interleave.
19//!
20//! ```no_run
21//! use std::sync::Arc;
22//! use bash_interop::rig::{
23//! Answer, Driving, Failure, Layout, Message, Provision, Reacting, Rig, Shell,
24//! };
25//!
26//! /// Keeps what one shell said, and tells it to use staging.
27//! struct Deploying;
28//!
29//! struct Told { shell: Arc<Shell>, heard: Vec<Message> }
30//!
31//! impl Rig for Deploying {
32//! type Reaction = Told;
33//!
34//! /// A word the subject's scripts can call, as one command over the
35//! /// core's, so it composes where any command does. Definitions only:
36//! /// sourcing this joins nothing.
37//! fn bash(&self, _at: &Layout) -> String {
38//! "alias STAGE='BC_SAY__ARG_LABEL=DEPLOY BC_SAY STAGE'\n".to_string()
39//! }
40//!
41//! async fn joined(&self, _at: &Layout, shell: Arc<Shell>) -> Result<Told, Failure> {
42//! Ok(Told { shell, heard: Vec::new() })
43//! }
44//! }
45//!
46//! /// The standard initiation — data the run's closure hands to `bash_env`;
47//! /// run only where a client or a provisioned file says so.
48//! fn deploy_join(at: &Layout) -> String {
49//! format!("BC_JOIN DEPLOY {}\n", bash_strings::emit_scalar(at.text()))
50//! }
51//!
52//! impl Reacting for Told {
53//! type Kept = Self;
54//!
55//! async fn hear(&mut self, said: Message) -> Result<(), Failure> {
56//! self.heard.push(said);
57//! Ok(())
58//! }
59//!
60//! async fn answer(&mut self, asked: Message) -> Result<Answer, Failure> {
61//! Ok(match asked.words.first().map(String::as_str) {
62//! Some("target") => Answer::of("declare", ["target=staging"]),
63//! _ => Answer::unknown(),
64//! })
65//! }
66//!
67//! async fn finish(self) -> Result<Self, Failure> { Ok(self) }
68//! }
69//!
70//! impl Driving for Deploying {}
71//!
72//! # #[tokio::main(flavor = "current_thread")]
73//! # async fn main() -> Result<(), Failure> {
74//! // The closure's return is the subject's whole environment. Provisioning
75//! // a joining file is the one auto-initiation there is, and it is stated
76//! // here, at the fringe — every other shell's initiation is its own code.
77//! let ran = Deploying
78//! .run(&["bash", "deploy.bash"], |at| {
79//! Ok(vec![at.bash_env(Provision::Joining(&deploy_join(at)))?])
80//! })
81//! .await?;
82//! for shell in ran.whole()?.shells {
83//! println!("pid {} said {} things", shell.shell.pid, shell.kept.heard.len());
84//! }
85//! # Ok(())
86//! # }
87//! ```
88//!
89//! Who started the shells is a second question with exactly two answers, and
90//! each is a trait that carries its own orchestration:
91//!
92//! | | who started the shells | how they find the address | what the session lasts for | what comes back |
93//! |---|---|---|---|---|
94//! | [`Driving`] | the run, in a process group of its own | exactly what the run's environment closure returned — [`Layout::bash_env`] with a stated [`Provision`] the usual pair | that process group | [`Run`], with the subject's [`ExitStatus`] |
95//! | [`Serving`] | a bash script, which named and made the workspace and started the server | its own choice: it feeds the same directory to start, probe, load and initiate | whoever holds the handle | [`Served`] |
96//!
97//! Either way, the address is the workspace directory. Loading its laid
98//! files defines; initiation is the client's own line — except where a
99//! provisioned `bash_env.bash` states [`Provision::Joining`], the one
100//! auto-initiation there is. The book's `docs/joining.md` shows every way
101//! a script joins, each as a whole script.
102//!
103//! **A session lasts as long as anyone who could still speak.** Nothing inside
104//! a rig ends one.
105//!
106//! The session is single-threaded: one `current_thread` runtime, one task per
107//! shell, and no `Send` bound anywhere. What shells share — a sink, a merged
108//! view — is the caller's own, handed in through [`Rig::joined`] as an
109//! `Rc<RefCell<_>>` or whatever it likes; a `RefCell` borrow must not be held
110//! across an `.await`.
111//!
112//! | | |
113//! |---|---|
114//! | `attended` | [`Layout`], [`Attended`], [`Kept`], [`Said`], [`heard`] |
115//! | `session`, `attend` | the conversation: the workspace, the control fifo, one task per shell |
116//! | `watch` | the descriptor a session ends on |
117//! | `driving`, `serving` | the two roles, and what each hands back |
118//! | `wire` | [`Message`], [`Answer`], and the protocol that carries them |
119
120mod attend;
121mod attended;
122mod driving;
123mod serving;
124mod session;
125mod watch;
126pub(crate) mod wire;
127
128use std::sync::Arc;
129
130pub use attended::{Attended, Kept, Layout, Provision, Said, heard};
131pub use driving::{Driving, ExitStatus, Run, Whole};
132pub use serving::{Served, Serving};
133
134pub use wire::{Answer, Message, Micros, Pid, Stamp, Verb, field};
135
136pub use crate::failure::{Doing, Failure};
137pub use crate::shell::Shell;
138
139/// What bash a rig gives the subject, and how a reaction is made once a
140/// shell is there.
141///
142/// A description: `&self` throughout, because nothing about it changes by
143/// running. **No method has a default body.**
144///
145/// | it is handed | it produces |
146/// |---|---|
147/// | [`&Layout`](Layout) — the workspace, and the files in it | [`Self::Reaction`](Rig::Reaction) |
148/// | [`Arc<Shell>`](Shell) — `bash: Bash`, `options: Options`, `brought`, `joined: Stamp` | |
149///
150/// The rig's bash is laid beside the protocol's own by the session;
151/// [`stack::with_walk`](crate::stack::with_walk) composes it where the
152/// rig reports a frame walk.
153// ANCHOR: rig-trait
154#[expect(async_fn_in_trait, reason = "single-threaded by design: no Send bound")]
155pub trait Rig {
156 /// What reacts to one shell.
157 type Reaction: Reacting;
158
159 /// The rig's own bash: **definitions only**. Its words, and at most a
160 /// channel-init function; sourcing it has no effect on a shell beyond
161 /// names coming into being, so it is inert, re-sourceable, and free of
162 /// the coordinate unless its author bakes one in.
163 fn bash(&self, at: &Layout) -> String;
164
165 /// A shell has joined, and everything about it is known. Awaited in the
166 /// accept loop, so a slow `joined` delays the next join and nothing else.
167 async fn joined(&self, at: &Layout, shell: Arc<Shell>) -> Result<Self::Reaction, Failure>;
168}
169// ANCHOR_END: rig-trait
170
171/// One shell's reaction, for as long as that shell can speak.
172///
173/// It runs as a task of its own, so it owns what it holds (`'static`) and is
174/// never sent to another thread. Awaiting inside a method yields to the other
175/// shells' tasks; synchronous work blocks them for its duration.
176///
177/// | | |
178/// |---|---|
179/// | [`hear`](Reacting::hear) | a [`Message`] nobody is waiting on |
180/// | [`answer`](Reacting::answer) | one the shell blocks on; the task writes the [`Answer`] back to it |
181/// | [`finish`](Reacting::finish) | what is left, which lands in [`Attended::kept`] |
182///
183/// **No method has a default body.** The two implementations below are the
184/// templates to copy.
185// ANCHOR: reacting-trait
186#[expect(async_fn_in_trait, reason = "single-threaded by design: no Send bound")]
187pub trait Reacting: Sized + 'static {
188 /// What is left when the shell can no longer speak. `Self` where nothing
189 /// is released at the end.
190 type Kept: 'static;
191
192 /// A `Failure` from this or [`answer`](Reacting::answer) ends the
193 /// conversation: under [`Driving`] the subject is killed and the run
194 /// yields that reason.
195 async fn hear(&mut self, said: Message) -> Result<(), Failure>;
196
197 /// An answer is a command, and every answer is the same kind of thing.
198 /// Saying no is a command that returns non-zero — [`Answer::unknown`] for
199 /// a word this rig has no answer for.
200 async fn answer(&mut self, asked: Message) -> Result<Answer, Failure>;
201
202 /// The conversation is over; release what this held.
203 async fn finish(self) -> Result<Self::Kept, Failure>;
204}
205// ANCHOR_END: reacting-trait
206
207/// A reaction that keeps every message, and has no answer to any of them.
208impl Reacting for Vec<Message> {
209 type Kept = Self;
210
211 async fn hear(&mut self, said: Message) -> Result<(), Failure> {
212 self.push(said);
213
214 Ok(())
215 }
216
217 async fn answer(&mut self, asked: Message) -> Result<Answer, Failure> {
218 self.hear(asked).await?;
219
220 Ok(Answer::unknown())
221 }
222
223 async fn finish(self) -> Result<Self, Failure> {
224 Ok(self)
225 }
226}
227
228/// A reaction that keeps nothing and answers nothing.
229impl Reacting for () {
230 type Kept = Self;
231
232 async fn hear(&mut self, _said: Message) -> Result<(), Failure> {
233 Ok(())
234 }
235
236 async fn answer(&mut self, _asked: Message) -> Result<Answer, Failure> {
237 Ok(Answer::unknown())
238 }
239
240 async fn finish(self) -> Result<Self, Failure> {
241 Ok(())
242 }
243}