Skip to main content

bash_interop/rig/
attended.rs

1//! Where a session puts its files, and what a run hands back.
2
3use std::ffi::OsString;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6
7use serde::Serialize;
8
9use super::{Message, Micros, Reacting, Rig, Shell};
10use crate::failure::{Doing, Failure};
11
12/// The session's workspace: the one coordinate, and the model of the files
13/// in it. Construction proves what every user needs: the directory exists
14/// (canonical), and is one line of text — it crosses into bash. Handed to
15/// every reaction at construction, since the instrument's own frames name a
16/// file in here.
17#[derive(Clone, Debug)]
18pub struct Layout {
19    dir: String,
20}
21
22/// The provisioned startup file: written only by [`Layout::bash_env`].
23const BASH_ENV: &str = "bash_env.bash";
24/// The protocol's half, laid verbatim.
25const PRELUDE: &str = "prelude.bash";
26/// The rig's half, laid from [`super::Rig::bash`].
27const RIG: &str = "rig.bash";
28/// The control fifo: present exactly while a session serves.
29const JOIN: &str = "join";
30/// Held `flock`ed for the session's life; the kernel releases it on any death.
31const LOCK: &str = "lock";
32
33impl Layout {
34    /// `dir` is canonical when handed in; what is proven here is that it can
35    /// cross: one line of text.
36    pub(super) fn new(dir: PathBuf) -> Result<Self, Failure> {
37        let display = dir.display().to_string();
38        let dir = dir
39            .into_os_string()
40            .into_string()
41            .ok()
42            .filter(|dir| !dir.contains('\n'))
43            .ok_or_else(|| {
44                Failure::new(
45                    format!("opening the workspace {display}"),
46                    "the path is not one line of text",
47                )
48            })?;
49
50        Ok(Self { dir })
51    }
52
53    /// The workspace: the session's address.
54    pub fn path(&self) -> &Path {
55        Path::new(&self.dir)
56    }
57
58    /// The workspace as text — what a rig splices into its bash, spelled
59    /// through [`bash_strings::emit_scalar`].
60    pub fn text(&self) -> &str {
61        &self.dir
62    }
63
64    pub(crate) fn prelude(&self) -> String {
65        self.file(PRELUDE)
66    }
67
68    pub(crate) fn rig(&self) -> String {
69        self.file(RIG)
70    }
71
72    pub(crate) fn join(&self) -> String {
73        self.file(JOIN)
74    }
75
76    pub(crate) fn lock(&self) -> String {
77        self.file(LOCK)
78    }
79
80    /// One shell's pipe, made by the shell; the token names it.
81    pub(crate) fn up(&self, token: &str) -> String {
82        self.file(&format!("up.{token}"))
83    }
84
85    /// One shell's reply pipe, made by the run before the shell can ask.
86    pub(crate) fn rep(&self, token: &str) -> String {
87        self.file(&format!("rep.{token}"))
88    }
89
90    fn file(&self, name: &str) -> String {
91        format!("{}/{name}", self.dir)
92    }
93
94    /// The one owner of `<dir>/bash_env.bash`: writes it — the two sources,
95    /// then the joining line iff provisioned — and yields the
96    /// `("BASH_ENV", <file>)` pair. Every non-interactive bash in the tree
97    /// the subject creates sources that file as it starts; whether that
98    /// initiates the channel is `provision`, stated by the caller. The core
99    /// consults neither this pair nor any other: a run's environment is
100    /// whatever its closure returns.
101    pub fn bash_env(&self, provision: Provision<'_>) -> Result<(OsString, OsString), Failure> {
102        let file = self.file(BASH_ENV);
103        let mut content = format!(
104            "source {}\nsource {}\n",
105            bash_strings::emit_scalar(&self.prelude()),
106            bash_strings::emit_scalar(&self.rig()),
107        );
108        if let Provision::Joining(line) = provision {
109            content.push_str(line);
110        }
111        std::fs::write(&file, content).doing(|| format!("provisioning {file}"))?;
112
113        Ok((OsString::from("BASH_ENV"), file.into()))
114    }
115}
116
117// ANCHOR: provision
118/// What the provisioned file does about the channel — the first thing a
119/// [`Layout::bash_env`] caller states.
120#[derive(Copy, Clone, Debug)]
121pub enum Provision<'a> {
122    /// The file ends with this line — supplied by the provisioner, usually
123    /// the rig's standard initiation: subjects with no prior knowledge join
124    /// as their shells start.
125    Joining(&'a str),
126
127    /// Definitions only: the client code initiates its own channel, and the
128    /// file carries no coordinate — the caller states one beside this pair
129    /// if its scripts need it.
130    Definitions,
131}
132// ANCHOR_END: provision
133
134/// What one shell's reaction leaves behind, for a given rig.
135pub type Kept<R> = <<R as Rig>::Reaction as Reacting>::Kept;
136
137/// One shell, what its reaction left behind, and when it went.
138#[derive(Debug)]
139pub struct Attended<K> {
140    pub shell: Arc<Shell>,
141    pub kept: K,
142
143    /// When nobody could write on its pipe any more. `None` for a shell the
144    /// session outlived — still running when the watch fired.
145    pub parted: Option<Micros>,
146}
147
148/// One message, and the shell that sent it.
149#[derive(Copy, Clone, Debug, Serialize)]
150pub struct Said<'a> {
151    pub shell: &'a Arc<Shell>,
152    pub message: &'a Message,
153}
154
155/// Everything the shells said, in the order it was said: by the sending
156/// shell's own clock, stably over join order and each shell's own order.
157pub fn heard<K: AsRef<[Message]>>(shells: &[Attended<K>]) -> Vec<Said<'_>> {
158    let mut said: Vec<Said<'_>> = shells
159        .iter()
160        .flat_map(|at| {
161            at.kept.as_ref().iter().map(|message| Said {
162                shell: &at.shell,
163                message,
164            })
165        })
166        .collect();
167
168    said.sort_by_key(|said| said.message.stamp.sent_at);
169    said
170}