Files
BakNRet/tools/lab/New-BakNRetLab.ps1
Shuery 232a82cd3c style: 逐条修静态分析告警(706 → 44),并把 MaxDepth 这条假承诺删掉
修掉的:空 catch 5 处;名词白名单 7 处;default-value 开关、自带 -WhatIf、lab 的明文口令与 irm|iex 各挂抑制并写明理由。

MaxDepth:它是分析器拓出来的真 bug —— 参数声明了却从未使用,也就是配置里的 CatalogMaxDepth 是假的,前缀补全实际只查 1 层,而配置注释与 README 都承诺「向下找几层」。按确认过的原则处理:**先让文档不撒谎**,所以把整条链路去掉(配置默认值、三个函数的参数、70 处实参、配置注释),而不是留一个假旋钮。零行为变化。想真的支持多层补全时,那是一个独立决定。

剩下 3 条都是分析器的误判,而且我实测确认过其中一条:$sourcePath 被报「赋值后从未使用」,我照着改成 $null = 之后,Set-StrictMode -Version 3.0 下读未定义变量直接抛错,Security 套件的 BeforeAll 挂掉、4 条用例连带失败。恢复后才绿。

这一类误判有共同成因:静态分析看不到「在传给 Test-Case / It / Where-Object 的 scriptblock 里被使用」。所以我只对能证明是误判的挂抑制并写明理由,不为了数字好看去改代码。

验收:test.ps1 9/9 全绿(7 与 5.1)、100 个文件两版解析零错、Run-RealSmoke 4/4。
2026-09-27 10:16:10 +08:00

