fix: lab 的 ACL 场景在 5.1 上会被原生命令的 stderr 中断

tools\lab\payload\run-acl-scenario.ps1 是 $ErrorActionPreference = Stop,而它要大量调用
rmdir / takeown / icacls / scoop / code —— 其中 rmdir 与 takeown 在"对象已经处理过"或
"连接点已经悬空"时就会往 stderr 写字。Windows PowerShell 5.1 会把那升级成终止性的
NativeCommandError(7 改了这条规矩),于是清理函数在设计要处理的**恰恰那个场景**里直接崩掉。

修法:新增 Invoke-NativeTolerant,在进入原生命令时把 EAP 放到 Continue、退出时还原,
把 stdout 与 stderr 合并返回;12 处调用全部收进它。判断"删掉了没有"本来就该用 Test-Path,
不需要让 stderr 变成异常。这与 tests\BakNRet.Security.Tests.ps1 里对 takeown / icacls
用的是同一招(那里已被 5.1 的失败实测逼出来过)。

范围说明:只改了 run-acl-scenario.ps1。provision.ps1 的同类写法**不需要**改 ——
它是 $ErrorActionPreference = Continue,那两处本来就不会踩。

验证边界(如实说明):这个文件要 Hyper-V + 一台 VM 才跑得到,本会话无法执行它。
所以这一步的证据是:两个版本上解析零错(Parse 层覆盖)、12 处替换逐条断言命中、以及
diff 逐行复核。它是"降低风险",不是"已验证修复"。

