fix(skill): @build 自持自释 build 锁,根治忘记 release 的泄漏
问题:锁不绑定进程存活(`lock acquire` 是短命进程,真正用 UV4 的是另一个 进程),所以释放只能靠调用方记得。Agent 手工 `lock acquire build` 后忘 了 `release`,锁要挂到 6 小时 TTL 才被下次 acquire 接管——期间其他任务 要么立即失败、要么干等。`done` 自己已用 try/finally 兜住,但手工路径没有。 改法:让**用资源的人**持锁。`keil_build.py` 进入 acquire、`finally` **无条件** release(构建失败 / 异常 / SystemExit 都会释放)。 - `--lock-task`:持有者名。默认 `$DTU_TASK_ID` → 从 cwd 的 `_wt/<ID>-<slug>/` 反推 `<ID>` → `build-cli`。反推意味着"手工 `lock acquire build --task <ID>` 后再跑 @build"仍幂等,不会自我死锁。 - `--lock-wait N`:锁被占时等待(默认 0 = 立即失败)。 - `--no-lock`:完全不加锁。 - 新增退出码 **4 = build 锁被占用**(原先无此语义,占用会直接去构建)。 - 锁工具 = 兄弟 skill `task-worktree`;**找不到则降级为不加锁并告警**, 保持本 skill 可独立搬运。 `done` 的兼容处理:它自己持锁,给子进程设 `DTU_LOCK_HELD=build` 告知 跳过自持自释。**对"工作树里还是旧 keil_build.py"的在途任务同样安全** ——旧版根本不看该变量、也不会自锁,故不会出现"双重持锁"死锁。 `bench` 锁**不动**:它是**会话作用域**(跨烧录→@test→探针多条命令), 逐命令自动释放会让别的任务插进来扰动设备,必须继续由 Agent 持有。 验证(主工作树实测): - 自持自释:acquire → BUILD_OK → release,前后 `lock status` 均空闲; - 被占:由 other-task 占锁 → exit 4,且**未触发构建**; - 调用方持锁:`DTU_LOCK_HELD=build` → 打印"跳过自持自释",且 **未释放调用方的锁**(锁仍归 __done__)——关键安全性质; - 错误路径:无调用方持锁 + `--project` 不存在 → 失败但 **finally 已释放**; - 持有者名推导:主工作树 → `build-cli`,`_wt/N33-stack-margins` → `N33`; - 门禁:`mem-numbers` 一致;pre-commit 全绿。
This commit is contained in:
@@ -18,7 +18,29 @@ whenToUse: 需要编译 Keil MDK 工程、验证代码改动是否 0 错误 0
|
||||
解析顺序(命令行 > 项目档案 `.serialtest/config.json` > 环境 > 探测):
|
||||
工程 `uvprojx`(档案缺省时自动搜 cwd 下唯一 `*.uvprojx`);UV4 `uv4` >
|
||||
`$DSH_KEIL_UV4` > `$DTU_KEIL_UV4` > PATH(**显式设了但路径无效 = 立即报错,不静默回退**)。
|
||||
退出码:`0`=PASS(0错0警) / `1`=构建失败(打印报错行)/ `2`=UV4 未找到/无效 / `3`=用法。
|
||||
退出码:`0`=PASS(0错0警) / `1`=构建失败(打印报错行)/ `2`=UV4 未找到/无效 / `3`=用法 /
|
||||
`4`=build 锁被占用(并发 `-b` 不可行)。
|
||||
|
||||
## build 锁:脚本自持自释(2026-10-12)
|
||||
|
||||
**UV4 不能两个实例同时 `-b`,但构建锁不该由调用方"记得释放"**——那正是泄漏源:
|
||||
Agent 手工 `lock acquire build` 后忘了 `release`,锁要挂到 6 小时 TTL 才被接管。
|
||||
故本脚本**自己**管:进入 acquire → `try` 构建 → `finally` **无条件** release
|
||||
(构建失败、异常、`SystemExit` 都会释放)。
|
||||
|
||||
| 开关 | 作用 |
|
||||
|---|---|
|
||||
| `--lock-task <ID>` | 持有者名。默认 `$DTU_TASK_ID` → 从 cwd 的 `_wt/<ID>-<slug>/` 反推 `<ID>` → `build-cli` |
|
||||
| `--lock-wait <N>` | 锁被占时等待 N 秒(默认 0 = 立即失败,退出码 4) |
|
||||
| `--no-lock` | 完全不加锁(危险:并发 `-b` 互相干扰) |
|
||||
| `DTU_LOCK_HELD=build` | 环境变量:**调用方已持锁**时设它,本脚本跳过自持自释(`task-worktree done` 的门禁即如此用) |
|
||||
|
||||
- 锁工具 = 兄弟 skill `task-worktree/scripts/task_worktree.py`;**找不到则降级为不加锁
|
||||
并告警**(保持本 skill 可独立搬运)。
|
||||
- ⚠ 从 `_wt/<ID>-<slug>/` 反推持有者名,意味着"手工 `lock acquire build --task <ID>`
|
||||
后再跑 `@build`"仍**幂等**(同名即视为自己持有),不会自我死锁。
|
||||
- **只 build 锁自动;`bench` 锁仍须 Agent 手工持**(它是**会话作用域**,跨
|
||||
烧录→`@test`→探针多条命令,逐命令自动释放会让别的任务插进来扰动设备)。
|
||||
|
||||
## 本仓库
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
python <skill>/scripts/keil_build.py
|
||||
[--project X.uvprojx] [--uv4 UV4.exe] [--target 名字]
|
||||
[--log build_log.txt] [--timeout 600]
|
||||
[--lock-task ID] [--lock-wait N] [--no-lock]
|
||||
|
||||
解析顺序(命令行 > 项目档案 > 环境 > 探测):
|
||||
工程 --project > 档案 "uvprojx" > 自动搜 cwd 下唯一 *.uvprojx
|
||||
@@ -19,7 +20,21 @@
|
||||
3. PASS = 日志存在 + 无 "error:"(大小写不敏感)+ 含 "0 Error(s), 0 Warning(s)"
|
||||
(0E0W 精确匹配同时把"只有警告"的构建判 FAIL)
|
||||
|
||||
退出码: 0=PASS(0错0警) 1=构建失败(看打印的报错行/日志) 2=UV4 未找到/无效 3=用法错误
|
||||
**build 锁:本脚本自持自释(2026-10-12)**
|
||||
UV4 不能两个实例同时 `-b`,所以构建锁不该由调用方"记得释放"——那正是泄漏源
|
||||
(Agent 手工 `lock acquire` 后忘了 `release`,锁要挂到 6 小时 TTL 才被接管)。
|
||||
改为:本脚本进入时 acquire、退出时**无条件** release(try/finally,构建失败/异常/
|
||||
SystemExit 都会释放)。
|
||||
- 锁工具 = 同级 skill `task-worktree/scripts/task_worktree.py`(**找不到则降级为
|
||||
不加锁并告警**,保持本 skill 可移植)
|
||||
- 持有者名 `--lock-task`:默认 `$DTU_TASK_ID` → 否则从 cwd 的 `_wt/<ID>-<slug>/`
|
||||
反推 `<ID>`(与工作树命名约定一致,故"手工锁了同名任务"仍幂等)→ 否则 `build-cli`
|
||||
- 被占用:默认**立即失败**(退出码 4,不静默等待);`--lock-wait N` 可等待 N 秒
|
||||
- 调用方**已持有** build 锁时(如 `task-worktree done` 的门禁),设
|
||||
`DTU_LOCK_HELD=build` 让本脚本跳过自持自释,避免自我死锁
|
||||
|
||||
退出码: 0=PASS(0错0警) 1=构建失败(看打印的报错行/日志) 2=UV4 未找到/无效
|
||||
3=用法错误 4=build 锁被占用(并发 -b 不可行)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -30,6 +45,8 @@ import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
LOCK_NAME = "build"
|
||||
|
||||
|
||||
def find_config_file():
|
||||
env = os.environ.get("DSH_SERIAL_CONFIG")
|
||||
@@ -101,21 +118,74 @@ def find_project(cli_arg, cfg):
|
||||
"\n ".join(str(h) for h in hits))
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
for stream in (sys.stdout, sys.stderr):
|
||||
try:
|
||||
stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
except Exception:
|
||||
pass
|
||||
# --------------------------------------------------------------- build 锁(自持自释)
|
||||
|
||||
ap = argparse.ArgumentParser(description="Keil UV4 CLI build + verdict (pure stdlib)")
|
||||
ap.add_argument("--project", default=None, help="*.uvprojx (default: config/auto-detect)")
|
||||
ap.add_argument("--uv4", default=None, help="UV4.exe path")
|
||||
ap.add_argument("--target", default=None, help="build target name (-t; default: project default)")
|
||||
ap.add_argument("--log", default=None, help="build log path (default: <proj dir>/build_log.txt)")
|
||||
ap.add_argument("--timeout", type=int, default=600, help="UV4 timeout seconds")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
def find_lock_tool():
|
||||
"""定位 task-worktree 的 lock CLI(同级 skill)。找不到 → None(降级为不加锁)。
|
||||
|
||||
keil-build 与 task-worktree 是兄弟目录:.dsh/skills/{keil-build,task-worktree}/
|
||||
故从本文件上溯 2 级即到 skills/。找不到不报错——保持本 skill 可独立搬运。
|
||||
"""
|
||||
try:
|
||||
skills = Path(__file__).resolve().parents[2]
|
||||
except IndexError:
|
||||
return None
|
||||
cand = skills / "task-worktree" / "scripts" / "task_worktree.py"
|
||||
return cand if cand.is_file() else None
|
||||
|
||||
|
||||
def default_lock_task():
|
||||
"""持有者名:$DTU_TASK_ID > cwd 反推 `_wt/<ID>-<slug>/` 的 `<ID>` > "build-cli"。"""
|
||||
v = os.environ.get("DTU_TASK_ID")
|
||||
if v:
|
||||
return v
|
||||
try:
|
||||
cwd = Path.cwd().resolve()
|
||||
except OSError:
|
||||
return "build-cli"
|
||||
for p in (cwd, *cwd.parents):
|
||||
if p.parent.name == "_wt":
|
||||
return p.name.split("-", 1)[0] or p.name
|
||||
return "build-cli"
|
||||
|
||||
|
||||
def lock_cli(tool, action, task, wait=0, force=False):
|
||||
"""调 task-worktree 的 `lock <action> build --task <task>`。返回 (ok, 输出)。"""
|
||||
cmd = [sys.executable, str(tool), "lock", action, LOCK_NAME, "--task", task]
|
||||
if action == "acquire" and wait > 0:
|
||||
cmd += ["--wait", str(wait)]
|
||||
if force:
|
||||
cmd += ["--force"]
|
||||
try:
|
||||
r = subprocess.run(cmd, capture_output=True, text=True,
|
||||
encoding="utf-8", errors="replace",
|
||||
stdin=subprocess.DEVNULL)
|
||||
except OSError as e:
|
||||
return False, "lock CLI 调用失败:{}".format(e)
|
||||
return r.returncode == 0, (r.stdout + r.stderr).strip()
|
||||
|
||||
|
||||
def acquire_build_lock(args):
|
||||
"""返回 (state, tool, task, note)。state ∈ ok / caller / skipped / busy。"""
|
||||
if args.no_lock:
|
||||
return "skipped", None, None, "--no-lock"
|
||||
held = [x.strip() for x in (os.environ.get("DTU_LOCK_HELD") or "").split(",")
|
||||
if x.strip()]
|
||||
if LOCK_NAME in held:
|
||||
return "caller", None, None, "调用方已持有(DTU_LOCK_HELD)"
|
||||
tool = find_lock_tool()
|
||||
if tool is None:
|
||||
return "skipped", None, None, "未找到 task-worktree skill"
|
||||
task = args.lock_task or default_lock_task()
|
||||
ok, out = lock_cli(tool, "acquire", task, wait=args.lock_wait)
|
||||
if not ok:
|
||||
return "busy", tool, task, out
|
||||
return "ok", tool, task, ""
|
||||
|
||||
|
||||
def run_build(args):
|
||||
"""真正的构建主体(锁由 main 负责)。返回退出码。"""
|
||||
cfg, err = load_config()
|
||||
if err:
|
||||
print("BUILD_ENV_ERROR: {}".format(err))
|
||||
@@ -184,5 +254,49 @@ def main(argv=None):
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
for stream in (sys.stdout, sys.stderr):
|
||||
try:
|
||||
stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ap = argparse.ArgumentParser(description="Keil UV4 CLI build + verdict (pure stdlib)")
|
||||
ap.add_argument("--project", default=None, help="*.uvprojx (default: config/auto-detect)")
|
||||
ap.add_argument("--uv4", default=None, help="UV4.exe path")
|
||||
ap.add_argument("--target", default=None, help="build target name (-t; default: project default)")
|
||||
ap.add_argument("--log", default=None, help="build log path (default: <proj dir>/build_log.txt)")
|
||||
ap.add_argument("--timeout", type=int, default=600, help="UV4 timeout seconds")
|
||||
ap.add_argument("--lock-task", default=None,
|
||||
help="build 锁持有者名 (default: $DTU_TASK_ID / _wt/<ID>-* / build-cli)")
|
||||
ap.add_argument("--lock-wait", type=int, default=0,
|
||||
help="build 锁等待秒数 (0=不等待,占用即退出码 4)")
|
||||
ap.add_argument("--no-lock", action="store_true",
|
||||
help="完全不加 build 锁(危险:并发 -b 会互相干扰)")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
state, tool, task, note = acquire_build_lock(args)
|
||||
if state == "busy":
|
||||
print("BUILD_LOCK_BUSY: build 锁被占用(UV4 不能并发构建)")
|
||||
if note:
|
||||
for ln in note.splitlines():
|
||||
print("BUILD_LOCK_BUSY: {}".format(ln))
|
||||
print("BUILD_LOCK_BUSY: 用 --lock-wait N 等待,或等对方 release")
|
||||
return 4
|
||||
if state == "ok":
|
||||
print("LOCK: 已获取 build 锁(task={})".format(task))
|
||||
elif state == "caller":
|
||||
print("LOCK: 由调用方持有,跳过自持自释")
|
||||
else:
|
||||
print("WARN: 本次构建**未加 build 锁**({})——并发 -b 会互相干扰".format(note))
|
||||
|
||||
try:
|
||||
return run_build(args)
|
||||
finally:
|
||||
if state == "ok":
|
||||
ok, out = lock_cli(tool, "release", task)
|
||||
print("LOCK: {}".format(out or ("已释放 build 锁" if ok else "释放 build 锁失败")))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
@@ -55,8 +55,11 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
|
||||
|
||||
1. **`done` 只能在主工作树执行**——主干分支检出在主工作树,git 不允许同一分支在两处检出。
|
||||
2. **`done` 不 push**。它结束于本地合入 + 门禁全绿;推送需用户明确许可(项目既有纪律)。
|
||||
3. **`build` 与 `bench` 都必须持锁**:UV4 不能两个实例同时 `-b`;串口 COM6、ST-LINK、
|
||||
3. **`build` 与 `bench` 都必须串行**:UV4 不能两个实例同时 `-b`;串口 COM6、ST-LINK、
|
||||
设备 IP `192.168.1.100` 各只有一份。**构建与烧录都不可并发。**
|
||||
- `build` 锁由 **`@build` 自持自释**,调用方无需手工 acquire/release;
|
||||
- `bench` 锁**必须手工**:`lock acquire bench --task <ID>` → 烧录/`@test`/探针 →
|
||||
`lock release bench --task <ID> --bench-pass`。
|
||||
4. **单写者文档**:`docs/任务规划/`(整个文件夹)、`AGENTS.md` 的任务表块、`MDK-ARM/*.uvprojx`、
|
||||
`.pre-commit-config.yaml`、内存快照 —— 任务期**不在任务工作树里改**,留到 `done` 统一处理。
|
||||
5. **清场必须先摘联接**:脚本会先 `os.rmdir` 掉 `.venv` / `simulator/SDL2` 联接再
|
||||
@@ -103,6 +106,10 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
|
||||
所以判「陈旧」只看**时效**(默认 6 小时,`LOCK_TTL_HOURS`)加显式 `--force`;pid 仅作排障记录。
|
||||
同任务重复 acquire 是幂等的(不会把自己锁死)。
|
||||
- **陈旧锁可自动接管**:超过 6 小时未释放即视为陈旧,下次 acquire 会接管并提示;活锁需 `--force`。
|
||||
- **`build` 锁不必手工持**(2026-10-12 起):`@build`(`keil_build.py`)**自持自释**——
|
||||
进入 acquire、`finally` 无条件 release,所以"Agent 忘了 release 泄漏 6 小时"这条在构建路径上已消除。
|
||||
`done` 的门禁自己持锁并给子进程设 `DTU_LOCK_HELD=build` 告知跳过(新旧脚本都兼容)。
|
||||
**`bench` 锁仍须 Agent 手工持**(会话作用域:跨烧录→`@test`→探针,见 `keil-build` SKILL.md)。
|
||||
|
||||
## 与其它 skill 的衔接
|
||||
|
||||
|
||||
@@ -379,11 +379,16 @@ def run_gates(cwd: Path, skip_build: bool) -> bool:
|
||||
if not got:
|
||||
print("!! build 锁被 {} 占用(UV4 不能并发构建),跳过 @build".format(info.get("task")))
|
||||
return False
|
||||
# 告知 @build「调用方已持锁」:新版 keil_build.py 自持自释(见其 docstring),
|
||||
# 读到本变量即跳过,避免自我死锁;**旧版根本不看此变量、也不会自锁**,
|
||||
# 故此处对「工作树里还是旧脚本」的在途任务同样安全(向后兼容)。
|
||||
os.environ["DTU_LOCK_HELD"] = "build"
|
||||
try:
|
||||
if not run_script(".dsh/skills/keil-build/scripts/keil_build.py", [], cwd,
|
||||
"@build(0 错 0 警)"):
|
||||
ok = False
|
||||
finally:
|
||||
os.environ.pop("DTU_LOCK_HELD", None)
|
||||
lock_release("build", "__done__", force=True)
|
||||
return ok
|
||||
|
||||
|
||||
Reference in New Issue
Block a user