From 8c58c43b1bf0875b26898951dea9337cb34352cd Mon Sep 17 00:00:00 2001 From: fabolous005 Date: Fri, 6 Nov 2026 14:15:07 +0100 Subject: [PATCH] prain front door and OpenRC user service for the prain stack --- README.md | 45 ++++++++++++++++++++++ openrc/conf.d/prain.vllm | 2 + openrc/init.d/prain | 33 +++++++++++++++++ scripts/prain | 76 ++++++++++++++++++++++++++++++++++++++ scripts/prain.conf.example | 19 ++++++++++ scripts/test_prain.sh | 44 ++++++++++++++++++++++ 6 files changed, 219 insertions(+) create mode 100644 README.md create mode 100644 openrc/conf.d/prain.vllm create mode 100755 openrc/init.d/prain create mode 100755 scripts/prain create mode 100644 scripts/prain.conf.example create mode 100755 scripts/test_prain.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..8a6e713 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# prmcp + +One endpoint for a prain-served model: MCP tools picked by model name (`gemma4+terminal,web`). +The proxy itself is not written yet; this repo holds the stack around it so far. + +## The prain stack: `prain up` + +`scripts/prain` is a front door for the `prain` command. `up`, `down`, `restart`, `status` +and `logs` drive the stack's OpenRC user services. Every other subcommand (`serve`, `web`, +`tui`, `check`, ...) goes to prain's own CLI in `PRAIN_ROOT/.venv`, with `scripts/env.sh` +sourced first, so you no longer need to source it yourself. (In a shell where you did, the +venv's `prain` comes first on PATH and `prain up` is unknown there.) + + prain up [NAME...] start the services (all by default), waiting until each is ready + prain down [NAME...] stop them, in reverse order + prain restart [NAME...] + prain status [NAME...] + prain logs [NAME...] tail -F their logs (~/.local/state/prain/NAME.log) + +Services live in `~/.config/prain/prain.conf` (`PRAIN_CONF` overrides): `PRAIN_SERVICES` +lists them, and per service `cmd_NAME` (the command, run in `PRAIN_ROOT` with env.sh +sourced), `ready_NAME` (a URL `prain up` waits for) and `need_NAME` (dependencies). See +`scripts/prain.conf.example`. + +One OpenRC script, `openrc/init.d/prain`, serves them all through links named `prain.NAME`. +OpenRC's supervise-daemon restarts a crashed service. It gives up after more than +`respawn_max` (3) crashes within `respawn_period` seconds (600; 1800 for vLLM in +`openrc/conf.d/prain.vllm`), so crashes further apart are restarted forever. + +### Install + + ln -s "$PWD/scripts/prain" ~/.local/bin/prain + mkdir -p ~/.config/prain ~/.config/rc/init.d ~/.config/rc/conf.d + cp scripts/prain.conf.example ~/.config/prain/prain.conf + ln -s "$PWD/openrc/init.d/prain" ~/.config/rc/init.d/prain + for n in vllm web; do ln -s prain ~/.config/rc/init.d/prain.$n; done + cp openrc/conf.d/prain.vllm ~/.config/rc/conf.d/ + rc-update --user add prain.web default # optional: start with the session (pulls in vllm) + +A new service is a `cmd_NAME` line, its name in `PRAIN_SERVICES` and one more `prain.NAME` +link. + +### Test + + bash scripts/test_prain.sh diff --git a/openrc/conf.d/prain.vllm b/openrc/conf.d/prain.vllm new file mode 100644 index 0000000..cfe795e --- /dev/null +++ b/openrc/conf.d/prain.vllm @@ -0,0 +1,2 @@ +# vLLM needs minutes to load: count crashes over 30 minutes, not the default 10. +respawn_period=1800 diff --git a/openrc/init.d/prain b/openrc/init.d/prain new file mode 100755 index 0000000..a316b76 --- /dev/null +++ b/openrc/init.d/prain @@ -0,0 +1,33 @@ +#!/sbin/openrc-run +# One service of the prain stack, picked by the link name: prain.vllm, prain.web, ... +# What it runs comes from prain.conf (`prain run NAME`); supervision settings from +# conf.d/prain.NAME (respawn_max, respawn_period, respawn_delay). + +stack_name=${RC_SVCNAME#prain.} +description="prain stack: ${stack_name}" +supervisor=supervise-daemon +command=${prain_bin:-$HOME/.local/bin/prain} +command_args="run ${stack_name}" +respawn_delay=${respawn_delay:-5} +respawn_max=${respawn_max:-3} +respawn_period=${respawn_period:-600} +output_log=${XDG_STATE_HOME:-$HOME/.local/state}/prain/${stack_name}.log +error_log=${output_log} + +depend() { + need $("${prain_bin:-$HOME/.local/bin/prain}" need "${RC_SVCNAME#prain.}") +} + +start_pre() { + if [ "${RC_SVCNAME}" = prain ]; then + eerror "Start a link to this script, like prain.vllm" + return 1 + fi + checkpath -d "${output_log%/*}" +} + +start_post() { + ebegin "Waiting until ${stack_name} is ready" + "${command}" wait "${stack_name}" + eend $? +} diff --git a/scripts/prain b/scripts/prain new file mode 100755 index 0000000..22cc92c --- /dev/null +++ b/scripts/prain @@ -0,0 +1,76 @@ +#!/bin/bash +# prain front door. up, down, restart, status and logs drive the stack's OpenRC user +# services (prain.NAME); run, need and wait are what those services call. Anything else +# goes to prain's own CLI in PRAIN_ROOT/.venv, with its scripts/env.sh sourced. +set -euo pipefail + +conf=${PRAIN_CONF:-${XDG_CONFIG_HOME:-$HOME/.config}/prain/prain.conf} +if [ -r "$conf" ]; then . "$conf"; fi +: "${PRAIN_ROOT:?set PRAIN_ROOT in $conf}" +read -ra services <<< "${PRAIN_SERVICES:-}" +logdir=${XDG_STATE_HOME:-$HOME/.local/state}/prain + +get() { # get cmd vllm -> $cmd_vllm from the config + local v="$1_$2" + printf '%s' "${!v:-}" +} + +prain_env() { + set +eu # env.sh is not written for -e/-u + . "$PRAIN_ROOT/scripts/env.sh" + set -eu +} + +names() { # the services named on the command line, else all of them + if [ $# -gt 0 ]; then printf '%s\n' "$@"; else printf '%s\n' "${services[@]}"; fi +} + +case ${1:-} in +up | down | restart | status) + action=${1/up/start} + action=${action/down/stop} + shift + mapfile -t list < <(names "$@") + if [ "$action" = stop ]; then mapfile -t list < <(printf '%s\n' "${list[@]}" | tac); fi + rc=0 + for n in "${list[@]}"; do rc-service --user "prain.$n" "$action" || rc=1; done + exit $rc + ;; +logs) + shift + mapfile -t list < <(names "$@") + files=() + for n in "${list[@]}"; do files+=("$logdir/$n.log"); done + exec tail -n "${PRAIN_LOG_LINES:-40}" -F "${files[@]}" + ;; +run) + cmd=$(get cmd "$2") + [ -n "$cmd" ] || { echo "prain: no cmd_$2 in $conf" >&2; exit 1; } + cd "$PRAIN_ROOT" + prain_env + eval "exec ${cmd//$'\n'/ }" + ;; +need) + get need "$2" + echo + ;; +wait) + url=$(get ready "$2") + [ -n "$url" ] || exit 0 + timeout=${PRAIN_READY_TIMEOUT:-900} + end=$((SECONDS + timeout)) + until curl -sf -o /dev/null "$url"; do + [ $SECONDS -lt $end ] || { echo "prain: $2 not ready at $url after $timeout s" >&2; exit 1; } + sleep 2 + done + ;; +'' | -h | --help) + echo "stack: prain up|down|restart|status|logs [NAME...] (services: ${services[*]})" + prain_env + exec "$PRAIN_ROOT/.venv/bin/prain" --help + ;; +*) + prain_env + exec "$PRAIN_ROOT/.venv/bin/prain" "$@" + ;; +esac diff --git a/scripts/prain.conf.example b/scripts/prain.conf.example new file mode 100644 index 0000000..e36a8b7 --- /dev/null +++ b/scripts/prain.conf.example @@ -0,0 +1,19 @@ +# prain stack config, read by the `prain` front door (bash). +# Install: cp scripts/prain.conf.example ~/.config/prain/prain.conf +# Commands run in PRAIN_ROOT with its scripts/env.sh sourced, so `prain` in them is the +# venv's CLI. Service names must be shell identifiers. + +PRAIN_ROOT=/home/fabian/coding/misc/prain +PRAIN_SERVICES="vllm web" # `prain up` starts in this order, `prain down` stops in reverse + +# cmd_NAME: what service prain.NAME runs. ready_NAME: URL that answers once it can serve +# (`prain up` waits for it, PRAIN_READY_TIMEOUT seconds, default 900). need_NAME: services +# it depends on (OpenRC starts them first and stops it with them). +cmd_vllm='prain serve build/agent-int4.toml models/gemma-4-26B-A4B-it-Uncensored-MAX -- + --served-model-name gemma4 --language-model-only --max-model-len 16384 + --max-num-batched-tokens 1024 --enable-prefix-caching --port 8000 + --attention-backend TRITON_ATTN --enable-auto-tool-choice --tool-call-parser gemma4' +ready_vllm=http://127.0.0.1:8000/health + +cmd_web='prain web --host 0.0.0.0 --port 8080 --url http://127.0.0.1:8000 --config build/agent-int4.toml' +need_web=vllm diff --git a/scripts/test_prain.sh b/scripts/test_prain.sh new file mode 100755 index 0000000..fdd3ad3 --- /dev/null +++ b/scripts/test_prain.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Checks the prain front door against a fake prain checkout and a fake rc-service. +# Run: bash scripts/test_prain.sh +set -u +p=$(cd "$(dirname "$0")" && pwd)/prain +t=$(mktemp -d) +trap 'rm -rf "$t"' EXIT +mkdir -p "$t/root/scripts" "$t/root/.venv/bin" "$t/bin" +cat > "$t/root/scripts/env.sh" < "$t/root/.venv/bin/prain" +printf '#!/bin/sh\necho "$*"\n' > "$t/bin/rc-service" +chmod +x "$t/root/.venv/bin/prain" "$t/bin/rc-service" +cat > "$t/prain.conf" </dev/null; echo $?)" +check need "a" "$("$p" need b)" +check need-none "" "$("$p" need a)" +check wait-without-url 0 "$("$p" wait a; echo $?)" +check wait-times-out 1 "$("$p" wait b 2>/dev/null; echo $?)" +check up-in-order "--user prain.a start +--user prain.b start" "$("$p" up)" +check down-in-reverse "--user prain.b stop +--user prain.a stop" "$("$p" down)" +check status-one "--user prain.b status" "$("$p" status b)" +exit $fail