fix: Backup.ps1 与 Restore.ps1 并发撞车时互踩账本(加运行锁)
问题:两个入口都会写 manifest.json,也都会在备份目录里用 <归档>.tmp 这个名字生成临时
归档。计划任务与手动运行撞在一起时,两边会互相覆盖对方的账本;更糟的是两边会把彼此的临时
归档当成自己的。计划任务的 -MultipleInstances IgnoreNew 只挡住"计划任务之间",挡不住手动运行。
修法:备份目录上的一把跨进程锁,用**独占文件句柄**(FileShare.None)而不是命名互斥体:
* 句柄由内核持有,进程被杀 / 崩溃时自动关闭,锁自动释放 —— 不会留下需要人工清理的陈旧锁;
命名互斥体要跨会话(计划任务在另一个会话里跑)还得用 Global\ 前缀,那需要额外权限。
* 它是文件系统的锁:不区分会话、不区分终端,计划任务与手动运行会互相看见。
* 锁文件里写明持有进程(pid / 起始时间 / 主机 / 用户)—— "到底是谁占着"不该靠猜。
拿不到锁就直接失败(退出码 1 + 明确消息),不等待:单个条目压缩可能十几分钟,"等它跑完"
对用户来说和挂住没区别。
只读模式不取锁(Backup 的 -DryRun;Restore 的 -DryRun / -WhatIf / -VerifyOnly):
它们一个字节都不写,没必要被正在跑的备份挡在外面。
踩到并记下的两个坑:
1) catch [System.IO.IOException] 接不住 —— PowerShell 把 .NET 方法抛出的异常包成
MethodInvocationException,按内层类型做的 catch 会漏。现在沿 InnerException 链找,
不是 IOException 就把原异常抛回去(目录不可写是 UnauthorizedAccessException,那是真
错误,不该伪装成"另一次运行在进行中")。
2) 锁文件是独占打开的,所以内容只能在**释放之后**读 —— 第一版断言在持锁时去 Get-Content,
被自己的锁拒了;这条断言现在挪到释放之后。
跨进程证据(真跑,不是推理):父进程持锁 → 另一个进程取锁得到 DENIED;持锁状态下跑
真实的 Backup.ps1 → 退出码 1、日志点名锁文件、manifest 的 SHA256 未变;释放后另一进程
得到 GOT。
验收:test.ps1 9/9 全绿(7 与 5.1);tests\Run-RealSmoke.ps1 4/4 全绿。
This commit is contained in:
1 parent
79f83f6760
commit
8d67a38fb7
5 files changed
+197
No files matched your search
@@ -1139,6 +1139,47 @@ Describe '外部命令退出码(旧实现的核心缺陷)' {
|
||||
Remove-Item -LiteralPath $sandbox -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
It '运行锁:同一份备份目录同时只能有一个持有者' {
|
||||
# Backup.ps1 与 Restore.ps1 都会写 manifest.json,也都会用 <归档>.tmp 这个名字;
|
||||
# 计划任务与手动运行撞在一起时,两边会互相覆盖对方的账本、抢对方的临时归档。
|
||||
$dir = Join-Path $env:TEMP ("bnr-lock-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
|
||||
$first = $null
|
||||
$second = $null
|
||||
try {
|
||||
$first = Enter-BaknretRunLock -Directory $dir
|
||||
$first | Should -Not -BeNullOrEmpty
|
||||
|
||||
$second = Enter-BaknretRunLock -Directory $dir
|
||||
$second | Should -BeNullOrEmpty
|
||||
|
||||
Test-Path -LiteralPath (Get-BaknretRunLockPath -Directory $dir) | Should -BeTrue
|
||||
} finally {
|
||||
Exit-BaknretRunLock -Lock $second
|
||||
Exit-BaknretRunLock -Lock $first
|
||||
}
|
||||
|
||||
# 锁文件是独占打开的(FileShare.None),内容只能在释放之后读
|
||||
(Get-Content -Encoding UTF8 -LiteralPath (Get-BaknretRunLockPath -Directory $dir) -Raw) |
|
||||
Should -Match "pid=$PID"
|
||||
|
||||
Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
It '运行锁:释放之后可以重新取得' {
|
||||
$dir = Join-Path $env:TEMP ("bnr-lock-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
|
||||
try {
|
||||
$first = Enter-BaknretRunLock -Directory $dir
|
||||
$first | Should -Not -BeNullOrEmpty
|
||||
Exit-BaknretRunLock -Lock $first
|
||||
|
||||
$second = Enter-BaknretRunLock -Directory $dir
|
||||
$second | Should -Not -BeNullOrEmpty
|
||||
Exit-BaknretRunLock -Lock $second
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
It '空目录的摘要给 0 而不是 $null(否则空间守卫会静默失效)' {
|
||||
# Measure-Object 对空输入返回 $null 而不是 0,于是 .Sum 也是 $null;而 $null / 1GB
|
||||
# 得 0,空间守卫判的是 -gt 0 —— 这一档就不再拦截了。
|
||||
|
||||
Reference in new issue
Block a user