@@ -0,0 +1,145 @@
|
||||
"""Persistent OTA update state in data/update_state.json."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
VALID_STATUSES = frozenset(
|
||||
{"idle", "in_progress", "success", "failed", "rolled_back", "pending_restart"}
|
||||
)
|
||||
|
||||
|
||||
def _utc_now_iso() -> str:
|
||||
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def update_state_path(base_dir: str) -> str:
|
||||
return os.path.join(base_dir, "data", "update_state.json")
|
||||
|
||||
|
||||
def read_update_state(base_dir: str) -> Dict[str, Any]:
|
||||
path = update_state_path(base_dir)
|
||||
if not os.path.isfile(path):
|
||||
return {"status": "idle"}
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if isinstance(data, dict):
|
||||
return data
|
||||
except (OSError, json.JSONDecodeError):
|
||||
pass
|
||||
return {"status": "idle"}
|
||||
|
||||
|
||||
def write_update_state(base_dir: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
os.makedirs(os.path.join(base_dir, "data"), exist_ok=True)
|
||||
path = update_state_path(base_dir)
|
||||
current = read_update_state(base_dir)
|
||||
if current.get("status") == "idle" and "status" not in payload:
|
||||
payload = {**payload, "status": "in_progress"}
|
||||
merged = {**current, **payload}
|
||||
status = str(merged.get("status", "idle"))
|
||||
if status not in VALID_STATUSES:
|
||||
merged["status"] = "failed"
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(merged, f, ensure_ascii=False, indent=2)
|
||||
f.write("\n")
|
||||
return merged
|
||||
|
||||
|
||||
def mark_in_progress(
|
||||
base_dir: str,
|
||||
*,
|
||||
target_version: str,
|
||||
previous_version: str,
|
||||
backup_path: str,
|
||||
) -> Dict[str, Any]:
|
||||
return write_update_state(
|
||||
base_dir,
|
||||
{
|
||||
"status": "in_progress",
|
||||
"target_version": target_version,
|
||||
"previous_version": previous_version,
|
||||
"backup_path": backup_path,
|
||||
"started_at": _utc_now_iso(),
|
||||
"stage": "prepare",
|
||||
"message": "Подготовка к обновлению…",
|
||||
"detail": None,
|
||||
"error": None,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def mark_success(base_dir: str, *, target_version: str) -> Dict[str, Any]:
|
||||
return write_update_state(
|
||||
base_dir,
|
||||
{
|
||||
"status": "success",
|
||||
"target_version": target_version,
|
||||
"finished_at": _utc_now_iso(),
|
||||
"message": f"Обновление до версии {target_version} завершено",
|
||||
"error": None,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def mark_failed(
|
||||
base_dir: str,
|
||||
*,
|
||||
message: str,
|
||||
stage: Optional[str] = None,
|
||||
detail: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
payload: Dict[str, Any] = {
|
||||
"status": "failed",
|
||||
"message": message,
|
||||
"finished_at": _utc_now_iso(),
|
||||
"error": message,
|
||||
}
|
||||
if stage:
|
||||
payload["stage"] = stage
|
||||
if detail:
|
||||
payload["detail"] = detail
|
||||
return write_update_state(base_dir, payload)
|
||||
|
||||
|
||||
def mark_rolled_back(
|
||||
base_dir: str,
|
||||
*,
|
||||
previous_version: str,
|
||||
message: str,
|
||||
) -> Dict[str, Any]:
|
||||
return write_update_state(
|
||||
base_dir,
|
||||
{
|
||||
"status": "rolled_back",
|
||||
"previous_version": previous_version,
|
||||
"message": message,
|
||||
"finished_at": _utc_now_iso(),
|
||||
"error": message,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def mark_pending_restart(base_dir: str) -> Dict[str, Any]:
|
||||
return write_update_state(
|
||||
base_dir,
|
||||
{
|
||||
"status": "pending_restart",
|
||||
"stage": "restart",
|
||||
"message": "Перезапуск системы…",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def clear_update_state(base_dir: str) -> None:
|
||||
path = update_state_path(base_dir)
|
||||
if os.path.isfile(path):
|
||||
try:
|
||||
os.remove(path)
|
||||
except OSError:
|
||||
pass
|
||||
Reference in New Issue
Block a user