5 Commits

Author SHA1 Message Date
Aaron
99bd87c7f6 Release 0.1.3-dev4: updater resilience 2025-12-14 19:16:29 -05:00
Aaron
e87b90bf9f Auto-clear stale updater state 2025-12-14 19:07:47 -05:00
Aaron
8557140193 Avoid restarting API during installs 2025-12-14 19:01:46 -05:00
Aaron
86438b11f3 Handle stale updater lockfiles by removing dead PID entries 2025-12-14 18:56:15 -05:00
Aaron
3a785832b1 Bump to 0.1.3-dev3 and update dev manifest 2025-12-14 18:51:03 -05:00
3 changed files with 70 additions and 23 deletions

View File

@@ -1,9 +1,9 @@
{ {
"version": "0.1.3-dev1", "version": "0.1.3-dev4",
"_release_date": "2025-12-14T22:23:00Z", "_release_date": "2025-12-15T00:15:43Z",
"bundle": "https://git.44r0n.cc/44r0n7/pi-kit/releases/download/v0.1.3-dev1/pikit-0.1.3-dev1.tar.gz", "bundle": "https://git.44r0n.cc/44r0n7/pi-kit/releases/download/v0.1.3-dev4/pikit-0.1.3-dev4.tar.gz",
"changelog": "https://git.44r0n.cc/44r0n7/pi-kit/releases/download/v0.1.3-dev1/CHANGELOG-0.1.3-dev1.txt", "changelog": "https://git.44r0n.cc/44r0n7/pi-kit/releases/download/v0.1.3-dev4/CHANGELOG-0.1.3-dev4.txt",
"files": [ "files": [
{ "path": "bundle.tar.gz", "sha256": "290bc3ef0acbac8ffc1d283fdf5413bdd0dd6a90a9ccd2253dfd406773951b62" } { "path": "bundle.tar.gz", "sha256": "e0a5dcadbde6d6f75afbfecbb8b01d943ac68c57b6b9f724a65d3ffa15d8648c" }
] ]
} }

View File

@@ -1,3 +1,3 @@
{ {
"version": "0.1.3-dev1" "version": "0.1.3-dev4"
} }

View File

@@ -43,16 +43,48 @@ def read_current_version() -> str:
def load_update_state() -> Dict[str, Any]: def load_update_state() -> Dict[str, Any]:
UPDATE_STATE_DIR.mkdir(parents=True, exist_ok=True) UPDATE_STATE_DIR.mkdir(parents=True, exist_ok=True)
def _reset_if_stale(state: Dict[str, Any]) -> Dict[str, Any]:
"""
If state thinks an update is running but the lock holder is gone,
clear it so the UI can recover instead of getting stuck forever.
"""
lock_alive = False
if UPDATE_LOCK.exists():
try:
pid = int(UPDATE_LOCK.read_text().strip() or "0")
if pid > 0:
os.kill(pid, 0)
lock_alive = True
else:
UPDATE_LOCK.unlink(missing_ok=True)
except OSError:
UPDATE_LOCK.unlink(missing_ok=True)
except Exception:
UPDATE_LOCK.unlink(missing_ok=True)
if state.get("in_progress") and not lock_alive:
state["in_progress"] = False
state["progress"] = None
if state.get("status") == "in_progress":
state["status"] = "up_to_date"
state["message"] = state.get("message") or "Recovered from interrupted update"
try:
save_update_state(state)
except Exception:
pass
return state
if UPDATE_STATE.exists(): if UPDATE_STATE.exists():
try: try:
state = json.loads(UPDATE_STATE.read_text()) state = json.loads(UPDATE_STATE.read_text())
state.setdefault("changelog_url", None) state.setdefault("changelog_url", None)
state.setdefault("latest_release_date", None) state.setdefault("latest_release_date", None)
state.setdefault("current_release_date", None) state.setdefault("current_release_date", None)
return state return _reset_if_stale(state)
except Exception: except Exception:
pass pass
return { return _reset_if_stale(
{
"current_version": read_current_version(), "current_version": read_current_version(),
"latest_version": None, "latest_version": None,
"last_check": None, "last_check": None,
@@ -66,6 +98,7 @@ def load_update_state() -> Dict[str, Any]:
"latest_release_date": None, "latest_release_date": None,
"current_release_date": None, "current_release_date": None,
} }
)
def save_update_state(state: Dict[str, Any]) -> None: def save_update_state(state: Dict[str, Any]) -> None:
@@ -400,6 +433,19 @@ def fetch_text_with_auth(url: str):
def acquire_lock(): def acquire_lock():
try: try:
ensure_dir(UPDATE_LOCK.parent) ensure_dir(UPDATE_LOCK.parent)
# Clear stale lock if the recorded PID is not running
if UPDATE_LOCK.exists():
try:
pid = int(UPDATE_LOCK.read_text().strip() or "0")
if pid > 0:
os.kill(pid, 0)
else:
UPDATE_LOCK.unlink(missing_ok=True)
except OSError:
# Process not running
UPDATE_LOCK.unlink(missing_ok=True)
except Exception:
UPDATE_LOCK.unlink(missing_ok=True)
lockfile = UPDATE_LOCK.open("w") lockfile = UPDATE_LOCK.open("w")
fcntl.flock(lockfile.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) fcntl.flock(lockfile.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
lockfile.write(str(os.getpid())) lockfile.write(str(os.getpid()))
@@ -523,8 +569,9 @@ def _install_manifest(manifest: Dict[str, Any], meta: Optional[Dict[str, Any]],
shutil.rmtree(API_PACKAGE_DIR, ignore_errors=True) shutil.rmtree(API_PACKAGE_DIR, ignore_errors=True)
shutil.copytree(staged_pkg, API_PACKAGE_DIR, dirs_exist_ok=True) shutil.copytree(staged_pkg, API_PACKAGE_DIR, dirs_exist_ok=True)
for svc in ("pikit-api.service", "dietpi-dashboard-frontend.service"): # Restart frontend to pick up new assets; avoid restarting this API process
subprocess.run(["systemctl", "restart", svc], check=False) # mid-apply to prevent leaving state in_progress.
subprocess.run(["systemctl", "restart", "dietpi-dashboard-frontend.service"], check=False)
VERSION_FILE.parent.mkdir(parents=True, exist_ok=True) VERSION_FILE.parent.mkdir(parents=True, exist_ok=True)
VERSION_FILE.write_text(str(latest)) VERSION_FILE.write_text(str(latest))