Smooth admin-triggered updates
This commit is contained in:
+34
-17
@@ -6,6 +6,7 @@ import signal
|
|||||||
import socketserver
|
import socketserver
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from http.server import BaseHTTPRequestHandler
|
from http.server import BaseHTTPRequestHandler
|
||||||
@@ -67,7 +68,10 @@ def is_authorized(headers):
|
|||||||
|
|
||||||
|
|
||||||
def watch_process(process):
|
def watch_process(process):
|
||||||
|
global current_process
|
||||||
exit_code = process.wait()
|
exit_code = process.wait()
|
||||||
|
with status_lock:
|
||||||
|
current_process = None
|
||||||
update_status(
|
update_status(
|
||||||
status="success" if exit_code == 0 else "error",
|
status="success" if exit_code == 0 else "error",
|
||||||
message="Synapsis update completed." if exit_code == 0 else "Synapsis update failed.",
|
message="Synapsis update completed." if exit_code == 0 else "Synapsis update failed.",
|
||||||
@@ -78,6 +82,32 @@ def watch_process(process):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def start_update_process():
|
||||||
|
global current_process
|
||||||
|
|
||||||
|
# Give the app enough time to return the 202 response before the container restarts.
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
with status_lock:
|
||||||
|
log_handle = open(LOG_FILE, "a", encoding="utf-8")
|
||||||
|
current_process = subprocess.Popen(
|
||||||
|
[UPDATE_SCRIPT],
|
||||||
|
cwd=INSTALL_DIR,
|
||||||
|
stdout=log_handle,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
start_new_session=True,
|
||||||
|
env={
|
||||||
|
**os.environ,
|
||||||
|
"INSTALL_DIR": INSTALL_DIR,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
update_status(pid=current_process.pid)
|
||||||
|
|
||||||
|
thread = threading.Thread(target=watch_process, args=(current_process,), daemon=True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
|
||||||
class ThreadedUnixServer(socketserver.ThreadingMixIn, socketserver.UnixStreamServer):
|
class ThreadedUnixServer(socketserver.ThreadingMixIn, socketserver.UnixStreamServer):
|
||||||
daemon_threads = True
|
daemon_threads = True
|
||||||
|
|
||||||
@@ -133,30 +163,17 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
log_handle = open(LOG_FILE, "a", encoding="utf-8")
|
|
||||||
current_process = subprocess.Popen(
|
|
||||||
[UPDATE_SCRIPT],
|
|
||||||
cwd=INSTALL_DIR,
|
|
||||||
stdout=log_handle,
|
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
start_new_session=True,
|
|
||||||
env={
|
|
||||||
**os.environ,
|
|
||||||
"INSTALL_DIR": INSTALL_DIR,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
update_status(
|
update_status(
|
||||||
status="updating",
|
status="updating",
|
||||||
message="Synapsis update started.",
|
message="Synapsis update scheduled.",
|
||||||
lastStartedAt=now_iso(),
|
lastStartedAt=now_iso(),
|
||||||
lastFinishedAt=None,
|
lastFinishedAt=None,
|
||||||
lastExitCode=None,
|
lastExitCode=None,
|
||||||
lastError=None,
|
lastError=None,
|
||||||
pid=current_process.pid,
|
pid=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
thread = threading.Thread(target=watch_process, args=(current_process,), daemon=True)
|
thread = threading.Thread(target=start_update_process, daemon=True)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
self.send_json(
|
self.send_json(
|
||||||
@@ -164,7 +181,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
{
|
{
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"status": "updating",
|
"status": "updating",
|
||||||
"message": "Synapsis update started.",
|
"message": "Synapsis update scheduled. The node will restart shortly.",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -63,11 +63,19 @@ case "${ACTIVE_PROXY}" in
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
download_file "docker/host-updater.py" "${INSTALL_DIR}/host-updater.py"
|
||||||
|
download_file "docker/update-local.sh" "${INSTALL_DIR}/update-local.sh"
|
||||||
|
chmod 755 "${INSTALL_DIR}/host-updater.py" "${INSTALL_DIR}/update-local.sh"
|
||||||
|
|
||||||
echo "🐳 Pulling latest Synapsis image"
|
echo "🐳 Pulling latest Synapsis image"
|
||||||
compose_cmd pull
|
compose_cmd pull
|
||||||
|
|
||||||
echo "🚀 Restarting Synapsis"
|
echo "🚀 Restarting Synapsis"
|
||||||
compose_cmd up -d --remove-orphans
|
compose_cmd up -d --remove-orphans
|
||||||
|
|
||||||
|
if command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files synapsis-updater.service >/dev/null 2>&1; then
|
||||||
|
systemctl restart synapsis-updater.service >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "✅ Synapsis has been updated to the latest published image."
|
echo "✅ Synapsis has been updated to the latest published image."
|
||||||
|
|||||||
+10
-2
@@ -231,7 +231,7 @@ export default function AdminPage() {
|
|||||||
const handleTriggerUpdate = async () => {
|
const handleTriggerUpdate = async () => {
|
||||||
setTriggeringUpdate(true);
|
setTriggeringUpdate(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/admin/update', { method: 'POST' });
|
const res = await fetch('/api/admin/update', { method: 'POST', keepalive: true });
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -241,7 +241,15 @@ export default function AdminPage() {
|
|||||||
showToast(data.message || 'Update started. Synapsis will restart shortly.', 'success');
|
showToast(data.message || 'Update started. Synapsis will restart shortly.', 'success');
|
||||||
await loadUpdateStatus();
|
await loadUpdateStatus();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(error instanceof Error ? error.message : 'Failed to start update', 'error');
|
const message = error instanceof Error ? error.message : 'Failed to start update';
|
||||||
|
if (message.toLowerCase().includes('fetch') || message.toLowerCase().includes('network')) {
|
||||||
|
showToast('Update likely started. The node is restarting, so this page may disconnect briefly.', 'success');
|
||||||
|
window.setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 5000);
|
||||||
|
} else {
|
||||||
|
showToast(message, 'error');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setTriggeringUpdate(false);
|
setTriggeringUpdate(false);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user