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` > 环境 > 探测):
|
解析顺序(命令行 > 项目档案 `.serialtest/config.json` > 环境 > 探测):
|
||||||
工程 `uvprojx`(档案缺省时自动搜 cwd 下唯一 `*.uvprojx`);UV4 `uv4` >
|
工程 `uvprojx`(档案缺省时自动搜 cwd 下唯一 `*.uvprojx`);UV4 `uv4` >
|
||||||
`$DSH_KEIL_UV4` > `$DTU_KEIL_UV4` > PATH(**显式设了但路径无效 = 立即报错,不静默回退**)。
|
`$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
|
python <skill>/scripts/keil_build.py
|
||||||
[--project X.uvprojx] [--uv4 UV4.exe] [--target 名字]
|
[--project X.uvprojx] [--uv4 UV4.exe] [--target 名字]
|
||||||
[--log build_log.txt] [--timeout 600]
|
[--log build_log.txt] [--timeout 600]
|
||||||
|
[--lock-task ID] [--lock-wait N] [--no-lock]
|
||||||
|
|
||||||
解析顺序(命令行 > 项目档案 > 环境 > 探测):
|
解析顺序(命令行 > 项目档案 > 环境 > 探测):
|
||||||
工程 --project > 档案 "uvprojx" > 自动搜 cwd 下唯一 *.uvprojx
|
工程 --project > 档案 "uvprojx" > 自动搜 cwd 下唯一 *.uvprojx
|
||||||
@@ -19,7 +20,21 @@
|
|||||||
3. PASS = 日志存在 + 无 "error:"(大小写不敏感)+ 含 "0 Error(s), 0 Warning(s)"
|
3. PASS = 日志存在 + 无 "error:"(大小写不敏感)+ 含 "0 Error(s), 0 Warning(s)"
|
||||||
(0E0W 精确匹配同时把"只有警告"的构建判 FAIL)
|
(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
|
import argparse
|
||||||
@@ -30,6 +45,8 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
LOCK_NAME = "build"
|
||||||
|
|
||||||
|
|
||||||
def find_config_file():
|
def find_config_file():
|
||||||
env = os.environ.get("DSH_SERIAL_CONFIG")
|
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))
|
"\n ".join(str(h) for h in hits))
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
# --------------------------------------------------------------- build 锁(自持自释)
|
||||||
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")
|
|
||||||
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()
|
cfg, err = load_config()
|
||||||
if err:
|
if err:
|
||||||
print("BUILD_ENV_ERROR: {}".format(err))
|
print("BUILD_ENV_ERROR: {}".format(err))
|
||||||
@@ -184,5 +254,49 @@ def main(argv=None):
|
|||||||
return 0
|
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__":
|
if __name__ == "__main__":
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|||||||
@@ -55,8 +55,11 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
|
|||||||
|
|
||||||
1. **`done` 只能在主工作树执行**——主干分支检出在主工作树,git 不允许同一分支在两处检出。
|
1. **`done` 只能在主工作树执行**——主干分支检出在主工作树,git 不允许同一分支在两处检出。
|
||||||
2. **`done` 不 push**。它结束于本地合入 + 门禁全绿;推送需用户明确许可(项目既有纪律)。
|
2. **`done` 不 push**。它结束于本地合入 + 门禁全绿;推送需用户明确许可(项目既有纪律)。
|
||||||
3. **`build` 与 `bench` 都必须持锁**:UV4 不能两个实例同时 `-b`;串口 COM6、ST-LINK、
|
3. **`build` 与 `bench` 都必须串行**:UV4 不能两个实例同时 `-b`;串口 COM6、ST-LINK、
|
||||||
设备 IP `192.168.1.100` 各只有一份。**构建与烧录都不可并发。**
|
设备 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`、
|
4. **单写者文档**:`docs/任务规划/`(整个文件夹)、`AGENTS.md` 的任务表块、`MDK-ARM/*.uvprojx`、
|
||||||
`.pre-commit-config.yaml`、内存快照 —— 任务期**不在任务工作树里改**,留到 `done` 统一处理。
|
`.pre-commit-config.yaml`、内存快照 —— 任务期**不在任务工作树里改**,留到 `done` 统一处理。
|
||||||
5. **清场必须先摘联接**:脚本会先 `os.rmdir` 掉 `.venv` / `simulator/SDL2` 联接再
|
5. **清场必须先摘联接**:脚本会先 `os.rmdir` 掉 `.venv` / `simulator/SDL2` 联接再
|
||||||
@@ -103,6 +106,10 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
|
|||||||
所以判「陈旧」只看**时效**(默认 6 小时,`LOCK_TTL_HOURS`)加显式 `--force`;pid 仅作排障记录。
|
所以判「陈旧」只看**时效**(默认 6 小时,`LOCK_TTL_HOURS`)加显式 `--force`;pid 仅作排障记录。
|
||||||
同任务重复 acquire 是幂等的(不会把自己锁死)。
|
同任务重复 acquire 是幂等的(不会把自己锁死)。
|
||||||
- **陈旧锁可自动接管**:超过 6 小时未释放即视为陈旧,下次 acquire 会接管并提示;活锁需 `--force`。
|
- **陈旧锁可自动接管**:超过 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 的衔接
|
## 与其它 skill 的衔接
|
||||||
|
|
||||||
|
|||||||
@@ -379,11 +379,16 @@ def run_gates(cwd: Path, skip_build: bool) -> bool:
|
|||||||
if not got:
|
if not got:
|
||||||
print("!! build 锁被 {} 占用(UV4 不能并发构建),跳过 @build".format(info.get("task")))
|
print("!! build 锁被 {} 占用(UV4 不能并发构建),跳过 @build".format(info.get("task")))
|
||||||
return False
|
return False
|
||||||
|
# 告知 @build「调用方已持锁」:新版 keil_build.py 自持自释(见其 docstring),
|
||||||
|
# 读到本变量即跳过,避免自我死锁;**旧版根本不看此变量、也不会自锁**,
|
||||||
|
# 故此处对「工作树里还是旧脚本」的在途任务同样安全(向后兼容)。
|
||||||
|
os.environ["DTU_LOCK_HELD"] = "build"
|
||||||
try:
|
try:
|
||||||
if not run_script(".dsh/skills/keil-build/scripts/keil_build.py", [], cwd,
|
if not run_script(".dsh/skills/keil-build/scripts/keil_build.py", [], cwd,
|
||||||
"@build(0 错 0 警)"):
|
"@build(0 错 0 警)"):
|
||||||
ok = False
|
ok = False
|
||||||
finally:
|
finally:
|
||||||
|
os.environ.pop("DTU_LOCK_HELD", None)
|
||||||
lock_release("build", "__done__", force=True)
|
lock_release("build", "__done__", force=True)
|
||||||
return ok
|
return ok
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user