From c64046411b186d50f6d04be73ced4b51e7f0831c Mon Sep 17 00:00:00 2001 From: Kroese Date: Sun, 9 Aug 2026 13:15:44 +0200 Subject: [PATCH] fix: Preserve web status after connection loss (#1361) --- src/network.sh | 1 + src/print.sh | 2 + src/server.sh | 3 +- src/socket.sh | 63 +++++++++---- web/js/script.js | 234 ++++++++++++++++++++++++++++++++++++++++------- 5 files changed, 251 insertions(+), 52 deletions(-) diff --git a/src/network.sh b/src/network.sh index 8ed43a2..d178449 100644 --- a/src/network.sh +++ b/src/network.sh @@ -2271,6 +2271,7 @@ if enabled "$DHCP"; then else if ! disabled "${WEB:-}"; then + writeAtomic "$WSD_COMMAND" "portal" sleep 1.2 closeWeb fi diff --git a/src/print.sh b/src/print.sh index 70ec303..a34bcfe 100644 --- a/src/print.sh +++ b/src/print.sh @@ -20,6 +20,7 @@ driver="/run/shm/qemu.nic" page="/run/shm/index.html" address="/run/shm/qemu.ip" shutdown="/run/shm/qemu.end" +command="/run/shm/status.cmd" socket="/run/shm/qemu-host-api.sock" template="/var/www/index.html" url="http://localhost/read?command=10" @@ -175,6 +176,7 @@ writeDhcpPage() { echo "$html" > "$page" echo "$body" > "$msgs" + writeAtomic "$command" "portal http://$location" return 0 } diff --git a/src/server.sh b/src/server.sh index 3138639..015b04b 100644 --- a/src/server.sh +++ b/src/server.sh @@ -8,13 +8,14 @@ WEB_PORT=$(strip "$WEB_PORT") WEB_PID="/run/nginx.pid" WSD_LOG="/var/log/websocketd.log" +WSD_COMMAND="$QEMU_DIR/status.cmd" WSD_PID="$QEMU_DIR/websocketd.pid" WSD_SOCKET="$QEMU_DIR/status-ws.sock" prepareWebFiles() { cp -r /var/www/* "$QEMU_DIR" || return 1 - rm -f -- "$WSD_PID" "$WSD_SOCKET" "$WEB_PID" "$WSD_LOG" || return 1 + rm -f -- "$WSD_PID" "$WSD_SOCKET" "$WSD_COMMAND" "$WEB_PID" "$WSD_LOG" || return 1 return 0 } diff --git a/src/socket.sh b/src/socket.sh index 3dace8e..5ba88d4 100644 --- a/src/socket.sh +++ b/src/socket.sh @@ -2,47 +2,70 @@ set -Eeuo pipefail lastmsg="" -path="/run/shm/msg.html" -dir=$(dirname -- "$path") -name=$(basename -- "$path") +lastcmd="" +status="/run/shm/msg.html" +command="/run/shm/status.cmd" +dir=$(dirname -- "$status") +status_name=$(basename -- "$status") +command_name=$(basename -- "$command") -refresh() { +# websocketd clients use s: for status updates and c: for control commands. +refreshStatus() { - [ ! -f "$path" ] && return 0 - [ ! -s "$path" ] && return 0 + [ ! -f "$status" ] && return 0 + [ ! -s "$status" ] && return 0 - msg=$(< "$path") || return 0 + msg=$(< "$status") || return 0 msg="${msg%$'\n'}" [ -z "$msg" ] && return 0 [[ "$msg" == "$lastmsg" ]] && return 0 lastmsg="$msg" - # websocketd clients interpret s: as a status update and c: as a command; - # suppress unchanged status to avoid redundant browser work. echo "s: $msg" return 0 } -refresh +refreshCommand() { + [ ! -f "$command" ] && return 0 + [ ! -s "$command" ] && return 0 + + cmd=$(< "$command") || return 0 + cmd="${cmd%$'\n'}" + + [ -z "$cmd" ] && return 0 + [[ "$cmd" == "$lastcmd" ]] && return 0 + + lastcmd="$cmd" + echo "c: $cmd" + + return 0 +} + +refreshStatus +refreshCommand + +# Watch the directory because status and command updates use atomic rename. inotifywait \ -m -q \ - -e close_write,moved_to,delete \ + -e close_write,moved_to \ --format '%e %f' \ "$dir" | while read -r event file; do - [[ "$file" == "$name" ]] || continue - - case "${event,,}" in - "delete"* ) - echo "c: vnc" ;; - # moved_to covers the atomic replacement used by html()/writeAtomic(), - # while close_write handles direct writers. - "close_write"* | "moved_to"* ) - refresh ;; + case "$file" in + "$status_name" ) + case "${event,,}" in + "close_write"* | "moved_to"* ) + refreshStatus ;; + esac ;; + "$command_name" ) + case "${event,,}" in + "close_write"* | "moved_to"* ) + refreshCommand ;; + esac ;; esac done diff --git a/web/js/script.js b/web/js/script.js index 28a5c41..1f3b7e5 100644 --- a/web/js/script.js +++ b/web/js/script.js @@ -1,7 +1,13 @@ var timer; var request; +var failureTimer; +var navigationTimer; var booting = false; +var portal = false; +var portalUrl = ""; var interval = 1000; +var lastStatus = ""; +var stopped = "The container has stopped. Check the container logs for details."; function abortRequest() { @@ -16,6 +22,64 @@ function abortRequest() { return true; } +function clearFailure() { + + if (!failureTimer) { + return false; + } + + clearTimeout(failureTimer); + failureTimer = null; + + return true; +} + +function connectionLost() { + + if (booting || (portal && portalUrl.length == 0) || + document.hidden || failureTimer) { + return false; + } + + failureTimer = setTimeout(function() { + + if (booting || (portal && portalUrl.length == 0) || + document.hidden) { + failureTimer = null; + return; + } + + setStopped(); + }, interval * 3); + + return true; +} + +function clearNavigation() { + + if (!navigationTimer) { + return false; + } + + clearTimeout(navigationTimer); + navigationTimer = null; + + return true; +} + +function visibilityChanged() { + + clearFailure(); + clearNavigation(); + + if (document.hidden) { + return false; + } + + getInfo(); + return true; +} + function getInfo() { var url = "msg.html"; @@ -46,18 +110,56 @@ function getURL() { return protocol + "//" + window.location.host + path; } -function processMsg(msg) { +function redirect(url) { - if (msg.toLowerCase().indexOf("href=") !== -1) { - var div = document.createElement("div"); - div.innerHTML = msg; - var url = div.querySelector("a").href; - setTimeout(() => { - window.location.assign(url); - }, 3000); + if (document.hidden || navigationTimer) { + return false; } + navigationTimer = setTimeout(function() { + navigationTimer = null; + window.location.assign(url); + }, 3000); + + return true; +} + +function beginPortal(url) { + + portal = true; + portalUrl = url || ""; + booting = false; + + clearFailure(); + setInfo("Connecting to web portal", true); + schedule(); + + if (portalUrl.length > 0) { + redirect(portalUrl); + } + + return true; +} + +function processCommand(msg) { + + if (msg == "portal") { + return beginPortal(""); + } + + if (msg.indexOf("portal ") == 0) { + return beginPortal(msg.substring(7)); + } + + console.warn("Unknown command: " + msg); + return false; +} + +function processMsg(msg) { + + rememberStatus(msg); setInfo(msg); + return true; } @@ -74,19 +176,15 @@ function processInfo() { var status = response.status; if (status == 502 || status == 503 || status == 504) { + connectionLost(); schedule(); return true; } var msg = response.responseText; if (msg == null || msg.length == 0) { - - if (booting) { - schedule(); - return true; - } - - window.location.reload(); + connectionLost(); + schedule(); return false; } @@ -96,17 +194,32 @@ function processInfo() { if (msg.toLowerCase().indexOf("") !== -1) { notFound = true; } else { + clearFailure(); processMsg(msg); - if (msg.toLowerCase().indexOf("href=") == -1) { + + if (portalUrl.length > 0) { + setInfo("Connecting to web portal", true); + redirect(portalUrl); + } else { schedule(); } + return true; } } if (notFound) { + clearFailure(); + booting = false; setInfo("Connecting to web portal", true); - reload(); + + if (portalUrl.length > 0) { + redirect(portalUrl); + } else { + portal = true; + reload(); + } + return true; } @@ -125,6 +238,27 @@ function extractContent(s) { return span.textContent || span.innerText; }; +function escapeContent(s) { + var span = document.createElement('span'); + span.textContent = s; + return span.innerHTML; +} + +function rememberStatus(msg) { + + var text = extractContent(msg).trim(); + if (text.length == 0) { + return false; + } + + if (text.includes("Booting ")) { + booting = true; + } + + lastStatus = text; + return true; +} + function setInfo(msg, loading, error) { try { @@ -132,10 +266,6 @@ function setInfo(msg, loading, error) { return false; } - if (msg.includes("Booting ")) { - booting = true; - } - var el = document.getElementById("info"); if (el.innerText == msg || el.innerHTML == msg) { @@ -178,6 +308,16 @@ function setError(text) { return setInfo(text, false, true); } +function setStopped() { + + var msg = stopped; + if (lastStatus.length > 0) { + msg += "
(Last status: " + escapeContent(lastStatus) + ")"; + } + + return setError(msg); +} + function schedule() { clearTimeout(timer); @@ -185,9 +325,19 @@ function schedule() { } function reload() { - setTimeout(() => { + + if (document.hidden) { + return false; + } + + clearNavigation(); + + navigationTimer = setTimeout(function() { + navigationTimer = null; window.location.reload(); }, 3000); + + return true; } function connect() { @@ -195,8 +345,19 @@ function connect() { var wsUrl = getURL() + "/status"; var ws = new WebSocket(wsUrl); + ws.onopen = function(e) { + + clearFailure(); + + if (portalUrl.length > 0) { + redirect(portalUrl); + } + }; + ws.onmessage = function(e) { + clearFailure(); + var pos = e.data.indexOf(":"); var cmd = e.data.substring(0, pos); var msg = e.data.substring(pos + 2); @@ -204,15 +365,17 @@ function connect() { switch (cmd) { case "s": - var aborted = abortRequest(); - - processMsg(msg); - - if (aborted && - msg.toLowerCase().indexOf("href=") == -1) { + if (abortRequest()) { schedule(); } + processMsg(msg); + break; + + case "c": + + abortRequest(); + processCommand(msg); break; case "e": @@ -221,6 +384,7 @@ function connect() { schedule(); } + rememberStatus(msg); setError(msg); break; @@ -231,18 +395,26 @@ function connect() { }; ws.onclose = function(e) { + + connectionLost(); + + if (portal && portalUrl.length == 0) { + return; + } + setTimeout(function() { connect(); }, interval); }; ws.onerror = function(e) { + connectionLost(); ws.close(); - if (!booting) { - window.location.reload(); - } }; } +document.addEventListener("visibilitychange", visibilityChanged); +rememberStatus(document.getElementById("info").innerHTML); + schedule(); connect();