#!/usr/bin/env bash # `scripts/discord-service.sh render|install|uninstall|status []`: # the systemd user unit that supervises `scripts/discord.sh run `. # The template is packages/discord/systemd/mosaic-discord@.service.in; the # rendered unit is mosaic-discord@.service in the systemd user directory, one # instance per binding (mosaic-discord@). 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 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/mosaic-discord@.service.in" UNIT_NAME="mosaic-discord@.service" 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 " 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 < start now and at login systemctl --user status mosaic-discord@ journalctl --user -u mosaic-discord@ -f the connector's log systemctl --user stop mosaic-discord@ SIGTERM; restartable scripts/discord.sh stop 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