验收:test.ps1 9/9 全绿(7 与 5.1);Run-RealSmoke 4/4 全绿。
This commit is contained in:
Shuery committed 2026-09-27 09:22:45 +08:00
1 parent 8d67a38fb7
commit 10f97246c8
1 file changed
+38 -11
+38 -11
View File
@@ -43,6 +43,33 @@ Import-Module (Join-Path $RepoPath 'Common.psm1') -Force
$script:Passed = 0
$script:Failures = @()
function Invoke-NativeTolerant {
<#
.SYNOPSIS
跑一个原生命令,把 stdout 与 stderr 合并成字符串数组返回,不让 stderr 变成错误。
.DESCRIPTION
Windows PowerShell 5.1 在 $ErrorActionPreference = 'Stop' 下会把原生命令写到
stderr 的内容升级成终止性的 NativeCommandError(PowerShell 7 改了这条规矩)。
本脚本要调用 rmdir / takeown / icacls / scoop / code,其中 rmdir 与 takeown 在
"对象已经处理过"或"连接点已经悬空"时就会往 stderr 写字 —— 那些话不是错误:真正该
判断的是"删掉了没有",用 Test-Path 看。所以在进入原生命令时把 EAP 放到 Continue,
退出时还原。这也是 tests\BakNRet.Security.Tests.ps1 里对 takeown / icacls 用的同一招。
#>
param(
[Parameter(Mandatory = $true)][string]$FilePath,
[string[]]$ArgumentList = @()
)
$previous = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
return @(& $FilePath @ArgumentList 2>&1)
} finally {
$ErrorActionPreference = $previous
}
}
function Test-Scenario {
param([Parameter(Mandatory = $true)][string]$Name, [bool]$Ok, [string]$Detail = '')
if ($Ok) {
@@ -159,17 +186,17 @@ function Remove-TreeHard {
$links = @(Get-ChildItem -LiteralPath $Path -Recurse -Force -Directory -ErrorAction SilentlyContinue |
Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint })
foreach ($link in $links) {
& cmd.exe /c ('rmdir "{0}"' -f $link.FullName) 2>&1 | Out-Null
$null = Invoke-NativeTolerant -FilePath 'cmd.exe' -ArgumentList @('/c', ('rmdir "{0}"' -f $link.FullName))
Remove-BaknretJunction -Path $link.FullName
}
& cmd.exe /c ('rmdir /s /q "{0}"' -f $Path) 2>&1 | Out-Null
$null = Invoke-NativeTolerant -FilePath 'cmd.exe' -ArgumentList @('/c', ('rmdir /s /q "{0}"' -f $Path))
if (Test-Path -LiteralPath $Path) {
# 只处理这一条路径:拿回属主 → 换成继承来的默认 ACL → 再删
& takeown.exe /F $Path /R /D Y 2>&1 | Out-Null
& icacls.exe $Path /reset /T /C /Q 2>&1 | Out-Null
& cmd.exe /c ('rmdir /s /q "{0}"' -f $Path) 2>&1 | Out-Null
$null = Invoke-NativeTolerant -FilePath 'takeown.exe' -ArgumentList @('/F', $Path, '/R', '/D', 'Y')
$null = Invoke-NativeTolerant -FilePath 'icacls.exe' -ArgumentList @($Path, '/reset', '/T', '/C', '/Q')
$null = Invoke-NativeTolerant -FilePath 'cmd.exe' -ArgumentList @('/c', ('rmdir /s /q "{0}"' -f $Path))
Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction SilentlyContinue
}
}
@@ -246,21 +273,21 @@ if (-not $SkipScoop) {
# 这台 VM 出厂不带 git,所以 main bucket 只能先用 zip 兜底进来,git 再由此装上。
if ((Test-Path -LiteralPath $scoopCmd) -and -not (Test-Path -LiteralPath (Join-Path $scoopRoot 'shims\git.exe'))) {
Write-Host '[A] scoop install git(后面 bucket add 要靠它)' -ForegroundColor Yellow
& $scoopCmd install git 2>&1 | ForEach-Object { ' ' + $_ }
Invoke-NativeTolerant -FilePath $scoopCmd -ArgumentList @('install', 'git') | ForEach-Object { ' ' + $_ }
}
# vscode 在 extras bucket,不在 main 里
if ((Test-Path -LiteralPath $scoopCmd) -and -not (Test-Path -LiteralPath (Join-Path $scoopRoot 'buckets\extras'))) {
Write-Host '[A] scoop bucket add extras' -ForegroundColor Yellow
& $scoopCmd bucket add extras 2>&1 | ForEach-Object { ' ' + $_ }
Invoke-NativeTolerant -FilePath $scoopCmd -ArgumentList @('bucket', 'add', 'extras') | ForEach-Object { ' ' + $_ }
}
if ((Test-Path -LiteralPath $scoopCmd) -and -not (Test-Path -LiteralPath $vscodeCli)) {
Write-Host '[A] scoop install vscode(从 extras 下载几百 MB,慢是正常的)' -ForegroundColor Yellow
& $scoopCmd install vscode 2>&1 | ForEach-Object { ' ' + $_ }
Invoke-NativeTolerant -FilePath $scoopCmd -ArgumentList @('install', 'vscode') | ForEach-Object { ' ' + $_ }
if (-not (Test-Path -LiteralPath $vscodeCli)) {
Write-Host '[A] 第一次没装上,重试一次(下载超时是常见原因)' -ForegroundColor Yellow
& $scoopCmd install vscode 2>&1 | ForEach-Object { ' ' + $_ }
Invoke-NativeTolerant -FilePath $scoopCmd -ArgumentList @('install', 'vscode') | ForEach-Object { ' ' + $_ }
}
}
}
@@ -282,7 +309,7 @@ if ($vscodeReady) {
$probe = 'baknret-' + [guid]::NewGuid().ToString('N').Substring(0, 8)
$settingsPath = $null
if ($vscodeReady) {
$versionText = (& $codeCmd --version 2>&1 | Out-String).Trim()
$versionText = (Invoke-NativeTolerant -FilePath $codeCmd -ArgumentList @('--version') | Out-String).Trim()
Test-Scenario 'A: vscode CLI 可执行(--version)' ($LASTEXITCODE -eq 0) ($versionText -split "`n" | Select-Object -First 1)
# scoop 的 vscode 清单带 persist:data,用户数据落在 persist 里(portable 模式);
@@ -401,7 +428,7 @@ Test-Scenario '恢复日志里出现安全描述符回放' ($restore.Output -mat
Write-Host ''
Write-Host '--- A 断言 ---' -ForegroundColor Cyan
if ($vscodeReady) {
$versionText = (& $codeCmd --version 2>&1 | Out-String).Trim()
$versionText = (Invoke-NativeTolerant -FilePath $codeCmd -ArgumentList @('--version') | Out-String).Trim()
Test-Scenario 'A: 恢复后 vscode CLI 仍可执行' ($LASTEXITCODE -eq 0) ($versionText -split "`n" | Select-Object -First 1)
$settingsOk = $false