209 lines
7.1 KiB
Python
Executable File
209 lines
7.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""持续记录工作手机 SDK、WS 与 Hook 稳定性,供 24h/长期验收使用。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import signal
|
|
import time
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from urllib.parse import urlencode
|
|
from urllib.request import Request, urlopen
|
|
|
|
|
|
def request_json(url: str, timeout: float) -> dict[str, Any]:
|
|
request = Request(url, headers={"Accept": "application/json", "User-Agent": "workphone-stability/1.0"})
|
|
with urlopen(request, timeout=timeout) as response:
|
|
return json.loads(response.read().decode("utf-8"))
|
|
|
|
|
|
def write_json(path: Path, data: dict[str, Any]) -> None:
|
|
temp = path.with_suffix(path.suffix + ".tmp")
|
|
temp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
temp.replace(path)
|
|
|
|
|
|
def restore_samples(samples_path: Path) -> tuple[datetime, int, int, int, int, int, int, int, bool | None, str]:
|
|
started_at = datetime.now().astimezone()
|
|
total = ok = failures = offline_events = reconnects = 0
|
|
latency_total = max_latency = 0
|
|
last_ok: bool | None = None
|
|
last_error = ""
|
|
if not samples_path.exists():
|
|
return (
|
|
started_at,
|
|
total,
|
|
ok,
|
|
failures,
|
|
offline_events,
|
|
reconnects,
|
|
latency_total,
|
|
max_latency,
|
|
last_ok,
|
|
last_error,
|
|
)
|
|
|
|
for line in samples_path.read_text(encoding="utf-8").splitlines():
|
|
try:
|
|
row = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
continue
|
|
if not isinstance(row, dict):
|
|
continue
|
|
try:
|
|
timestamp = datetime.fromisoformat(str(row.get("timestamp", "")))
|
|
if total == 0:
|
|
started_at = timestamp
|
|
except ValueError:
|
|
pass
|
|
row_ok = bool(row.get("ok"))
|
|
total += 1
|
|
ok += int(row_ok)
|
|
failures += int(not row_ok)
|
|
elapsed_ms = int(row.get("request_elapsed_ms") or 0)
|
|
latency_total += elapsed_ms
|
|
max_latency = max(max_latency, elapsed_ms)
|
|
if row_ok:
|
|
if last_ok is False:
|
|
reconnects += 1
|
|
last_error = ""
|
|
else:
|
|
if last_ok is not False:
|
|
offline_events += 1
|
|
last_error = str(row.get("error") or "sdk/ws/hook not ready")
|
|
last_ok = row_ok
|
|
|
|
return (
|
|
started_at,
|
|
total,
|
|
ok,
|
|
failures,
|
|
offline_events,
|
|
reconnects,
|
|
latency_total,
|
|
max_latency,
|
|
last_ok,
|
|
last_error,
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--base", default="http://127.0.0.1:8899")
|
|
parser.add_argument("--device-id", default="xgfe65eimrrofyws")
|
|
parser.add_argument("--interval-seconds", type=float, default=60)
|
|
parser.add_argument("--timeout-seconds", type=float, default=45)
|
|
parser.add_argument("--output-dir", required=True)
|
|
args = parser.parse_args()
|
|
|
|
output_dir = Path(args.output_dir).expanduser().resolve()
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
samples_path = output_dir / "stability_samples.jsonl"
|
|
summary_path = output_dir / "stability_latest_summary.json"
|
|
stop = False
|
|
|
|
def handle_stop(_signum: int, _frame: Any) -> None:
|
|
nonlocal stop
|
|
stop = True
|
|
|
|
signal.signal(signal.SIGTERM, handle_stop)
|
|
signal.signal(signal.SIGINT, handle_stop)
|
|
|
|
(
|
|
started_at,
|
|
total,
|
|
ok,
|
|
failures,
|
|
offline_events,
|
|
reconnects,
|
|
latency_total,
|
|
max_latency,
|
|
last_ok,
|
|
last_error,
|
|
) = restore_samples(samples_path)
|
|
|
|
while not stop:
|
|
sample_started = time.time()
|
|
now = datetime.now().astimezone()
|
|
row: dict[str, Any] = {
|
|
"timestamp": now.isoformat(),
|
|
"device_id": args.device_id,
|
|
"ok": False,
|
|
}
|
|
try:
|
|
health = request_json(f"{args.base}/health", args.timeout_seconds)
|
|
query = urlencode({"device_id": args.device_id, "samples": 1, "interval_seconds": 0})
|
|
watch = request_json(f"{args.base}/api/v3/stability/watch?{query}", args.timeout_seconds)
|
|
watch_data = watch.get("data") if isinstance(watch.get("data"), dict) else {}
|
|
samples = watch_data.get("samples") if isinstance(watch_data.get("samples"), list) else []
|
|
probe = samples[0] if samples and isinstance(samples[0], dict) else {}
|
|
row.update(
|
|
{
|
|
"sdk_healthy": health.get("status") == "healthy",
|
|
"devices_online": health.get("devices_online"),
|
|
"ws_online": bool(probe.get("ws_online")),
|
|
"adb_online": bool(probe.get("adb_online")),
|
|
"hook_ok": bool(probe.get("hook_ok")),
|
|
"wechat_version": probe.get("wechat_version", ""),
|
|
"probe_latency_ms": probe.get("latency_ms", 0),
|
|
"error": probe.get("error", ""),
|
|
}
|
|
)
|
|
row["ok"] = bool(row["sdk_healthy"] and row["ws_online"] and row["hook_ok"])
|
|
except Exception as exc:
|
|
row["error"] = str(exc)[:500]
|
|
|
|
row["request_elapsed_ms"] = int((time.time() - sample_started) * 1000)
|
|
total += 1
|
|
latency_total += row["request_elapsed_ms"]
|
|
max_latency = max(max_latency, row["request_elapsed_ms"])
|
|
if row["ok"]:
|
|
ok += 1
|
|
if last_ok is False:
|
|
reconnects += 1
|
|
last_error = ""
|
|
else:
|
|
failures += 1
|
|
last_error = str(row.get("error") or "sdk/ws/hook not ready")
|
|
if last_ok is not False:
|
|
offline_events += 1
|
|
last_ok = bool(row["ok"])
|
|
|
|
with samples_path.open("a", encoding="utf-8") as stream:
|
|
stream.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
|
|
elapsed_seconds = max((now - started_at).total_seconds(), 0)
|
|
summary = {
|
|
"started_at": started_at.isoformat(),
|
|
"updated_at": now.isoformat(),
|
|
"elapsed_seconds": int(elapsed_seconds),
|
|
"elapsed_hours": round(elapsed_seconds / 3600, 3),
|
|
"device_id": args.device_id,
|
|
"base": args.base,
|
|
"interval_seconds": args.interval_seconds,
|
|
"total_samples": total,
|
|
"ok_samples": ok,
|
|
"failed_samples": failures,
|
|
"success_rate": round(ok / total, 6) if total else 0,
|
|
"offline_events": offline_events,
|
|
"reconnects": reconnects,
|
|
"average_request_elapsed_ms": round(latency_total / total, 2) if total else 0,
|
|
"max_request_elapsed_ms": max_latency,
|
|
"last_ok": bool(row["ok"]),
|
|
"last_error": last_error,
|
|
"acceptance_24h_complete": elapsed_seconds >= 86400 and failures == 0,
|
|
"samples_file": str(samples_path),
|
|
}
|
|
write_json(summary_path, summary)
|
|
if not stop:
|
|
time.sleep(max(args.interval_seconds, 5))
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|