fixed updater
This commit is contained in:
@@ -24,9 +24,9 @@ FLAKE_LOCK_PATH = "/etc/nixos/flake.lock"
|
|||||||
FLAKE_INPUT_NAME = "Sovran_Systems"
|
FLAKE_INPUT_NAME = "Sovran_Systems"
|
||||||
GITEA_API_BASE = "https://git.sovransystems.com/api/v1/repos/Sovran_Systems/Sovran_SystemsOS/commits"
|
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_LOG = "/var/log/sovran-hub-update.log"
|
UPDATE_STATUS = "/var/log/sovran-hub-update.status"
|
||||||
UPDATE_LOCK = "/run/sovran-hub-update.lock"
|
UPDATE_UNIT = "sovran-hub-update.service"
|
||||||
|
|
||||||
REBOOT_COMMAND = ["reboot"]
|
REBOOT_COMMAND = ["reboot"]
|
||||||
|
|
||||||
@@ -156,51 +156,15 @@ def _get_external_ip() -> str:
|
|||||||
return "unavailable"
|
return "unavailable"
|
||||||
|
|
||||||
|
|
||||||
# ── Update unit helpers ──────────────────────────────────────────
|
# ── Update helpers (file-based, no systemctl) ────────────────────
|
||||||
|
|
||||||
def _update_is_active() -> bool:
|
def _read_update_status() -> str:
|
||||||
"""Return True if the update unit is currently running."""
|
"""Read the status file. Returns RUNNING, SUCCESS, FAILED, or IDLE."""
|
||||||
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."""
|
|
||||||
try:
|
try:
|
||||||
with open(UPDATE_LOCK, "w") as f:
|
with open(UPDATE_STATUS, "r") as f:
|
||||||
f.write(str(os.getpid()))
|
return f.read().strip()
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _remove_update_lock():
|
|
||||||
"""Remove the lock file."""
|
|
||||||
try:
|
|
||||||
os.unlink(UPDATE_LOCK)
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
return "IDLE"
|
||||||
|
|
||||||
|
|
||||||
def _read_log(offset: int = 0) -> tuple[str, int]:
|
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)."""
|
Returns (new_text, new_offset)."""
|
||||||
try:
|
try:
|
||||||
with open(UPDATE_LOG, "rb") as f:
|
with open(UPDATE_LOG, "rb") as f:
|
||||||
f.seek(0, 2) # seek to end
|
f.seek(0, 2)
|
||||||
size = f.tell()
|
size = f.tell()
|
||||||
if offset > size:
|
if offset > size:
|
||||||
# Log was truncated (new run), start over
|
|
||||||
offset = 0
|
offset = 0
|
||||||
f.seek(offset)
|
f.seek(offset)
|
||||||
chunk = f.read()
|
chunk = f.read()
|
||||||
@@ -351,8 +314,8 @@ async def api_updates_run():
|
|||||||
"""Kick off the detached update systemd unit."""
|
"""Kick off the detached update systemd unit."""
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
running = await loop.run_in_executor(None, _update_is_active)
|
status = await loop.run_in_executor(None, _read_update_status)
|
||||||
if running:
|
if status == "RUNNING":
|
||||||
return {"ok": True, "status": "already_running"}
|
return {"ok": True, "status": "already_running"}
|
||||||
|
|
||||||
# Reset failed state if any
|
# Reset failed state if any
|
||||||
@@ -362,9 +325,6 @@ async def api_updates_run():
|
|||||||
stderr=asyncio.subprocess.DEVNULL,
|
stderr=asyncio.subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a file-based lock that survives server restarts
|
|
||||||
_create_update_lock()
|
|
||||||
|
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
"systemctl", "start", "--no-block", UPDATE_UNIT,
|
"systemctl", "start", "--no-block", UPDATE_UNIT,
|
||||||
stdout=asyncio.subprocess.DEVNULL,
|
stdout=asyncio.subprocess.DEVNULL,
|
||||||
@@ -377,38 +337,17 @@ async def api_updates_run():
|
|||||||
|
|
||||||
@app.get("/api/updates/status")
|
@app.get("/api/updates/status")
|
||||||
async def api_updates_status(offset: int = 0):
|
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()
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
active = await loop.run_in_executor(None, _update_is_active)
|
status = await loop.run_in_executor(None, _read_update_status)
|
||||||
result = await loop.run_in_executor(None, _update_result)
|
|
||||||
lock_exists = _update_lock_exists()
|
|
||||||
new_log, new_offset = await loop.run_in_executor(None, _read_log, offset)
|
new_log, new_offset = await loop.run_in_executor(None, _read_log, offset)
|
||||||
|
|
||||||
# If the unit is active, it's definitely still running
|
running = (status == "RUNNING")
|
||||||
if active:
|
result = "pending" if running else status.lower()
|
||||||
return {
|
|
||||||
"running": True,
|
|
||||||
"result": "pending",
|
|
||||||
"log": new_log,
|
|
||||||
"offset": new_offset,
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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 {
|
return {
|
||||||
"running": False,
|
"running": running,
|
||||||
"result": result,
|
"result": result,
|
||||||
"log": new_log,
|
"log": new_log,
|
||||||
"offset": new_offset,
|
"offset": new_offset,
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ async function apiFetch(path, options = {}) {
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Render: initial build ────────────────────────<EFBFBD><EFBFBD>────────────────
|
// ── Render: initial build ─────────────────────────────────────────
|
||||||
|
|
||||||
function buildTiles(services, categoryLabels) {
|
function buildTiles(services, categoryLabels) {
|
||||||
_servicesCache = services;
|
_servicesCache = services;
|
||||||
@@ -341,24 +341,28 @@ async function pollUpdateStatus() {
|
|||||||
if ($modalStatus) $modalStatus.textContent = "Updating…";
|
if ($modalStatus) $modalStatus.textContent = "Updating…";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append any new log content
|
// Append new log content
|
||||||
if (data.log) {
|
if (data.log) {
|
||||||
appendLog(data.log);
|
appendLog(data.log);
|
||||||
}
|
}
|
||||||
_updateLogOffset = data.offset;
|
_updateLogOffset = data.offset;
|
||||||
|
|
||||||
// Check if finished
|
// RUNNING → keep polling
|
||||||
if (!data.running) {
|
if (data.running) {
|
||||||
_updateFinished = true;
|
return;
|
||||||
stopUpdatePoll();
|
}
|
||||||
if (data.result === "success") {
|
|
||||||
onUpdateDone(true);
|
// Finished — check result
|
||||||
} else {
|
_updateFinished = true;
|
||||||
onUpdateDone(false);
|
stopUpdatePoll();
|
||||||
}
|
|
||||||
|
if (data.result === "success") {
|
||||||
|
onUpdateDone(true);
|
||||||
|
} else {
|
||||||
|
onUpdateDone(false);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Server is likely restarting during nixos-rebuild switch — keep polling
|
// Server is restarting during nixos-rebuild switch — keep polling
|
||||||
if (!_serverWasDown) {
|
if (!_serverWasDown) {
|
||||||
_serverWasDown = true;
|
_serverWasDown = true;
|
||||||
appendLog("\n[Server restarting — waiting for it to come back…]\n");
|
appendLog("\n[Server restarting — waiting for it to come back…]\n");
|
||||||
|
|||||||
@@ -473,14 +473,14 @@ button:disabled {
|
|||||||
border-top: 1px solid var(--border-color);
|
border-top: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Reboot button: green */
|
/* Reboot button */
|
||||||
.btn-reboot {
|
#btn-reboot {
|
||||||
background-color: var(--green);
|
background-color: #2ec27e !important;
|
||||||
color: #fff;
|
color: #fff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-reboot:hover:not(:disabled) {
|
#btn-reboot:hover:not(:disabled) {
|
||||||
background-color: #27ae6e;
|
background-color: #27ae6e !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-save {
|
.btn-save {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ let
|
|||||||
{ name = "Matrix-Synapse"; unit = "matrix-synapse.service"; type = "system"; icon = "synapse"; enabled = cfg.services.synapse; category = "communication"; }
|
{ 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"; }
|
{ 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 = "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"; }
|
{ 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"
|
export PATH="${lib.makeBinPath [ pkgs.nix pkgs.nixos-rebuild pkgs.git pkgs.flatpak pkgs.coreutils ]}:$PATH"
|
||||||
|
|
||||||
LOG="/var/log/sovran-hub-update.log"
|
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)
|
# Mark as RUNNING
|
||||||
echo $$ > "$LOCK"
|
echo "RUNNING" > "$STATUS"
|
||||||
|
|
||||||
# Truncate the log and redirect ALL output (stdout + stderr) into it
|
# Truncate the log and redirect ALL output (stdout + stderr) into it
|
||||||
: > "$LOG"
|
: > "$LOG"
|
||||||
exec > >(tee -a "$LOG") 2>&1
|
exec > >(tee -a "$LOG") 2>&1
|
||||||
|
|
||||||
# Ensure lock is removed on exit (success or failure)
|
|
||||||
trap 'rm -f "$LOCK"' EXIT
|
|
||||||
|
|
||||||
echo "══════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════"
|
||||||
echo " Sovran_SystemsOS Update — $(date)"
|
echo " Sovran_SystemsOS Update — $(date)"
|
||||||
echo "══════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════"
|
||||||
@@ -105,10 +102,12 @@ let
|
|||||||
echo "══════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════"
|
||||||
echo " ✓ Update completed successfully"
|
echo " ✓ Update completed successfully"
|
||||||
echo "══════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════"
|
||||||
|
echo "SUCCESS" > "$STATUS"
|
||||||
else
|
else
|
||||||
echo "══════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════"
|
||||||
echo " ✗ Update failed — see errors above"
|
echo " ✗ Update failed — see errors above"
|
||||||
echo "══════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════"
|
||||||
|
echo "FAILED" > "$STATUS"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exit "$RC"
|
exit "$RC"
|
||||||
|
|||||||
Reference in New Issue
Block a user