#!/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