fix(on-device-install): persistent install path + syslog-based logging

Bundles the install-time hygiene work for issues #268 and #250.

# Install location — #268

Stock SoundTouch rootfs has only a few MB free (~4 MB on the ST20
the reporter captured); the AfterTouch binary is ~12 MB. The previous
flow downloaded into tmpfs (/media/aftertouch) and then `mv`'d the
binary into /opt/aftertouch on rootfs — which fails with
"No space left on device" on any speaker with the standard layout.

install.sh now installs to /mnt/nv/aftertouch by default (the
persistent partition, ~30 MB free on the same captures) and points
/opt/aftertouch at it via a symlink so the init script's hardcoded
DAEMON path keeps working unchanged. Power users can override with
INSTALL_DIR=/some/other/path. The interactive prompt from the
community patch in #268's thread is dropped — STDIN is the curl
pipe under the documented `curl | sh` invocation, so a read prompt
would hang or read garbage.

uninstall.sh is updated to resolve the symlink and remove the
target before unlinking, so the 12 MB binary doesn't get orphaned
on /mnt/nv when users uninstall.

# Logging — #250

Issue #250 surfaced a "running but unreachable" state: the install
script reported AfterTouch as running, the init script's status
agreed, but `curl :8000` returned connection-refused. start-stop-
daemon's --background detaches stdout/stderr, so any panic the
daemon emitted before dying went to /dev/null with no diagnostic
trail.

