# -*- coding: utf-8 -*- r"""运维动作(重算/清产物)的**当前状态**读取口(单一实现)。 为什么需要它: 2026-09-19 用户报"重算后页面没有输出"。实况是——门户里点了「清除产物 → 执行重算」, 产物被清掉后重算要跑很久(本次 ④b 振动侧摄入要处理 150 GB 的 CMS 导出), 期间各页面只能显示 "无产物"。但那句话现在只说"要么还没放数据, 要么刚清过产物", **没说"重算正在跑"** —— 于是同一现象被读成"系统坏了"。 状态真源: `scripts/_ops_run.py` 写的 `run/ops_job.json`(每 15 s 心跳更新 mtime)。 判定"在跑"= `status == 'running'` **且** 文件 mtime 在 `fresh_s` 秒内(只看 pid 会被复用骗过)。 """ from __future__ import annotations try: from ._root import install_root as _install_root except ImportError: # 直接当脚本跑(python <本文件>)时没有包上下文 from _root import install_root as _install_root import json import pathlib import time ROOT = _install_root(__file__) JOB = ROOT / 'run' / 'ops_job.json' def current(fresh_s: int = 90) -> dict: """→ dict(running=bool, kind=, cmd=, started=, elapsed_s=, age_s=, stale=bool, note=)。读不到就 running=False。""" out = dict(running=False, kind=None, cmd=None, started=None, elapsed_s=None, age_s=None, stale=False, note=None, job_file=str(JOB)) if not JOB.is_file(): return out try: j = json.loads(JOB.read_text(encoding='utf-8')) except Exception as e: return {**out, 'note': f'任务文件读不出 ({type(e).__name__})'} age = time.time() - JOB.stat().st_mtime out.update(kind=j.get('kind'), cmd=j.get('cmd'), started=j.get('started'), age_s=round(age), status=j.get('status'), rc=j.get('rc')) running = (j.get('status') == 'running') and age < fresh_s out['running'] = bool(running) out['stale'] = (j.get('status') == 'running') and age >= fresh_s if out.get('started'): try: t0 = time.mktime(time.strptime(out['started'], '%Y-%m-%d %H:%M:%S')) out['elapsed_s'] = round(time.time() - t0) except Exception: pass if out['stale']: out['note'] = (f"任务文件写着 running 但已 {round(age / 60, 1)} 分钟没心跳 —— 可能是被强杀, " f"不是正在跑") return out def running_text() -> str: """给人看的一句话(页面/接口用): 在跑就写明"起点 + 已跑多久", 否则空串。""" s = current() if not s['running']: return '' mins = (s['elapsed_s'] or 0) / 60 return (f"⚠ 检测到**重算正在进行中**({s['kind']},起于 {s['started']},已跑 {mins:.0f} 分钟)—— " f"产物是一步步长出来的,页面会在对应步骤跑完后自己回来(无需重启服务)。")