#!/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 print the rendered unit on stdout # install [--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 | install [--dir DIR] [--no-reload] | uninstall [--dir DIR] [--no-reload] | status " die() { echo "discord-service: $*" >&2; exit 1; } usage() { echo "$USAGE" >&2; exit 4; } render() { [ -f "$TEMPLATE" ] || die "template missing: $TEMPLATE" 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 sed -e "s|@REPO@|$REPO|g" -e "s|@PATH@|$path|g" "$TEMPLATE" } # 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 --dir) [ $# -ge 2 ] || usage; UNIT_DIR="$2"; shift 2 ;; --no-reload) RELOAD=0; shift ;; *) usage ;; esac done } install_unit() { parse_flags "$@" mkdir -p "$UNIT_DIR" local target="$UNIT_DIR/$UNIT_NAME" tmp tmp="$(mktemp "$UNIT_DIR/.$UNIT_NAME.XXXXXX")" render > "$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 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 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 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) [ $# -eq 0 ] || usage; render ;; install) install_unit "$@" ;; uninstall) uninstall_unit "$@" ;; status) status_unit "$@" ;; -h|--help) echo "$USAGE" ;; *) usage ;; esac