mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-20 17:46:19 +00:00
On an on-device install, soundtouch-service defaulted its server URL to os.Hostname() when --server-url wasn't set. Since the service runs on the speaker's own Linux, that returns the speaker's internal variant codename (e.g. "spotty", "mojo") -- never resolvable, not even by the speaker itself -- breaking TuneIn/BMX playback with CURL ErrorCode 6 (issue #546). Add a --deployment-mode/DEPLOYMENT_MODE flag (on-device, private-network, public-network) so the fallback is chosen deliberately instead of guessed: on-device defaults to localhost, public-network refuses to start rather than guess a public address, and the previous hostname-guessing behavior is kept for private-network/unset installs, now with a startup warning. The on-device init script sets DEPLOYMENT_MODE=on-device automatically and now auto-exports aftertouch.conf into the daemon's environment generally, which also unblocks discussion #610 (setting MGMT_USERNAME/MGMT_PASSWORD on-device) without any further code change. Verified end-to-end on real ST20 hardware: service now resolves http://localhost:8000, a re-migrate updates the speaker's own runtime config to match, and TuneIn playback works again. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
333 lines
13 KiB
Bash
333 lines
13 KiB
Bash
#!/bin/sh
|
|
### BEGIN INIT INFO
|
|
# Provides: aftertouch-service
|
|
# Required-Start: $network $local_fs
|
|
# Required-Stop: $network $local_fs
|
|
# Default-Start: 2 3 4 5
|
|
# Default-Stop: 0 1 6
|
|
# Short-Description: Run AfterTouch on this device
|
|
# Description: Start/stop AfterTouch soundtouch-service
|
|
### END INIT INFO
|
|
|
|
|
|
NAME="aftertouch-service"
|
|
DESC="Bose AfterTouch service"
|
|
DAEMON="/opt/aftertouch/aftertouch-service"
|
|
PIDFILE="/var/run/$NAME.pid"
|
|
DATADIR="/opt/aftertouch/data"
|
|
CONFFILE="/opt/aftertouch/aftertouch.conf"
|
|
SCRIPTNAME="/etc/init.d/$NAME"
|
|
USER="root"
|
|
LOG_TAG="aftertouch"
|
|
|
|
|
|
# Export PATH
|
|
export PATH="/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin"
|
|
|
|
|
|
# Optional settings written by install.sh (AFTERTOUCH_LAN_PORT, SERVICE_PORT),
|
|
# or added by hand for anything the daemon reads from its environment
|
|
# (SERVER_URL, MGMT_USERNAME, MGMT_PASSWORD, DEPLOYMENT_MODE, ...). `set -a`
|
|
# auto-exports every assignment while the file is sourced, so any such
|
|
# variable actually reaches the daemon -- it's forked from this same shell's
|
|
# environment further down via `--startas "/bin/sh" -- -c "... \"$DAEMON\" ..."`.
|
|
# Sourced before the defaults below so it can override either.
|
|
if [ -r "$CONFFILE" ]; then
|
|
set -a
|
|
# shellcheck source=/dev/null
|
|
. "$CONFFILE"
|
|
set +a
|
|
fi
|
|
|
|
# Port the daemon binds locally. Kept in one variable because it appears in
|
|
# the daemon arguments, the readiness poll and `status` -- three places that
|
|
# used to hardcode 8000 independently, so changing one silently broke the
|
|
# other two.
|
|
SERVICE_PORT="${SERVICE_PORT:-8000}"
|
|
|
|
# LAN entry port: a port number, "auto" (default), or "none".
|
|
LAN_PORT_MODE="${AFTERTOUCH_LAN_PORT:-auto}"
|
|
|
|
# This script only ever runs on the speaker itself, so the deployment mode is
|
|
# not a guess -- default it here (overridable via aftertouch.conf, though that
|
|
# should never be needed). Exported so soundtouch-service picks it up via
|
|
# DEPLOYMENT_MODE without needing a --deployment-mode flag threaded through
|
|
# the daemon invocation below.
|
|
export DEPLOYMENT_MODE="${DEPLOYMENT_MODE:-on-device}"
|
|
|
|
|
|
# Sanity check executable
|
|
test -x "$DAEMON" || {
|
|
echo "ERROR: Cannot execute $DAEMON (check path and permissions)." >&2
|
|
exit 1
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# LAN entry-port redirect
|
|
#
|
|
# On chassis built around a BCO ("SMSC") Wi-Fi/Bluetooth co-processor,
|
|
# inbound LAN traffic only reaches this Linux SoC for a fixed set of Bose's
|
|
# own service ports, which appears to be compiled into the co-processor's
|
|
# firmware. AfterTouch's :8000 is not on that list, so a LAN client's SYN
|
|
# never arrives here at all -- confirmed on an ST20 (`spotty`), where
|
|
# `tcpdump -i eth0` on the speaker saw zero packets for :8000 while Bose's
|
|
# own :8090/:8091/:17000 answered normally from the same client. The usual
|
|
# suspects were all ruled out: the service does bind 0.0.0.0 correctly, the
|
|
# speaker's iptables is empty, and SSH over the same path works.
|
|
#
|
|
# Workaround: NAT one of the relayed Bose ports to ours. The default, 17008,
|
|
# is Bose's SoftwareUpdate listener -- its cloud is gone, so taking over its
|
|
# inbound traffic costs nothing real. Only external traffic is matched
|
|
# (`! -i lo`), so anything running on the speaker still reaches both the real
|
|
# service on loopback and AfterTouch on :8000 as before.
|
|
#
|
|
# Credit: the STR / SoundTouch Reborn project (github.com/JRpersonal/streborn)
|
|
# documented and shipped this REDIRECT technique first, using the same entry
|
|
# port for the same reason.
|
|
#
|
|
# Which models need this is tracked in
|
|
# docs/content/docs/reference/MODEL-SUPPORT-MATRIX.md.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Resolve LAN_PORT_MODE into $LAN_PORT. Returns non-zero when no redirect
|
|
# should be installed.
|
|
lan_redirect_port() {
|
|
case "$LAN_PORT_MODE" in
|
|
none|off|disabled|0)
|
|
return 1
|
|
;;
|
|
auto|"")
|
|
# Only auto-enable where direct LAN access is known not to work.
|
|
# has-bco is Bose's own helper: [ "$(cat /proc/module_type)" = scm ]
|
|
has-bco >/dev/null 2>&1 || return 1
|
|
LAN_PORT=17008
|
|
;;
|
|
*[!0-9]*)
|
|
echo "WARNING: ignoring AFTERTOUCH_LAN_PORT='$LAN_PORT_MODE'; expected a port number, 'auto' or 'none'." >&2
|
|
return 1
|
|
;;
|
|
*)
|
|
LAN_PORT="$LAN_PORT_MODE"
|
|
;;
|
|
esac
|
|
return 0
|
|
}
|
|
|
|
# Remove every PREROUTING rule pointing at our service port, whatever entry
|
|
# port it used, so changing AFTERTOUCH_LAN_PORT cannot orphan the old rule.
|
|
lan_redirect_purge() {
|
|
iptables -t nat -S PREROUTING 2>/dev/null \
|
|
| grep -- "--to-ports $SERVICE_PORT" \
|
|
| sed 's/^-A /-D /' \
|
|
| while read -r rule; do
|
|
# shellcheck disable=SC2086
|
|
iptables -t nat $rule 2>/dev/null || true
|
|
done
|
|
}
|
|
|
|
lan_redirect_apply() {
|
|
lan_redirect_port || return 0
|
|
|
|
if ! iptables -t nat -L PREROUTING -n >/dev/null 2>&1; then
|
|
echo "WARNING: this kernel has no iptables nat table; :$LAN_PORT was not" >&2
|
|
echo " redirected. Reach AfterTouch over an SSH tunnel instead." >&2
|
|
return 0
|
|
fi
|
|
|
|
lan_redirect_purge
|
|
|
|
# Safety net for a kernel whose iptables lacks -S (purge would no-op):
|
|
# without this, every restart would stack another duplicate rule.
|
|
if iptables -t nat -C PREROUTING ! -i lo -p tcp --dport "$LAN_PORT" \
|
|
-j REDIRECT --to-ports "$SERVICE_PORT" 2>/dev/null; then
|
|
echo "LAN access already active on port $LAN_PORT."
|
|
return 0
|
|
fi
|
|
|
|
if iptables -t nat -I PREROUTING 1 ! -i lo -p tcp --dport "$LAN_PORT" \
|
|
-j REDIRECT --to-ports "$SERVICE_PORT" 2>/dev/null; then
|
|
echo "LAN access: port $LAN_PORT now reaches AfterTouch on :$SERVICE_PORT."
|
|
else
|
|
echo "WARNING: could not install the :$LAN_PORT -> :$SERVICE_PORT redirect." >&2
|
|
fi
|
|
}
|
|
|
|
lan_redirect_remove() {
|
|
lan_redirect_purge
|
|
}
|
|
|
|
|
|
case "$1" in
|
|
start)
|
|
echo "Starting $DESC..."
|
|
|
|
mount -o remount,rw / >/dev/null 2>&1 || {
|
|
echo "ERROR: remount failed." >&2
|
|
exit 1
|
|
}
|
|
|
|
mkdir -p "$DATADIR"
|
|
|
|
# Route stdout + stderr through `logger -t $LOG_TAG` so the
|
|
# daemon's output lands in busybox syslog (bounded ring buffer,
|
|
# never grows on disk). Users diagnose with:
|
|
#
|
|
# logread | grep aftertouch | tail -20
|
|
# logread -f | grep aftertouch # live tail
|
|
#
|
|
# This used to be a `--startas "/bin/sh" -- -c "exec $DAEMON | logger"`
|
|
# pipeline, on the theory that `exec` replaces /bin/sh so --make-pidfile
|
|
# records the daemon's own PID. That's wrong for a *piped* command:
|
|
# POSIX requires each side of a pipe to run in its own forked process,
|
|
# so the top-level /bin/sh forks two children (one execs into the
|
|
# daemon, one becomes logger) and stays alive itself, blocked in
|
|
# wait() -- --make-pidfile recorded *that* wrapper's PID, not the
|
|
# daemon's. `stop` then killed the wrapper, which doesn't forward
|
|
# SIGTERM to its children, orphaning the real daemon (reparented to
|
|
# init) to keep running -- and keep holding :8000 -- forever, silently
|
|
# surviving every later stop/start/restart.
|
|
#
|
|
# A first fix attempt dropped the wrapper shell entirely in favor of
|
|
# `--exec "$DAEMON"` directly, with a plain shell-level `>FIFO`
|
|
# redirection on the start-stop-daemon invocation. That broke logging
|
|
# instead: this busybox's `--background` resets the backgrounded
|
|
# child's own stdio, ignoring the outer redirection, so the daemon's
|
|
# output never reached the FIFO -- confirmed on hardware (`logger`
|
|
# exited immediately with nothing to read, `logread` showed nothing
|
|
# new).
|
|
#
|
|
# This version keeps a wrapper shell -- its *own* FIFO redirection,
|
|
# set up by its own script logic rather than inherited from outside,
|
|
# isn't affected by whatever --background did to its stdio -- but has
|
|
# the wrapper record the daemon's real PID itself instead of trusting
|
|
# --make-pidfile. $! after a single, non-piped backgrounded command is
|
|
# portably that command's own PID; --make-pidfile can only ever see
|
|
# whatever process start-stop-daemon directly forked (the wrapper),
|
|
# never a PID from inside it.
|
|
LOGFIFO="/tmp/$NAME.fifo"
|
|
rm -f "$LOGFIFO"
|
|
mkfifo "$LOGFIFO"
|
|
|
|
# --pidfile (without --make-pidfile, since the wrapper writes it itself
|
|
# once it knows the daemon's real PID) makes start-stop-daemon's own
|
|
# "already running?" check keyed on *our* pidfile, not on "/bin/sh"
|
|
# identity. Without this, --startas "/bin/sh" is itself the match
|
|
# criterion -- and since the wrapper stays alive for the daemon's whole
|
|
# lifetime (blocked in its own `wait`), and `stop` only confirms the
|
|
# *daemon* PID died (not that the wrapper has finished tearing down),
|
|
# a `restart` firing `start` right after `stop` can catch the previous
|
|
# wrapper still mid-teardown. start-stop-daemon then silently refuses
|
|
# ("/bin/sh is already running", swallowed by --quiet) while the
|
|
# script burns its full 120s timeout waiting for a daemon that was
|
|
# never launched. Confirmed on hardware: a bare
|
|
# `start-stop-daemon --startas "/bin/sh" -- -c "echo hi"` was refused
|
|
# with exactly that message while a prior wrapper was still alive.
|
|
start-stop-daemon --start \
|
|
--quiet \
|
|
--pidfile "$PIDFILE" \
|
|
--background \
|
|
--chuid "$USER" \
|
|
--startas "/bin/sh" \
|
|
-- -c "logger -t $LOG_TAG <'$LOGFIFO' & \"$DAEMON\" --data-dir '$DATADIR' --port '$SERVICE_PORT' --record-interactions=false --discovery-interval=60m >'$LOGFIFO' 2>&1 & echo \$! >'$PIDFILE'; wait"
|
|
|
|
tries=0
|
|
max_tries=60
|
|
while [ $tries -lt $max_tries ]; do
|
|
if curl -fsS "http://localhost:$SERVICE_PORT" >/dev/null 2>&1; then
|
|
# Only once the service actually answers is it worth pointing LAN
|
|
# traffic at it.
|
|
lan_redirect_apply
|
|
exit 0
|
|
fi
|
|
sleep 2
|
|
tries=$((tries + 1))
|
|
done
|
|
|
|
echo "ERROR: daemon started but http://localhost:$SERVICE_PORT never responded within $((max_tries * 2))s." >&2
|
|
echo " Inspect the daemon's syslog output:" >&2
|
|
echo " logread | grep $LOG_TAG | tail -20" >&2
|
|
exit 1
|
|
;;
|
|
|
|
stop)
|
|
echo "Stopping $DESC..."
|
|
# Drop the LAN redirect first: leaving it in place while nothing listens
|
|
# would silently blackhole the entry port.
|
|
lan_redirect_remove
|
|
if [ -f "$PIDFILE" ]; then
|
|
PID=$(cat "$PIDFILE")
|
|
start-stop-daemon --stop \
|
|
--quiet \
|
|
--oknodo \
|
|
--pidfile "$PIDFILE"
|
|
# Wait up to 15 s for SIGTERM to take effect before escalating.
|
|
# The Go HTTP server exits promptly on SIGTERM in normal conditions;
|
|
# the loop handles the rare case where it is stuck in a blocking syscall.
|
|
tries=0
|
|
while [ $tries -lt 15 ] && kill -0 "$PID" 2>/dev/null; do
|
|
sleep 1
|
|
tries=$((tries + 1))
|
|
done
|
|
if kill -0 "$PID" 2>/dev/null; then
|
|
echo "Warning: $NAME (PID $PID) still alive after ${tries}s; sending SIGKILL..." >&2
|
|
kill -9 "$PID" 2>/dev/null || true
|
|
sleep 1
|
|
fi
|
|
rm -f "$PIDFILE"
|
|
else
|
|
echo "No $NAME running (no PID file)." >&2
|
|
fi
|
|
;;
|
|
|
|
restart|force-reload)
|
|
"$0" stop
|
|
sleep 2
|
|
"$0" start
|
|
;;
|
|
|
|
status)
|
|
if [ -f "$PIDFILE" ]; then
|
|
PID=$(cat "$PIDFILE")
|
|
if kill -0 "$PID" 2>/dev/null; then
|
|
# PID is alive — does it actually serve HTTP? A live process
|
|
# with a dead listener is the symptom behind issue #250
|
|
# (Gustour's ST30: status said running, curl said
|
|
# connection-refused). Distinguish the two states here so
|
|
# status isn't a false-positive.
|
|
if curl -fsS --max-time 3 "http://localhost:$SERVICE_PORT" >/dev/null 2>&1; then
|
|
echo "$NAME is running (PID $PID, http://localhost:$SERVICE_PORT responding)."
|
|
if lan_redirect_port; then
|
|
if iptables -t nat -C PREROUTING ! -i lo -p tcp --dport "$LAN_PORT" \
|
|
-j REDIRECT --to-ports "$SERVICE_PORT" 2>/dev/null; then
|
|
echo "LAN access: reachable from other machines on port $LAN_PORT."
|
|
else
|
|
echo "LAN access: redirect for port $LAN_PORT is NOT installed." >&2
|
|
fi
|
|
fi
|
|
exit 0
|
|
else
|
|
echo "$NAME PID $PID is alive but http://localhost:$SERVICE_PORT is not responding." >&2
|
|
echo "Recent log:" >&2
|
|
logread 2>/dev/null | grep "$LOG_TAG" | tail -10 >&2
|
|
exit 3
|
|
fi
|
|
else
|
|
echo "$NAME is not running (PID file exists but process is dead)." >&2
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "$NAME is not running."
|
|
exit 3
|
|
fi
|
|
;;
|
|
|
|
*)
|
|
echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload|status}"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
|
|
exit 0
|