bash_interop/rig/serving.rs
1//! bash orchestrates: a script that is already running names and makes the
2//! workspace, starts the server, joins at the coordinate its own choice
3//! fixed, and lets go when it is done.
4//!
5//! Nothing here starts a process or ends one. What the client started, the
6//! client cleans up, which is why the only thing this side watches is the
7//! handle.
8
9use std::io;
10use std::os::fd::{AsFd, OwnedFd};
11use std::path::Path;
12
13use tokio::task::LocalSet;
14
15use super::session::Session;
16use super::watch::Watch;
17use super::{Attended, Kept, Rig};
18use crate::failure::{Doing, Failure};
19
20/// What a served session produced.
21///
22/// Reaching one means the conversation ran and was seen out. A `Failure`
23/// instead means it never got that far.
24pub struct Served<K> {
25 /// Every shell that joined, in the order they did.
26 pub shells: Vec<Attended<K>>,
27
28 /// What went wrong closing up, if anything: a line left half-read, or a
29 /// reaction that would not let go.
30 pub failed: Option<Failure>,
31}
32
33/// A rig a running bash may attach to.
34///
35/// | | |
36/// |---|---|
37/// | who chose the workspace | the client: `at` is required, exists, and is the address |
38/// | what the client is handed | nothing — liveness is the workspace's to show: the join fifo is present exactly while the session serves |
39/// | what ends it | the handle the initiator holds, watched and never closed here |
40/// | what comes back | [`Served`] |
41///
42/// `at` is the workspace the client prescribed and made — left behind: a
43/// reading taken later may follow source paths into it. `held` is a
44/// descriptor the initiator holds open for as long as it wants the session:
45/// serving ends when the last holder has let go.
46///
47/// A serving application is a complete standalone program: it owes nobody a
48/// byte on any channel. A client that wants to know the session is up asks
49/// the workspace — the join fifo is present exactly while a session serves
50/// — sources the laid definitions and initiates its own channel, feeding
51/// every step the same coordinate it gave the server. What the session
52/// reaches is the client's decision: joining instruments that shell, its
53/// functions, its subshells and what it sources; a client that wants its
54/// child processes reached writes its own startup file and exports
55/// `BASH_ENV` to it.
56///
57/// A `Failure` while serving still sees the session out: every shell
58/// released or finished, the workspace's fifos gone.
59#[expect(async_fn_in_trait, reason = "single-threaded by design: no Send bound")]
60pub trait Serving: Rig {
61 async fn serve(&self, at: &Path, held: OwnedFd) -> Result<Served<Kept<Self>>, Failure>
62 where
63 Self: Sized,
64 {
65 LocalSet::new()
66 .run_until(async {
67 let mut session = Session::open(self, Some(at))?;
68
69 let served = async {
70 let watch = Watch::held(held)?;
71 session.serve(&watch).await
72 }
73 .await;
74 let (shells, failed) = session.close().await;
75 served?;
76
77 Ok(Served { shells, failed })
78 })
79 .await
80 }
81
82 /// Serve the client that started this process as a coprocess: it holds
83 /// this process's standard input — the write end `coproc` left it — and
84 /// lets go by closing it. The book's `docs/joining.md` shows the
85 /// client's half, whole.
86 async fn serve_coprocess(&self, at: &Path) -> Result<Served<Kept<Self>>, Failure>
87 where
88 Self: Sized,
89 {
90 let held = io::stdin()
91 .as_fd()
92 .try_clone_to_owned()
93 .doing(|| "taking hold of the handle the client kept".into())?;
94
95 self.serve(at, held).await
96 }
97}