Smooth admin-triggered updates

This commit is contained in:
cyph3rasi
2026-03-07 22:29:49 -08:00
parent 830c062fa1
commit a3a5b83f04
3 changed files with 52 additions and 19 deletions
+34 -17
View File
@@ -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.",
}, },
) )
+8
View File
@@ -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
View File
@@ -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);
} }