fix(task-worktree): build/bench 分锁 TTL + done 自动接管陈旧锁
A:LOCK_TTL_HOURS 由全局 6h 改为按锁分设(build 1h / bench 6h)—— 兜底窗口应与操作真实时长同量级:编译是秒~分钟级,6h 太钝。 并支持 lock acquire --ttl 自报窗口(keil_build 按其 --timeout 自报, timeout/3600+15min,下限 0.5h),使 build 锁窗口自动跟随构建上限。 B:done 收口前置把陈旧锁视为空闲并自动释放(原为硬失败), 仅当锁仍被有效占用时才拒绝。 新增 lock_ttl_hours / lock_is_stale / release_stale_locks 三个辅助; lock status 显示各锁 TTL;cmd_done 改调 release_stale_locks。 验证:20 项 TTL 探针 + 5 项 lock_cli 接线探针全绿; 假 UV4 端到端(exit 0 自持自释、payload ttl=0.5)与 busy 路径(exit 4)。
This commit is contained in:
@@ -29,7 +29,7 @@ whenToUse: 需要编译 Keil MDK 工程、验证代码改动是否 0 错误 0
|
|||||||
## build 锁:脚本自持自释(2026-10-12)
|
## build 锁:脚本自持自释(2026-10-12)
|
||||||
|
|
||||||
**UV4 不能两个实例同时 `-b`,但构建锁不该由调用方"记得释放"**——那正是泄漏源:
|
**UV4 不能两个实例同时 `-b`,但构建锁不该由调用方"记得释放"**——那正是泄漏源:
|
||||||
Agent 手工 `lock acquire build` 后忘了 `release`,锁要挂到 6 小时 TTL 才被接管。
|
Agent 手工 `lock acquire build` 后忘了 `release`,锁要挂到 TTL 才被接管。
|
||||||
故本脚本**自己**管:进入 acquire → `try` 构建 → `finally` **无条件** release
|
故本脚本**自己**管:进入 acquire → `try` 构建 → `finally` **无条件** release
|
||||||
(构建失败、异常、`SystemExit` 都会释放)。
|
(构建失败、异常、`SystemExit` 都会释放)。
|
||||||
|
|
||||||
@@ -40,6 +40,9 @@ Agent 手工 `lock acquire build` 后忘了 `release`,锁要挂到 6 小时 TT
|
|||||||
| `--no-lock` | 完全不加锁(危险:并发 `-b` 互相干扰) |
|
| `--no-lock` | 完全不加锁(危险:并发 `-b` 互相干扰) |
|
||||||
| `DTU_LOCK_HELD=build` | 环境变量:**调用方已持锁**时设它,本脚本跳过自持自释(`task-worktree done` 的门禁即如此用) |
|
| `DTU_LOCK_HELD=build` | 环境变量:**调用方已持锁**时设它,本脚本跳过自持自释(`task-worktree done` 的门禁即如此用) |
|
||||||
|
|
||||||
|
- **TTL 自报**(2026-10-12):acquire 时按 `--timeout` 声明陈旧窗口
|
||||||
|
= `timeout/3600 + 15min`,下限 0.5h(默认 600s → **0.5h**)。这样崩溃/强杀留下的锁,
|
||||||
|
其自动接管窗口与本次构建上限同量级,而不再是全局 6h —— 编译很快,兜底窗口也该短。
|
||||||
- 锁工具 = 兄弟 skill `task-worktree/scripts/task_worktree.py`;**找不到则降级为不加锁
|
- 锁工具 = 兄弟 skill `task-worktree/scripts/task_worktree.py`;**找不到则降级为不加锁
|
||||||
并告警**(保持本 skill 可独立搬运)。
|
并告警**(保持本 skill 可独立搬运)。
|
||||||
- ⚠ 从 `_wt/<ID>-<slug>/` 反推持有者名,意味着"手工 `lock acquire build --task <ID>`
|
- ⚠ 从 `_wt/<ID>-<slug>/` 反推持有者名,意味着"手工 `lock acquire build --task <ID>`
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
**build 锁:本脚本自持自释(2026-10-12)**
|
**build 锁:本脚本自持自释(2026-10-12)**
|
||||||
UV4 不能两个实例同时 `-b`,所以构建锁不该由调用方"记得释放"——那正是泄漏源
|
UV4 不能两个实例同时 `-b`,所以构建锁不该由调用方"记得释放"——那正是泄漏源
|
||||||
(Agent 手工 `lock acquire` 后忘了 `release`,锁要挂到 6 小时 TTL 才被接管)。
|
(Agent 手工 `lock acquire` 后忘了 `release`,锁要挂到 TTL 才被接管)。
|
||||||
改为:本脚本进入时 acquire、退出时**无条件** release(try/finally,构建失败/异常/
|
改为:本脚本进入时 acquire、退出时**无条件** release(try/finally,构建失败/异常/
|
||||||
SystemExit 都会释放)。
|
SystemExit 都会释放)。
|
||||||
- 锁工具 = 同级 skill `task-worktree/scripts/task_worktree.py`(**找不到则降级为
|
- 锁工具 = 同级 skill `task-worktree/scripts/task_worktree.py`(**找不到则降级为
|
||||||
@@ -32,6 +32,9 @@
|
|||||||
- 被占用:默认**立即失败**(退出码 4,不静默等待);`--lock-wait N` 可等待 N 秒
|
- 被占用:默认**立即失败**(退出码 4,不静默等待);`--lock-wait N` 可等待 N 秒
|
||||||
- 调用方**已持有** build 锁时(如 `task-worktree done` 的门禁),设
|
- 调用方**已持有** build 锁时(如 `task-worktree done` 的门禁),设
|
||||||
`DTU_LOCK_HELD=build` 让本脚本跳过自持自释,避免自我死锁
|
`DTU_LOCK_HELD=build` 让本脚本跳过自持自释,避免自我死锁
|
||||||
|
- **TTL 自报**:acquire 时按 `--timeout` 声明陈旧窗口(`timeout/3600 + 15min`,
|
||||||
|
下限 0.5h)。故崩溃/强杀留下的锁,其自动接管窗口与本次构建上限同量级,
|
||||||
|
而不再是全局 6h(编译很快,兜底窗口也该短)。
|
||||||
|
|
||||||
退出码(与 task-worktree 同一约定;2026-10-12 修正——此前实现与文档不符):
|
退出码(与 task-worktree 同一约定;2026-10-12 修正——此前实现与文档不符):
|
||||||
0 = PASS(0 错 0 警)
|
0 = PASS(0 错 0 警)
|
||||||
@@ -174,11 +177,26 @@ def default_lock_task():
|
|||||||
return "build-cli"
|
return "build-cli"
|
||||||
|
|
||||||
|
|
||||||
def lock_cli(tool, action, task, wait=0, force=False):
|
def build_lock_ttl_hours(timeout_s):
|
||||||
|
"""build 锁自报的陈旧窗口(小时)≈ UV4 超时上限 + 15min 余量,下限 0.5h。
|
||||||
|
|
||||||
|
这样「崩溃/被强杀」留下的锁,其自动接管窗口与本次构建的真实上限同量级,
|
||||||
|
而不是全局 6h —— 编译很快,锁的兜底窗口也该短。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
t = max(0.0, float(timeout_s))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
t = 600.0
|
||||||
|
return max(0.5, t / 3600.0 + 0.25)
|
||||||
|
|
||||||
|
|
||||||
|
def lock_cli(tool, action, task, wait=0, force=False, ttl=None):
|
||||||
"""调 task-worktree 的 `lock <action> build --task <task>`。返回 (ok, 输出)。"""
|
"""调 task-worktree 的 `lock <action> build --task <task>`。返回 (ok, 输出)。"""
|
||||||
cmd = [sys.executable, str(tool), "lock", action, LOCK_NAME, "--task", task]
|
cmd = [sys.executable, str(tool), "lock", action, LOCK_NAME, "--task", task]
|
||||||
if action == "acquire" and wait > 0:
|
if action == "acquire" and wait > 0:
|
||||||
cmd += ["--wait", str(wait)]
|
cmd += ["--wait", str(wait)]
|
||||||
|
if action == "acquire" and ttl:
|
||||||
|
cmd += ["--ttl", "{:.4f}".format(ttl)]
|
||||||
if force:
|
if force:
|
||||||
cmd += ["--force"]
|
cmd += ["--force"]
|
||||||
try:
|
try:
|
||||||
@@ -202,7 +220,8 @@ def acquire_build_lock(args):
|
|||||||
if tool is None:
|
if tool is None:
|
||||||
return "skipped", None, None, "未找到 task-worktree skill"
|
return "skipped", None, None, "未找到 task-worktree skill"
|
||||||
task = args.lock_task or default_lock_task()
|
task = args.lock_task or default_lock_task()
|
||||||
ok, out = lock_cli(tool, "acquire", task, wait=args.lock_wait)
|
ok, out = lock_cli(tool, "acquire", task, wait=args.lock_wait,
|
||||||
|
ttl=build_lock_ttl_hours(args.timeout))
|
||||||
if not ok:
|
if not ok:
|
||||||
return "busy", tool, task, out
|
return "busy", tool, task, out
|
||||||
return "ok", tool, task, ""
|
return "ok", tool, task, ""
|
||||||
|
|||||||
@@ -103,11 +103,16 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
|
|||||||
- **`--lean` 只省磁盘不省时间**:实测全量 smudge 7.25 s / 83 MB,跳过 smudge 7.05 s / 45 MB,
|
- **`--lean` 只省磁盘不省时间**:实测全量 smudge 7.25 s / 83 MB,跳过 smudge 7.05 s / 45 MB,
|
||||||
两者工作区都干净。默认走全量(参考图可用),磁盘紧张时才用 `--lean`。
|
两者工作区都干净。默认走全量(参考图可用),磁盘紧张时才用 `--lean`。
|
||||||
- **锁不绑定进程存活**:`lock acquire` 本身是短命进程,而真正用资源的烧录/构建在别的进程里跑。
|
- **锁不绑定进程存活**:`lock acquire` 本身是短命进程,而真正用资源的烧录/构建在别的进程里跑。
|
||||||
所以判「陈旧」只看**时效**(默认 6 小时,`LOCK_TTL_HOURS`)加显式 `--force`;pid 仅作排障记录。
|
所以判「陈旧」只看**时效**(该锁的 TTL)加显式 `--force`;pid 仅作排障记录。
|
||||||
同任务重复 acquire 是幂等的(不会把自己锁死)。
|
同任务重复 acquire 是幂等的(不会把自己锁死)。
|
||||||
- **陈旧锁可自动接管**:超过 6 小时未释放即视为陈旧,下次 acquire 会接管并提示;活锁需 `--force`。
|
- **TTL 按锁分设**(2026-10-12):`build` = **1h**、`bench` = **6h**(`LOCK_TTL_HOURS`)。
|
||||||
|
口径是「兜底窗口应与操作真实时长同量级」——编译是秒~分钟级,6h 的兜底太钝;
|
||||||
|
bench 是会话级(flash→`@test`→探针),才配得上小时级。持有者还可用
|
||||||
|
`lock acquire ... --ttl <小时>` **自报**更精确的窗口(`keil_build.py` 即按其 `--timeout` 自报)。
|
||||||
|
- **陈旧锁自动接管**:超过该锁 TTL 未释放即视为陈旧,下次 `acquire` 会接管并提示;
|
||||||
|
未到 TTL 的活锁需 `--force`。`done` 的收口前置也会**自动释放陈旧锁**(不再硬失败)。
|
||||||
- **`build` 锁不必手工持**(2026-10-12 起):`@build`(`keil_build.py`)**自持自释**——
|
- **`build` 锁不必手工持**(2026-10-12 起):`@build`(`keil_build.py`)**自持自释**——
|
||||||
进入 acquire、`finally` 无条件 release,所以"Agent 忘了 release 泄漏 6 小时"这条在构建路径上已消除。
|
进入 acquire、`finally` 无条件 release,所以"Agent 忘了 release、锁一直挂到 TTL"这条在构建路径上已消除。
|
||||||
`done` 的门禁自己持锁并给子进程设 `DTU_LOCK_HELD=build` 告知跳过(新旧脚本都兼容)。
|
`done` 的门禁自己持锁并给子进程设 `DTU_LOCK_HELD=build` 告知跳过(新旧脚本都兼容)。
|
||||||
**`bench` 锁仍须 Agent 手工持**(会话作用域:跨烧录→`@test`→探针,见 `keil-build` SKILL.md)。
|
**`bench` 锁仍须 Agent 手工持**(会话作用域:跨烧录→`@test`→探针,见 `keil-build` SKILL.md)。
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,14 @@ EXIT_OK, EXIT_FAIL, EXIT_ENV, EXIT_USAGE = 0, 1, 2, 3
|
|||||||
BRANCH_PREFIX = "task/"
|
BRANCH_PREFIX = "task/"
|
||||||
MAX_CONCURRENT = 3
|
MAX_CONCURRENT = 3
|
||||||
LOCK_NAMES = ("build", "bench")
|
LOCK_NAMES = ("build", "bench")
|
||||||
LOCK_TTL_HOURS = 6.0 # 超过此持有时间即视为陈旧锁,下次 acquire 自动接管
|
# 按锁分设陈旧窗口(小时)——兜底窗口应与「操作真实时长」同量级:
|
||||||
|
# build:编译是秒~分钟级(keil_build.py 自持自释,持有窗口 ≈ UV4 运行时长,
|
||||||
|
# 上限 = --timeout,默认 600s);崩溃泄漏时应在数十分钟内即可接管 → 1h。
|
||||||
|
# bench:会话级(flash → @test → 探针,天然几十分钟~数小时)→ 保留 6h。
|
||||||
|
# 另:锁 payload 里持有者可用 `ttl` 字段自报窗口(见 lock_acquire(ttl=...)),
|
||||||
|
# 优先于这里的默认值——这样 build 锁的窗口能自动跟随其 --timeout。
|
||||||
|
LOCK_TTL_HOURS = {"build": 1.0, "bench": 6.0}
|
||||||
|
DEFAULT_LOCK_TTL_HOURS = 6.0
|
||||||
JUNCTION_SPECS = (
|
JUNCTION_SPECS = (
|
||||||
(".venv", "Python 环境"),
|
(".venv", "Python 环境"),
|
||||||
("simulator/SDL2", "模拟器 SDL2 SDK"),
|
("simulator/SDL2", "模拟器 SDL2 SDK"),
|
||||||
@@ -212,6 +219,26 @@ def lock_age_hours(info) -> float:
|
|||||||
return float("inf")
|
return float("inf")
|
||||||
|
|
||||||
|
|
||||||
|
def lock_ttl_hours(name: str, info=None) -> float:
|
||||||
|
"""锁 `name` 的陈旧判定窗口(小时)。
|
||||||
|
|
||||||
|
优先用锁 payload 里持有者自报的 `ttl`(例如 keil_build.py 按其 --timeout 声明),
|
||||||
|
否则回落到该锁的默认窗口(build 1h / bench 6h)。
|
||||||
|
"""
|
||||||
|
if info:
|
||||||
|
try:
|
||||||
|
v = info.get("ttl")
|
||||||
|
if v:
|
||||||
|
return max(0.0, float(v))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
return LOCK_TTL_HOURS.get(name, DEFAULT_LOCK_TTL_HOURS)
|
||||||
|
|
||||||
|
|
||||||
|
def lock_is_stale(name: str, info) -> bool:
|
||||||
|
return lock_age_hours(info) > lock_ttl_hours(name, info)
|
||||||
|
|
||||||
|
|
||||||
def lock_path(name: str) -> Path:
|
def lock_path(name: str) -> Path:
|
||||||
return LOCKS_DIR / (name + ".lock")
|
return LOCKS_DIR / (name + ".lock")
|
||||||
|
|
||||||
@@ -226,12 +253,14 @@ def lock_read(name: str):
|
|||||||
return {"task": "?", "pid": 0, "epoch": 0, "at": "?"}
|
return {"task": "?", "pid": 0, "epoch": 0, "at": "?"}
|
||||||
|
|
||||||
|
|
||||||
def lock_acquire(name: str, task: str, wait: int = 0, force: bool = False, note: str = ""):
|
def lock_acquire(name: str, task: str, wait: int = 0, force: bool = False, note: str = "",
|
||||||
|
ttl: float = None):
|
||||||
"""抢占独占资源。
|
"""抢占独占资源。
|
||||||
|
|
||||||
⚠ 锁**不**绑定进程存活:`lock acquire` 本身是短命进程,而用锁的烧录/构建
|
⚠ 锁**不**绑定进程存活:`lock acquire` 本身是短命进程,而用锁的烧录/构建
|
||||||
在另一个进程里跑。因此判「陈旧」只看**时效**(超过 LOCK_TTL_HOURS),
|
在另一个进程里跑。因此判「陈旧」只看**时效**(超过该锁的 TTL),外加显式
|
||||||
外加显式 `--force`;pid 仅作记录便于排障。
|
`--force`;pid 仅作记录便于排障。TTL 按锁分设(build 1h / bench 6h),
|
||||||
|
持有者也可用 `ttl`(小时)自报更精确的窗口(如 build 按 --timeout 声明)。
|
||||||
"""
|
"""
|
||||||
LOCKS_DIR.mkdir(parents=True, exist_ok=True)
|
LOCKS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
p = lock_path(name)
|
p = lock_path(name)
|
||||||
@@ -244,9 +273,10 @@ def lock_acquire(name: str, task: str, wait: int = 0, force: bool = False, note:
|
|||||||
if info.get("task") == task:
|
if info.get("task") == task:
|
||||||
return True, info
|
return True, info
|
||||||
age = lock_age_hours(info)
|
age = lock_age_hours(info)
|
||||||
if force or age > LOCK_TTL_HOURS:
|
ttl_h = lock_ttl_hours(name, info)
|
||||||
print("[lock] 接管 {} 锁(原持有者 {},已持有 {:.1f} 小时{})".format(
|
if force or age > ttl_h:
|
||||||
name, info.get("task"), age, ",--force" if force else ",视为陈旧"))
|
print("[lock] 接管 {} 锁(原持有者 {},已持有 {:.1f} 小时 / TTL {:.1f} 小时{})".format(
|
||||||
|
name, info.get("task"), age, ttl_h, ",--force" if force else ",视为陈旧"))
|
||||||
try:
|
try:
|
||||||
p.unlink()
|
p.unlink()
|
||||||
except OSError:
|
except OSError:
|
||||||
@@ -254,8 +284,8 @@ def lock_acquire(name: str, task: str, wait: int = 0, force: bool = False, note:
|
|||||||
continue
|
continue
|
||||||
if time.time() >= deadline:
|
if time.time() >= deadline:
|
||||||
return False, info
|
return False, info
|
||||||
print("[lock] {} 被 {} 占用(已持有 {:.1f} 小时),等待…".format(
|
print("[lock] {} 被 {} 占用(已持有 {:.1f} / TTL {:.1f} 小时),等待…".format(
|
||||||
name, info.get("task"), age))
|
name, info.get("task"), age, ttl_h))
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
else:
|
else:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
@@ -265,6 +295,7 @@ def lock_acquire(name: str, task: str, wait: int = 0, force: bool = False, note:
|
|||||||
"epoch": now,
|
"epoch": now,
|
||||||
"at": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(now)),
|
"at": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(now)),
|
||||||
"note": note,
|
"note": note,
|
||||||
|
"ttl": ttl,
|
||||||
}
|
}
|
||||||
os.write(fd, (json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8"))
|
os.write(fd, (json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8"))
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
@@ -631,13 +662,14 @@ def cmd_lock(a) -> int:
|
|||||||
for n in LOCK_NAMES:
|
for n in LOCK_NAMES:
|
||||||
info = lock_read(n)
|
info = lock_read(n)
|
||||||
if not info:
|
if not info:
|
||||||
print("{:<6} 空闲".format(n))
|
print("{:<6} 空闲(TTL {:.1f}h)".format(n, lock_ttl_hours(n)))
|
||||||
else:
|
else:
|
||||||
age = lock_age_hours(info)
|
age = lock_age_hours(info)
|
||||||
mark = " [陈旧,可被自动接管]" if age > LOCK_TTL_HOURS else ""
|
ttl_h = lock_ttl_hours(n, info)
|
||||||
print("{:<6} {} 持有(pid={},于 {},已 {:.1f} 小时,备注 {}){}".format(
|
mark = " [陈旧,可被自动接管]" if age > ttl_h else ""
|
||||||
|
print("{:<6} {} 持有(pid={},于 {},已 {:.1f} / TTL {:.1f} 小时,备注 {}){}".format(
|
||||||
n, info.get("task"), info.get("pid"), info.get("at"), age,
|
n, info.get("task"), info.get("pid"), info.get("at"), age,
|
||||||
info.get("note") or "-", mark))
|
ttl_h, info.get("note") or "-", mark))
|
||||||
return EXIT_OK
|
return EXIT_OK
|
||||||
|
|
||||||
if a.name not in LOCK_NAMES:
|
if a.name not in LOCK_NAMES:
|
||||||
@@ -646,12 +678,15 @@ def cmd_lock(a) -> int:
|
|||||||
if a.action == "acquire":
|
if a.action == "acquire":
|
||||||
if not a.task:
|
if not a.task:
|
||||||
raise EnvError("acquire 需要 --task <ID>")
|
raise EnvError("acquire 需要 --task <ID>")
|
||||||
got, info = lock_acquire(a.name, a.task, wait=a.wait, force=a.force, note=a.note or "")
|
got, info = lock_acquire(a.name, a.task, wait=a.wait, force=a.force,
|
||||||
|
note=a.note or "", ttl=a.ttl)
|
||||||
if not got:
|
if not got:
|
||||||
print("获取失败:{} 被 {} 占用(pid={},于 {})".format(
|
print("获取失败:{} 被 {} 占用(pid={},于 {})".format(
|
||||||
a.name, info.get("task"), info.get("pid"), info.get("at")))
|
a.name, info.get("task"), info.get("pid"), info.get("at")))
|
||||||
return EXIT_FAIL
|
return EXIT_FAIL
|
||||||
print("已获取 {} 锁(task={} pid={})".format(a.name, a.task, os.getpid()))
|
print("已获取 {} 锁(task={} pid={} ttl={})".format(
|
||||||
|
a.name, a.task, os.getpid(),
|
||||||
|
"{:.2f}h".format(a.ttl) if a.ttl else "默认"))
|
||||||
return EXIT_OK
|
return EXIT_OK
|
||||||
|
|
||||||
if a.bench_pass:
|
if a.bench_pass:
|
||||||
@@ -666,6 +701,27 @@ def cmd_lock(a) -> int:
|
|||||||
return EXIT_OK if ok else EXIT_FAIL
|
return EXIT_OK if ok else EXIT_FAIL
|
||||||
|
|
||||||
|
|
||||||
|
def release_stale_locks() -> None:
|
||||||
|
"""`done` 前置:陈旧锁自动接管;仍被有效占用的则拒绝收口。
|
||||||
|
|
||||||
|
「陈旧」= 持有时间 > 该锁 TTL(build 1h / bench 6h,或持有者自报的 ttl)。
|
||||||
|
崩溃/遗忘留下的锁不该阻塞收口——故自动释放而非硬失败(2026-10-12)。
|
||||||
|
"""
|
||||||
|
for n in LOCK_NAMES:
|
||||||
|
info = lock_read(n)
|
||||||
|
if not info:
|
||||||
|
continue
|
||||||
|
age = lock_age_hours(info)
|
||||||
|
ttl_h = lock_ttl_hours(n, info)
|
||||||
|
if age > ttl_h:
|
||||||
|
ok, msg = lock_release(n, force=True)
|
||||||
|
print("[i] {} 锁已陈旧({} 持有 {:.1f}h > TTL {:.1f}h),自动接管:{}".format(
|
||||||
|
n, info.get("task"), age, ttl_h, msg))
|
||||||
|
continue
|
||||||
|
raise Fail("独占资源 {} 仍被 {} 占用(已 {:.1f} / TTL {:.1f} 小时),先 release".format(
|
||||||
|
n, info.get("task"), age, ttl_h))
|
||||||
|
|
||||||
|
|
||||||
def cmd_done(a) -> int:
|
def cmd_done(a) -> int:
|
||||||
st = load_state(a.task_id)
|
st = load_state(a.task_id)
|
||||||
wt = Path(st["path"])
|
wt = Path(st["path"])
|
||||||
@@ -683,11 +739,7 @@ def cmd_done(a) -> int:
|
|||||||
cur = git_out("rev-parse", "--abbrev-ref", "HEAD", cwd=main)
|
cur = git_out("rev-parse", "--abbrev-ref", "HEAD", cwd=main)
|
||||||
if cur != base:
|
if cur != base:
|
||||||
raise Fail("主工作树当前在 {},不是集成分支 {};先切过去再 done".format(cur, base))
|
raise Fail("主工作树当前在 {},不是集成分支 {};先切过去再 done".format(cur, base))
|
||||||
for n in LOCK_NAMES:
|
release_stale_locks()
|
||||||
info = lock_read(n)
|
|
||||||
if info:
|
|
||||||
raise Fail("独占资源 {} 仍被 {} 占用(已 {:.1f} 小时),先 release".format(
|
|
||||||
n, info.get("task"), lock_age_hours(info)))
|
|
||||||
|
|
||||||
ahead = int(git_out("rev-list", "--count", "{}..{}".format(base, branch), cwd=wt) or "0")
|
ahead = int(git_out("rev-list", "--count", "{}..{}".format(base, branch), cwd=wt) or "0")
|
||||||
already = git("merge-base", "--is-ancestor", branch, base, cwd=main, check=False).returncode == 0
|
already = git("merge-base", "--is-ancestor", branch, base, cwd=main, check=False).returncode == 0
|
||||||
@@ -816,6 +868,8 @@ def build_parser():
|
|||||||
p.add_argument("--wait", type=int, default=0, help="等待秒数(0=不等待)")
|
p.add_argument("--wait", type=int, default=0, help="等待秒数(0=不等待)")
|
||||||
p.add_argument("--force", action="store_true")
|
p.add_argument("--force", action="store_true")
|
||||||
p.add_argument("--note", default="")
|
p.add_argument("--note", default="")
|
||||||
|
p.add_argument("--ttl", type=float, default=None,
|
||||||
|
help="本次持有自报的陈旧窗口(小时);缺省用该锁默认值(build 1h / bench 6h)")
|
||||||
p.add_argument("--bench-pass", action="store_true",
|
p.add_argument("--bench-pass", action="store_true",
|
||||||
help="release bench 时顺带为该任务记录台架 PASS")
|
help="release bench 时顺带为该任务记录台架 PASS")
|
||||||
p.set_defaults(func=cmd_lock)
|
p.set_defaults(func=cmd_lock)
|
||||||
|
|||||||
+11
-4
@@ -180,13 +180,20 @@ D:/Code/DTU Code/
|
|||||||
- ST-LINK SN `0B250D12A218303030303032`
|
- ST-LINK SN `0B250D12A218303030303032`
|
||||||
- 设备 IP `192.168.1.100`
|
- 设备 IP `192.168.1.100`
|
||||||
|
|
||||||
机制:`_wt/.state/locks/{build,bench}.lock` 记录持有者(任务 ID + pid + 时间戳 + 备注)。
|
机制:`_wt/.state/locks/{build,bench}.lock` 记录持有者(任务 ID + pid + 时间戳 + 备注 + 自报 TTL)。
|
||||||
|
|
||||||
**锁不绑定进程存活**:`lock acquire` 本身是短命进程,而真正用资源的烧录/构建在另一个进程里跑。
|
**锁不绑定进程存活**:`lock acquire` 本身是短命进程,而真正用资源的烧录/构建在另一个进程里跑。
|
||||||
若按 pid 判活,acquire 一退出锁就被判为陈旧 → 锁形同虚设(实测踩到)。
|
若按 pid 判活,acquire 一退出锁就被判为陈旧 → 锁形同虚设(实测踩到)。
|
||||||
故判「陈旧」只看**时效**(`LOCK_TTL_HOURS` = 6 小时)加显式 `--force`;pid 仅作排障记录。
|
故判「陈旧」只看**时效**(该锁的 TTL)加显式 `--force`;pid 仅作排障记录。
|
||||||
同任务重复 acquire 幂等;他人持锁时 acquire 拒绝(`--wait` 可轮询等待)。
|
同任务重复 acquire 幂等;他人持锁时 acquire 拒绝(`--wait` 可轮询等待)。
|
||||||
`done` 前置检查会在任一锁被占用时拒绝回收。
|
|
||||||
|
**TTL 按锁分设**(2026-10-12):`LOCK_TTL_HOURS = {"build": 1.0, "bench": 6.0}`。
|
||||||
|
口径 = 兜底窗口应与**操作真实时长**同量级:编译是秒~分钟级(`keil_build.py` 自持自释,
|
||||||
|
窗口 ≈ UV4 运行时长),1h 已足够宽松;bench 是**会话级**(flash→`@test`→探针),才配得上 6h。
|
||||||
|
持有者还可用 `lock acquire ... --ttl <小时>` **自报**更精确的窗口——
|
||||||
|
`keil_build.py` 即按其 `--timeout` 自报(`timeout/3600 + 15min`,下限 0.5h)。
|
||||||
|
|
||||||
|
`done` 前置检查:**陈旧锁自动接管释放**(不再硬失败),仅当锁仍被有效占用时才拒绝回收。
|
||||||
|
|
||||||
> ⚠ 台架相关的既有陷阱(`--require-selftest-pass` 的陈旧台账、回读校验会复位目标)
|
> ⚠ 台架相关的既有陷阱(`--require-selftest-pass` 的陈旧台账、回读校验会复位目标)
|
||||||
> 不因引入工作树而改变,仍按 `docs/测试说明/自动化测试流程.md` 执行。
|
> 不因引入工作树而改变,仍按 `docs/测试说明/自动化测试流程.md` 执行。
|
||||||
@@ -366,7 +373,7 @@ docs\内存管理\RAM使用分析与优化方案.md:585 [路径引用] docs/内
|
|||||||
**实测暴露并已修复的 4 个缺陷**:
|
**实测暴露并已修复的 4 个缺陷**:
|
||||||
|
|
||||||
1. **锁绑定进程存活 → 形同虚设**:`lock acquire` 是短命进程,一退出锁就被判陈旧、下次必被接管。
|
1. **锁绑定进程存活 → 形同虚设**:`lock acquire` 是短命进程,一退出锁就被判陈旧、下次必被接管。
|
||||||
改为**时效判定**(6 小时 TTL)+ 显式 `--force`,pid 仅作记录。
|
改为**时效判定**(当时全局 6h;2026-10-12 起改为按锁分设 TTL,见 §7.1)+ 显式 `--force`,pid 仅作记录。
|
||||||
2. **`git branch -d` 按环境 HEAD 判「已合并」**:集成点被改走时误判、拒绝删除。
|
2. **`git branch -d` 按环境 HEAD 判「已合并」**:集成点被改走时误判、拒绝删除。
|
||||||
改为显式 `merge-base --is-ancestor <branch> <base>` 后再删。
|
改为显式 `merge-base --is-ancestor <branch> <base>` 后再删。
|
||||||
3. **直调 Win32 建联接被拒**(Win32 4392):改走 `mklink /J`(详见 3.3)。
|
3. **直调 Win32 建联接被拒**(Win32 4392):改走 `mklink /J`(详见 3.3)。
|
||||||
|
|||||||
Reference in New Issue
Block a user