初始化奇妙小屏幕控制器项目
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from app.persistence import atomic_write_bytes
|
||||
|
||||
|
||||
MAX_FAILURE_LOG_BYTES = 1024 * 1024
|
||||
FAILURE_LOG_RELATIVE = Path("ota") / "last-failure.log"
|
||||
_TRUNCATION_MARKER = (
|
||||
"\n\n========== 日志已截断 =========="
|
||||
"\n原始日志超过 1 MiB;保留开头和最新的失败尾部。"
|
||||
"\n========== 继续显示最新日志 ==========\n\n"
|
||||
).encode("utf-8")
|
||||
_HEAD_BYTES = 128 * 1024
|
||||
|
||||
|
||||
def _timestamp() -> str:
|
||||
return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with Path(path).open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def bounded_failure_log(body: bytes, *, maximum: int = MAX_FAILURE_LOG_BYTES) -> bytes:
|
||||
normalized = bytes(body).decode("utf-8", errors="replace").encode("utf-8")
|
||||
if len(normalized) <= maximum:
|
||||
return normalized
|
||||
head = min(_HEAD_BYTES, maximum // 4)
|
||||
tail = maximum - head - len(_TRUNCATION_MARKER)
|
||||
if tail <= 0:
|
||||
return normalized[-maximum:]
|
||||
return normalized[:head] + _TRUNCATION_MARKER + normalized[-tail:]
|
||||
|
||||
|
||||
def failure_log_path(data_root: Path) -> Path:
|
||||
return Path(data_root) / FAILURE_LOG_RELATIVE
|
||||
|
||||
|
||||
def persist_failure_log(data_root: Path, runtime_log: Path) -> Path:
|
||||
source = Path(runtime_log)
|
||||
body = bounded_failure_log(source.read_bytes())
|
||||
target = failure_log_path(data_root)
|
||||
atomic_write_bytes(target, body)
|
||||
try:
|
||||
target.chmod(0o600)
|
||||
except OSError:
|
||||
target.unlink(missing_ok=True)
|
||||
raise
|
||||
return target
|
||||
|
||||
|
||||
def clear_failure_log(data_root: Path) -> None:
|
||||
failure_log_path(data_root).unlink(missing_ok=True)
|
||||
|
||||
|
||||
def failure_log_metadata(data_root: Path, last_result: dict[str, Any] | None) -> tuple[bool, int]:
|
||||
if not isinstance(last_result, dict) or last_result.get("status") != "failed":
|
||||
return False, 0
|
||||
path = failure_log_path(data_root)
|
||||
try:
|
||||
size = path.stat().st_size
|
||||
except OSError:
|
||||
return False, 0
|
||||
if size <= 0 or size > MAX_FAILURE_LOG_BYTES:
|
||||
return False, 0
|
||||
return True, size
|
||||
|
||||
|
||||
def read_failure_log(data_root: Path, last_result: dict[str, Any] | None) -> bytes | None:
|
||||
available, expected_size = failure_log_metadata(data_root, last_result)
|
||||
if not available:
|
||||
return None
|
||||
try:
|
||||
body = failure_log_path(data_root).read_bytes()
|
||||
except OSError:
|
||||
return None
|
||||
if len(body) != expected_size:
|
||||
return None
|
||||
return body
|
||||
|
||||
|
||||
class DiagnosticLog:
|
||||
def __init__(self, path: Path) -> None:
|
||||
self.path = Path(path)
|
||||
self._recent = bytearray()
|
||||
self._degraded = False
|
||||
|
||||
def reset(self) -> None:
|
||||
self._recent.clear()
|
||||
try:
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.path.write_bytes(b"")
|
||||
except OSError:
|
||||
self._degraded = True
|
||||
self.write("OTA 诊断日志已创建")
|
||||
|
||||
def write(self, message: str) -> None:
|
||||
rendered = str(message).replace("\r\n", "\n").replace("\r", "\n")
|
||||
entry = f"[{_timestamp()}] {rendered}\n".encode("utf-8", errors="replace")
|
||||
self._recent.extend(entry)
|
||||
del self._recent[:-MAX_FAILURE_LOG_BYTES]
|
||||
try:
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
restore = self._degraded or not self.path.exists()
|
||||
with self.path.open("wb" if restore else "ab") as handle:
|
||||
handle.write(bytes(self._recent) if restore else entry)
|
||||
self._degraded = False
|
||||
except OSError as exc:
|
||||
self._degraded = True
|
||||
try:
|
||||
# Avoid printing OS messages, paths or the command's content here.
|
||||
sys.stderr.write(f"OTA diagnostic storage unavailable ({type(exc).__name__}); recovery continues\n")
|
||||
except (OSError, UnicodeError):
|
||||
pass
|
||||
|
||||
def persist(self, data_root: Path) -> Path:
|
||||
try:
|
||||
body = self.path.read_bytes() if not self._degraded else bytes(self._recent)
|
||||
except OSError:
|
||||
body = bytes(self._recent)
|
||||
target = failure_log_path(data_root)
|
||||
atomic_write_bytes(target, bounded_failure_log(body))
|
||||
target.chmod(0o600)
|
||||
return target
|
||||
|
||||
def section(self, name: str) -> None:
|
||||
self.write(f"========== {name} ==========")
|
||||
|
||||
def host_summary(self, *, test_root: Path) -> None:
|
||||
self.section("安全主机摘要")
|
||||
self.write(f"system={platform.system()} release={platform.release()} machine={platform.machine()}")
|
||||
self.write(f"python={sys.version.splitlines()[0]}")
|
||||
for path in (Path("/opt"), Path("/run"), Path(test_root)):
|
||||
probe = path if path.exists() else path.parent
|
||||
try:
|
||||
usage = shutil.disk_usage(probe)
|
||||
self.write(f"space path={path} free={usage.free} total={usage.total}")
|
||||
except OSError as exc:
|
||||
self.write(f"space path={path} unavailable={exc.__class__.__name__}")
|
||||
|
||||
def run(
|
||||
self,
|
||||
command: list[str],
|
||||
*,
|
||||
cwd: Path | None = None,
|
||||
env: dict[str, str] | None = None,
|
||||
check: bool = True,
|
||||
failure_label: str | None = None,
|
||||
log_output: bool = True,
|
||||
) -> subprocess.CompletedProcess[bytes]:
|
||||
display = (
|
||||
shlex.join([str(item) for item in command])
|
||||
if log_output
|
||||
else f"{shlex.quote(str(command[0]))} <参数已按脱敏规则隐藏>"
|
||||
)
|
||||
self.section(f"命令开始:{failure_label or Path(command[0]).name}")
|
||||
self.write(f"cwd={cwd if cwd is not None else os.getcwd()}")
|
||||
self.write(f"command={display}")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
except OSError as exc:
|
||||
self.write(f"command_start_failed={exc.__class__.__name__}: {exc}")
|
||||
raise RuntimeError(f"{failure_label or '命令'}无法启动:{exc}") from exc
|
||||
output = result.stdout.decode("utf-8", errors="replace")
|
||||
if output and log_output:
|
||||
self.write("stdout_stderr:\n" + output.rstrip("\n"))
|
||||
try:
|
||||
sys.stdout.write(output)
|
||||
sys.stdout.flush()
|
||||
except (UnicodeError, OSError):
|
||||
pass
|
||||
elif output:
|
||||
self.write("stdout_stderr=<已按脱敏规则隐藏>")
|
||||
self.write(f"exit_code={result.returncode}")
|
||||
if check and result.returncode != 0:
|
||||
tail = (
|
||||
output.rstrip()[-12000:] or "命令没有输出。"
|
||||
if log_output
|
||||
else "命令输出已按脱敏规则隐藏。"
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"{failure_label or '命令'}失败(退出码 {result.returncode}):\n{tail}"
|
||||
)
|
||||
return result
|
||||
Reference in New Issue
Block a user