Serving
Under the served role the program is already running, and it starts the
session, usually by starting a tool's serve verb as a coprocess. This is the
mode for a script that instruments itself: it decides when the session begins,
does its work, and collects the reading when it is done. The Rust side starts
nothing, kills nothing, and writes nothing back to the client.
The surface, abridged; rustdoc is authoritative:
#![allow(unused)] fn main() { pub trait Serving: Rig { async fn serve(&self, at: &Path, held: OwnedFd) -> Result<Served<Kept<Self>>, Failure>; /// serve, with the handle being this process's own standard input — /// the coprocess convention's server half. async fn serve_coprocess(&self, at: &Path) -> Result<Served<Kept<Self>>, Failure>; } pub struct Served<K> { pub shells: Vec<Attended<K>>, pub failed: Option<Failure> } }
Two parameters carry the contract.
at is the workspace, which the client names and makes. It is required, has
no fallback, and must already exist, so the client holds the session's address
before the server has done anything and nothing needs to be communicated back.
The directory is left behind when the session ends, since readings taken later
may follow source paths into it, and removing it is the client's job.
held is a descriptor whose release ends the session. The session watches
this fd and serves as long as somebody could still hold it open. That somebody
is plural: file descriptors are inherited, so a subshell or child of the client
keeps the session alive for as long as it lives, and the session cannot end
while a process that might still speak exists. When the last holder closes it,
deliberately or by dying, the watch fires and the session closes. A shell that
talks after that writes into a fifo whose reader is gone and takes SIGPIPE.
The workspace shows whether the session is up. Its join fifo exists exactly
while a session serves, kept truthful by the lock and the sweep
(rigs.md), so the client gates on the same directory it named. The
boundary case is a server killed with SIGKILL, which removes nothing; its
stale fifo stands until the directory is next opened and swept, or removed.
A Failure while serving still sees the session out — every shell released or
finished, the fifos gone — before it is returned in Served::failed.
The coprocess convention
Bash's coproc starts a process and hands the script both ends, the process's
stdin as a write end and its stdout as a read end. The convention here is that
the client keeps the server's stdin as the handle and reads nothing, and
serve_coprocess is the server half, taking its own stdin as held.
The client's half is four moves of plain bash: start, probe, load and
initiate, let go. The whole script, which also lives in
bashprof/__fixtures/book/ where the tool's cli suite runs it as printed:
#!/usr/bin/env bash
# Owns the session: names the workspace, starts the server, probes, loads,
# initiates — and leaves by closing the handle coproc left it.
set -euo pipefail
declare -- workspace="$PWD/prof.d" # an address is absolute — initiation refuses else
mkdir -p "$workspace"
coproc SERVER { bashprof serve --at "$workspace" --into build.times; }
until [[ -p "$workspace/join" ]]; do sleep 0.01; done # up exactly while serving
source "$workspace/prelude.bash" # the protocol's words
source "$workspace/rig.bash" # the rig's words
BASHPROF_INIT "$workspace"
build() { sleep 0.1; }
BASHPROF_TIMETHIS build build
declare -- handle="${SERVER[1]}"
exec {handle}>&- # let go: what was held is the server's standard input
wait "$SERVER_PID" # it sees the session out; this script exits with its status
Three details in that script deserve explanation.
The until gate is there because coproc returns before the server has
parsed its arguments, and sourcing the laid files before they exist would
fail. The gate polls the one truthful signal. A client joining much later,
with the session serving all along, needs no gate.
The server's descriptors are SERVER[0] and SERVER[1], because coproc NAME { … } takes a literal name; that also means one server per shell under
this convention. The copy into handle before the close is because exec {name}>&- closes the descriptor a variable names.
The wait returns the server's own exit status, so a client under set -e
stops when its server failed. By the time it returns, whatever the server
writes is on disk, because the server writes after seeing the session out.
Every other way a shell can join a served session, by hand from the pieces or
published to children, is a whole script in joining.md.
bashprof's __fixtures/joined/build.bash is a working client of this shape,
exercised by its cli suite.