Row 83. scripts/webui-service.sh render and install take --send, only
with the full preview set; otherwise the script refuses before writing
anything. With --send the rendered ExecStart ends in --send; renders
without it are byte-identical to the base. status says whether the
installed unit sends. The script header, install message, README and
TOOLS.md carry the decision 102 start wording: Jason or Mos starts and
restarts the Console, no other agent.
Dewey's round 1, candidate manifest f764cd9e, base d13b9300. Approved
by Darkwing (27721) and Filbert (27722). Sage's gate green 04:07:30Z to
04:10:47Z.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
228 lines
10 KiB
Bash
Executable File
228 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# `scripts/webui-service.sh render|install|uninstall|status`: the systemd
|
|
# user unit that runs the Console, `node packages/webui/src/cli.mjs serve`
|
|
# (lead decision 94). The template is
|
|
# packages/webui/systemd/mosaic-webui.service.in; the rendered unit is
|
|
# mosaic-webui.service in the systemd user directory, one instance.
|
|
#
|
|
# render [FLAGS] print the rendered unit on stdout
|
|
# install [--dir DIR] [--no-reload] [FLAGS]
|
|
# write the unit (write to a temp file, then
|
|
# rename) and run `systemctl --user daemon-reload`
|
|
# uninstall [--dir DIR] [--no-reload]
|
|
# remove the unit; refuses while it is active
|
|
# status [--dir DIR] unit state, whether it sends, then GET
|
|
# /healthz on its port
|
|
#
|
|
# FLAGS are serve's: --port N (default 7332; 7330 is the operator's own
|
|
# Console), --business ID, --board URL, and the preview set --public-host
|
|
# NAME --auth-header HEADER --allow-user ID, all three or none, and --send
|
|
# (row 83), taken only with the preview set: the unit's Console then sends
|
|
# human messages to roles and needs the system config at start. systemd
|
|
# expands % and $ and splits on spaces, so each value is held to a plain
|
|
# character set and refused otherwise. None is a secret.
|
|
#
|
|
# There is no start, stop, enable or restart verb. The Console reads the
|
|
# bus through the human transport, so whoever starts it holds the human's
|
|
# capability. Jason starts and restarts it
|
|
# (`systemctl --user enable --now mosaic-webui`), and so may Mos under lead
|
|
# decision 102, which accepts that sends from a Console Mos started enter
|
|
# the trail as Jason (human-cli). Any other agent starting it, even detached
|
|
# through systemd, works around the human proof (lead decisions 62 and 94).
|
|
#
|
|
# Exit codes: 0 ok, 1 operation failed, 4 usage.
|
|
set -euo pipefail
|
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
TEMPLATE="$REPO/packages/webui/systemd/mosaic-webui.service.in"
|
|
UNIT_NAME="mosaic-webui.service"
|
|
USAGE="usage: scripts/webui-service.sh render [FLAGS] | install [--dir DIR] [--no-reload] [FLAGS] | uninstall [--dir DIR] [--no-reload] | status [--dir DIR]
|
|
FLAGS: [--port N] [--business ID] [--board URL] [--public-host NAME --auth-header HEADER --allow-user ID [--send]]"
|
|
|
|
die() { echo "webui-service: $*" >&2; exit 1; }
|
|
usage() { echo "$USAGE" >&2; exit 4; }
|
|
|
|
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
|
RELOAD=1
|
|
DIR_FLAGS=0
|
|
PORT=7332
|
|
BUSINESS=""
|
|
BOARD=""
|
|
PUBLIC_HOST=""
|
|
AUTH_HEADER=""
|
|
ALLOW_USER=""
|
|
SEND=0
|
|
SERVE_SET=0
|
|
|
|
parse_flags() {
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--dir) [ $# -ge 2 ] || usage; UNIT_DIR="$2"; DIR_FLAGS=1; shift 2 ;;
|
|
--no-reload) RELOAD=0; DIR_FLAGS=1; shift ;;
|
|
--port) [ $# -ge 2 ] || usage; PORT="$2"; SERVE_SET=1; shift 2 ;;
|
|
--business) [ $# -ge 2 ] || usage; BUSINESS="$2"; SERVE_SET=1; shift 2 ;;
|
|
--board) [ $# -ge 2 ] || usage; BOARD="$2"; SERVE_SET=1; shift 2 ;;
|
|
--public-host) [ $# -ge 2 ] || usage; PUBLIC_HOST="$2"; SERVE_SET=1; shift 2 ;;
|
|
--auth-header) [ $# -ge 2 ] || usage; AUTH_HEADER="$2"; SERVE_SET=1; shift 2 ;;
|
|
--allow-user) [ $# -ge 2 ] || usage; ALLOW_USER="$2"; SERVE_SET=1; shift 2 ;;
|
|
--send) SEND=1; SERVE_SET=1; shift ;;
|
|
*) usage ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# The serve flags as one ExecStart fragment. Each character set excludes
|
|
# space, %, $, quotes, backslash, & and |, so neither systemd nor the sed
|
|
# below can read a value as anything but itself.
|
|
serve_flags() {
|
|
case "$PORT" in ''|*[!0-9]*) die "port must be 1..65535: $PORT" ;; esac
|
|
[ "${#PORT}" -le 5 ] && [ "$PORT" -ge 1 ] && [ "$PORT" -le 65535 ] || die "port must be 1..65535: $PORT"
|
|
local flags="--port $PORT"
|
|
if [ -n "$BUSINESS" ]; then
|
|
case "$BUSINESS" in [a-z0-9]*) ;; *) die "business must be a business id: $BUSINESS" ;; esac
|
|
case "$BUSINESS" in *[!a-z0-9._-]*) die "business must be a business id: $BUSINESS" ;; esac
|
|
[ "${#BUSINESS}" -le 64 ] || die "business must be a business id: $BUSINESS"
|
|
flags="$flags --business $BUSINESS"
|
|
fi
|
|
if [ -n "$BOARD" ]; then
|
|
# serve checks the rest (a loopback origin, no path or credentials).
|
|
case "$BOARD" in http://?*) ;; *) die "board must be an http:// URL: $BOARD" ;; esac
|
|
case "$BOARD" in *[!A-Za-z0-9.:/_-]*) die "board contains a character the unit cannot carry: $BOARD" ;; esac
|
|
flags="$flags --board $BOARD"
|
|
fi
|
|
local preview=0
|
|
[ -z "$PUBLIC_HOST" ] || preview=$((preview + 1))
|
|
[ -z "$AUTH_HEADER" ] || preview=$((preview + 1))
|
|
[ -z "$ALLOW_USER" ] || preview=$((preview + 1))
|
|
# serve refuses --send without the preview set; refuse it here first,
|
|
# so no unit is written that would only exit 2 at start.
|
|
[ "$SEND" = 0 ] || [ "$preview" -eq 3 ] || die "--send needs the preview mode (--public-host, --auth-header and --allow-user)"
|
|
if [ "$preview" -ne 0 ]; then
|
|
[ "$preview" -eq 3 ] || die "--public-host, --auth-header and --allow-user go together: all three or none"
|
|
case "$PUBLIC_HOST" in [A-Za-z0-9]*) ;; *) die "public host must be a hostname, no port or scheme: $PUBLIC_HOST" ;; esac
|
|
case "$PUBLIC_HOST" in *[!A-Za-z0-9.-]*) die "public host must be a hostname, no port or scheme: $PUBLIC_HOST" ;; esac
|
|
case "$AUTH_HEADER" in [A-Za-z0-9]*) ;; *) die "auth header must be a header name: $AUTH_HEADER" ;; esac
|
|
case "$AUTH_HEADER" in *[!A-Za-z0-9-]*) die "auth header must be a header name: $AUTH_HEADER" ;; esac
|
|
case "$ALLOW_USER" in *[!A-Za-z0-9._@+-]*) die "allowed user contains a character the unit cannot carry: $ALLOW_USER" ;; esac
|
|
flags="$flags --public-host $PUBLIC_HOST --auth-header $AUTH_HEADER --allow-user $ALLOW_USER"
|
|
fi
|
|
[ "$SEND" = 0 ] || flags="$flags --send"
|
|
echo "$flags"
|
|
}
|
|
|
|
render() {
|
|
[ -f "$TEMPLATE" ] || die "template missing: $TEMPLATE"
|
|
local node
|
|
node="$(command -v node || true)"
|
|
[ -n "$node" ] || die "node not found on PATH"
|
|
# systemd does not look an ExecStart command up in Environment=PATH,
|
|
# so the unit runs node by the path found here, kept as given.
|
|
case "$node" in /*) ;; *) die "node path must be absolute: $node" ;; esac
|
|
case "$node" in *[!A-Za-z0-9/._+-]*) die "node path contains a character the unit cannot carry: $node" ;; esac
|
|
case "$REPO" in *[!A-Za-z0-9/._+-]*) die "repository path contains a character the unit cannot carry: $REPO" ;; esac
|
|
local node_dir path="/usr/local/bin:/usr/bin:/bin"
|
|
node_dir="$(dirname "$node")"
|
|
case ":$path:" in *":$node_dir:"*) ;; *) path="$node_dir:$path" ;; esac
|
|
local flags
|
|
flags="$(serve_flags)" || exit 1
|
|
sed -e "s|@REPO@|$REPO|g" -e "s|@PATH@|$path|g" -e "s|@NODE@|$node|g" -e "s|@FLAGS@|$flags|g" "$TEMPLATE"
|
|
}
|
|
|
|
install_unit() {
|
|
parse_flags "$@"
|
|
local unit
|
|
unit="$(render)" || exit 1
|
|
mkdir -p "$UNIT_DIR"
|
|
local target="$UNIT_DIR/$UNIT_NAME" tmp
|
|
tmp="$(mktemp "$UNIT_DIR/.$UNIT_NAME.XXXXXX")"
|
|
printf '%s\n' "$unit" > "$tmp"
|
|
chmod 0644 "$tmp"
|
|
if [ -f "$target" ] && cmp -s "$tmp" "$target"; then
|
|
rm -f "$tmp"
|
|
echo "unchanged: $target"
|
|
else
|
|
mv -f "$tmp" "$target"
|
|
echo "written: $target"
|
|
fi
|
|
echo "serve: $(sed -n 's/^ExecStart=//p' "$target")"
|
|
if [ "$RELOAD" = 1 ]; then
|
|
systemctl --user daemon-reload || die "daemon-reload failed"
|
|
echo "daemon-reload done"
|
|
fi
|
|
cat <<MSG
|
|
this script starts nothing: the Console holds the human's capability. Jason
|
|
starts and restarts it, or Mos under lead decision 102 (sends then enter the
|
|
trail as Jason); no other agent does:
|
|
systemctl --user enable --now mosaic-webui start now and at login
|
|
systemctl --user status mosaic-webui
|
|
journalctl --user -u mosaic-webui -f the Console's log
|
|
systemctl --user stop mosaic-webui SIGTERM; restartable
|
|
scripts/webui-service.sh status unit state and /healthz
|
|
survive logout and reboot only with lingering on: loginctl enable-linger ${USER:-$(id -un)}
|
|
MSG
|
|
}
|
|
|
|
uninstall_unit() {
|
|
parse_flags "$@"
|
|
[ "$SERVE_SET" = 0 ] || usage
|
|
local target="$UNIT_DIR/$UNIT_NAME"
|
|
if [ "$RELOAD" = 1 ]; then
|
|
local active
|
|
active="$(systemctl --user list-units --no-legend --plain "$UNIT_NAME" 2>/dev/null | awk '$3 == "active" || $3 == "activating" || $3 == "deactivating" {print $1}')"
|
|
[ -z "$active" ] || die "refusing: $UNIT_NAME is still running; the human stops it first (systemctl --user stop mosaic-webui)"
|
|
fi
|
|
if [ -f "$target" ]; then
|
|
rm -f "$target"
|
|
echo "removed: $target"
|
|
else
|
|
echo "absent: $target"
|
|
fi
|
|
if [ "$RELOAD" = 1 ]; then
|
|
systemctl --user daemon-reload || die "daemon-reload failed"
|
|
echo "daemon-reload done"
|
|
fi
|
|
}
|
|
|
|
status_unit() {
|
|
parse_flags "$@"
|
|
[ "$SERVE_SET" = 0 ] && [ "$RELOAD" = 1 ] || usage
|
|
local target="$UNIT_DIR/$UNIT_NAME"
|
|
echo "unit: $UNIT_NAME"
|
|
echo " enabled: $(systemctl --user is-enabled "$UNIT_NAME" 2>&1 || true)"
|
|
echo " active: $(systemctl --user is-active "$UNIT_NAME" 2>&1 || true)"
|
|
systemctl --user show "$UNIT_NAME" -p MainPID -p NRestarts -p ExecMainStartTimestamp -p Result 2>/dev/null | sed 's/^/ /' || true
|
|
if [ ! -f "$target" ]; then
|
|
echo "healthz: not checked; no unit at $target"
|
|
return 0
|
|
fi
|
|
# render puts --send right after the allowed user, which may itself be
|
|
# the value --send; a unit that sends ends with both.
|
|
if grep -q '^ExecStart=.* --allow-user [^ ][^ ]* --send$' "$target"; then
|
|
echo "send: on (--send; human messages to roles)"
|
|
else
|
|
echo "send: off (read-only)"
|
|
fi
|
|
local port
|
|
port="$(sed -n 's/^ExecStart=.* --port \([0-9][0-9]*\).*$/\1/p' "$target")"
|
|
[ -n "$port" ] || { echo "healthz: not checked; no --port in $target"; return 0; }
|
|
local url="http://127.0.0.1:$port/healthz"
|
|
echo "healthz: $url $(node -e '
|
|
fetch(process.argv[1], { signal: AbortSignal.timeout(5000) })
|
|
.then(async (res) => console.log(`${res.status} ${(await res.text()).trim()}`))
|
|
.catch((err) => console.log(`unreachable (${err.cause?.code ?? err.name})`));
|
|
' "$url")"
|
|
}
|
|
|
|
[ $# -ge 1 ] || usage
|
|
cmd="$1"; shift
|
|
case "$cmd" in
|
|
render)
|
|
parse_flags "$@"
|
|
[ "$DIR_FLAGS" = 0 ] || usage
|
|
render ;;
|
|
install) install_unit "$@" ;;
|
|
uninstall) uninstall_unit "$@" ;;
|
|
status) status_unit "$@" ;;
|
|
-h|--help) echo "$USAGE" ;;
|
|
*) usage ;;
|
|
esac
|