fix: Preserve web status after connection loss (#1361)

This commit is contained in:
Kroese
2026-08-09 13:15:44 +02:00
committed by GitHub
parent cb8f440564
commit c64046411b
5 changed files with 251 additions and 52 deletions
+1
View File
@@ -2271,6 +2271,7 @@ if enabled "$DHCP"; then
else
if ! disabled "${WEB:-}"; then
writeAtomic "$WSD_COMMAND" "portal"
sleep 1.2
closeWeb
fi
+2
View File
@@ -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
}
+2 -1
View File
@@ -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
}
+43 -20
View File
@@ -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
+203 -31
View File
@@ -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("<html>") !== -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 += "<br>(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();