305 lines
11 KiB
Bash
Executable File
305 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Make the device plugin notice a freshly-written leds.toml.
|
|
#
|
|
# apply-leds.sh --strategy <none|signal|restart-plugin|restart-opendeck>
|
|
# 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 <name>] [--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 <<EOF
|
|
|
|
Stream Dock ring-reload probe
|
|
=============================
|
|
LED file : ${LEDS_PATH}
|
|
Plugin : processes matching '${PLUGIN_PATTERN}'
|
|
|
|
Have the dock plugged in and OpenDeck running, and be able to see the four knob
|
|
rings. Each test writes an obvious colour and asks whether the rings changed.
|
|
|
|
EOF
|
|
|
|
if [[ ! -e "$LEDS_PATH" ]]; then
|
|
echo "warn: ${LEDS_PATH} does not exist yet — run stream-dock/setup-stream-dock.sh first." >&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=<something from the list above>)"
|
|
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" <<EOF
|
|
# Written by apply-leds.sh --probe
|
|
brightness = 100
|
|
|
|
[mode.Static]
|
|
colors = [[$1], [$1], [$1], [$1]]
|
|
EOF
|
|
mv "${LEDS_PATH}.tmp" "$LEDS_PATH"
|
|
}
|
|
|
|
ask() {
|
|
local answer
|
|
read -r -p "$1 [y/N] " answer
|
|
[[ "$answer" == "y" || "$answer" == "Y" ]]
|
|
}
|
|
|
|
WINNER=""
|
|
|
|
echo
|
|
echo "--- Test 1 of 4: does the plugin already watch the file? (strategy: none) ---"
|
|
write_test_file "0, 255, 0"
|
|
echo "Wrote all four rings GREEN. Waiting 3 seconds."
|
|
sleep 3
|
|
if ask "Did the rings turn green?"; then
|
|
WINNER="none"
|
|
else
|
|
echo
|
|
echo "--- Test 2 of 4: SIGHUP the plugin (strategy: signal) ---"
|
|
BEFORE="$(plugin_pids | tr '\n' ' ')"
|
|
write_test_file "0, 0, 255"
|
|
if do_signal; then
|
|
sleep 3
|
|
AFTER="$(plugin_pids | tr '\n' ' ')"
|
|
if [[ -z "$AFTER" ]]; then
|
|
echo "note: the plugin process is GONE — this build does not handle SIGHUP, and the"
|
|
echo " default action terminated it. If the rings changed anyway, OpenDeck"
|
|
echo " respawned it, which is the restart-plugin strategy in disguise."
|
|
elif [[ "$BEFORE" != "$AFTER" ]]; then
|
|
echo "note: the process id changed — it was restarted, not reloaded."
|
|
fi
|
|
if ask "Did the rings turn blue?"; then
|
|
[[ -n "$AFTER" && "$BEFORE" == "$AFTER" ]] && WINNER="signal" || WINNER="restart-plugin"
|
|
fi
|
|
else
|
|
echo "signal could not be sent — skipping."
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "$WINNER" ]]; then
|
|
echo
|
|
echo "--- Test 3 of 4: restart the plugin (strategy: restart-plugin) ---"
|
|
write_test_file "255, 0, 0"
|
|
if do_restart_plugin; then
|
|
echo "Asked the plugin to exit. Waiting 5 seconds for OpenDeck to respawn it."
|
|
sleep 5
|
|
if [[ -z "$(plugin_pids)" ]]; then
|
|
echo "note: nothing came back — OpenDeck does not respawn this plugin on its own,"
|
|
echo " so this strategy is not usable. The dock may need OpenDeck restarted."
|
|
fi
|
|
ask "Did the rings turn red?" && WINNER="restart-plugin"
|
|
else
|
|
echo "could not signal the plugin — skipping."
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "$WINNER" ]]; then
|
|
echo
|
|
echo "--- Test 4 of 4: restart OpenDeck (strategy: restart-opendeck) ---"
|
|
echo "This takes the whole dock away for a moment."
|
|
if ask "Try it?"; then
|
|
write_test_file "255, 0, 255"
|
|
if do_restart_opendeck; then
|
|
echo "Waiting 10 seconds for OpenDeck to come back."
|
|
sleep 10
|
|
ask "Did the rings turn magenta?" && WINNER="restart-opendeck"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
echo
|
|
if [[ -n "$WINNER" ]]; then
|
|
cat <<EOF
|
|
=== Result: '${WINNER}' works ===
|
|
|
|
Set it in CoreSystemConfig.json:
|
|
|
|
"stream_dock": { "knob_leds": { "apply_strategy": "${WINNER}" } }
|
|
|
|
EOF
|
|
if [[ "$WINNER" == "none" ]]; then
|
|
cat <<'EOF'
|
|
That is the good outcome: the plugin picks the file up on its own, so the rings are
|
|
live with no reload hack at all. You can also set apply_min_interval_seconds to 0 —
|
|
there is nothing expensive to pace.
|
|
EOF
|
|
else
|
|
cat <<EOF
|
|
Keep apply_min_interval_seconds at 2 or more: '${WINNER}' re-initialises the device,
|
|
and doing that on every detent of a spun dial is how a dock ends up blinking instead
|
|
of lighting.
|
|
EOF
|
|
fi
|
|
echo
|
|
echo "Then re-run: stream-dock/setup-stream-dock.sh"
|
|
else
|
|
cat <<'EOF'
|
|
=== Result: nothing tested here makes the plugin re-read the file ===
|
|
|
|
That is a real answer, not a failure of the probe. The rings will keep showing
|
|
whatever they had at plugin start, and the fix is upstream: see
|
|
stream-dock/upstream-file-watch-request.md, which is written and ready to file
|
|
against the device plugin. Everything on this side is already done — the moment the
|
|
plugin watches the file, the rings go live with no change here.
|
|
EOF
|
|
fi
|