修复 AGC 项目写锁回收判据的跨平台 PID 边界
Project CI / Repository checks (pull_request) Successful in 3m8s
Project CI / Frontend tests (pull_request) Successful in 4m15s
Project CI / Backend tests (pull_request) Successful in 6m47s
Project CI / Native shell tests (pull_request) Successful in 17m58s

- 项目写锁存活判定把平台不可能分配出的进程号(0 或超出平台 pid 宽度)判为持有者不存在并直接回收,不再落回 600 秒保守分支
- 回归用例的死进程 PID fixture 由 0xFFFF_FFF0 改为 i32::MAX - 1,同时落在两个平台进程号空间之外且在 Unix 有符号 32 位范围内
- 新增 project_write_lock_reclaims_unrepresentable_owner_pid 用例,覆盖 u64::MAX 非法进程号残留锁
- 修正 project_lock_recovery 头部注释,说明这些用例是回归护栏而不是待修缺陷
- 同步 decision-log 的 PID 边界决策与 pitfalls 的跨平台 fixture 经验
This commit is contained in:
2026-09-09 18:00:53 +08:00
parent 78ba9e33b6
commit b7f27721c5
4 changed files with 49 additions and 8 deletions
@@ -55,7 +55,11 @@ impl Drop for ProjectWriteLock {
#[cfg(unix)]
fn project_write_lock_process_is_alive(process_id: u64) -> Option<bool> {
let process_id = i32::try_from(process_id).ok().filter(|value| *value > 0)?;
// Unix 的 pid_t 是有符号 32 位且恒大于 0,超出该范围的取值不可能是本机
// 任何进程,说明锁文件里的 PID 已经损坏,可以直接判定持有者不存在。
let Some(process_id) = i32::try_from(process_id).ok().filter(|value| *value > 0) else {
return Some(false);
};
let result = unsafe { libc::kill(process_id, 0) };
if result == 0 {
return Some(true);
@@ -78,7 +82,11 @@ fn project_write_lock_process_is_alive(process_id: u64) -> Option<bool> {
fn CloseHandle(handle: *mut c_void) -> i32;
}
let process_id = u32::try_from(process_id).ok().filter(|value| *value > 0)?;
// Windows 进程号是 32 位且恒大于 0,超出该范围的取值不可能是本机任何
// 进程,说明锁文件里的 PID 已经损坏,可以直接判定持有者不存在。
let Some(process_id) = u32::try_from(process_id).ok().filter(|value| *value > 0) else {
return Some(false);
};
const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
const STILL_ACTIVE: u32 = 259;
// SAFETY: OpenProcess returns an owned kernel handle or null; it is
@@ -4,13 +4,15 @@ use std::process::Stdio;
// Issue #310 复现:异常退出后在项目里残留 `.agent/project.lock`,下一次打开
// 项目时所有写操作都被拒绝。
//
// 下面的用例描述的是期望行为(残留锁必须被安全回收)。在当前实现下它们会
// 失败,用来证明缺陷;修复后应当全部通过,并作为回归用例保留
// 下面的用例是回归护栏:持有者确实不存在时残留锁必须被安全回收,同时不能抢走
// 仍然活着持有者的锁
const PROJECT_LOCK_RELATIVE_PATH: &str = ".agent/project.lock";
/// 一个确定不会被占用的进程号:Windows 的 OpenProcess 对它返回
/// ERROR_INVALID_PARAMETERUnix 的 kill(pid, 0) 返回 ESRCH。
const DEAD_OWNER_PID: u64 = 0xFFFF_FFF0;
/// 一个确定不会被占用的进程号:高于两个平台实际分配的进程号上限,因此 Windows
/// 的 OpenProcess 对它返回 ERROR_INVALID_PARAMETERUnix 的 kill(pid, 0) 返回
/// ESRCH。取值必须落在 Unix 有符号 32 位 pid 范围内,否则在 Unix 上会先命中
/// “进程号不可表示”分支,而不是这条“死进程”分支。
const DEAD_OWNER_PID: u64 = i32::MAX as u64 - 1;
fn write_project_lock_fixture(root: &Path, content: &[u8]) {
fs::write(root.join(PROJECT_LOCK_RELATIVE_PATH), content).expect("写入项目写锁 fixture");
@@ -80,6 +82,28 @@ fn project_write_lock_reclaims_dead_owner_pid() {
fs::remove_dir_all(root).ok();
}
/// 复现 A0:锁文件被外部改写或截断损坏,PID 超出平台进程号空间(例如
/// u64::MAX)。它不可能属于任何活进程,必须直接回收,而不是让项目再等满
/// 600 秒;Unix 的 pid_t 只有有符号 32 位,越界取值尤其容易在这里被漏掉。
#[test]
fn project_write_lock_reclaims_unrepresentable_owner_pid() {
let root = unique_project_path();
init_local_game_project_at(&root, "lock-invalid-pid", "锁回收-非法进程号").expect("初始化项目");
write_project_lock_fixture(
&root,
&project_lock_fixture_payload(u64::MAX, unix_timestamp()),
);
let acquired = acquire_project_write_lock(&root, "repro.acquire-after-crash");
assert!(
acquired.is_ok(),
"非法进程号的残留锁未被回收,实际错误:{:?}",
acquired.err()
);
drop(acquired);
fs::remove_dir_all(root).ok();
}
/// 复现 A:崩溃发生在 create_new 成功、payload 写盘之前,留下 0 字节锁。
/// 现在要等满 600 秒才会回收,重启后 10 分钟内所有写操作都失败。
#[test]