Skip to main content

bash_interop/rig/
mod.rs

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