mirror of
https://github.com/vdsm/virtual-dsm.git
synced 2026-08-28 11:56:03 +00:00
fix: Preserve web status after connection loss (#1361)
This commit is contained in:
@@ -2271,6 +2271,7 @@ if enabled "$DHCP"; then
|
||||
else
|
||||
|
||||
if ! disabled "${WEB:-}"; then
|
||||
writeAtomic "$WSD_COMMAND" "portal"
|
||||
sleep 1.2
|
||||
closeWeb
|
||||
fi
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user