The fix is to route the daemon's stdout/stderr through `logger -t
aftertouch` so output lands in BusyBox syslog — a bounded in-memory
ring buffer that never grows on disk (writing to a file in /mnt/nv
would have eaten the volume over months). Diagnostic flow is now:

    logread        | grep aftertouch | tail -20
    logread -f     | grep aftertouch     # live tail

Matches the recipe already documented in TROUBLESHOOTING.md for the
speaker's own logs (Curl 7 section).

Tightening on top of the syslog change:

  - The init script's `status` case now also curls localhost:8000
    when the PID is alive — distinguishes "PID alive, listener up"
    from "PID alive, listener silently died" (which is what fooled
    everyone on #250). A bare PID-liveness check returned "running"
    in both cases.

  - install.sh's post-install verification now does its own 10s
    curl probe after the init script returns; on failure it tails
    the aftertouch syslog so the user sees the actual error rather
    than the install script claiming success.

  - `exec` is added inside the start-stop-daemon's shell wrapper so
    --make-pidfile records the daemon's own PID (not the shell's),
    which keeps `stop` semantics correct.

README updated to document the install location, INSTALL_DIR
override, and the syslog tag.

No automated tests — these are shell scripts the install pipeline
runs once on the device. All three scripts pass `bash -n` /
`sh -n` syntax checks. Real validation is end-user retest, gated on
the next release.

Refs #268, refs #250.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-05-15 17:11:02 +02:00
co-authored by Claude Opus 4.7
parent 554fa78c0b
commit 064fe80e18
4 changed files with 123 additions and 18 deletions
+12 -1
View File
@@ -16,10 +16,21 @@ If your device doesn't expose the port, you can still use the on-device installe
### Space Limitation
The storage space on the SoundTouch devices is very limited. At the moment only one AfterTouch installation barely fits on them with enough room for the data it needs to maintain. When installing, make sure that you have removed any binaries and folders of previous installation attempts.
The storage space on the SoundTouch devices is very limited — stock rootfs typically has only a few MB free (e.g. ~4 MB on the ST20, see issue #268), well below the AfterTouch binary's ~12 MB. To work around this, the installer puts everything on `/mnt/nv/aftertouch` by default (the persistent partition, typically ~30 MB free) and points `/opt/aftertouch` at it via a symlink so the init script and runtime paths stay unchanged. Override the install target with `INSTALL_DIR=/some/path` if you've got room elsewhere.
The space limitation also means we are currently unsure on how to update the system, because two binaries are already too large. We are currently working on this - both by checking how we can make the binaries smaller, but also on how we can extend the storage space (e.g. by running AfterTouch from a USB drive).
### Logs
The daemon writes to BusyBox syslog (tagged `aftertouch`) rather than to a file. Disk usage stays bounded — the syslog ring buffer is in memory — and the same `logread` recipe used elsewhere in this project works:
```sh
logread | grep aftertouch | tail -20 # recent entries
logread -f | grep aftertouch # live tail
```
If the install command reports "running but :8000 not responding" or `aftertouch status` reports the listener is down, the syslog tail is the first place to look.
## Installation
Enable SSH on your SoundTouch device using the usual "Stick with remote_services" method. Connect with the following command.
+34 -4
View File
@@ -17,6 +17,7 @@ PIDFILE="/var/run/$NAME.pid"
DATADIR="/opt/aftertouch/data"
SCRIPTNAME="/etc/init.d/$NAME"
USER="root"
LOG_TAG="aftertouch"
# Export PATH
@@ -41,6 +42,17 @@ case "$1" in
mkdir -p "$DATADIR"
# Pipe 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
#
# `exec` on the daemon replaces /bin/sh so --make-pidfile records
# the daemon's own PID (not the shell wrapper). The `logger`
# process sits on the read end of the pipe and exits cleanly
# when the daemon dies and closes its end.
start-stop-daemon --start \
--quiet \
--pidfile "$PIDFILE" \
@@ -48,7 +60,7 @@ case "$1" in
--make-pidfile \
--chuid "$USER" \
--startas "/bin/sh" \
-- -c "\"$DAEMON\" --data-dir '$DATADIR' --record-interactions=false --discovery-interval=60m"
-- -c "exec \"$DAEMON\" --data-dir '$DATADIR' --record-interactions=false --discovery-interval=60m 2>&1 | logger -t $LOG_TAG"
tries=0
max_tries=60
@@ -60,6 +72,9 @@ case "$1" in
tries=$((tries + 1))
done
echo "ERROR: daemon started but http://localhost:8000 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
;;
@@ -86,12 +101,27 @@ case "$1" in
if [ -f "$PIDFILE" ]; then
PID=$(cat "$PIDFILE")
if kill -0 "$PID" 2>/dev/null; then
echo "$NAME is running."
# 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:8000 >/dev/null 2>&1; then
echo "$NAME is running (PID $PID, http://localhost:8000 responding)."
exit 0
else
echo "$NAME PID $PID is alive but http://localhost:8000 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)."
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
;;
@@ -102,4 +132,4 @@ case "$1" in
esac
exit 0
exit 0
+56 -10
View File
@@ -5,22 +5,47 @@ VERSION=${VERSION:-0.79.0}
GH_REPO=${GH_REPO:-gesellix/Bose-SoundTouch}
BINARY_URL=${BINARY_URL:-https://github.com/$GH_REPO/releases/download/v$VERSION/soundtouch-service-v$VERSION-linux-armv7}
INIT_SCRIPT_URL=${INIT_SCRIPT_URL:-https://raw.githubusercontent.com/$GH_REPO/v$VERSION/scripts/on-device-install/aftertouch}
UPDATE_TMP_DIR=${UPDATE_TMP_DIR:-/media/aftertouch}
# Default install location is /mnt/nv/aftertouch (the persistent
# partition), not /opt/aftertouch on rootfs. Stock SoundTouch rootfs
# has ~4 MB free on devices like the ST20 (issue #268); the
# AfterTouch binary is ~12 MB. /mnt/nv typically has tens of MB
# free and persists across reboots the same way /opt would.
#
# /opt/aftertouch becomes a symlink into the install target so the
# init script's hardcoded DAEMON path keeps working unchanged.
#
# Power users can override with INSTALL_DIR=/some/other/path.
INSTALL_DIR=${INSTALL_DIR:-/mnt/nv/aftertouch}
# Scratch directory for the download. /media is tmpfs on most
# SoundTouch firmware, fine for transient files but unrelated to
# the persistent install target.
UPDATE_TMP_DIR=${UPDATE_TMP_DIR:-/media/aftertouch}
rm -rf "$UPDATE_TMP_DIR" || true
mkdir -p "$UPDATE_TMP_DIR"
echo "Installing Aftertouch $VERSION ..."
mkdir -p /opt/aftertouch
echo "Installing AfterTouch $VERSION to $INSTALL_DIR ..."
mkdir -p "$INSTALL_DIR"
# Wire /opt/aftertouch -> $INSTALL_DIR so the init script
# (DAEMON=/opt/aftertouch/aftertouch-service) finds the binary
# regardless of which target we picked. Replace any prior
# /opt/aftertouch (directory or stale symlink) before re-creating.
if [ "$INSTALL_DIR" != "/opt/aftertouch" ]; then
rm -rf /opt/aftertouch
ln -sf "$INSTALL_DIR" /opt/aftertouch
fi
curl \
-sSL \
-o "$UPDATE_TMP_DIR/binary" \
--fail \
"$BINARY_URL"
mv "$UPDATE_TMP_DIR/binary" /opt/aftertouch/aftertouch-service
chmod +x /opt/aftertouch/aftertouch-service
mv "$UPDATE_TMP_DIR/binary" "$INSTALL_DIR/aftertouch-service"
chmod +x "$INSTALL_DIR/aftertouch-service"
echo "Creating init script..."
curl \
@@ -33,12 +58,33 @@ mv "$UPDATE_TMP_DIR/init-script" /etc/init.d/aftertouch
chmod +x /etc/init.d/aftertouch
update-rc.d aftertouch defaults
echo "Installation complete. Running initial startup to accelerate future startups..."
echo "Installation complete. Running initial startup..."
/etc/init.d/aftertouch start
/etc/init.d/aftertouch status
echo "Installation complete. Aftertouch $VERSION is now running on your device."
echo "You can try to connect to at http://<your-device-ip>:8000 ."
echo "If the connection fails, reconnect ssh with port forwarding like:"
echo "ssh -L 8000:localhost:8000 root@<IP_ADDRESS_OF_SPEAKER>"
# Post-install verification: the init script's own poll loop only
# checks that the daemon registered a PID file; that's not enough
# evidence the listener is actually serving HTTP. Issue #250 shipped
# with a "running but unreachable" state where status was green and
# `curl :8000` got connection-refused. Re-check directly here and
# surface the recent syslog if it fails — the init script pipes the
# daemon's stdout/stderr through `logger -t aftertouch`, so panics
# land in busybox syslog and `logread` reads them out.
if curl -fsS --max-time 10 http://localhost:8000 >/dev/null 2>&1; then
echo "Installation complete. AfterTouch $VERSION is now running on your device."
echo "Connect to http://<your-device-ip>:8000 from another machine on the LAN."
echo "If the device doesn't expose :8000 directly, port-forward via SSH:"
echo " ssh -L 8000:localhost:8000 root@<IP_ADDRESS_OF_SPEAKER>"
else
echo "WARNING: the init script reports AfterTouch as running, but" >&2
echo " http://localhost:8000 isn't responding. The daemon may have" >&2
echo " panicked shortly after start. Recent aftertouch syslog:" >&2
echo "" >&2
logread 2>/dev/null | grep aftertouch | tail -20 >&2 || \
echo " (logread returned nothing for tag 'aftertouch'; the daemon" >&2
echo "" >&2
echo " For a live view of the daemon's output, run:" >&2
echo " logread -f | grep aftertouch" >&2
exit 1
fi
+21 -3
View File
@@ -1,4 +1,22 @@
/etc/init.d/aftertouch stop
rm -rf /etc/init.d/aftertouch
#!/bin/sh
# Uninstall AfterTouch on-device. Handles both the historical
# layout (/opt/aftertouch as a directory) and the post-#268 layout
# (/opt/aftertouch as a symlink into /mnt/nv/aftertouch).
set -eu
/etc/init.d/aftertouch stop || true
rm -f /etc/init.d/aftertouch
update-rc.d -f aftertouch remove
rm -rf /opt/aftertouch
# If /opt/aftertouch is a symlink, resolve it and remove the target
# before unlinking, so we don't leave ~12 MB of orphan binary on
# /mnt/nv. Tolerate either layout — readlink -f returns the same
# path for a real directory, and rm -rf on a missing path with
# set -eu would abort.
target="$(readlink -f /opt/aftertouch 2>/dev/null || echo /opt/aftertouch)"
if [ -e "$target" ]; then
rm -rf "$target"
fi
if [ -L /opt/aftertouch ] || [ -e /opt/aftertouch ]; then
rm -rf /opt/aftertouch
fi