#!/usr/bin/env bash # # Make the device plugin notice a freshly-written leds.toml. # # apply-leds.sh --strategy # apply-leds.sh --probe # find out which one your machine needs # # WHY THIS IS A SCRIPT AND NOT A LINE OF PYTHON. The akp05 device plugin reads # leds.toml at startup, and nothing documents it re-reading. Nothing else can drive # those LEDs either: the plugin holds the USB device open. So "the rings update live" # comes down to one unknown — what makes that process pick the file up again — and the # answer is a property of your installed build, not of this repo. The probe finds it # in about two minutes with the dock in front of you; everything else here is already # written and tested. # # The strategies, cheapest first: # # none write the file and stop. CORRECT IF the plugin already watches # the file — which is the FIRST thing the probe tests, because if # it does, the rings are live for free and every strategy below is # a worse answer. # signal SIGHUP the plugin process. Reload-on-SIGHUP is a common daemon # convention, so it is worth one test — but be clear-eyed: the # DEFAULT action for an unhandled SIGHUP is to terminate the # process, so a build that does not implement it lands you in # restart-plugin territory with extra steps. The probe checks # whether the process survived and says so. # restart-plugin kill the plugin and let OpenDeck respawn it. Re-initialises the # device: expect a visible blink and the keys redrawing. Only # tolerable behind apply_min_interval_seconds, never per detent. # restart-opendeck restart the whole application. The last resort — it takes the # whole dock away for a second or two. # # The real fix is upstream and small: make the device plugin watch leds.toml. See # upstream-file-watch-request.md in this directory, which is written and ready to file. set -uo pipefail STRATEGY="${STREAM_DOCK_LED_APPLY_STRATEGY:-none}" PLUGIN_PATTERN="${STREAM_DOCK_PLUGIN_PATTERN:-akp05}" PROBE=false while [[ $# -gt 0 ]]; do case "$1" in --strategy) STRATEGY="${2:-none}"; shift 2 ;; --probe) PROBE=true; shift ;; *) echo "usage: apply-leds.sh [--strategy ] [--probe]" >&2; exit 2 ;; esac done # The pattern is config-supplied, so it is never interpolated into a shell string: # pgrep takes it as a separate argument, and -x is deliberately NOT used because the # plugin's real process name is one of the things the probe is for. # # `pgrep -f` matches whole command lines, which includes THIS script's own and its # parent shell's — a shell invoked as `STREAM_DOCK_PLUGIN_PATTERN=akp05 apply-leds.sh` # carries the pattern in its command line and matches itself. Sending TERM to that is # how a probe kills the terminal it is being run from, so every caller filters out its # own process and its parent. Found the hard way, on the first test run. # Every process between this script and init, so a match on an ancestor's command line # can never be signalled. The parent alone is not enough: run the probe from a shell # that was itself launched with the pattern on its command line and the match lands on # the GRANDparent, which is exactly how the first test run took down the terminal. ancestry() { local pid=$$ ppid while [[ -n "$pid" && "$pid" != "0" && "$pid" != "1" ]]; do echo "$pid" ppid="$(awk '{print $4}' "/proc/$pid/stat" 2>/dev/null)" || break [[ -n "$ppid" ]] || break pid="$ppid" done } exclude_self() { grep -v -x -F -f <(ancestry) || true; } plugin_pids() { pgrep -f -- "$PLUGIN_PATTERN" 2>/dev/null | exclude_self; } opendeck_pids() { pgrep -f -- "opendeck" 2>/dev/null | exclude_self; } do_none() { return 0; } do_signal() { local pids pids="$(plugin_pids || true)" [[ -n "$pids" ]] || { echo "apply-leds: no process matching '$PLUGIN_PATTERN'" >&2; return 1; } # shellcheck disable=SC2086 kill -HUP $pids 2>/dev/null || return 1 return 0 } do_restart_plugin() { local pids pids="$(plugin_pids || true)" [[ -n "$pids" ]] || { echo "apply-leds: no process matching '$PLUGIN_PATTERN'" >&2; return 1; } # TERM, never KILL: the plugin owns a USB device, and giving it the chance to close # the handle is the difference between OpenDeck respawning cleanly and the next # instance finding the device busy. # shellcheck disable=SC2086 kill -TERM $pids 2>/dev/null || return 1 return 0 } do_restart_opendeck() { # Flatpak first, since that is how OpenDeck is normally installed on Linux, then a # plain process. Deliberately no `systemctl --user restart` guess: OpenDeck ships no # user unit, and inventing a name here would fail silently every time. if command -v flatpak >/dev/null && flatpak ps --columns=application 2>/dev/null | grep -q opendeck; then flatpak kill me.amankhanna.opendeck >/dev/null 2>&1 sleep 1 setsid flatpak run me.amankhanna.opendeck >/dev/null 2>&1 & return 0 fi local pids pids="$(opendeck_pids || true)" [[ -n "$pids" ]] || { echo "apply-leds: OpenDeck does not appear to be running" >&2; return 1; } # shellcheck disable=SC2086 kill -TERM $pids 2>/dev/null || return 1 echo "apply-leds: OpenDeck was asked to exit — it is NOT restarted automatically" >&2 echo "apply-leds: outside a Flatpak install; start it again yourself" >&2 return 1 } run_strategy() { case "$1" in none) do_none ;; signal) do_signal ;; restart-plugin) do_restart_plugin ;; restart-opendeck) do_restart_opendeck ;; *) echo "apply-leds: unknown strategy '$1'" >&2; return 2 ;; esac } # --------------------------------------------------------------------------- # Probe # --------------------------------------------------------------------------- if ! $PROBE; then run_strategy "$STRATEGY" exit $? fi ENV_FILE="${STREAM_DOCK_ENV_FILE:-$HOME/.config/stream-dock/led-sync.env}" LEDS_PATH="${STREAM_DOCK_LEDS_PATH:-}" if [[ -z "$LEDS_PATH" && -r "$ENV_FILE" ]]; then LEDS_PATH="$(sed -n 's/^STREAM_DOCK_LEDS_PATH=//p' "$ENV_FILE" | head -1)" fi LEDS_PATH="${LEDS_PATH:-$HOME/.config/opendeck-akp05/leds.toml}" LEDS_PATH="${LEDS_PATH/#\~/$HOME}" cat <&2 fi echo "Processes that look like they could be the device plugin:" ps -eo pid,comm,args 2>/dev/null | grep -iE "opendeck|akp05|streamdock|stream-dock" | grep -v grep \ | sed 's/^/ /' || true echo " (if none of these match '${PLUGIN_PATTERN}', re-run with" echo " STREAM_DOCK_PLUGIN_PATTERN=)" echo if systemctl --user is-active --quiet stream-dock-led-sync 2>/dev/null; then echo "Stopping stream-dock-led-sync for the duration of the probe, so it does not" echo "overwrite the test colours." systemctl --user stop stream-dock-led-sync RESTART_SYNC=true else RESTART_SYNC=false fi restore() { if [[ "${RESTART_SYNC:-false}" == "true" ]]; then echo "Restarting stream-dock-led-sync." systemctl --user start stream-dock-led-sync fi } trap restore EXIT write_test_file() { mkdir -p "$(dirname "$LEDS_PATH")" cat > "${LEDS_PATH}.tmp" <