muse-agent-example/工具/验证锁.py
zizi 8a44364539 工程(索引): 索引只维护会提交内容;修复独占锁内收集自锁
- 维护索引按 git 视角枚举(已跟踪 + 未跟踪且未忽略):被 .gitignore 忽略的运行痕迹与
  过程性检查报告不再进入目录派生或影响索引门禁;非 git 场景退回原磁盘枚举
- conftest 验证闸门在 `验证锁 --执行` 启动的会话内不再取共享锁,修复 `make 索引生成`
  在独占锁内收集用例时的自锁(原 180s 超时失败 → 现 6s 通过)
- .gitignore 登记过程性检查报告与工具会话缓存(.zcode/):可复跑,不入历史
- 新增两条测试:忽略内容不参与目录派生、独占锁内收集不自锁
2026-09-18 15:07:32 +08:00

166 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""验证期写闸门:验证会话持锁,生成与格式化入口在锁被持有时拒绝改写。
两个反复踩到的坑都从这里堵:
- 验证跑着的时候改 `src/`,本轮结论作废,但报告已经按旧结论写出去;
- `make 格式写入` 也算改源码,改完没重新 `make 生成`,整层用例报「已安装源码与资源发布身份不符」,
几百个错误全是假的。
锁是 `fcntl.flock`,进程退出即释放,不靠残留 PID 文件判断;锁文件里记一句当前验证说明。
"""
from __future__ import annotations
import argparse
import fcntl
import json
import os
import sys
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path
from typing import Any
锁路径 = Path(os.environ.get("MUSE_验证锁", "/tmp/muse-验证锁.lock")) # noqa: S108
_持锁标记 = "MUSE_写入锁已持有"
def 已在独占锁内() -> bool:
"""本进程是否由 `--执行` 启动:父进程已持独占写锁,不能再取共享锁。"""
return os.environ.get(_持锁标记) == "1"
def 正在验证() -> str | None:
"""返回当前验证说明;没有验证在跑时返回 None。
`--执行` 会在独占锁内跑写命令,被它启动的子进程(如构建编排)本身就是持锁方,
不能把自己判成「验证正在运行」;用环境变量把这一事实传给子进程。
"""
if 已在独占锁内():
return None
if not 锁路径.exists():
return None
with 锁路径.open("a+", encoding="utf-8") as 文件:
try:
fcntl.flock(文件, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
return "有一个或多个验证会话在运行"
else:
fcntl.flock(文件, fcntl.LOCK_UN)
return None
def 登记说明(说明: str) -> None:
"""把本次验证写进锁文件,供人工查看谁在跑。"""
with 锁路径.open("a+", encoding="utf-8") as 文件:
文件.write(f"{说明}\n")
@contextmanager
def 持有(说明: str) -> Iterator[None]:
"""验证会话全程持共享锁;写入方要求独占,故分片并行验证互不阻塞。"""
锁路径.parent.mkdir(parents=True, exist_ok=True)
with 锁路径.open("a+", encoding="utf-8") as 文件:
fcntl.flock(文件, fcntl.LOCK_SH)
try:
yield
finally:
fcntl.flock(文件, fcntl.LOCK_UN)
def 已安装代码哈希() -> str | None:
"""直接读安装清单里的代码哈希;不需要打开资源提供器。"""
清单 = Path(__file__).resolve().parents[1] / "src" / "muse" / "资源" / "清单.json"
if not 清单.is_file():
return None
try:
数据: dict[str, Any] = json.loads(清单.read_text(encoding="utf-8"))
except (OSError, ValueError):
return None
return 数据.get("代码哈希")
def 源码身份核对() -> str | None:
"""返回不一致说明;一致返回 None。只算一次源码哈希,约 0.3 秒。"""
from muse.基础设施.系统运行 import 运行代码哈希
期望 = 已安装代码哈希()
if 期望 is None:
return "缺少已安装资源清单;先执行 make 生成"
当前 = 运行代码哈希()
if 当前 != 期望:
return "已安装源码与资源发布身份不符;先执行 make 生成(改过 src/ 或跑过格式写入都要重跑)"
return None
def 独占执行(命令: list[str]) -> int:
"""在独占锁内执行写命令:写窗口全程持锁,验证会话无法插入。
与 `--要求空闲` 的区别:后者查完即放锁,检查与写入之间仍可被验证会话插入。
"""
import subprocess
锁路径.parent.mkdir(parents=True, exist_ok=True)
with 锁路径.open("a+", encoding="utf-8") as 文件:
try:
fcntl.flock(文件, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
print(
"拒绝改写:验证正在运行;等它结束再改,否则本轮验证结果作废",
file=sys.stderr,
)
return 1
try:
os.environ[_持锁标记] = "1"
return subprocess.run(命令, check=False).returncode
finally:
os.environ.pop(_持锁标记, None)
fcntl.flock(文件, fcntl.LOCK_UN)
def main(argv: list[str] | None = None) -> int:
解析 = argparse.ArgumentParser(description=__doc__)
解析.add_argument(
"--要求空闲", action="store_true", help="锁被持有时非零退出,供写入入口前置检查(查完即放锁)"
)
解析.add_argument("--核对源码身份", action="store_true", help="比较源码哈希与安装清单")
解析.add_argument(
"--执行",
nargs=argparse.REMAINDER,
metavar="命令",
help="在独占锁内执行写命令,例如 --执行 uv run --no-sync python 工具/构建编排.py",
)
值 = 解析.parse_args(argv)
if 值.要求空闲:
说明 = 正在验证()
if 说明:
print(
f"拒绝改写:验证正在运行({说明});等它结束再改,否则本轮验证结果作废",
file=sys.stderr,
)
return 1
print("验证空闲:可以改写源码")
if 值.核对源码身份:
问题 = 源码身份核对()
if 问题:
print(问题, file=sys.stderr)
return 1
print("源码身份一致:安装清单与当前源码同哈希")
if 值.执行 is not None:
if not 值.执行:
print("--执行 需要给出命令", file=sys.stderr)
return 2
return 独占执行(list(值.执行))
return 0
if __name__ == "__main__":
raise SystemExit(main())