Candidate manifest 68c60a79914692f3f6d4923f7bcff41de109ccca33bf6fba687fb88efa24bcac from Rocko's round 1 packet (a94a943c9), on BASE72c9ac36c. No manifest path changed between BASE and the landing target. Filbert approved round 1 in 28497 with no findings; the TOOLS.md paragraph break is a doc-only follow-up (decision 137). Sage reran the full gate at the landing target with the candidate applied and Docker live. Nothing is installed or restarted; the live switch is Mos's step after the SetSpark freeze lifts. Co-Authored-By: Claude Opus 5.5 <[email protected]>
184 lines
7.9 KiB
Bash
Executable File
184 lines
7.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# `scripts/discord-service.sh render|install|uninstall|status [<binding>]`:
|
|
# the systemd user unit that supervises `scripts/discord.sh run <binding>`.
|
|
# The template is packages/discord/systemd/[email protected]; the
|
|
# rendered unit is [email protected] in the systemd user directory, one
|
|
# instance per binding (mosaic-discord@<binding>). Nothing here reads the
|
|
# binding or the token. See packages/discord/README.md, "Service unit".
|
|
#
|
|
# render [--copy ID [--copies DIR]] print the rendered unit on stdout
|
|
# install [--copy ID [--copies DIR]] [--dir DIR] [--no-reload]
|
|
# 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 an instance is active
|
|
# status <binding> unit state, STOP and run.lock for one binding
|
|
#
|
|
# Exit codes: 0 ok, 1 operation failed, 4 usage.
|
|
set -euo pipefail
|
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
TEMPLATE="$REPO/packages/discord/systemd/[email protected]"
|
|
UNIT_NAME="[email protected]"
|
|
USAGE="usage: scripts/discord-service.sh render [--copy ID [--copies DIR]] | install [--copy ID [--copies DIR]] [--dir DIR] [--no-reload] | uninstall [--dir DIR] [--no-reload] | status <binding>"
|
|
|
|
die() { echo "discord-service: $*" >&2; exit 1; }
|
|
usage() { echo "$USAGE" >&2; exit 4; }
|
|
|
|
COPY_ID=""
|
|
COPY_SET=0
|
|
COPIES="${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-stack/copies"
|
|
copy_root() {
|
|
[[ "$COPY_ID" =~ ^[0-9a-f]{40}$ ]] || die '--copy must be a full 40-character commit id'
|
|
case "$COPIES" in /*) ;; *) die 'copies directory must be absolute' ;; esac
|
|
case "$COPIES" in *[!A-Za-z0-9/._+-]*) die 'copies directory contains a character the unit cannot carry' ;; esac
|
|
local root="$COPIES/$COPY_ID"
|
|
[ -f "$root/packages/discord/src/cli.mjs" ] || die "copy lacks packages/discord/src/cli.mjs: $root"
|
|
[ -f "$root/node_modules/.bin/pi" ] || die "copy lacks node_modules/.bin/pi: $root"
|
|
echo "$root"
|
|
}
|
|
|
|
render() {
|
|
[ -f "$TEMPLATE" ] || die "template missing: $TEMPLATE"
|
|
local root=""
|
|
[ "$COPY_SET" = 0 ] || root="$(copy_root)" || return 1
|
|
local node_dir
|
|
node_dir="$(dirname "$(command -v node || true)")"
|
|
[ -n "$node_dir" ] && [ "$node_dir" != "." ] || die "node not found on PATH"
|
|
local path="/usr/local/bin:/usr/bin:/bin"
|
|
case ":$path:" in *":$node_dir:"*) ;; *) path="$node_dir:$path" ;; esac
|
|
case "$REPO" in *@*|*'|'*) die "repository path contains a character the template cannot carry: $REPO" ;; esac
|
|
local unit
|
|
unit="$(sed -e "s|@REPO@|$REPO|g" -e "s|@PATH@|$path|g" "$TEMPLATE")" || return 1
|
|
if [ -n "$root" ]; then
|
|
local node
|
|
node="$(command -v node)"
|
|
case "$node" in *[!A-Za-z0-9/._+-]*) die 'node path contains a character the unit cannot carry' ;; esac
|
|
sed -e "s|^WorkingDirectory=.*$|WorkingDirectory=$root|" -e "s|^ExecStart=.*$|ExecStart=$node $root/packages/discord/src/cli.mjs run %i --supervised --repo $root|" <<< "$unit"
|
|
else
|
|
printf '%s\n' "$unit"
|
|
fi
|
|
}
|
|
|
|
# Parse --dir and --no-reload for install and uninstall.
|
|
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
|
RELOAD=1
|
|
parse_flags() {
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--copy) [ "$cmd" != uninstall ] && [ $# -ge 2 ] || usage; COPY_ID="$2"; COPY_SET=1; shift 2 ;;
|
|
--copies) [ "$cmd" != uninstall ] && [ $# -ge 2 ] || usage; COPIES="$2"; shift 2 ;;
|
|
--dir) [ "$cmd" != render ] && [ $# -ge 2 ] || usage; UNIT_DIR="$2"; shift 2 ;;
|
|
--no-reload) [ "$cmd" != render ] || usage; RELOAD=0; shift ;;
|
|
*) usage ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
install_unit() {
|
|
parse_flags "$@"
|
|
local rendered
|
|
rendered="$(render)" || exit 1
|
|
mkdir -p "$UNIT_DIR"
|
|
local target="$UNIT_DIR/$UNIT_NAME" tmp
|
|
tmp="$(mktemp "$UNIT_DIR/.$UNIT_NAME.XXXXXX")"
|
|
printf '%s\n' "$rendered" > "$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
|
|
if [ "$RELOAD" = 1 ]; then
|
|
systemctl --user daemon-reload || die "daemon-reload failed"
|
|
echo "daemon-reload done"
|
|
fi
|
|
if [ -n "$COPY_ID" ]; then
|
|
echo "copy: $COPY_ID"
|
|
else
|
|
echo "checkout: $REPO (use --copy ID to pin the connector)"
|
|
fi
|
|
cat <<MSG
|
|
next, for one binding:
|
|
systemctl --user enable --now mosaic-discord@<binding> start now and at login
|
|
systemctl --user status mosaic-discord@<binding>
|
|
journalctl --user -u mosaic-discord@<binding> -f the connector's log
|
|
systemctl --user stop mosaic-discord@<binding> SIGTERM; restartable
|
|
scripts/discord.sh stop <binding> the brake: writes STOP, the unit stays down until STOP is removed
|
|
survive logout and reboot only with lingering on: loginctl enable-linger ${USER:-$(id -un)}
|
|
MSG
|
|
}
|
|
|
|
uninstall_unit() {
|
|
parse_flags "$@"
|
|
local target="$UNIT_DIR/$UNIT_NAME"
|
|
if [ "$RELOAD" = 1 ]; then
|
|
local active
|
|
active="$(systemctl --user list-units --no-legend --plain 'mosaic-discord@*' 2>/dev/null | awk '$3 == "active" || $3 == "activating" || $3 == "deactivating" {print $1}')"
|
|
[ -z "$active" ] || die "refusing: instance(s) still running: $(echo "$active" | tr '\n' ' ')stop them first"
|
|
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() {
|
|
[ $# -eq 1 ] || usage
|
|
local binding="$1"
|
|
case "$binding" in ''|*[!A-Za-z0-9_-]*) die "binding name must be [A-Za-z0-9_-]+" ;; esac
|
|
local unit="mosaic-discord@$binding.service"
|
|
echo "unit: $unit"
|
|
echo " enabled: $(systemctl --user is-enabled "$unit" 2>&1 || true)"
|
|
echo " active: $(systemctl --user is-active "$unit" 2>&1 || true)"
|
|
systemctl --user show "$unit" -p MainPID -p NRestarts -p ExecMainStartTimestamp -p Result 2>/dev/null | sed 's/^/ /'
|
|
local working id
|
|
working="$(systemctl --user show "$unit" -p WorkingDirectory --value 2>/dev/null)" || working=""
|
|
# Accept property-form output too, for systemctl fixtures and older wrappers.
|
|
working="${working#WorkingDirectory=}"
|
|
echo " tree: ${working:-absent (WorkingDirectory unavailable)}"
|
|
id="${working##*/}"
|
|
if [[ "$id" =~ ^[0-9a-f]{40}$ ]] && [ -f "$working/packages/discord/src/cli.mjs" ]; then
|
|
echo " copy: $id"
|
|
elif [ -n "$working" ]; then
|
|
echo " checkout: $working"
|
|
fi
|
|
local config="${XDG_CONFIG_HOME:-$HOME/.config}/mosaic-dev/config.json"
|
|
local data_root
|
|
data_root="$(node -e 'const c=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")); process.stdout.write(String(c.dataRoot||""))' "$config" 2>/dev/null || true)"
|
|
if [ -z "$data_root" ]; then
|
|
echo " journal: dataRoot not readable from $config"
|
|
return 0
|
|
fi
|
|
data_root="${data_root/#\~/$HOME}"
|
|
local dir="$data_root/discord/$binding"
|
|
if [ -f "$dir/STOP" ]; then
|
|
echo " STOP: present ($(tail -n 1 "$dir/STOP"))"
|
|
else
|
|
echo " STOP: absent"
|
|
fi
|
|
if [ -d "$dir/run.lock" ]; then
|
|
echo " run.lock: present$( [ -f "$dir/run.lock/owner.json" ] && printf ' (owner pid %s)' "$(node -e 'process.stdout.write(String(JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")).pid))' "$dir/run.lock/owner.json" 2>/dev/null || echo '?')" )"
|
|
else
|
|
echo " run.lock: absent"
|
|
fi
|
|
}
|
|
|
|
[ $# -ge 1 ] || usage
|
|
cmd="$1"; shift
|
|
case "$cmd" in
|
|
render) parse_flags "$@"; render ;;
|
|
install) install_unit "$@" ;;
|
|
uninstall) uninstall_unit "$@" ;;
|
|
status) status_unit "$@" ;;
|
|
-h|--help) echo "$USAGE" ;;
|
|
*) usage ;;
|
|
esac
|