Producer helper for the yah process-control channel — the two lines a workload writes so its supervisor stops having to guess at it.
What the channel is
A supervisor watching a process from outside can ask exactly two questions: is the pid alive, and is the port open. Both are proxies. A process that has finished booting, one still replaying a WAL, and one wedged on a lock answer them identically — so the real answer has always been somewhere in stdout, and reading it means grepping a log tail for a sentence nobody agreed on.
W315's rule: any process built to run under a yah camp SHOULD expose a
control channel. One verb, status, answering with a status document:
state is the only required field, and its vocabulary is exactly
[kamaji_proto::WorkloadState] — pending | starting | running | draining | exited | failed. That is the whole compatibility story: the supervisor
already answers a Probe verb in these words, so a workload reporting in
the same words is believed verbatim instead of run through a translation
table that rots the first time either side gains a state. Build with the
kamaji feature and the From impls between the two enums are exhaustive
matches — adding a state to either side stops compiling until both agree.
Using it
use procctl::{ProcState, ProcStatus};
# fn windows_open() -> usize { 3 }
# fn booted() -> bool { true }
// Hold the guard for as long as the process should answer. Dropping it
// stops the listener and unlinks the socket.
let _control = procctl::serve_env(|| {
if booted() {
ProcStatus::new(ProcState::Running)
.with_detail(format!("{} windows open", windows_open()))
} else {
ProcStatus::new(ProcState::Starting)
}
})?;
# Ok::<(), std::io::Error>(())
[serve_env] returns Ok(None) when YAH_CONTROL_SOCK is unset, which is
the contract: the same binary runs unchanged outside a camp, and a process
MUST NOT fail for the variable's absence.
The closure runs on the listener thread, on demand, once per request. It must not block for long and must not panic — a panic there takes the listener down with it, which reads to the supervisor as a process that stopped answering.
What this crate deliberately is not
It is not required. The protocol is one newline-delimited JSON verb
precisely so the producer side is implementable in twenty lines, with no
dependency, in any language, by someone whose actual job that day is their
own app. A Bun script or a Python daemon emitting the same line is a
first-class conforming producer. This crate is ergonomics for the Rust
case, not a gate — and that is also why it does not reach for
kamaji-proto's postcard wire, which would make conforming mean linking a
Rust crate (W315 §"Why newline-JSON").
The client feature adds the consumer half (async, tokio) for supervisors.
Producers should leave it off; the default build is std-only.
@arch:see(.yah/docs/working/W315-process-control-channel.md)