_ops_run.py 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """运维控制台的动作执行器 (2026-09-12) —— 跑一条命令, 并把**真实退出码**写回 run/ops_job.json。
  4. 为什么不让网关直接 Popen 就完事: 那样只知道"进程还在不在", 进程一结束就只能猜它成功没成功 ——
  5. 实测就这么吃过一次(停止服务脚本崩在 TypeError, 页面却显示"已停组件服务")。由本执行器包一层,
  6. 它在子进程结束后把 `status/rc/finished` 落盘, 页面就能显示"完成(退出码 0)"或"失败(退出码 1)"。
  7. 日志: 本进程的 stdout/stderr 已被调用方重定向到 logs/ops_<tag>_<时间>.log, 子命令**继承**它,
  8. 所以命令自己的输出原样进日志(页面显示的就是这些尾巴)。
  9. 用法: python scripts/_ops_run.py --tag rebuild -- <命令与参数...>
  10. """
  11. from __future__ import annotations
  12. import argparse
  13. import json
  14. import os
  15. import pathlib
  16. import subprocess
  17. import sys
  18. import threading
  19. import time
  20. ROOT = pathlib.Path(__file__).resolve().parents[1]
  21. JOB = ROOT / 'run' / 'ops_job.json'
  22. HEARTBEAT_S = 15
  23. def _write(j: dict):
  24. JOB.parent.mkdir(exist_ok=True)
  25. JOB.write_text(json.dumps(j, ensure_ascii=False, indent=1), encoding='utf-8')
  26. def _heartbeat(stop_evt: threading.Event):
  27. """每 15 s 更新 job 文件的 mtime —— 页面据此区分"还在跑"与"被强杀"(pid 会被复用, 只看 pid 会误判)。"""
  28. while not stop_evt.wait(HEARTBEAT_S):
  29. try:
  30. os.utime(JOB, None)
  31. except OSError:
  32. pass
  33. def main() -> int:
  34. ap = argparse.ArgumentParser()
  35. ap.add_argument('--tag', required=True)
  36. ap.add_argument('cmd', nargs=argparse.REMAINDER, help='-- 之后是真正的命令')
  37. a = ap.parse_args()
  38. cmd = [c for c in a.cmd if c != '--']
  39. if not cmd:
  40. print('[X] 没给命令'); return 2
  41. job = {}
  42. try:
  43. job = json.loads(JOB.read_text(encoding='utf-8'))
  44. except Exception:
  45. pass
  46. job.update(kind=a.tag, cmd=' '.join(cmd), pid=os.getpid(), status='running',
  47. started=job.get('started') or time.strftime('%Y-%m-%d %H:%M:%S'))
  48. _write(job)
  49. print(f'$ {" ".join([sys.executable] + cmd)}\n', flush=True)
  50. stop_evt = threading.Event()
  51. threading.Thread(target=_heartbeat, args=(stop_evt,), daemon=True).start()
  52. t0 = time.time()
  53. try:
  54. rc = subprocess.run([sys.executable] + cmd, cwd=str(ROOT), env=dict(os.environ)).returncode
  55. except Exception as e:
  56. print(f'[X] 命令起不来: {type(e).__name__}: {e}', flush=True)
  57. rc = 99
  58. stop_evt.set()
  59. dt = time.time() - t0
  60. job.update(status='done', rc=rc, seconds=round(dt, 1), finished=time.strftime('%Y-%m-%d %H:%M:%S'))
  61. _write(job)
  62. print(f'\n[ops] {a.tag} 结束: 退出码 {rc}, 耗时 {dt:.1f}s', flush=True)
  63. return rc
  64. if __name__ == '__main__':
  65. for _s in (sys.stdout, sys.stderr):
  66. try: _s.reconfigure(errors='replace')
  67. except Exception: pass
  68. sys.exit(main())