Skip to main content

Crate procctl

Crate procctl 

Source
Expand description

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":"running","pid":71455,"uptime_secs":41,"detail":"3 windows open"}

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};

// 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)
    }
})?;

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)

Structs§

ControlServer
A live control channel. Answers status until dropped.
ProcStatus
A workload’s self-description. Only Self::state is required.

Enums§

ProcState
Lifecycle vocabulary of a supervised process.

Constants§

CONTROL_SOCK_ENV
Environment variable naming the control socket a supervised process should bind. Absent → the process is not running under a supervisor that wants a control channel, and MUST NOT fail for its absence.
DEFAULT_HTTP_PATH
Conventional HTTP path for the status document on a process that already serves HTTP. Not enforced — [process.control] http_path overrides it — but a service with no reason to differ should use this one.
STATUS_CMD
The one verb the channel requires.

Functions§

control_sock_path
The socket path this process was told to bind, or None outside a camp.
serve_at
Bind an explicit path and answer status from status_fn.
serve_env
Bind $YAH_CONTROL_SOCK and answer status from status_fn.