257 lines
13 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<#
.SYNOPSIS
从零搭出 BakNRet 的 Hyper-V 隔离测试 VM(真机级:真 NTFS、真 ACL、真连接点、真重启)。
.DESCRIPTION
全流程无人值守、不需要点任何安装向导,也不需要 VM 的图形界面:
1. disk —— 挂载 Windows ISO,建 80 GB 动态 VHDX,按 UEFI 规范 GPT 分区,
用 DISM 把 install.wim 的指定版本展开进去,注入 7-Zip / PowerShell 7 /
Pester / 供给脚本,写入 C:\Windows\Panther\unattend.xml,最后 bcdboot 写引导;
2. vm —— 建 Gen2 虚拟机(8 GB / 8 vCPU、Default Switch、"来宾服务接口"打开、
关闭安全启动以便离线注入的引导链可用),挂载系统盘并启动;
3. provision —— 等首次登录的供给脚本跑完,读回 VM 自报的真机事实,然后打检查点
clean-baseline(之后 Lab.ps1 -Verb reset 可秒回到干净状态)。
幂等:已存在的 VHDX / VM 会复用,除非显式加 -Recreate。
.PARAMETER ListImages
只打印 ISO 里的映像索引清单,不建任何东西。
.PARAMETER Stage
all(默认)/ disk / vm / provision,可单独重跑某一段排查问题。
.EXAMPLE
gsudo pwsh -NoProfile -File .\tools\lab\New-BakNRetLab.ps1 -ListImages
.EXAMPLE
gsudo pwsh -NoProfile -File .\tools\lab\New-BakNRetLab.ps1
#>
[CmdletBinding()]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '',
Justification = '误判:-Recreate 是在遍历 VHDX 的 scriptblock 里用的,静态分析看不到那层使用。')]
param(
[ValidateSet('all', 'disk', 'vm', 'provision')][string]$Stage = 'all',
[switch]$Recreate,
[switch]$ListImages,
[int]$ImageIndex = 0
)
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'Lab-Common.ps1')
$cfg = Get-LabConfig
if ($ImageIndex -gt 0) { $cfg.ImageIndex = $ImageIndex }
# ---------------------------------------------------------------------------
# ISO 与映像清单
# ---------------------------------------------------------------------------
function Get-IsoVolume {
$di = Get-DiskImage -ImagePath $cfg.IsoPath -ErrorAction SilentlyContinue
if (-not $di -or -not $di.Attached) { $di = Mount-DiskImage -ImagePath $cfg.IsoPath -PassThru }
Start-Sleep -Milliseconds 1200
return $di
}
function Get-ImageList {
param([Parameter(Mandatory)][string]$IsoLetter)
$wim = @('install.wim', 'install.esd') |
ForEach-Object { Join-Path "$IsoLetter`:\sources" $_ } |
Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
if (-not $wim) { throw "ISO ($IsoLetter`:) 里找不到 sources\install.wim|esd" }
$info = & dism.exe /English /Get-WimInfo /WimFile:"$wim" 2>&1
$list = @(); $cur = $null
foreach ($line in $info) {
if ($line -match '^Index\s*:\s*(\d+)') { if ($cur) { $list += $cur }; $cur = [ordered]@{ Index = [int]$Matches[1]; Name = ''; Size = '' } }
elseif ($cur -and $line -match '^Name\s*:\s*(.+?)\s*$') { $cur.Name = $Matches[1] }
elseif ($cur -and $line -match '^Size\s*:\s*(.+?)\s*$') { $cur.Size = $Matches[1] }
}
if ($cur) { $list += $cur }
return [pscustomobject]@{ WimPath = $wim; Images = $list }
}
if ($ListImages) {
Assert-LabElevated -Why '挂载 ISO 需要管理员'
$di = Get-IsoVolume
$letter = ($di | Get-Volume).DriveLetter
$il = Get-ImageList -IsoLetter $letter
Write-Host "映像文件:$($il.WimPath)" -ForegroundColor Cyan
$il.Images | ForEach-Object { " [{0}] {1} {2}" -f $_.Index, $_.Name, $_.Size }
return
}
# ---------------------------------------------------------------------------
# 1. 系统盘
# ---------------------------------------------------------------------------
function New-LabSystemDisk {
Assert-LabElevated -Why '创建/分区 VHDX 与 DISM 展开映像'
$espGuid = '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}'
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $cfg.VhdxPath) | Out-Null
if ((Test-Path -LiteralPath $cfg.VhdxPath) -and $Recreate) {
Write-LabLog "删除已有 VHDX:$($cfg.VhdxPath)" 'WARN'
$mounted = Get-VHD -Path $cfg.VhdxPath -ErrorAction SilentlyContinue
if ($mounted -and $mounted.Attached) { Dismount-VHD -Path $cfg.VhdxPath }
Remove-Item -LiteralPath $cfg.VhdxPath -Force
}
if (-not (Test-Path -LiteralPath $cfg.VhdxPath)) {
New-VHD -Path $cfg.VhdxPath -SizeBytes ($cfg.VhdxSizeGB * 1GB) -Dynamic | Out-Null
Write-LabLog "已创建动态 VHDX($($cfg.VhdxSizeGB) GB):$($cfg.VhdxPath)" 'STEP'
}
$vhd = Mount-VHD -Path $cfg.VhdxPath -Passthru
$disk = $vhd | Get-Disk
if ($disk.PartitionStyle -eq 'RAW') {
# Initialize-Disk 会顺手塞一个 MSR,先删掉,按 UEFI 规范自己建:ESP(300MB FAT32) + Windows(剩余 NTFS)
Initialize-Disk -Number $disk.Number -PartitionStyle GPT -Confirm:$false | Out-Null
Get-Partition -DiskNumber $disk.Number -ErrorAction SilentlyContinue |
Where-Object { $_.Type -eq 'Reserved' } | ForEach-Object { Remove-Partition -DiskNumber $_.DiskNumber -PartitionNumber $_.PartitionNumber -Confirm:$false }
$efi = New-Partition -DiskNumber $disk.Number -Size 300MB -GptType $espGuid -AssignDriveLetter
Format-Volume -Partition $efi -FileSystem FAT32 -NewFileSystemLabel 'System' -Confirm:$false -Force | Out-Null
$win = New-Partition -DiskNumber $disk.Number -UseMaximumSize -AssignDriveLetter
Format-Volume -Partition $win -FileSystem NTFS -NewFileSystemLabel 'Windows' -Confirm:$false -Force | Out-Null
Write-LabLog '已完成 GPT 分区(ESP 类型已按 EFI System Partition 建立)与格式化' 'STEP'
}
$efiPart = Get-Partition -DiskNumber $disk.Number | Where-Object GptType -EQ $espGuid
$winPart = Get-Partition -DiskNumber $disk.Number | Where-Object { $_.GptType -eq '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}' -and $_.Size -gt 1GB }
if (-not $efiPart -or -not $winPart) { throw '分区布局不符合预期(ESP / Windows 分区没找到)' }
$efiLetter = $efiPart.DriveLetter
$winLetter = $winPart.DriveLetter
if (-not $efiLetter) { Set-Partition -DiskNumber $disk.Number -PartitionNumber $efiPart.PartitionNumber -NewDriveLetter 'S'; $efiLetter = 'S' }
if (-not $winLetter) { Set-Partition -DiskNumber $disk.Number -PartitionNumber $winPart.PartitionNumber -NewDriveLetter 'W'; $winLetter = 'W' }
Write-LabLog "ESP = $efiLetter`:,Windows = $winLetter`:" 'STEP'
# ---- 展开映像 ----
if (-not (Test-Path -LiteralPath "$winLetter`:\Windows\System32\ntoskrnl.exe")) {
$di = Get-IsoVolume
$isoLetter = ($di | Get-Volume).DriveLetter
$il = Get-ImageList -IsoLetter $isoLetter
$pick = $il.Images | Where-Object Index -EQ $cfg.ImageIndex
if (-not $pick) { throw "ISO 里没有索引 $($cfg.ImageIndex);可用:$($il.Images.Index -join ', ')" }
Write-LabLog "展开映像 [$($pick.Index)] $($pick.Name) -> $winLetter`:(需要十几分钟)" 'STEP'
$scratch = Get-LabPath 'scratch'
$out = Get-LabPath 'logs\dism-apply.out'
$err = Get-LabPath 'logs\dism-apply.err'
$proc = Start-Process -FilePath 'dism.exe' -NoNewWindow -Wait -PassThru -RedirectStandardOutput $out -RedirectStandardError $err `
-ArgumentList @('/English', '/Apply-Image', "/ImageFile:$($il.WimPath)", "/Index:$($pick.Index)", "/ApplyDir:$winLetter`:\", "/ScratchDir:$scratch")
if ($proc.ExitCode -ne 0) { throw "DISM 展开失败,退出码 $($proc.ExitCode),见 $out / $err" }
Write-LabLog '映像展开完成' 'STEP'
}
else {
Write-LabLog '系统盘上已有 Windows,跳过展开' 'WARN'
}
# ---- 注入负载与无人值守应答文件 ----
Write-LabLog '注入 7-Zip / PowerShell 7 / Pester / 供给脚本' 'STEP'
$payloadSrc = Join-Path $PSScriptRoot 'payload'
$guestLab = Join-Path "$winLetter`:\" ($cfg.GuestLabPath.TrimStart('\'))
foreach ($item in '7zip', 'pwsh', 'Pester', 'provision.ps1') {
$src = Join-Path $payloadSrc $item
$dst = Join-Path $guestLab ('payload\' + $item)
if (Test-Path -LiteralPath $src) {
$null = robocopy $src $dst /MIR /NFL /NDL /NJH /NJS /NP /R:1 /W:1
}
else {
Write-LabLog "负载缺失(跳过):$src" 'WARN'
}
}
# 口令:随机生成,只留在宿主机 D:\VMs\BakNRet-Lab\state\credentials.json
$password = New-LabPassword
$credPath = Save-LabCredential -Password $password
Write-LabLog "已生成 VM 凭据($credPath)" 'STEP'
$unattendSrc = Get-Content -LiteralPath (Join-Path $payloadSrc 'unattend.xml') -Raw -Encoding UTF8
$unattendXml = $unattendSrc.Replace('__LABPASSWORD__', $password)
$panther = Join-Path "$winLetter`:\" 'Windows\Panther'
New-Item -ItemType Directory -Force -Path $panther | Out-Null
[System.IO.File]::WriteAllText((Join-Path $panther 'unattend.xml'), $unattendXml, [System.Text.UTF8Encoding]::new($true))
Write-LabLog "已写入 $panther\unattend.xml" 'STEP'
# ---- 引导 ----
Write-LabLog 'bcdboot 写 UEFI 引导' 'STEP'
& bcdboot.exe "$winLetter`:\Windows" /s "$efiLetter`:" /f UEFI | ForEach-Object { Write-LabLog " $_" }
if ($LASTEXITCODE -ne 0) { throw "bcdboot 失败,退出码 $LASTEXITCODE" }
$bootMgr = Join-Path "$efiLetter`:\" 'EFI\Microsoft\Boot\bootmgfw.efi'
if (-not (Test-Path -LiteralPath $bootMgr)) { throw "ESP 上没有 bootmgfw.efi:$bootMgr" }
Write-LabLog "引导文件就位:$bootMgr" 'STEP'
Dismount-VHD -Path $cfg.VhdxPath
Write-LabLog '系统盘已完成并卸载' 'STEP'
}
# ---------------------------------------------------------------------------
# 2. 虚拟机
# ---------------------------------------------------------------------------
function New-LabVM {
Assert-LabElevated -Why '创建/配置 Hyper-V 虚拟机'
$vm = Get-LabVm
if (-not $vm) {
Write-LabLog "创建虚拟机 $($cfg.VmName)(Gen2 / $($cfg.MemoryStartupGB) GB / $($cfg.CpuCount) vCPU)" 'STEP'
$vm = New-VM -Name $cfg.VmName -Generation 2 -MemoryStartupBytes ($cfg.MemoryStartupGB * 1GB) `
-VHDPath $cfg.VhdxPath -SwitchName $cfg.SwitchName
Set-VMProcessor -VMName $cfg.VmName -Count $cfg.CpuCount
Set-VMFirmware -VMName $cfg.VmName -EnableSecureBoot Off
Set-VM -Name $cfg.VmName -AutomaticStopAction TurnOff -AutomaticStartAction Nothing
# 集成服务名随系统语言变化(中文是「来宾服务接口」),按状态启用而不是按名字找
Get-VMIntegrationService -VMName $cfg.VmName | Where-Object { -not $_.Enabled } |
ForEach-Object { Enable-VMIntegrationService -VMName $cfg.VmName -Name $_.Name }
}
else {
Write-LabLog "虚拟机 $($cfg.VmName) 已存在(状态 $($vm.State))" 'WARN'
if (-not (Get-VMHardDiskDrive -VMName $cfg.VmName | Where-Object Path -EQ $cfg.VhdxPath)) {
Add-VMHardDiskDrive -VMName $cfg.VmName -Path $cfg.VhdxPath
}
}
$vm = Get-LabVm
if ($vm.State -ne 'Running') {
Write-LabLog '启动虚拟机' 'STEP'
Start-VM -Name $cfg.VmName
if (-not (Wait-LabVMRunning -TimeoutSeconds 180)) { throw '虚拟机没有进入 Running' }
}
Write-LabLog "虚拟机状态:$((Get-LabVm).State)" 'STEP'
}
# ---------------------------------------------------------------------------
# 3. 供给与检查点
# ---------------------------------------------------------------------------
function Wait-LabProvision {
Assert-LabElevated -Why 'PowerShell Direct 需要管理员'
Write-LabLog '等待 VM 内供给脚本完成(首次启动要几分钟)' 'STEP'
$sw = [Diagnostics.Stopwatch]::StartNew()
while ($sw.Elapsed.TotalMinutes -lt 30) {
if (Test-LabGuestReady) {
Write-LabLog "供给完成,耗时 $([math]::Round($sw.Elapsed.TotalMinutes,1)) 分钟" 'STEP'
$facts = Invoke-LabCommand -ScriptBlock { Get-Content -Encoding UTF8 'C:\BakNRet-Lab\state\provisioned.json' -Raw }
Write-Host $facts
return
}
Start-Sleep -Seconds 15
}
throw '等待供给超时(30 分钟);用 Lab.ps1 -Verb console 到 VM 里看一眼 provision.log'
}
function New-LabCheckpoint {
Assert-LabElevated -Why '创建 Hyper-V 检查点'
$existing = Get-VMSnapshot -VMName $cfg.VmName -ErrorAction SilentlyContinue | Where-Object Name -EQ $cfg.CheckpointName
if ($existing) { Write-LabLog "检查点 $($cfg.CheckpointName) 已存在,跳过" 'WARN'; return }
Checkpoint-VM -Name $cfg.VmName -SnapshotName $cfg.CheckpointName
Write-LabLog "已创建检查点 $($cfg.CheckpointName)(Lab.ps1 -Verb reset 可回到此状态)" 'STEP'
}
# ---------------------------------------------------------------------------
# 主流程
# ---------------------------------------------------------------------------
if ($Stage -in @('all', 'disk')) { New-LabSystemDisk }
if ($Stage -in @('all', 'vm')) { New-LabVM }
if ($Stage -in @('all', 'provision')) { Wait-LabProvision; New-LabCheckpoint }
Write-LabLog '搭建流程结束' 'STEP'