fix: close bare open() in host.py and ResourceWarning in test_host_thread_cli

- host.py: 7 one-shot reads switched to with open (run_health_check
  pidfile, daemon-status pid/log, daemon-stop pid, daemon old pid,
  interrupt poll state, parallel jobs); read content, encoding args
  and error semantics unchanged.
- tests/test_host_thread_cli.py: test_old_archive_unchanged state
  read switched to with open; added a warnings-capture assertion
  scoped to the state read so a regression fails the test.
- Intentionally untouched: daemonize log handle (daemon-lifetime
  dup2 target) and the two _load_config_file / init sites owned
  by PR #266.
This commit is contained in:
host committed 2026-11-04 20:56:06 +00:00
1 parent 1462195872
commit 32e2b70f20
2 files changed
+29 -9

No files matched your search

+14 -8
View File
@@ -1120,7 +1120,8 @@ def run_health_check(state_path, cfg, codex_bin, codex_home, approval,
checks["daemon_pidfile"] = pidfile checks["daemon_pidfile"] = pidfile
if os.path.exists(pidfile): if os.path.exists(pidfile):
try: try:
pid = int(open(pidfile).read().strip()) with open(pidfile) as f:
pid = int(f.read().strip())
os.kill(pid, 0) os.kill(pid, 0)
checks["daemon_alive"] = True checks["daemon_alive"] = True
checks["daemon_pid"] = pid checks["daemon_pid"] = pid
@@ -2734,7 +2735,8 @@ def main():
pid = None pid = None
if os.path.exists(pidfile): if os.path.exists(pidfile):
try: try:
pid = int(open(pidfile).read().strip()) with open(pidfile) as f:
pid = int(f.read().strip())
os.kill(pid, 0) os.kill(pid, 0)
alive = True alive = True
except Exception: except Exception:
@@ -2742,7 +2744,8 @@ def main():
tail = "" tail = ""
if os.path.exists(logfile): if os.path.exists(logfile):
try: try:
tail = "\n".join(open(logfile).read().splitlines()[-5:]) with open(logfile) as f:
tail = "\n".join(f.read().splitlines()[-5:])
except Exception: except Exception:
pass pass
print(json.dumps({"alive": alive, "pid": pid, "pidfile": pidfile, print(json.dumps({"alive": alive, "pid": pid, "pidfile": pidfile,
@@ -2755,7 +2758,8 @@ def main():
print("daemon 未运行(无 pidfile)") print("daemon 未运行(无 pidfile)")
return return
try: try:
pid = int(open(pidfile).read().strip()) with open(pidfile) as f:
pid = int(f.read().strip())
except Exception: except Exception:
print("pidfile 损坏,直接删除") print("pidfile 损坏,直接删除")
os.remove(pidfile) os.remove(pidfile)
@@ -2785,7 +2789,8 @@ def main():
if not args.foreground: if not args.foreground:
if os.path.exists(pidfile): if os.path.exists(pidfile):
try: try:
old_pid = int(open(pidfile).read().strip()) with open(pidfile) as f:
old_pid = int(f.read().strip())
os.kill(old_pid, 0) os.kill(old_pid, 0)
print(f"daemon 已在运行:pid {old_pid},先 daemon-stop 再起") print(f"daemon 已在运行:pid {old_pid},先 daemon-stop 再起")
return return
@@ -3301,8 +3306,8 @@ def main():
for _ in range(10): for _ in range(10):
time.sleep(1) time.sleep(1)
try: try:
cur = json.load(open(args.state, with open(args.state, encoding="utf-8") as f:
encoding="utf-8")) cur = json.load(f)
if (cur.get("tasks", {}).get(args.task, {}) if (cur.get("tasks", {}).get(args.task, {})
.get("status") == "interrupted"): .get("status") == "interrupted"):
print(f"interrupt {args.task}: 属主已响应," print(f"interrupt {args.task}: 属主已响应,"
@@ -3454,7 +3459,8 @@ def main():
last_turn_text=last_result.get("text", "") or "", last_turn_text=last_result.get("text", "") or "",
resume_note=args.resume_note) resume_note=args.resume_note)
elif args.cmd == "parallel": elif args.cmd == "parallel":
jobs = json.load(open(args.jobs)) with open(args.jobs) as f:
jobs = json.load(f)
# 依赖 + 优先级:先拓扑排序,缺依赖/成环直接报错不跑; # 依赖 + 优先级:先拓扑排序,缺依赖/成环直接报错不跑;
# 按波次派发 + 跑:依赖都 completed 的任务才进本波, # 按波次派发 + 跑:依赖都 completed 的任务才进本波,
# 依赖 failed/skipped 的任务标 skipped 不跑,避免在坏 # 依赖 failed/skipped 的任务标 skipped 不跑,避免在坏
+15 -1
View File
@@ -1,9 +1,11 @@
import gc
import io import io
import json import json
import os import os
import sys import sys
import tempfile import tempfile
import unittest import unittest
import warnings
from contextlib import redirect_stdout from contextlib import redirect_stdout
from unittest import mock from unittest import mock
import host import host
@@ -246,7 +248,19 @@ class ThreadCliTest(unittest.TestCase):
code = e.code if isinstance(e.code, int) else 1 code = e.code if isinstance(e.code, int) else 1
self.assertEqual(code, 0) self.assertEqual(code, 0)
self.assertIn("archived t1", out.getvalue()) self.assertIn("archived t1", out.getvalue())
saved = json.loads(Path(state_path).read_text(encoding="utf-8")) # 第 4 项回归:此处裸 open() 曾在 GC 时报 ResourceWarning;
# 只捕获本段读 state 的警告断言(_load_config_file 的
# 警告属 #266 轨,不在此断言范围内)。
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always", ResourceWarning)
with open(state_path, encoding="utf-8") as f:
saved = json.load(f)
gc.collect()
self.assertEqual(
[w for w in caught
if issubclass(w.category, ResourceWarning)
and "host-state.json" in str(w.message)],
[])
self.assertEqual(saved["tasks"]["t1"]["status"], "archived") self.assertEqual(saved["tasks"]["t1"]["status"], "archived")
calls = self._calls_before_close(fake) calls = self._calls_before_close(fake)
self.assertEqual(calls, [("archive", "th-1")]) self.assertEqual(calls, [("archive", "th-1")])