Skip to main content

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}