146 lines
3.9 KiB
Python
146 lines
3.9 KiB
Python
"""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
|