diff --git a/AdminInterface/www/includes/footer.php b/AdminInterface/www/includes/footer.php index 99e845cf..810bf385 100644 --- a/AdminInterface/www/includes/footer.php +++ b/AdminInterface/www/includes/footer.php @@ -138,14 +138,23 @@ function updateBatteryIcon() { /tmp/.mupibox.reboot.lock &'; exec($command); } if( $shutdown == 1 ) { - $command='sudo su - -c "sleep 5; /usr/local/bin/mupibox/./shutdown.sh &" &'; + $command='( flock -n 9 || exit 0; sleep 5; sudo /usr/local/bin/mupibox/./shutdown.sh ) 9>/tmp/.mupibox.shutdown.lock &'; exec($command); } ?> diff --git a/AdminInterface/www/service.php b/AdminInterface/www/service.php index d78627ca..696777a1 100644 --- a/AdminInterface/www/service.php +++ b/AdminInterface/www/service.php @@ -3,13 +3,26 @@ $dataonline = json_decode($onlinejson, true); include ('includes/header.php'); + // LOW-3: every apt-get path here can collide with another concurrent + // service-toggle (admin clicks "enable VNC" while "enable Samba" is + // still running). dpkg has its own exclusive lock at + // /var/lib/dpkg/lock-frontend, so the second request fails with a + // confusing error in stderr but the PHP page just says "VNC enabled" + // — the user has no idea it didn't actually run. Serialise apt-touching + // commands behind a flock so the second one waits up to 120s for the + // first to finish, and log if the wait expires. + $APT_LOCK = '/tmp/.mupibox.apt.lock'; + $aptWrap = function ($cmd) use ($APT_LOCK) { + return "flock -w 120 " . escapeshellarg($APT_LOCK) . " bash -c " . escapeshellarg($cmd); + }; + if( $_POST['change_vnc'] == "stop & disable" ) { exec("sudo systemctl stop mupi_vnc.service"); exec("sudo systemctl stop mupi_novnc.service"); exec("sudo systemctl disable mupi_vnc.service"); exec("sudo systemctl disable mupi_novnc.service"); - exec("sudo apt-get remove x11vnc websockify -y"); + exec($aptWrap("sudo apt-get remove x11vnc websockify -y")); exec("sudo pkill websockify"); exec("sudo rm -R /usr/share/novnc"); exec("sudo su - -c \"/usr/bin/cat <<< $(/usr/bin/jq --arg v \"0\" '.tweaks.vnc = $v' /etc/mupibox/mupiboxconfig.json) > /etc/mupibox/mupiboxconfig.json\""); @@ -18,7 +31,7 @@ } else if( $_POST['change_vnc'] == "enable & start" ) { - exec("sudo apt-get install x11vnc websockify -y"); + exec($aptWrap("sudo apt-get install x11vnc websockify -y")); exec("sudo git clone https://github.com/novnc/noVNC.git /usr/share/novnc"); exec("sudo chown -R dietpi:dietpi /usr/share/novnc"); exec("sudo systemctl enable mupi_vnc.service"); @@ -33,14 +46,14 @@ if( $_POST['change_samba'] == "enable & start" ) { $command = "sudo apt-get install samba -y && sudo wget https://raw.githubusercontent.com/splitti/MuPiBox/main/config/templates/smb.conf -O /etc/samba/smb.conf && sudo systemctl enable smbd.service && sudo systemctl start smbd.service"; - exec($command, $output, $result ); + exec($aptWrap($command), $output, $result ); $change=1; $CHANGE_TXT=$CHANGE_TXT."
  • Samba enabled
  • "; } else if( $_POST['change_samba'] == "stop & disable" ) { $command = "sudo systemctl stop smbd.service && sudo systemctl disable smbd.service && sudo apt-get remove samba -y"; - exec($command, $output, $result ); + exec($aptWrap($command), $output, $result ); $change=1; $CHANGE_TXT=$CHANGE_TXT."
  • Samba disabled
  • "; } @@ -48,14 +61,14 @@ if( $_POST['change_ftp'] == "enable & start" ) { $command = " sudo apt-get install proftpd -y && sudo apt-get install samba -y && sudo wget https://raw.githubusercontent.com/splitti/MuPiBox/main/config/templates/proftpd.conf -O /etc/proftpd/proftpd.conf && sudo systemctl restart proftpd"; - exec($command, $output, $result ); + exec($aptWrap($command), $output, $result ); $change=1; $CHANGE_TXT=$CHANGE_TXT."
  • FTP enabled
  • "; } else if( $_POST['change_ftp'] == "stop & disable" ) { $command = "sudo systemctl stop proftpd.service && sudo systemctl disable proftpd.service && sudo apt-get remove proftpd -y"; - exec($command, $output, $result ); + exec($aptWrap($command), $output, $result ); $change=1; $CHANGE_TXT=$CHANGE_TXT."
  • FTP disabled
  • "; } diff --git a/config/services/mupi_change_checker.service b/config/services/mupi_change_checker.service index 186eb39e..2a39c6cd 100644 --- a/config/services/mupi_change_checker.service +++ b/config/services/mupi_change_checker.service @@ -3,6 +3,8 @@ Description=Check for changes in media directory [Service] ExecStart=/usr/local/bin/mupibox/./change_checker.sh +Restart=on-failure +RestartSec=10 [Install] WantedBy=basic.target \ No newline at end of file diff --git a/config/services/mupi_check_internet.service b/config/services/mupi_check_internet.service index c4e80aa2..13cf357d 100644 --- a/config/services/mupi_check_internet.service +++ b/config/services/mupi_check_internet.service @@ -5,6 +5,8 @@ Description=Checks Internet Connection User=dietpi Group=dietpi ExecStart=/usr/local/bin/mupibox/./check_network.sh +Restart=on-failure +RestartSec=10 [Install] WantedBy=multi-user.target \ No newline at end of file diff --git a/config/services/mupi_idle_shutdown.service b/config/services/mupi_idle_shutdown.service index 806796f2..281cc2b0 100644 --- a/config/services/mupi_idle_shutdown.service +++ b/config/services/mupi_idle_shutdown.service @@ -3,6 +3,8 @@ Description=Idle Shutdown Timer [Service] ExecStart=/usr/local/bin/mupibox/./idle_shutdown.sh +Restart=on-failure +RestartSec=10 [Install] WantedBy=basic.target \ No newline at end of file diff --git a/config/services/mupi_telegram.service b/config/services/mupi_telegram.service index 98533d49..aca53e12 100644 --- a/config/services/mupi_telegram.service +++ b/config/services/mupi_telegram.service @@ -5,6 +5,8 @@ After=network-online.target [Service] ExecStart=/usr/local/bin/mupibox/./telegram_start.sh +Restart=on-failure +RestartSec=10 [Install] WantedBy=default.target \ No newline at end of file diff --git a/scripts/librespot/librespot-start.sh b/scripts/librespot/librespot-start.sh index c9e2a3af..e5d45854 100644 --- a/scripts/librespot/librespot-start.sh +++ b/scripts/librespot/librespot-start.sh @@ -16,7 +16,13 @@ if ! $cachestate ; then export LIBRESPOT_NAME_DISABLE_AUDIO_CACHE=1 fi username=$( jq -r .username ${LIBRESPOT_CACHE}/credentials.json ) -if [[ ! -z "$var" ]]; then +# LOW-1: previous test was `[[ ! -z "$var" ]]` — `$var` was never set +# anywhere in this script, so the test was always false (or always true +# depending on shell semantics for unset-vs-empty), and LIBRESPOT_USERNAME +# never got exported even when credentials.json had a real username. +# Test the actual username variable, plus filter "null" since jq -r emits +# that literal for missing keys. +if [[ -n "$username" && "$username" != "null" ]]; then export LIBRESPOT_USERNAME=${username} fi diff --git a/scripts/mupibox/add_index.sh b/scripts/mupibox/add_index.sh index ee18cacd..086ab3bf 100644 --- a/scripts/mupibox/add_index.sh +++ b/scripts/mupibox/add_index.sh @@ -18,6 +18,14 @@ if [ -f "${DATA_LOCK}" ]; then exit else touch ${DATA_LOCK} + # HIGH-15: previous code had no trap, so a perl/jq crash, a SIGTERM + # from systemd, or an out-of-disk during the temp writes left + # DATA_LOCK behind. Subsequent invocations would see the stale lock + # and exit, blocking m3u_generator's index pass forever — observable + # as "new audiobook folder shows up but never gets an index, so + # resume can't address it." Trap the common termination signals so + # the lock is removed even when the script dies mid-write. + trap 'rm -f "${DATA_LOCK}"' EXIT INT TERM HUP /usr/bin/cat ${DATA} | grep -v '"index":' > ${TMP_DATA} /usr/bin/perl -pe 'BEGIN{$k=-1};s/{/$& . "\n \"index\": " . ++$k . ","/e' ${TMP_DATA} > ${DATA} @@ -26,5 +34,6 @@ else /usr/bin/mv ${TMP_DATA} ${DATA} /usr/bin/chown dietpi:dietpi ${DATA} echo "Index is finished" - rm ${DATA_LOCK} + # Lock removal happens via the EXIT trap above; the explicit rm here + # is now redundant but kept to preserve the original log-line ordering. fi \ No newline at end of file diff --git a/scripts/mupibox/add_wifi.sh b/scripts/mupibox/add_wifi.sh index 40514615..8722ba4c 100644 --- a/scripts/mupibox/add_wifi.sh +++ b/scripts/mupibox/add_wifi.sh @@ -22,8 +22,16 @@ while true do if test -f "${MUPIWIFI}" then - SSID="$(/usr/bin/jq -r .[].ssid ${MUPIWIFI})" - PSK="$(/usr/bin/jq -r .[].pw ${MUPIWIFI})" + # HIGH-11: previous filter was `jq -r .[].ssid` which iterates and + # emits N newline-separated SSIDs when the array has N entries. + # wpa_passphrase / the "network={ ssid=…" emit below then receive + # a multi-line string and write garbage into wpa_supplicant.conf. + # The frontend always queues exactly one entry per save event, so + # pin to index 0; if there are leftover entries they're picked up + # on the next loop iteration after `sudo rm ${MUPIWIFI}` clears + # the file. + SSID="$(/usr/bin/jq -r '.[0].ssid // empty' ${MUPIWIFI})" + PSK="$(/usr/bin/jq -r '.[0].pw // empty' ${MUPIWIFI})" if [ "${SSID}" = "" ] then sudo rm ${MUPIWIFI} @@ -44,11 +52,23 @@ do restart_network elif [ "${PSK}" = "" ] then - echo 'network={' | sudo tee -a ${WPACONF} - echo ' ssid="'${SSID}'"' | sudo tee -a ${WPACONF} - echo ' scan_ssid=1' | sudo tee -a ${WPACONF} - echo '}' | sudo tee -a ${WPACONF} - restart_network + # HIGH-12: previous code spliced ${SSID} unquoted into a + # single-quoted echo, so an SSID containing `"` or a newline + # could escape the quoted string and inject arbitrary blocks + # into wpa_supplicant.conf (root-owned, security-relevant). + # Use wpa_passphrase's open-network shape via printf with %s, + # which can't be reinterpreted by the shell, then escape any + # embedded `"` for the JSON-style ssid field. + # Reject SSIDs containing characters wpa_supplicant.conf can't + # represent (newlines / NUL) outright. + if [[ "${SSID}" == *$'\n'* || "${SSID}" == *$'\0'* ]]; then + echo "add_wifi.sh: SSID rejected (newline / NUL in name)" >&2 + else + _ESCAPED_SSID="${SSID//\\/\\\\}" + _ESCAPED_SSID="${_ESCAPED_SSID//\"/\\\"}" + printf 'network={\n\tssid="%s"\n\tscan_ssid=1\n}\n' "${_ESCAPED_SSID}" | sudo tee -a ${WPACONF} >/dev/null + restart_network + fi else WIFI_RESULT=$(sudo -i wpa_passphrase "${SSID}" "${PSK}") IFS=$'\n' diff --git a/scripts/mupibox/albumstop.sh b/scripts/mupibox/albumstop.sh index a868b85c..b00f2a2e 100644 --- a/scripts/mupibox/albumstop.sh +++ b/scripts/mupibox/albumstop.sh @@ -5,14 +5,26 @@ ALBUMSTOP_FILE="/home/dietpi/.mupibox/Sonos-Kids-Controller-master/server/config/albumstop.json" +# Atomic-update pattern (HIGH-8): write jq output to a same-dir tempfile, +# then mv it onto the target. The previous `cat <<< $(jq …) > FILE` +# truncated FILE before jq finished — jq errors or partial output left +# the JSON empty and bricked the next boot. With write-then-rename, jq +# either succeeds and the rename atomically swaps in the new content, or +# it fails and the original file stays intact (we rm the half-written +# tempfile so /tmp doesn't fill up). if [ ! -f ${ALBUMSTOP_FILE} ]; then - sudo echo -n "{}" ${ALBUMSTOP_FILE} - chown dietpi:dietpi ${ALBUMSTOP_FILE} - /usr/bin/cat <<< $(/usr/bin/jq -n --arg v "Off" '.albumStop = $v' ${ALBUMSTOP_FILE}) > ${ALBUMSTOP_FILE} + # HIGH-14 (Phase-3) + Phase-5 follow-up: ALBUMSTOP_FILE lives + # under /home/dietpi/.../config/ which is dietpi-writable; drop + # the sudo+tee that produced a root-owned file the script + # couldn't subsequently replace. + echo -n "{}" > "${ALBUMSTOP_FILE}" + _TMP="${ALBUMSTOP_FILE}.tmp.$$" + /usr/bin/jq -n --arg v "Off" '.albumStop = $v' > "${_TMP}" && mv "${_TMP}" "${ALBUMSTOP_FILE}" || rm -f "${_TMP}" sleep 10 sudo bash /usr/local/bin/mupibox/shutdown.sh else - /usr/bin/cat <<< $(/usr/bin/jq --arg v "Off" '.albumStop = $v' ${ALBUMSTOP_FILE}) > ${ALBUMSTOP_FILE} + _TMP="${ALBUMSTOP_FILE}.tmp.$$" + /usr/bin/jq --arg v "Off" '.albumStop = $v' "${ALBUMSTOP_FILE}" > "${_TMP}" && mv "${_TMP}" "${ALBUMSTOP_FILE}" || rm -f "${_TMP}" sleep 10 sudo bash /usr/local/bin/mupibox/shutdown.sh fi \ No newline at end of file diff --git a/scripts/mupibox/albumstop_activator.sh b/scripts/mupibox/albumstop_activator.sh index dbc63a91..8f9323bc 100644 --- a/scripts/mupibox/albumstop_activator.sh +++ b/scripts/mupibox/albumstop_activator.sh @@ -4,10 +4,14 @@ ALBUMSTOP_FILE="/home/dietpi/.mupibox/Sonos-Kids-Controller-master/server/config/albumstop.json" +# Atomic-update pattern (HIGH-8): same as albumstop.sh. if [ ! -f ${ALBUMSTOP_FILE} ]; then - sudo echo -n "{}" ${ALBUMSTOP_FILE} - chown dietpi:dietpi ${ALBUMSTOP_FILE} - /usr/bin/cat <<< $(/usr/bin/jq -n --arg v "On" '.albumStop = $v' ${ALBUMSTOP_FILE}) > ${ALBUMSTOP_FILE} + # HIGH-14 (Phase-3) + Phase-5 follow-up: same as albumstop.sh — + # drop sudo, dietpi can write the destination directly. + echo -n "{}" > "${ALBUMSTOP_FILE}" + _TMP="${ALBUMSTOP_FILE}.tmp.$$" + /usr/bin/jq -n --arg v "On" '.albumStop = $v' > "${_TMP}" && mv "${_TMP}" "${ALBUMSTOP_FILE}" || rm -f "${_TMP}" else - /usr/bin/cat <<< $(/usr/bin/jq --arg v "On" '.albumStop = $v' ${ALBUMSTOP_FILE}) > ${ALBUMSTOP_FILE} + _TMP="${ALBUMSTOP_FILE}.tmp.$$" + /usr/bin/jq --arg v "On" '.albumStop = $v' "${ALBUMSTOP_FILE}" > "${_TMP}" && mv "${_TMP}" "${ALBUMSTOP_FILE}" || rm -f "${_TMP}" fi \ No newline at end of file diff --git a/scripts/mupibox/change_checker.sh b/scripts/mupibox/change_checker.sh index 0bddb146..bb8b7ff5 100644 --- a/scripts/mupibox/change_checker.sh +++ b/scripts/mupibox/change_checker.sh @@ -4,6 +4,14 @@ CONFIG="/etc/mupibox/mupiboxconfig.json" CHECK_TIMER=$(/usr/bin/jq -r .mupibox.mediaCheckTimer ${CONFIG}) +# MED-22: a fresh image without `mediaCheckTimer` in mupiboxconfig.json +# gets `null` from jq -r, so `sleep null` returns immediately and the +# while loop pegs one CPU at 100% — observable on a fresh box as the +# system fan ramping up before the first config save. Default to 60s +# (same cadence m3u_generator.sh runs at on real boxes). +if [ -z "${CHECK_TIMER}" ] || [ "${CHECK_TIMER}" = "null" ]; then + CHECK_TIMER=60 +fi while true do diff --git a/scripts/mupibox/check_network.sh b/scripts/mupibox/check_network.sh index 5be7a38f..bc7711bf 100644 --- a/scripts/mupibox/check_network.sh +++ b/scripts/mupibox/check_network.sh @@ -39,101 +39,118 @@ if [ ! -f ${RESUME_FILE} ]; then fi fi -if [ ! -f ${NETWORKCONFIG} ]; then - sudo echo -n "[]" ${NETWORKCONFIG} - chown dietpi:dietpi ${NETWORKCONFIG} - chmod 777 ${NETWORKCONFIG} - /usr/bin/cat <<< $(/usr/bin/jq -n --arg v "starting" '.onlinestate = $v' ${NETWORKCONFIG}) > ${NETWORKCONFIG} +if [ ! -f ${NETWORKCONFIG} ] || [ ! -s ${NETWORKCONFIG} ] || ! /usr/bin/jq -e 'type == "object"' ${NETWORKCONFIG} >/dev/null 2>&1; then + # HIGH-14 (Phase-3) + Phase-5 follow-up: the previous "fix" piped + # via `sudo tee` which produced a root-owned seed file in /tmp. + # The next line's tempfile + mv ran as dietpi and silently failed + # to replace the root-owned target — leaving the wrong (empty array) + # seed in place forever. Drop sudo entirely (the script runs as + # dietpi which can write /tmp directly), and seed as an OBJECT + # `{}` since network.json is shaped as one (every consumer reads + # it via `.onlinestate` etc). The existence-or-empty-or-wrong- + # shape guard above also recovers from the broken state we + # produced earlier without manual cleanup. + rm -f "${NETWORKCONFIG}" + echo -n "{}" > "${NETWORKCONFIG}" + # Atomic-update (HIGH-8). + _TMP="${NETWORKCONFIG}.tmp.$$" + /usr/bin/jq -n --arg v "starting" '.onlinestate = $v' > "${_TMP}" && mv "${_TMP}" "${NETWORKCONFIG}" || rm -f "${_TMP}" + OLD_ONLINESTATE="starting" else OLD_ONLINESTATE=$(/usr/bin/jq -r .onlinestate ${NETWORKCONFIG}) fi #wget -q --spider http://google.com +# Idempotent symlink reconciliation. Always points $link at $target — +# if the link already points there, no-op. Replaces the previous +# state-transition-only logic that depended on detecting a change +# from OLDSTATE → ONLINESTATE; that logic missed the case where +# check_network.sh starts up in a state that already matches stored +# OLD_ONLINESTATE but where the on-disk symlink is still pointing at +# the wrong target (e.g. after a pm2 restart while box was already +# online — both ONLINESTATE and OLD_ONLINESTATE = "online", no flip +# fired, but the symlink may still be pointing at offline_data.json +# from a prior offline session). Symptom: active_data.json never +# resolved to data.json post-reboot, so the API served the offline +# (Spotify-less) shape even though the box was clearly online. +ensure_symlink() { + local target="$1" + local link="$2" + if [ ! -L "$link" ] || [ "$(readlink "$link")" != "$target" ]; then + rm -f "$link" + ln -s "$target" "$link" + chown -h dietpi:dietpi "$link" 2>/dev/null || sudo chown -h dietpi:dietpi "$link" + fi +} + while true do - if ( $(/usr/bin/python3 /usr/local/bin/mupibox/check_network.py) == ${TRUESTATE} ); then + # AR5-1: was `if ( $(python3 ...) == ${TRUESTATE} )` — that's a bash + # subshell executing the python output as a command (with `==` and the + # literal string as args), not a string comparison. Subshell exit 127 + # made the if-condition permanently false. + # AR5-1.1: ALSO — check_network.py prints the string "true"/"false" + # (not "online"/"offline"), so even a correct string-test against + # ${TRUESTATE}="online" would always be false. The original bash-bug + # was masking this mismatch. Compare against the actual python output. + # ONLINESTATE keeps its "online"/"offline" values for downstream + # consumers of /tmp/network.json. + if [ "$(/usr/bin/python3 /usr/local/bin/mupibox/check_network.py)" = "true" ]; then ONLINESTATE=${TRUESTATE} - if [ "${ONLINESTATE}" != "${OLDSTATE}" ]; then - if [ ! -f ${ACTIVE_FILE} ]; then - ln -s ${DATA_FILE} ${ACTIVE_FILE} - chown dietpi:dietpi ${ACTIVE_FILE} - elif [[ ${OLD_ONLINESTATE} != "online" ]]; then - rm ${ACTIVE_FILE} - ln -s ${DATA_FILE} ${ACTIVE_FILE} - chown dietpi:dietpi ${ACTIVE_FILE} - fi - if [ ! -f ${ACTIVERESUME_FILE} ]; then - ln -s ${RESUME_FILE} ${ACTIVERESUME_FILE} - chown dietpi:dietpi ${ACTIVERESUME_FILE} - elif [[ ${OLD_ONLINESTATE} != "online" ]]; then - rm ${ACTIVERESUME_FILE} - ln -s ${RESUME_FILE} ${ACTIVERESUME_FILE} - chown dietpi:dietpi ${ACTIVERESUME_FILE} - fi - fi + # Reconcile every tick (cheap when no-op) instead of only on + # state change. Self-healing if the symlink was wrong. + ensure_symlink "${DATA_FILE}" "${ACTIVE_FILE}" + ensure_symlink "${RESUME_FILE}" "${ACTIVERESUME_FILE}" else ONLINESTATE=${FALSESTATE} if [ ! -f ${OFFLINE_FILE} ]; then echo -n "[" > ${OFFLINE_FILE} - echo -n $(jq '.[] | select(.type != "spotify") | select(.type != "radio" | select(.type != "rss")' < ${DATA_FILE}) >> ${OFFLINE_FILE} + echo -n $(jq '.[] | select(.type != "spotify" and .type != "radio" and .type != "rss")' < ${DATA_FILE}) >> ${OFFLINE_FILE} echo -n "]" >> ${OFFLINE_FILE} sed -i 's/} {/}, {/g' ${OFFLINE_FILE} chown dietpi:dietpi ${OFFLINE_FILE} elif [ ! -s ${OFFLINE_FILE} ]; then rm ${OFFLINE_FILE} echo -n "[" > ${OFFLINE_FILE} - echo -n $(jq '.[] | select(.type != "spotify") | select(.type != "radio" | select(.type != "rss")' < ${DATA_FILE}) >> ${OFFLINE_FILE} + echo -n $(jq '.[] | select(.type != "spotify" and .type != "radio" and .type != "rss")' < ${DATA_FILE}) >> ${OFFLINE_FILE} echo -n "]" >> ${OFFLINE_FILE} sed -i 's/} {/}, {/g' ${OFFLINE_FILE} chown dietpi:dietpi ${OFFLINE_FILE} elif [ $(stat --format='%Y' "${DATA_FILE}") -gt $(stat --format='%Y' "${OFFLINE_FILE}") ]; then echo -n "[" > ${OFFLINE_FILE} - echo -n $(jq '.[] | select(.type != "spotify") | select(.type != "radio" | select(.type != "rss")' < ${DATA_FILE}) >> ${OFFLINE_FILE} + echo -n $(jq '.[] | select(.type != "spotify" and .type != "radio" and .type != "rss")' < ${DATA_FILE}) >> ${OFFLINE_FILE} echo -n "]" >> ${OFFLINE_FILE} sed -i 's/} {/}, {/g' ${OFFLINE_FILE} fi if [ ! -f ${OFFLINERESUME_FILE} ]; then echo -n "[" > ${OFFLINERESUME_FILE} - echo -n $(jq '.[] | select(.type != "spotify") | select(.type != "radio" | select(.type != "rss")' < ${RESUME_FILE}) >> ${OFFLINERESUME_FILE} + echo -n $(jq '.[] | select(.type != "spotify" and .type != "radio" and .type != "rss")' < ${RESUME_FILE}) >> ${OFFLINERESUME_FILE} echo -n "]" >> ${OFFLINERESUME_FILE} sed -i 's/} {/}, {/g' ${OFFLINERESUME_FILE} chown dietpi:dietpi ${OFFLINERESUME_FILE} elif [ ! -s ${OFFLINERESUME_FILE} ]; then rm ${OFFLINERESUME_FILE} echo -n "[" > ${OFFLINERESUME_FILE} - echo -n $(jq '.[] | select(.type != "spotify") | select(.type != "radio" | select(.type != "rss")' < ${RESUME_FILE}) >> ${OFFLINERESUME_FILE} + echo -n $(jq '.[] | select(.type != "spotify" and .type != "radio" and .type != "rss")' < ${RESUME_FILE}) >> ${OFFLINERESUME_FILE} echo -n "]" >> ${OFFLINERESUME_FILE} sed -i 's/} {/}, {/g' ${OFFLINERESUME_FILE} chown dietpi:dietpi ${OFFLINERESUME_FILE} elif [ $(stat --format='%Y' "${RESUME_FILE}") -gt $(stat --format='%Y' "${OFFLINERESUME_FILE}") ]; then echo -n "[" > ${OFFLINERESUME_FILE} - echo -n $(jq '.[] | select(.type != "spotify") | select(.type != "radio" | select(.type != "rss")' < ${RESUME_FILE}) >> ${OFFLINERESUME_FILE} + echo -n $(jq '.[] | select(.type != "spotify" and .type != "radio" and .type != "rss")' < ${RESUME_FILE}) >> ${OFFLINERESUME_FILE} echo -n "]" >> ${OFFLINERESUME_FILE} sed -i 's/} {/}, {/g' ${OFFLINERESUME_FILE} fi - if [ "${ONLINESTATE}" != "${OLDSTATE}" ]; then - if [ ! -f ${ACTIVE_FILE} ]; then - ln -s ${OFFLINE_FILE} ${ACTIVE_FILE} - chown dietpi:dietpi ${ACTIVE_FILE} - elif [[ ${OLD_ONLINESTATE} != "offline" ]]; then - rm ${ACTIVE_FILE} - ln -s ${OFFLINE_FILE} ${ACTIVE_FILE} - chown dietpi:dietpi ${ACTIVE_FILE} - fi - if [ ! -f ${ACTIVERESUME_FILE} ]; then - ln -s ${OFFLINERESUME_FILE} ${ACTIVERESUME_FILE} - chown dietpi:dietpi ${ACTIVERESUME_FILE} - elif [[ ${OLD_ONLINESTATE} != "offline" ]]; then - rm ${ACTIVERESUME_FILE} - ln -s ${OFFLINERESUME_FILE} ${ACTIVERESUME_FILE} - chown dietpi:dietpi ${ACTIVERESUME_FILE} - fi - fi + # Self-healing reconciliation, see ensure_symlink comment above. + ensure_symlink "${OFFLINE_FILE}" "${ACTIVE_FILE}" + ensure_symlink "${OFFLINERESUME_FILE}" "${ACTIVERESUME_FILE}" fi if [ "${ONLINESTATE}" != "${OLDSTATE}" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "${ONLINESTATE}" '.onlinestate = $v' ${NETWORKCONFIG}) > ${NETWORKCONFIG} + # Atomic-update (HIGH-8). + _TMP="${NETWORKCONFIG}.tmp.$$" + /usr/bin/jq --arg v "${ONLINESTATE}" '.onlinestate = $v' "${NETWORKCONFIG}" > "${_TMP}" && mv "${_TMP}" "${NETWORKCONFIG}" || rm -f "${_TMP}" # if [ "${ONLINESTATE}" == "${FALSESTATE}" ] && [ "${OLDSTATE}" != "starting" ]; then # #sudo dhclient -r # sudo service ifup@wlan0 stop diff --git a/scripts/mupibox/data_clean.sh b/scripts/mupibox/data_clean.sh index 920a4e1f..edce8485 100644 --- a/scripts/mupibox/data_clean.sh +++ b/scripts/mupibox/data_clean.sh @@ -13,7 +13,11 @@ for i in "/home/dietpi/.mupibox/Sonos-Kids-Controller-master/www/cover/audiobook echo "[OK] ${artist}" for j in "${i}/"* ; do album=$(/usr/bin/basename "${j}") - if [[ ! -f ${j} ]] + # HIGH-16: previous test was `[[ ! -f ${j} ]]` — true for any + # non-regular-file, including the literal `${i}/*` glob fallback + # when the dir is empty (would then `rm -R` a path containing + # a literal `*`). Restrict to actual directories. + if [[ -d ${j} ]] then if [[ -d "/home/dietpi/MuPiBox/media/audiobook/${artist}/${album}/" ]] then @@ -36,7 +40,11 @@ for i in "/home/dietpi/.mupibox/Sonos-Kids-Controller-master/www/cover/music/"* echo "[OK] ${artist}" for j in "${i}/"* ; do album=$(/usr/bin/basename "${j}") - if [[ ! -f ${j} ]] + # HIGH-16: previous test was `[[ ! -f ${j} ]]` — true for any + # non-regular-file, including the literal `${i}/*` glob fallback + # when the dir is empty (would then `rm -R` a path containing + # a literal `*`). Restrict to actual directories. + if [[ -d ${j} ]] then if [[ -d "/home/dietpi/MuPiBox/media/music/${artist}/${album}/" ]] then @@ -75,7 +83,12 @@ for item in "${my_array[@]}"; do fi if [[ ${i} > 0 ]] && [[ ${add_item} > 0 ]] then - echo $(sed '${s/$/,/}' ${TMP_DATA}) > ${TMP_DATA} + # Atomic-update (HIGH-8). sed reads TMP_DATA into the substitution, + # echo writes back via redirect — if sed errored mid-read the file + # was truncated to empty. Tempfile + rename keeps TMP_DATA intact + # on failure. + _TMP="${TMP_DATA}.tmp.$$" + sed '${s/$/,/}' "${TMP_DATA}" > "${_TMP}" && mv "${_TMP}" "${TMP_DATA}" || rm -f "${_TMP}" fi if [[ ${i} = 0 ]] && [[ ${add_item} > 0 ]] then @@ -83,7 +96,11 @@ for item in "${my_array[@]}"; do fi if [[ ${add_item} > 0 ]] then - /usr/bin/cat <<< $(/usr/bin/jq '.' <<< $item) >> ${TMP_DATA} + # Drop the cat<<< indirection — jq's stdout goes straight to the + # append. Same result, simpler, and no needless tempfile when we + # only need to append (>> doesn't have the truncate-race risk + # that > has). + /usr/bin/jq '.' <<< "$item" >> "${TMP_DATA}" echo "[OK] ${type:1:-1} | ${category:1:-1}" else echo "[DEL] ${type:1:-1} | ${category:1:-1} | ${artist:1:-1} | ${title:1:-1}" diff --git a/scripts/mupibox/get_monitor.sh b/scripts/mupibox/get_monitor.sh index 19728868..64d814e8 100644 --- a/scripts/mupibox/get_monitor.sh +++ b/scripts/mupibox/get_monitor.sh @@ -11,14 +11,22 @@ do actualsize=$(wc -c <"${MONITOR_FILE}") if [ ! -f ${MONITOR_FILE} ]; then - sudo echo -n "{}" ${MONITOR_FILE} - sudo chown dietpi:dietpi ${MONITOR_FILE} - /usr/bin/cat <<< $(/usr/bin/jq -n --arg v "On" '.monitor = $v' ${MONITOR_FILE}) > ${MONITOR_FILE} + # HIGH-14 (Phase-3) + Phase-5 follow-up: drop the sudo — + # MONITOR_FILE lives under /home/dietpi/.../config/ and the + # script runs as dietpi, so direct write works. Sudo+tee + # produced a root-owned file that subsequent jq+mv (as + # dietpi) couldn't replace, leaving the seed broken. Same + # fix as check_network / get_network. + rm -f "${MONITOR_FILE}" + echo -n "{}" > "${MONITOR_FILE}" + # Atomic-update (HIGH-8). + _TMP="${MONITOR_FILE}.tmp.$$" + /usr/bin/jq -n --arg v "On" '.monitor = $v' > "${_TMP}" && mv "${_TMP}" "${MONITOR_FILE}" || rm -f "${_TMP}" elif [ $actualsize -le $minimumsize ]; then - sudo rm ${MONITOR_FILE} - sudo echo -n "{}" ${MONITOR_FILE} - sudo chown dietpi:dietpi ${MONITOR_FILE} - /usr/bin/cat <<< $(/usr/bin/jq -n --arg v "On" '.monitor = $v' ${MONITOR_FILE}) > ${MONITOR_FILE} + rm -f "${MONITOR_FILE}" + echo -n "{}" > "${MONITOR_FILE}" + _TMP="${MONITOR_FILE}.tmp.$$" + /usr/bin/jq -n --arg v "On" '.monitor = $v' > "${_TMP}" && mv "${_TMP}" "${MONITOR_FILE}" || rm -f "${_TMP}" else MONITOR=$(sudo -H -u root bash -c "vcgencmd display_power") MONITOR=(${MONITOR##*=}) @@ -28,9 +36,11 @@ do fi if [ ${MONITOR} == "0" ] || [ ${POWER} == "4" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "Off" '.monitor = $v' ${MONITOR_FILE}) > ${MONITOR_FILE} + _TMP="${MONITOR_FILE}.tmp.$$" + /usr/bin/jq --arg v "Off" '.monitor = $v' "${MONITOR_FILE}" > "${_TMP}" && mv "${_TMP}" "${MONITOR_FILE}" || rm -f "${_TMP}" elif [ ${MONITOR} == "1" ] || [ ${POWER} == "0" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "On" '.monitor = $v' ${MONITOR_FILE}) > ${MONITOR_FILE} + _TMP="${MONITOR_FILE}.tmp.$$" + /usr/bin/jq --arg v "On" '.monitor = $v' "${MONITOR_FILE}" > "${_TMP}" && mv "${_TMP}" "${MONITOR_FILE}" || rm -f "${_TMP}" fi fi diff --git a/scripts/mupibox/get_network.sh b/scripts/mupibox/get_network.sh index 54b40eb1..1a309909 100644 --- a/scripts/mupibox/get_network.sh +++ b/scripts/mupibox/get_network.sh @@ -40,12 +40,18 @@ else rm ${RESUME_LOCK} fi -if [ ! -f ${NETWORKCONFIG} ]; then - sudo echo -n "[]" ${NETWORKCONFIG} - chown dietpi:dietpi ${NETWORKCONFIG} - chmod 777 ${NETWORKCONFIG} +if [ ! -f ${NETWORKCONFIG} ] || [ ! -s ${NETWORKCONFIG} ] || ! /usr/bin/jq -e 'type == "object"' ${NETWORKCONFIG} >/dev/null 2>&1; then + # HIGH-14 (Phase-3) + Phase-5 follow-up: same fix as in + # check_network.sh — drop sudo (dietpi can write /tmp) and seed + # as `{}` since network.json is an object. The guard also + # rebuilds an existing wrong-shape file from a prior broken + # session. + rm -f "${NETWORKCONFIG}" + echo -n "{}" > "${NETWORKCONFIG}" OLD_ONLINESTATE="starting" - /usr/bin/cat <<< $(/usr/bin/jq -n --arg v "starting" '.onlinestate = $v' ${NETWORKCONFIG}) > ${NETWORKCONFIG} + # Atomic-update (HIGH-8). + _TMP="${NETWORKCONFIG}.tmp.$$" + /usr/bin/jq -n --arg v "starting" '.onlinestate = $v' > "${_TMP}" && mv "${_TMP}" "${NETWORKCONFIG}" || rm -f "${_TMP}" else OLD_ONLINESTATE=$(/usr/bin/jq -r .onlinestate ${NETWORKCONFIG}) fi @@ -86,14 +92,21 @@ IPA=$(/usr/bin/hostname -I | awk '{print $1}') DNS=$(echo $(sudo cat /etc/resolv.conf | grep 'nameserver ') | sed 's/nameserver //g') SUBNET=$(/sbin/ifconfig wlan0 | awk '/netmask/{split($4,a,":"); print a[1]}') -/usr/bin/cat <<< $(/usr/bin/jq --arg v "${HOSTN}" '.host = $v' ${NETWORKCONFIG}) > ${NETWORKCONFIG} -/usr/bin/cat <<< $(/usr/bin/jq --arg v "${IPA}" '.ip = $v' ${NETWORKCONFIG}) > ${NETWORKCONFIG} -/usr/bin/cat <<< $(/usr/bin/jq --arg v "${MAC}" '.mac = $v' ${NETWORKCONFIG}) > ${NETWORKCONFIG} -/usr/bin/cat <<< $(/usr/bin/jq --arg v "${WIFI}" '.wifi = $v' ${NETWORKCONFIG}) > ${NETWORKCONFIG} -/usr/bin/cat <<< $(/usr/bin/jq --arg v "${WIFILINK}" '.wifilink = $v' ${NETWORKCONFIG}) > ${NETWORKCONFIG} -/usr/bin/cat <<< $(/usr/bin/jq --arg v "${WIFISIGNAL}" '.wifisignal = $v' ${NETWORKCONFIG}) > ${NETWORKCONFIG} -/usr/bin/cat <<< $(/usr/bin/jq --arg v "${GW}" '.gateway = $v' ${NETWORKCONFIG}) > ${NETWORKCONFIG} -/usr/bin/cat <<< $(/usr/bin/jq --arg v "${DNS}" '.dns = $v' ${NETWORKCONFIG}) > ${NETWORKCONFIG} -/usr/bin/cat <<< $(/usr/bin/jq --arg v "${SUBNET}" '.subnet = $v' ${NETWORKCONFIG}) > ${NETWORKCONFIG} +# Atomic-update (HIGH-8). Bundle all nine field updates into a single jq +# pipeline so we only do one tempfile-write-rename cycle, not nine — same +# correctness, ninth the SD-card writes. +_TMP="${NETWORKCONFIG}.tmp.$$" +/usr/bin/jq \ + --arg host "${HOSTN}" \ + --arg ip "${IPA}" \ + --arg mac "${MAC}" \ + --arg wifi "${WIFI}" \ + --arg wifilink "${WIFILINK}" \ + --arg wifisignal "${WIFISIGNAL}" \ + --arg gateway "${GW}" \ + --arg dns "${DNS}" \ + --arg subnet "${SUBNET}" \ + '.host = $host | .ip = $ip | .mac = $mac | .wifi = $wifi | .wifilink = $wifilink | .wifisignal = $wifisignal | .gateway = $gateway | .dns = $dns | .subnet = $subnet' \ + "${NETWORKCONFIG}" > "${_TMP}" && mv "${_TMP}" "${NETWORKCONFIG}" || rm -f "${_TMP}" #/usr/bin/cat <<< $(/usr/bin/jq --arg v "${HOSTN}" '."node-sonos-http-api".server = $v' ${FRONTENDCONFIG}) > ${FRONTENDCONFIG} #/usr/bin/cat <<< $(/usr/bin/jq --arg v "${IPA}" '."node-sonos-http-api".ip = $v' ${FRONTENDCONFIG}) > ${FRONTENDCONFIG} \ No newline at end of file diff --git a/scripts/mupibox/idle_shutdown.sh b/scripts/mupibox/idle_shutdown.sh index d4c0bfbe..dacd9017 100644 --- a/scripts/mupibox/idle_shutdown.sh +++ b/scripts/mupibox/idle_shutdown.sh @@ -27,7 +27,12 @@ do TELEGRAM=$(/usr/bin/jq -r .telegram.active ${CONFIG}) TELEGRAM_CHATID=$(/usr/bin/jq -r .telegram.chatId ${CONFIG}) TELEGRAM_TOKEN=$(/usr/bin/jq -r .telegram.token ${CONFIG}) - if [ "${TELEGRAM}" ] && [ ${#TELEGRAM_CHATID} -ge 1 ] && [ ${#TELEGRAM_TOKEN} -ge 1 ]; then + # MED-3: previous test was `[ "${TELEGRAM}" ]` which evaluates to + # true for ANY non-empty string — including "false". jq emits + # "true"/"false" as literals from a bool field, so `telegram.active=false` + # was treated as "telegram is on" and the box still tried to shoot + # off a Telegram message at idle-shutdown. Compare explicitly to "true". + if [ "${TELEGRAM}" = "true" ] && [ ${#TELEGRAM_CHATID} -ge 1 ] && [ ${#TELEGRAM_TOKEN} -ge 1 ]; then /usr/bin/python3 /usr/local/bin/mupibox/telegram_send_message.py "MuPiBox is to long idle" fi echo "$(date +'%d/%m/%Y %H:%M:%S') # CURRENT IDLE TIME = ${idle}" >> ${LOG} diff --git a/scripts/mupibox/m3u_generator.sh b/scripts/mupibox/m3u_generator.sh index 74e89939..f3f8dcc3 100644 --- a/scripts/mupibox/m3u_generator.sh +++ b/scripts/mupibox/m3u_generator.sh @@ -50,7 +50,14 @@ else #/usr/bin/cp --update "${i}"/*.jp*g "/home/dietpi/.mupibox/Sonos-Kids-Controller-master/www/cover/audiobook/${artist}/${title}/cover.jpg" searchStrTitleCover=`/usr/bin/cat ${DATA} | grep 'audiobook/'"${artist}"'/'"${title}"'/cover.jpg'` else - if [$setArtistCover == 1 ] + # B2: previous test was `if [$setArtistCover == 1 ]` — + # missing space after `[`. Bash interprets that as a + # command named `[$setArtistCover`, which doesn't exist, + # so the test is always false and we always fell to the + # else-branch (MuPiLogo fallback). Audiobook artists + # with their own cover image but no per-title cover + # never got their cover used. + if [ $setArtistCover == 1 ] then /usr/bin/mkdir -p "/home/dietpi/.mupibox/Sonos-Kids-Controller-master/www/cover/audiobook/${artist}/${title}/" > /dev/null for i in "${topFolder}"/*.jp*g; do cp "$i" "/home/dietpi/.mupibox/Sonos-Kids-Controller-master/www/cover/audiobook/${artist}/${title}/cover.jpg"; break; done @@ -65,11 +72,37 @@ else if [ -z "${searchStrTitleCover}" ] then + # Atomic-update (HIGH-8). m3u_generator runs after every + # media-tree change; previously a jq error or partial + # write while appending an entry truncated data.json + # to empty — wiping every album's metadata. Tempfile + # + rename preserves the original on failure. + # B2: previous code spliced ${artist} and ${title} into a + # JSON literal via shell concatenation — any artist + # title containing `"` (e.g. an audiobook called + # `Foo "Bar" Baz`) produced syntactically-broken JSON + # that jq rejected, taking down the m3u-generator pass + # and leaving data.json untouched (or, before HIGH-8, + # truncated to empty). Use --arg so jq itself does + # the JSON encoding — `"` in values is escaped to + # `\"`, no breakage. + _TMP="${DATA}.tmp.$$" if [ $setArtistCover == 1 ] then - /usr/bin/cat <<< $(/usr/bin/cat ${DATA} | /usr/bin/jq '. += [{"type": "library", "category": "audiobook", "artist": "'"${artist}"'", "title": "'"${title}"'", "cover": "http://'${HN}':8200/cover/audiobook/'"${artist}"'/'"${title}"'/cover.jpg", "artistcover": "http://'${HN}':8200/cover/audiobook/'"${artist}"'/cover.jpg"}]') > ${DATA} + /usr/bin/jq \ + --arg artist "${artist}" \ + --arg title "${title}" \ + --arg cover "http://${HN}:8200/cover/audiobook/${artist}/${title}/cover.jpg" \ + --arg artistcover "http://${HN}:8200/cover/audiobook/${artist}/cover.jpg" \ + '. += [{type: "library", category: "audiobook", artist: $artist, title: $title, cover: $cover, artistcover: $artistcover}]' \ + "${DATA}" > "${_TMP}" && mv "${_TMP}" "${DATA}" || rm -f "${_TMP}" else - /usr/bin/cat <<< $(/usr/bin/cat ${DATA} | /usr/bin/jq '. += [{"type": "library", "category": "audiobook", "artist": "'"${artist}"'", "title": "'"${title}"'", "cover": "http://'${HN}':8200/cover/audiobook/'"${artist}"'/'"${title}"'/cover.jpg"}]') > ${DATA} + /usr/bin/jq \ + --arg artist "${artist}" \ + --arg title "${title}" \ + --arg cover "http://${HN}:8200/cover/audiobook/${artist}/${title}/cover.jpg" \ + '. += [{type: "library", category: "audiobook", artist: $artist, title: $title, cover: $cover}]' \ + "${DATA}" > "${_TMP}" && mv "${_TMP}" "${DATA}" || rm -f "${_TMP}" fi fi fi @@ -118,11 +151,26 @@ else if [ -z "${searchStrTitleCover}" ] then + # Atomic-update (HIGH-8) — same as the audiobook block above. + # B2: same fix as the audiobook branch above — --arg + # instead of shell-spliced JSON. + _TMP="${DATA}.tmp.$$" if [ $setArtistCover == 1 ] then - /usr/bin/cat <<< $(/usr/bin/cat ${DATA} | /usr/bin/jq '. += [{"type": "library", "category": "music", "artist": "'"${artist}"'", "title": "'"${title}"'", "cover": "http://'${HN}':8200/cover/music/'"${artist}"'/'"${title}"'/cover.jpg", "artistcover": "http://'${HN}':8200/cover/music/'"${artist}"'/cover.jpg"}]') > ${DATA} + /usr/bin/jq \ + --arg artist "${artist}" \ + --arg title "${title}" \ + --arg cover "http://${HN}:8200/cover/music/${artist}/${title}/cover.jpg" \ + --arg artistcover "http://${HN}:8200/cover/music/${artist}/cover.jpg" \ + '. += [{type: "library", category: "music", artist: $artist, title: $title, cover: $cover, artistcover: $artistcover}]' \ + "${DATA}" > "${_TMP}" && mv "${_TMP}" "${DATA}" || rm -f "${_TMP}" else - /usr/bin/cat <<< $(/usr/bin/cat ${DATA} | /usr/bin/jq '. += [{"type": "library", "category": "music", "artist": "'"${artist}"'", "title": "'"${title}"'", "cover": "http://'${HN}':8200/cover/music/'"${artist}"'/'"${title}"'/cover.jpg"}]') > ${DATA} + /usr/bin/jq \ + --arg artist "${artist}" \ + --arg title "${title}" \ + --arg cover "http://${HN}:8200/cover/music/${artist}/${title}/cover.jpg" \ + '. += [{type: "library", category: "music", artist: $artist, title: $title, cover: $cover}]' \ + "${DATA}" > "${_TMP}" && mv "${_TMP}" "${DATA}" || rm -f "${_TMP}" fi fi fi @@ -171,11 +219,25 @@ else if [ -z "${searchStrTitleCover}" ] then + # Atomic-update (HIGH-8) — same as the audiobook/music blocks above. + # B2: same fix as the audiobook/music branches above. + _TMP="${DATA}.tmp.$$" if [ $setArtistCover == 1 ] then - /usr/bin/cat <<< $(/usr/bin/cat ${DATA} | /usr/bin/jq '. += [{"type": "library", "category": "other", "artist": "'"${artist}"'", "title": "'"${title}"'", "cover": "http://'${HN}':8200/cover/other/'"${artist}"'/'"${title}"'/cover.jpg", "artistcover": "http://'${HN}':8200/cover/other/'"${artist}"'/cover.jpg"}]') > ${DATA} + /usr/bin/jq \ + --arg artist "${artist}" \ + --arg title "${title}" \ + --arg cover "http://${HN}:8200/cover/other/${artist}/${title}/cover.jpg" \ + --arg artistcover "http://${HN}:8200/cover/other/${artist}/cover.jpg" \ + '. += [{type: "library", category: "other", artist: $artist, title: $title, cover: $cover, artistcover: $artistcover}]' \ + "${DATA}" > "${_TMP}" && mv "${_TMP}" "${DATA}" || rm -f "${_TMP}" else - /usr/bin/cat <<< $(/usr/bin/cat ${DATA} | /usr/bin/jq '. += [{"type": "library", "category": "other", "artist": "'"${artist}"'", "title": "'"${title}"'", "cover": "http://'${HN}':8200/cover/other/'"${artist}"'/'"${title}"'/cover.jpg"}]') > ${DATA} + /usr/bin/jq \ + --arg artist "${artist}" \ + --arg title "${title}" \ + --arg cover "http://${HN}:8200/cover/other/${artist}/${title}/cover.jpg" \ + '. += [{type: "library", category: "other", artist: $artist, title: $title, cover: $cover}]' \ + "${DATA}" > "${_TMP}" && mv "${_TMP}" "${DATA}" || rm -f "${_TMP}" fi fi fi diff --git a/scripts/mupibox/mupi_start_led.sh b/scripts/mupibox/mupi_start_led.sh index 457e4988..47e878a7 100644 --- a/scripts/mupibox/mupi_start_led.sh +++ b/scripts/mupibox/mupi_start_led.sh @@ -13,11 +13,15 @@ ledMin=$(/usr/bin/jq -r .shim.ledBrightnessMin ${MUPIBOX_CONFIG}) ledMin=$(printf '%d' "$ledMin") echo "{}" | tee ${TMP_LEDFILE} -/usr/bin/cat <<< $(/usr/bin/jq --argjson v ${ledPin} '.led_gpio = $v' ${TMP_LEDFILE}) > ${TMP_LEDFILE} -/usr/bin/cat <<< $(/usr/bin/jq --argjson v ${ledMax} '.led_max_brightness = $v' ${TMP_LEDFILE}) > ${TMP_LEDFILE} -/usr/bin/cat <<< $(/usr/bin/jq --argjson v ${ledMin} '.led_min_brightness = $v' ${TMP_LEDFILE}) > ${TMP_LEDFILE} -/usr/bin/cat <<< $(/usr/bin/jq '.led_current_brightness = 0' ${TMP_LEDFILE}) > ${TMP_LEDFILE} -/usr/bin/cat <<< $(/usr/bin/jq '.led_dim_mode = 0' ${TMP_LEDFILE}) > ${TMP_LEDFILE} +# Atomic-update (HIGH-8). Bundle five field updates into one jq invocation +# instead of cat<< "${_TMP}" && mv "${_TMP}" "${TMP_LEDFILE}" || rm -f "${_TMP}" /usr/bin/python3 /usr/local/bin/mupibox/led_control.py & #/usr/local/bin/mupibox/./led_control & @@ -63,7 +67,9 @@ do wled_data='{"bri":'${wled_brightness_def}'}' /usr/bin/python3 /usr/local/bin/mupibox/wled_send_data.py -s ${wled_com_port} -b ${wled_baud_rate} -j ${wled_data} fi - /usr/bin/cat <<< $(/usr/bin/jq '.led_dim_mode = 1' ${TMP_LEDFILE}) > ${TMP_LEDFILE} + # Atomic-update (HIGH-8). + _TMP="${TMP_LEDFILE}.tmp.$$" + /usr/bin/jq '.led_dim_mode = 1' "${TMP_LEDFILE}" > "${_TMP}" && mv "${_TMP}" "${TMP_LEDFILE}" || rm -f "${_TMP}" OLD_STATE=${displayState} elif [ ${displayState} -eq 0 ] && [ ${OLD_STATE} -ne ${displayState} ] then @@ -71,7 +77,8 @@ do wled_data='{"bri":'${wled_brightness_dim}'}' /usr/bin/python3 /usr/local/bin/mupibox/wled_send_data.py -s ${wled_com_port} -b ${wled_baud_rate} -j ${wled_data} fi - /usr/bin/cat <<< $(/usr/bin/jq '.led_dim_mode = 0' ${TMP_LEDFILE}) > ${TMP_LEDFILE} + _TMP="${TMP_LEDFILE}.tmp.$$" + /usr/bin/jq '.led_dim_mode = 0' "${TMP_LEDFILE}" > "${_TMP}" && mv "${_TMP}" "${TMP_LEDFILE}" || rm -f "${_TMP}" OLD_STATE=${displayState} fi done diff --git a/scripts/mupibox/optimize_wifi.sh b/scripts/mupibox/optimize_wifi.sh index bab551ba..debb95dc 100644 --- a/scripts/mupibox/optimize_wifi.sh +++ b/scripts/mupibox/optimize_wifi.sh @@ -27,15 +27,20 @@ add_config() { # /etc/network/interfaces if ! grep -q "wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf" "$NETWORKINTERFACES"; then - rm "$NETWORKINTERFACES.bak" + # HIGH-13: `rm "$file.bak"` without -f errors on first run when no + # .bak exists. Without `set -e` it's just a stderr noise; with set -e + # (e.g. anyone tightening this script later) it would abort BEFORE + # the cp can create the new backup, leaving no backup at all. Use -f + # so the rm is idempotent. + rm -f "$NETWORKINTERFACES.bak" cp "$NETWORKINTERFACES" "$NETWORKINTERFACES.bak" sed -i 's|wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf|wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf|' $NETWORKINTERFACES sed -i 's|iface wlan0 inet dhcp|iface wlan0 inet manual|' $NETWORKINTERFACES echo "iface default inet dhcp" | tee -a $NETWORKINTERFACES > /dev/null fi -# /etc/wpa_supplicant/wpa_supplicant -rm "$WPACONF.bak" +# /etc/wpa_supplicant/wpa_supplicant — same idempotency fix (HIGH-13). +rm -f "$WPACONF.bak" cp "$WPACONF" "$WPACONF.bak" add_config 'bgscan="simple:30:-70:60"' #add_config 'roam_timeout=5' diff --git a/scripts/mupibox/repair_config.sh b/scripts/mupibox/repair_config.sh index 631e5e9b..a79a0ed4 100644 --- a/scripts/mupibox/repair_config.sh +++ b/scripts/mupibox/repair_config.sh @@ -10,5 +10,7 @@ rm ${FRONTENDCONFIG} wget ${SRC}/config/templates/www.json -O ${FRONTENDCONFIG} /usr/local/bin/mupibox/./set_hostname.sh -/usr/bin/cat <<< $(/usr/bin/jq --arg v "${HOSTN}" '."node-sonos-http-api".server = $v' ${FRONTENDCONFIG}) > ${FRONTENDCONFIG} +# Atomic-update (HIGH-8). +_TMP="${FRONTENDCONFIG}.tmp.$$" +/usr/bin/jq --arg v "${HOSTN}" '."node-sonos-http-api".server = $v' "${FRONTENDCONFIG}" > "${_TMP}" && mv "${_TMP}" "${FRONTENDCONFIG}" || rm -f "${_TMP}" chown dietpi:dietpi ${FRONTENDCONFIG} diff --git a/scripts/mupibox/set_deviceid.sh b/scripts/mupibox/set_deviceid.sh index 869124e3..58bc965b 100644 --- a/scripts/mupibox/set_deviceid.sh +++ b/scripts/mupibox/set_deviceid.sh @@ -7,9 +7,25 @@ HOSTNAME=$(sudo /usr/bin/jq -r .mupibox.host ${CONFIG}) DEVICES=$(curl http://${HOSTNAME}:5005/getDevices 2>/dev/null) devID=$(echo ${DEVICES} | jq '.[] | select(.name=='\"${HOSTNAME}\"')' | jq '.id') devID=$(echo ${devID} | sed 's/\"//g') -if [ ${#devID} > 5 ]; +# HIGH-9: previous `[ ${#devID} > 5 ]` was a string-test of `${#devID}` +# (always non-empty so always true) plus a stray `>` redirect that +# silently created a file named `5`. Use the numeric test `-gt`. +if [ "${#devID}" -gt 5 ]; then - sudo /usr/bin/cat <<< $(/usr/bin/jq --arg v "${devID}" '.spotify.deviceId = $v' ${CONFIG}) > ${CONFIG} + # HIGH-8 + HIGH-10: previous `sudo /usr/bin/cat <<< $(jq …) > ${CONFIG}` + # had two bugs at once. (a) the `>` redirect runs as the calling user + # not as root, so writing to root-owned ${CONFIG} would EACCES at the + # redirect step, leaving CONFIG unchanged but the script silently + # "succeeding". (b) the cat<<< would truncate ${CONFIG} on jq error. + # New approach: stage the new content in /tmp (always writable for + # dietpi), then `sudo install` it onto ${CONFIG} preserving owner + + # mode in one atomic syscall. Same pattern as admin.php's + # write_json() and jsoneditor.php's atomic save. + _TMP="/tmp/.deviceid.$$.json" + if /usr/bin/jq --arg v "${devID}" '.spotify.deviceId = $v' "${CONFIG}" > "${_TMP}"; then + sudo install -m 644 -o root -g www-data "${_TMP}" "${CONFIG}" + fi + rm -f "${_TMP}" fi sudo /usr/local/bin/mupibox/./setting_update.sh diff --git a/scripts/mupibox/set_hostname.sh b/scripts/mupibox/set_hostname.sh index a5d9157b..7ee98f35 100644 --- a/scripts/mupibox/set_hostname.sh +++ b/scripts/mupibox/set_hostname.sh @@ -6,4 +6,6 @@ FRONTENDCONFIG="/home/dietpi/.mupibox/Sonos-Kids-Controller-master/server/config HOSTN=$(/usr/bin/hostname) -/usr/bin/cat <<< $(/usr/bin/jq --arg v "${HOSTN}" '."node-sonos-http-api".server = $v' ${FRONTENDCONFIG}) > ${FRONTENDCONFIG} \ No newline at end of file +# Atomic-update (HIGH-8). +_TMP="${FRONTENDCONFIG}.tmp.$$" +/usr/bin/jq --arg v "${HOSTN}" '."node-sonos-http-api".server = $v' "${FRONTENDCONFIG}" > "${_TMP}" && mv "${_TMP}" "${FRONTENDCONFIG}" || rm -f "${_TMP}" \ No newline at end of file diff --git a/scripts/mupibox/setting_update.sh b/scripts/mupibox/setting_update.sh index be2926e9..59852a36 100644 --- a/scripts/mupibox/setting_update.sh +++ b/scripts/mupibox/setting_update.sh @@ -12,52 +12,78 @@ DISPLAY_STANDBY="/etc/X11/xorg.conf.d/98-dietpi-disable_dpms.conf" THEME_FILE="/home/dietpi/.mupibox/Sonos-Kids-Controller-master/www/active_theme.css" NEW_THEME=$(/usr/bin/jq -r .mupibox.theme ${MUPIBOX_CONFIG}) -newTheme=$(ls -l ${THEME_FILE} | grep ${NEW_THEME}) +# B4: NEW_THEME comes from mupiboxconfig.json — admin-controlled. +# `ls -l ${THEME_FILE} | grep ${NEW_THEME}` was unquoted, so a theme +# name with a space (or a `/`) would either truncate or pull in +# unrelated grep flags. Quote both args, plus -F so theme names with +# regex metachars (`.`, `*`) match literally. +newTheme=$(ls -l "${THEME_FILE}" | grep -F -- "${NEW_THEME}") if (( ${#newTheme} == 0 )) then - xargs rm <<< ${THEME_FILE} - ln -s /home/dietpi/MuPiBox/themes/${NEW_THEME}.css ${THEME_FILE} + # Same quoting fix on the symlink replace. + rm -f "${THEME_FILE}" + ln -s "/home/dietpi/MuPiBox/themes/${NEW_THEME}.css" "${THEME_FILE}" fi +# Atomic-update (HIGH-8). Read all source values first, then bundle the +# writes into one jq invocation per target file. Each cat<<< pattern was +# its own truncate-race window; collapsing to one pipeline per file means +# 16+ tempfile cycles → 2 cycles, plus far less SD wear. deviceId=$(/usr/bin/jq -r .spotify.deviceId ${MUPIBOX_CONFIG}) -/usr/bin/cat <<< $(/usr/bin/jq --arg v "${deviceId}" '.["node-sonos-http-api"].rooms = [$v]' ${SONOS_CONFIG}) > ${SONOS_CONFIG} -/usr/bin/cat <<< $(/usr/bin/jq --arg v "${deviceId}" '.spotify.deviceId = $v' ${SPOTIFYCONTROLLER_CONFIG}) > ${SPOTIFYCONTROLLER_CONFIG} - clientId=$(/usr/bin/jq -r .spotify.clientId ${MUPIBOX_CONFIG}) -/usr/bin/cat <<< $(/usr/bin/jq --arg v "${clientId}" '.spotify.clientId = $v' ${SONOS_CONFIG}) > ${SONOS_CONFIG} -/usr/bin/cat <<< $(/usr/bin/jq --arg v "${clientId}" '.spotify.clientId = $v' ${SPOTIFYCONTROLLER_CONFIG}) > ${SPOTIFYCONTROLLER_CONFIG} - clientSecret=$(/usr/bin/jq -r .spotify.clientSecret ${MUPIBOX_CONFIG}) -/usr/bin/cat <<< $(/usr/bin/jq --arg v "${clientSecret}" '.spotify.clientSecret = $v' ${SONOS_CONFIG}) > ${SONOS_CONFIG} -/usr/bin/cat <<< $(/usr/bin/jq --arg v "${clientSecret}" '.spotify.clientSecret = $v' ${SPOTIFYCONTROLLER_CONFIG}) > ${SPOTIFYCONTROLLER_CONFIG} - accessToken=$(/usr/bin/jq -r .spotify.accessToken ${MUPIBOX_CONFIG}) -/usr/bin/cat <<< $(/usr/bin/jq --arg v "${accessToken}" '.spotify.accessToken = $v' ${SPOTIFYCONTROLLER_CONFIG}) > ${SPOTIFYCONTROLLER_CONFIG} - refreshToken=$(/usr/bin/jq -r .spotify.refreshToken ${MUPIBOX_CONFIG}) -/usr/bin/cat <<< $(/usr/bin/jq --arg v "$refreshToken" '.spotify.refreshToken = $v' ${SPOTIFYCONTROLLER_CONFIG}) > ${SPOTIFYCONTROLLER_CONFIG} - ttsLanguage=$(/usr/bin/jq -r .mupibox.ttsLanguage ${MUPIBOX_CONFIG}) -/usr/bin/cat <<< $(/usr/bin/jq --arg v "$ttsLanguage" '.ttsLanguage = $v' ${SPOTIFYCONTROLLER_CONFIG}) > ${SPOTIFYCONTROLLER_CONFIG} - hostname=$(/usr/bin/jq -r .mupibox.host ${MUPIBOX_CONFIG}) -/usr/bin/cat <<< $(/usr/bin/jq --arg v "${hostname}" '.["node-sonos-http-api"].server = $v' ${SONOS_CONFIG}) > ${SONOS_CONFIG} - hat_active=$(/usr/bin/jq -r .mupihat.hat_active ${MUPIBOX_CONFIG}) -/usr/bin/cat <<< $(/usr/bin/jq --argjson v "$hat_active" '.["node-sonos-http-api"].hat_active = $v' ${SONOS_CONFIG}) > ${SONOS_CONFIG} - ip_control_backend=$(/usr/bin/jq -r .mupibox.ip_control_backend ${MUPIBOX_CONFIG}) + +# Resolve the IP once so the SONOS_CONFIG update can be a single pipeline +# regardless of ip_control_backend. Default to "" (= same as the +# old false-branch behaviour). +SONOS_IP="" if [ "$ip_control_backend" = true ] ; then - IP=$(hostname -I | sed 's/ *$//') - if [ "$IP" = "" ] ; then - IP=$(/usr/bin/jq -r .ip ${NETWORK_CONFIG}) - fi - /usr/bin/cat <<< $(/usr/bin/jq --arg v "${IP}" '.["node-sonos-http-api"].ip = $v' ${SONOS_CONFIG}) > ${SONOS_CONFIG} + SONOS_IP=$(hostname -I | sed 's/ *$//') + if [ -z "$SONOS_IP" ] ; then + SONOS_IP=$(/usr/bin/jq -r .ip ${NETWORK_CONFIG}) + fi fi -if [ "$ip_control_backend" = false ] ; then - /usr/bin/cat <<< $(/usr/bin/jq '.["node-sonos-http-api"].ip = ""' ${SONOS_CONFIG}) > ${SONOS_CONFIG} -fi -/usr/bin/cat <<< $(/usr/bin/jq --arg v "5005" '.["node-sonos-http-api"].port = $v' ${SONOS_CONFIG}) > ${SONOS_CONFIG} + +# One atomic update for SONOS_CONFIG (was 6 cat<< "${_TMP}" && mv "${_TMP}" "${SONOS_CONFIG}" || rm -f "${_TMP}" + +# One atomic update for SPOTIFYCONTROLLER_CONFIG (was 6 cat<< "${_TMP}" && mv "${_TMP}" "${SPOTIFYCONTROLLER_CONFIG}" || rm -f "${_TMP}" #cachepath=$(/usr/bin/jq -r .spotify.cachepath ${MUPIBOX_CONFIG}) #/usr/bin/sed -i 's@.*cache_path.*@ cache_path = "'${cachepath}'"@g' ${SPOTIFYD_CONFIG} @@ -75,11 +101,27 @@ fi #/usr/bin/sed -i 's/.*username.*/ username = '\"${username}\"'/g' ${SPOTIFYD_CONFIG} #password=$(/usr/bin/jq -r .spotify.password ${MUPIBOX_CONFIG}) #/usr/bin/sed -i 's/.*password.*/ password = '\"${password}\"'/g' ${SPOTIFYD_CONFIG} +# B4: hostname/timeout get spliced into a sed `s/`-delimited pattern. +# `/` is the default sed delimiter; if either value contained `/` +# (legitimate for hostnames with FQDN dots — also legal in some user- +# typed values), sed would interpret it as a delimiter and produce a +# garbled config. Switch to `|` as the sed delimiter (extremely +# unlikely to appear in a hostname or numeric timeout) and validate +# the timeout numerically before splicing — sed-injection via a +# numeric field would only happen if the config layer was already +# compromised, but defence in depth. hostname=$(/usr/bin/jq -r .mupibox.host ${MUPIBOX_CONFIG}) -/usr/bin/sed -i 's/.*device_name.*/ device_name = '\"${hostname}\"'/g' ${SPOTIFYD_CONFIG} +# Strip anything that isn't a hostname char to be belt-and-braces; +# RFC1123 hostnames are letters, digits, `.`, `-` only. +hostname_safe=$(printf '%s' "${hostname}" | tr -dc 'A-Za-z0-9.-') +/usr/bin/sed -i 's|.*device_name.*| device_name = "'"${hostname_safe}"'"|g' ${SPOTIFYD_CONFIG} timeout=$(/usr/bin/jq -r .timeout.idleDisplayOff ${MUPIBOX_CONFIG}) -/usr/bin/sed -i 's/.*Option \"BlankTime\".*/ Option \"BlankTime\" '\"${timeout}\"'/g' ${DISPLAY_STANDBY} +# Force-numeric — empty or non-int collapses to 0 (sane "no timeout" +# default for the X server's BlankTime). +timeout_int=$(printf '%s' "${timeout}" | tr -dc '0-9') +: "${timeout_int:=0}" +/usr/bin/sed -i 's|.*Option "BlankTime".*| Option "BlankTime" "'"${timeout_int}"'"|g' ${DISPLAY_STANDBY} #currentIP=$(hostname -I) #/usr/bin/cat <<< $(/usr/bin/jq --arg v "${currentIP}" '.ip = $v' ${SONOS_NETWORK}) > ${SONOS_NETWORK} diff --git a/update/start_mupibox_update.sh b/update/start_mupibox_update.sh index 7e024bea..656e5c0f 100644 --- a/update/start_mupibox_update.sh +++ b/update/start_mupibox_update.sh @@ -25,6 +25,22 @@ killall -s 9 -w -q -r chromium CONFIG="/etc/mupibox/mupiboxconfig.json" LOG="/boot/mupibox_update.log" exec 3>${LOG} + +# H5: previously the script ran the destructive `rm -R Sonos-Kids- +# Controller-master/` BEFORE verifying that the downloaded archive was +# usable. A flaky internet-dropout, a 404 response, or a corrupted +# unzip would leave the box with no installation and no rollback path. +# Add fail_update() to bail BEFORE any destructive op when a pre-flight +# check fails, plus an atomic-swap pattern around the rm so a failure +# at extract time can restore the previous install. +fail_update() { + local msg=$1 + echo "## UPDATE ABORTED: ${msg}" >&3 2>&3 + echo "## (no destructive operation performed yet — your installation is intact)" >&3 2>&3 + # Surface to dialog/whiptail so the user actually sees the failure + echo -e "XXX\n100\nUpdate aborted: ${msg}\nXXX" + exit 1 +} service mupi_idle_shutdown stop packages2install="lighttpd-mod-openssl gpiod git libasound2 mplayer pulseaudio-module-bluetooth pip id3tool bluez zip rrdtool scrot net-tools wireless-tools autoconf automake bc build-essential python3-gpiozero python3-rpi.gpio python3-lgpio python3-serial python3-requests python3-paho-mqtt libgles2-mesa mesa-utils libsdl2-dev preload python3-smbus2 pigpio libjson-c-dev i2c-tools libi2c-dev python3-smbus python3-alsaaudio python3-netifaces libwidevinecdm0 python3-flask" packages2remove="jq" @@ -232,19 +248,27 @@ echo "========================================================================== ############################################################################################### - echo -e "XXX\n${STEP}\nDownload MuPiBox Version ${VERSION_LONG}... \nXXX" + echo -e "XXX\n${STEP}\nDownload MuPiBox Version ${VERSION_LONG}... \nXXX" before=$(date +%s) wget -q -O /home/dietpi/mupibox.zip ${MUPIBOX_URL} >&3 2>&3 + # H5: hard-fail before any destructive step if the download didn't land. + [ -s /home/dietpi/mupibox.zip ] || fail_update "MuPiBox download failed (file empty or missing — check internet)" + unzip -t /home/dietpi/mupibox.zip >/dev/null 2>&1 || fail_update "MuPiBox download corrupted (zip integrity check failed)" after=$(date +%s) echo -e "## MuPiBox Download ## finished after $((after - $before)) seconds" >&3 2>&3 STEP=$(($STEP + 1)) ############################################################################################### - echo -e "XXX\n${STEP}\nUnzip MuPiBox Version ${VERSION_LONG}... \nXXX" + echo -e "XXX\n${STEP}\nUnzip MuPiBox Version ${VERSION_LONG}... \nXXX" before=$(date +%s) unzip -q -d /home/dietpi /home/dietpi/mupibox.zip >&3 2>&3 rm /home/dietpi/mupibox.zip >&3 2>&3 + # H5: verify the source dir + the inner deploy.zip exist before we + # wipe the live install. If either is missing the user gets a + # clean abort instead of a half-installed box. + [ -d "${MUPI_SRC}" ] || fail_update "Expected source directory ${MUPI_SRC} not found after unzip" + [ -s "${MUPI_SRC}/bin/nodejs/deploy.zip" ] || fail_update "Backend deploy.zip missing or empty in update package" #MUPI_SRC="/home/dietpi/MuPiBox-${VERSION}" after=$(date +%s) @@ -259,21 +283,44 @@ echo "========================================================================== mv /home/dietpi/.mupibox/Sonos-Kids-Controller-master/www/cover /tmp/cover >&3 2>&3 #mv /home/dietpi/.mupibox/Sonos-Kids-Controller-master/server/config/config.json /tmp/config.json >&3 2>&3 mv /home/dietpi/.mupibox/Sonos-Kids-Controller-master/www/active_theme.css /tmp/active_theme.css >&3 2>&3 + # H5: data.json holds resume/library state — non-recoverable. cover + # and active_theme.css can be restored from defaults if missing, + # so only the data.json backup is fail-fast. + [ -f /tmp/data.json ] || fail_update "data.json backup failed (resume/library state would be lost)" after=$(date +%s) echo -e "## Backup Data ## finished after $((after - $before)) seconds" >&3 2>&3 - + STEP=$(($STEP + 1)) ############################################################################################### - echo -e "XXX\n${STEP}\nUpdate frontend, backend-api, and backend-player ... \nXXX" + echo -e "XXX\n${STEP}\nUpdate frontend, backend-api, and backend-player ... \nXXX" before=$(date +%s) sudo -H -u dietpi bash -c "pm2 stop server" >&3 2>&3 #su - dietpi -c "pm2 save" >&3 2>&3 - rm -R /home/dietpi/.mupibox/Sonos-Kids-Controller-master/ >&3 2>&3 + # H5: atomic-swap with rollback. Move old install aside instead of + # deleting it; if the deploy.zip extract fails, restore the backup + # so the box keeps running on the previous version. The .upd-bak + # directory is removed after a successful extract. + BAK_DIR="/home/dietpi/.mupibox/Sonos-Kids-Controller-master.upd-bak" + rm -rf "${BAK_DIR}" >&3 2>&3 + if [ -d /home/dietpi/.mupibox/Sonos-Kids-Controller-master ]; then + mv /home/dietpi/.mupibox/Sonos-Kids-Controller-master "${BAK_DIR}" >&3 2>&3 || \ + fail_update "Could not move old install aside (filesystem full?)" + fi mkdir -p /home/dietpi/.mupibox/Sonos-Kids-Controller-master/server/config/ >&3 2>&3 - unzip ${MUPI_SRC}/bin/nodejs/deploy.zip -d /home/dietpi/.mupibox/Sonos-Kids-Controller-master/ >&3 2>&3 + if ! unzip ${MUPI_SRC}/bin/nodejs/deploy.zip -d /home/dietpi/.mupibox/Sonos-Kids-Controller-master/ >&3 2>&3; then + # Rollback: remove the partially-extracted dir and restore the backup. + rm -rf /home/dietpi/.mupibox/Sonos-Kids-Controller-master >&3 2>&3 + if [ -d "${BAK_DIR}" ]; then + mv "${BAK_DIR}" /home/dietpi/.mupibox/Sonos-Kids-Controller-master >&3 2>&3 + sudo -H -u dietpi bash -c "pm2 start server" >&3 2>&3 + fi + fail_update "deploy.zip extraction failed — rolled back to previous install" + fi + # Extract succeeded — drop the backup. + rm -rf "${BAK_DIR}" >&3 2>&3 mv ${MUPI_SRC}/config/templates/monitor.json /home/dietpi/.mupibox/Sonos-Kids-Controller-master/server/config/monitor.json >&3 2>&3 mv ${MUPI_SRC}/config/templates/www.json /home/dietpi/.mupibox/Sonos-Kids-Controller-master/server/config/config.json >&3 2>&3 chown dietpi:dietpi -R /home/dietpi/.mupibox/Sonos-Kids-Controller-master/www >&3 2>&3 @@ -439,19 +486,23 @@ echo "========================================================================== ############################################################################################### - echo -e "XXX\n${STEP}\nCopy some media files... \nXXX" + echo -e "XXX\n${STEP}\nCopy some media files... \nXXX" # Splash and Media before=$(date +%s) #mv ${MUPI_SRC}/config/templates/splash.txt /boot/splash.txt >&3 2>&3 wget https://gitlab.com/DarkElvenAngel/initramfs-splash/-/raw/master/boot/initramfs.img -O /boot/initramfs.img >&3 2>&3 - cp ${MUPI_SRC}/media/images/goodbye.png /home/dietpiMuPiBox/sysmedia/images/goodbye.png >&3 2>&3 + # MED-6: previous code had `/home/dietpiMuPiBox/sysmedia/...` (missing + # slash between dietpi and MuPiBox). The cp's silently failed because + # that path doesn't exist on a real box; the destination files never + # got refreshed. Fixed to /home/dietpi/MuPiBox/. + cp ${MUPI_SRC}/media/images/goodbye.png /home/dietpi/MuPiBox/sysmedia/images/goodbye.png >&3 2>&3 #mv ${MUPI_SRC}/media/images/splash.png /boot/splash.png >&3 2>&3 - #cp ${MUPI_SRC}/media/images/MuPiLogo.jpg /home/dietpiMuPiBox/sysmedia/images/MuPiLogo.jpg >&3 2>&3 - #cp ${MUPI_SRC}/media/sound/shutdown.wav /home/dietpiMuPiBox/sysmedia/sound/shutdown.wav >&3 2>&3 - #cp ${MUPI_SRC}/media/sound/startup.wav /home/dietpiMuPiBox/sysmedia/sound/startup.wav >&3 2>&3 + #cp ${MUPI_SRC}/media/images/MuPiLogo.jpg /home/dietpi/MuPiBox/sysmedia/images/MuPiLogo.jpg >&3 2>&3 + #cp ${MUPI_SRC}/media/sound/shutdown.wav /home/dietpi/MuPiBox/sysmedia/sound/shutdown.wav >&3 2>&3 + #cp ${MUPI_SRC}/media/sound/startup.wav /home/dietpi/MuPiBox/sysmedia/sound/startup.wav >&3 2>&3 cp ${MUPI_SRC}/media/sound/button_shutdown.wav /home/dietpi/MuPiBox/sysmedia/sound/button_shutdown.wav >&3 2>&3 - cp ${MUPI_SRC}/media/sound/low.wav /home/dietpiMuPiBox/sysmedia/sound/low.wav >&3 2>&3 - cp ${MUPI_SRC}/media/images/installation.jpg /home/dietpiMuPiBox/sysmedia/images/installation.jpg >&3 2>&3 + cp ${MUPI_SRC}/media/sound/low.wav /home/dietpi/MuPiBox/sysmedia/sound/low.wav >&3 2>&3 + cp ${MUPI_SRC}/media/images/installation.jpg /home/dietpi/MuPiBox/sysmedia/images/installation.jpg >&3 2>&3 cp ${MUPI_SRC}/media/images/battery_low.jpg /home/dietpi/MuPiBox/sysmedia/images/battery_low.jpg >&3 2>&3 after=$(date +%s) @@ -575,13 +626,33 @@ echo "========================================================================== ############################################################################################### - echo -e "XXX\n{STEP}\nUpdate Admin-Interface... \nXXX" + # MED-6: typo `{STEP}` (no $) just printed literal "{STEP}" in the + # update progress UI. Plus: `rm -R /var/www/*` then unzip wiped any + # admin-customised files (active_theme.css from the theme picker, + # the cover/ symlink, theme-data/ for custom-theme backgrounds). + # The next admin save would re-create active_theme.css, but custom + # themes and cover/-symlink-content were lost. Preserve them around + # the wipe. + echo -e "XXX\n${STEP}\nUpdate Admin-Interface... \nXXX" before=$(date +%s) - rm -R /var/www/* >&3 2>&3 + # Stage user-customised files in /tmp so they survive the wipe. + UPDATE_PRESERVE=$(mktemp -d /tmp/mupibox-www-preserve.XXXXXX) + for item in active_theme.css cover theme-data; do + [ -e "/var/www/${item}" ] && cp -a "/var/www/${item}" "${UPDATE_PRESERVE}/" >&3 2>&3 + done + rm -R /var/www/* >&3 2>&3 mv ${MUPI_SRC}/AdminInterface/release/www.zip /var/www/www.zip >&3 2>&3 unzip /var/www/www.zip -d /var/www/ >&3 2>&3 rm /var/www/www.zip >&3 2>&3 - ln -s /home/dietpi/MuPiBox/media/cover /var/www/cover >&3 2>&3 + # Restore preserved files after the new www/ unzipped so they + # overwrite anything the package would otherwise have shipped. + for item in active_theme.css cover theme-data; do + [ -e "${UPDATE_PRESERVE}/${item}" ] && cp -a "${UPDATE_PRESERVE}/${item}" /var/www/ >&3 2>&3 + done + rm -rf "${UPDATE_PRESERVE}" + # `cover` is supposed to be a symlink to /home/dietpi/MuPiBox/media/cover — + # only re-create if the preserve step didn't already restore it. + [ -L /var/www/cover ] || ln -s /home/dietpi/MuPiBox/media/cover /var/www/cover >&3 2>&3 chown -R www-data:www-data /var/www/ >&3 2>&3 chmod -R 755 /var/www/ >&3 2>&3 chown -R dietpi:www-data /home/dietpi/MuPiBox/media/cover >&3 2>&3