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}