fixed updater

This commit is contained in:
2026-04-02 13:42:24 -05:00
parent a66e8e736f
commit bb7db0693a
4 changed files with 45 additions and 103 deletions

View File

@@ -24,9 +24,9 @@ FLAKE_LOCK_PATH = "/etc/nixos/flake.lock"
FLAKE_INPUT_NAME = "Sovran_Systems"
GITEA_API_BASE = "https://git.sovransystems.com/api/v1/repos/Sovran_Systems/Sovran_SystemsOS/commits"
UPDATE_UNIT = "sovran-hub-update.service"
UPDATE_LOG = "/var/log/sovran-hub-update.log"
UPDATE_LOCK = "/run/sovran-hub-update.lock"
UPDATE_STATUS = "/var/log/sovran-hub-update.status"
UPDATE_UNIT = "sovran-hub-update.service"
REBOOT_COMMAND = ["reboot"]
@@ -156,51 +156,15 @@ def _get_external_ip() -> str:
return "unavailable"
# ── Update unit helpers ──────────────────────────────────────────
# ── Update helpers (file-based, no systemctl) ────────────────────
def _update_is_active() -> bool:
"""Return True if the update unit is currently running."""
r = subprocess.run(
["systemctl", "is-active", "--quiet", UPDATE_UNIT],
capture_output=True,
)
return r.returncode == 0
def _update_result() -> str:
"""Return 'success', 'failed', or 'unknown'."""
r = subprocess.run(
["systemctl", "show", "-p", "Result", "--value", UPDATE_UNIT],
capture_output=True, text=True,
)
val = r.stdout.strip()
if val == "success":
return "success"
elif val:
return "failed"
return "unknown"
def _update_lock_exists() -> bool:
"""Check if the file-based update lock exists (survives server restart)."""
return os.path.exists(UPDATE_LOCK)
def _create_update_lock():
"""Create the lock file to indicate an update is in progress."""
def _read_update_status() -> str:
"""Read the status file. Returns RUNNING, SUCCESS, FAILED, or IDLE."""
try:
with open(UPDATE_LOCK, "w") as f:
f.write(str(os.getpid()))
except OSError:
pass
def _remove_update_lock():
"""Remove the lock file."""
try:
os.unlink(UPDATE_LOCK)
with open(UPDATE_STATUS, "r") as f:
return f.read().strip()
except FileNotFoundError:
pass
return "IDLE"
def _read_log(offset: int = 0) -> tuple[str, int]:
@@ -208,10 +172,9 @@ def _read_log(offset: int = 0) -> tuple[str, int]:
Returns (new_text, new_offset)."""
try:
with open(UPDATE_LOG, "rb") as f:
f.seek(0, 2) # seek to end
f.seek(0, 2)
size = f.tell()
if offset > size:
# Log was truncated (new run), start over
offset = 0
f.seek(offset)
chunk = f.read()
@@ -351,8 +314,8 @@ async def api_updates_run():
"""Kick off the detached update systemd unit."""
loop = asyncio.get_event_loop()
running = await loop.run_in_executor(None, _update_is_active)
if running:
status = await loop.run_in_executor(None, _read_update_status)
if status == "RUNNING":
return {"ok": True, "status": "already_running"}
# Reset failed state if any
@@ -362,9 +325,6 @@ async def api_updates_run():
stderr=asyncio.subprocess.DEVNULL,
)
# Create a file-based lock that survives server restarts
_create_update_lock()
proc = await asyncio.create_subprocess_exec(
"systemctl", "start", "--no-block", UPDATE_UNIT,
stdout=asyncio.subprocess.DEVNULL,
@@ -377,38 +337,17 @@ async def api_updates_run():
@app.get("/api/updates/status")
async def api_updates_status(offset: int = 0):
"""Poll endpoint: returns running state, result, and new log content."""
"""Poll endpoint: reads status file + log file. No systemctl needed."""
loop = asyncio.get_event_loop()
active = await loop.run_in_executor(None, _update_is_active)
result = await loop.run_in_executor(None, _update_result)
lock_exists = _update_lock_exists()
status = await loop.run_in_executor(None, _read_update_status)
new_log, new_offset = await loop.run_in_executor(None, _read_log, offset)
# If the unit is active, it's definitely still running
if active:
return {
"running": True,
"result": "pending",
"log": new_log,
"offset": new_offset,
}
running = (status == "RUNNING")
result = "pending" if running else status.lower()
# If the lock file exists but the unit is not active, the update
# finished (or the server just restarted after nixos-rebuild switch).
# The lock file persists across server restarts because it's on disk.
if lock_exists:
_remove_update_lock()
return {
"running": False,
"result": result,
"log": new_log,
"offset": new_offset,
}
# No lock, not active — nothing happening
return {
"running": False,
"running": running,
"result": result,
"log": new_log,
"offset": new_offset,

View File

@@ -72,7 +72,7 @@ async function apiFetch(path, options = {}) {
return res.json();
}
// ── Render: initial build ────────────────────────<EFBFBD><EFBFBD>────────────────
// ── Render: initial build ────────────────────────────────────────
function buildTiles(services, categoryLabels) {
_servicesCache = services;
@@ -341,24 +341,28 @@ async function pollUpdateStatus() {
if ($modalStatus) $modalStatus.textContent = "Updating…";
}
// Append any new log content
// Append new log content
if (data.log) {
appendLog(data.log);
}
_updateLogOffset = data.offset;
// Check if finished
if (!data.running) {
// RUNNING → keep polling
if (data.running) {
return;
}
// Finished — check result
_updateFinished = true;
stopUpdatePoll();
if (data.result === "success") {
onUpdateDone(true);
} else {
onUpdateDone(false);
}
}
} catch (err) {
// Server is likely restarting during nixos-rebuild switch — keep polling
// Server is restarting during nixos-rebuild switch — keep polling
if (!_serverWasDown) {
_serverWasDown = true;
appendLog("\n[Server restarting — waiting for it to come back…]\n");

View File

@@ -473,14 +473,14 @@ button:disabled {
border-top: 1px solid var(--border-color);
}
/* Reboot button: green */
.btn-reboot {
background-color: var(--green);
color: #fff;
/* Reboot button */
#btn-reboot {
background-color: #2ec27e !important;
color: #fff !important;
}
.btn-reboot:hover:not(:disabled) {
background-color: #27ae6e;
#btn-reboot:hover:not(:disabled) {
background-color: #27ae6e !important;
}
.btn-save {

View File

@@ -28,7 +28,7 @@ let
{ name = "Matrix-Synapse"; unit = "matrix-synapse.service"; type = "system"; icon = "synapse"; enabled = cfg.services.synapse; category = "communication"; }
{ name = "Element-Call"; unit = "livekit.service"; type = "system"; icon = "livekit"; enabled = cfg.features.element-calling; category = "communication"; }
]
# ── Self-Hosted Apps ────────────────<EFBFBD><EFBFBD>──────────────────────
# ── Self-Hosted Apps ──────────────────────────────────────
++ [
{ name = "VaultWarden"; unit = "vaultwarden.service"; type = "system"; icon = "vaultwarden"; enabled = cfg.services.vaultwarden; category = "apps"; }
{ name = "Nextcloud"; unit = "phpfpm-nextcloud.service"; type = "system"; icon = "nextcloud"; enabled = cfg.services.nextcloud; category = "apps"; }
@@ -58,18 +58,15 @@ let
export PATH="${lib.makeBinPath [ pkgs.nix pkgs.nixos-rebuild pkgs.git pkgs.flatpak pkgs.coreutils ]}:$PATH"
LOG="/var/log/sovran-hub-update.log"
LOCK="/run/sovran-hub-update.lock"
STATUS="/var/log/sovran-hub-update.status"
# Create lock file (survives server restarts, cleared on reboot since /run is tmpfs)
echo $$ > "$LOCK"
# Mark as RUNNING
echo "RUNNING" > "$STATUS"
# Truncate the log and redirect ALL output (stdout + stderr) into it
: > "$LOG"
exec > >(tee -a "$LOG") 2>&1
# Ensure lock is removed on exit (success or failure)
trap 'rm -f "$LOCK"' EXIT
echo ""
echo " Sovran_SystemsOS Update $(date)"
echo ""
@@ -105,10 +102,12 @@ let
echo ""
echo " Update completed successfully"
echo ""
echo "SUCCESS" > "$STATUS"
else
echo ""
echo " Update failed see errors above"
echo ""
echo "FAILED" > "$STATUS"
fi
exit "$RC"