修复 AGC 项目写锁回收判据的跨平台 PID 边界
- 项目写锁存活判定把平台不可能分配出的进程号(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:
@@ -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_PARAMETER,Unix 的 kill(pid, 0) 返回 ESRCH。
|
||||
const DEAD_OWNER_PID: u64 = 0xFFFF_FFF0;
|
||||
/// 一个确定不会被占用的进程号:高于两个平台实际分配的进程号上限,因此 Windows
|
||||
/// 的 OpenProcess 对它返回 ERROR_INVALID_PARAMETER,Unix 的 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]
|
||||
|
||||
@@ -8194,4 +8194,5 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 决策:`.agent/project.lock` 记录 `processStartedAt`;PID 存活时用启动身份区分“原持有者仍在”与“PID 被复用”,身份不一致才回收。旧锁无该字段时用“进程启动时间晚于锁 `createdAt` + 5 秒容差”推断。空锁 / 坏锁(崩溃停在 `create_new` 与落盘之间)宽限期 30 秒,无法判定存活时保持 600 秒。活持有者始终不回收。
|
||||
- 决策:启动诊断日志用 `StartupLogSlot` 先按标识符推导 APPDATA 路径、配置目录就绪后切换,`startup.*.failed` 与 `show_startup_error_dialog` 必须可达;Windows 启动失败弹系统消息框,其它平台写 stderr。
|
||||
- 边界:`agent-runner.lock` / `agent-runner.gui-owner.lock` 是 OS 独占句柄锁,进程退出即释放,残留文件不阻塞下次启动;不要把它们当成项目写锁的同类残留处理。
|
||||
- 验证:`project_lock_recovery` 6 条与 `diagnostic_log` 5 条定向测试通过,真实二进制双实例复现“第二个实例写 `startup.runner.owner-lock.failed` 并弹出可见提示”。
|
||||
- 边界:锁文件里的 PID 若超出平台进程号空间(Unix `pid_t` 是有符号 32 位、Windows 是 32 位,均恒大于 0),它不可能属于任何活进程,按“持有者不存在”直接回收,不再落回 600 秒保守分支。
|
||||
- 验证:`project_lock_recovery` 7 条与 `diagnostic_log` 5 条定向测试通过,真实二进制双实例复现“第二个实例写 `startup.runner.owner-lock.failed` 并弹出可见提示”。
|
||||
|
||||
@@ -5031,3 +5031,11 @@
|
||||
- 处理:框选命中判断使用 `Element.closest('.genarrative-image-canvas__world')`,并保留 viewport 自身命中路径;回归测试通过真实 `CanvasWorld` DOM 的 `pointerdown` 冒泡覆盖内层 world content。
|
||||
- 验证:`src/components/image-editor/useImageCanvasStageInteractions.test.tsx` 覆盖内层 world content 命中,定向交互测试通过。
|
||||
- 关联:`packages/image-canvas-react/src/useImageCanvasStageInteractions.ts`、`packages/image-canvas-react/src/CanvasWorld.tsx`。
|
||||
|
||||
## 跨平台“死进程 PID” fixture 必须落在 Unix 有符号 32 位范围内(2026-09-09)
|
||||
|
||||
- 现象:`project_lock_recovery::project_write_lock_reclaims_dead_owner_pid` 在 Windows 本地通过,在 Linux CI 报“死进程残留锁未被回收,实际错误:项目正在被其他写操作占用”。
|
||||
- 原因:fixture 用 `0xFFFF_FFF0` 当死 PID;Unix 的 `pid_t` 是有符号 32 位,`i32::try_from` 直接失败,存活判定返回 `None`(无法判定)而不是 `Some(false)`,于是落回 600 秒保守分支,残留锁不再被回收。
|
||||
- 处理:实现层把“平台不可能分配出的进程号”(0 或超出平台 pid 宽度)判为持有者不存在并直接回收;fixture 改用 `i32::MAX as u64 - 1`,另加 `u64::MAX` 非法进程号用例。
|
||||
- 验证:WSL Ubuntu 上 `cargo test --bin genarrative-ai-game-creator-shell project_lock_recovery` 7 条全过;Windows 上把可表示性判据临时回退到 HEAD 后,只有 `project_write_lock_reclaims_unrepresentable_owner_pid` 失败,说明该用例确实覆盖这条分支;Linux CI 的原始失败记录覆盖越界 PID 分支。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/project/filesystem.rs`、`apps/ai-game-creator-shell/src-tauri/src/tests/project_lock_recovery.rs`。
|
||||
|
||||
Reference in New Issue
Block a user