Files
BakNRet/tools/lab/Lab-Common.ps1
T
Shuery 187d2759fd style: 按微软规范落地静态分析,并全仓机械重排
三件事:

1) tools\Install-TestDependencies.ps1 现在也把 PSScriptAnalyzer 装进仓库内的 .tools\modules
(不动机器上的全局模块,与 Pester 同一策略)。

2) PSScriptAnalyzerSettings.psd1:这是必要的,不是装饰 —— 那 6 条格式规则
(括号、缩进、空格、对齐、大小写)默认全是 Disabled,所以不带 -Settings 的
`Invoke-ScriptAnalyzer -Severity Warning,Error` 会**静默漏掉全部排版问题**。本文件用 Rules
把它们打开(而不是用 IncludeRules 换一套),于是默认规则与格式规则同时生效。
三条有意的排除都写明了理由:PSAvoidUsingWriteHost(彩色控制台输出是这份工具的刻意设计)、
PSUseShouldProcessForStateChangingFunctions(WhatIf 的边界在入口脚本,给库里 27 个改状态的
函数都加上反而会"静默跳过",备份看着成功却什么都没做)、PSAvoidUsingPlainTextForPassword
(7z 只接受命令行口令,这是 7z 的限制,README 里写明了取舍)。

3) tools\Invoke-Analyzer.ps1:独立门禁(不塞进 Pester 用例 —— 套件跑一次二十多秒,
混进去会让"测试红了"这句话失去分辨力),路径过滤与验收门槛的 Encode/Parse 两层一致。

全仓重排结果:706 条告警 -> 67 条。修掉的 639 条全部是格式(闭括号 168、空格 80、
对齐 68、缩进 60、行长 229)。重排后 9/9 验收全绿、100 个文件两版解析零错、
276 个断言原样通过 —— 机械重排没有改变任何可观察行为。

如实说明两件事:

  * 行长上限设成 160,**不是**官方默认的 120。120 在本仓库意味着 270 处改动(主要是
    中文注释与测试夹具里的一行式目录),160 意味着 41 处。160 仍是"宽但可读",而理由是写在
    配置文件里的:这不是悄悄放宽,想收紧到 120 时那份清单就在分析器输出里。

  * 剩余 67 条里,41 条是上面那批行长,其余 26 条是分析器找出的真问题(未使用参数 6、
    空 catch 6、MD5 指纹 1、覆盖内置命令 1、switch 默认值 1 等)。其中
    Find-ChildDirectoryByName 的 MaxDepth 参数从未被使用 —— 也就是配置里的
    CatalogMaxDepth = 5 是假的,前缀补全实际只查 1 层。这条要改行为、且影响真实名录的解析
    结果,留给你拍板,不在本提交里动手。
2026-09-27 09:46:08 +08:00

186 lines
7.3 KiB
PowerShell
Raw 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)。
.DESCRIPTION
被 tools\lab\New-BakNRetLab.ps1(一次性搭建)与 tools\lab\Lab.ps1(日常使用)共同导入。
设计约定:
* 宿主机侧的一切状态(VHDX、日志、凭据、暂存包)都放在 $LabRoot 下,它刻意位于
**仓库之外**(默认 D:\VMs\BakNRet-Lab);测试用的归档、日志因此不会落进真实仓库,
真实仓库的 Backups\ 与 logs\ 在整套流程里只被读取、从不写入。
* VM 内的仓库副本在 C:\BakNRet,工具负载在 C:\BakNRet-Lab。
* 与 VM 的一切交互走 PowerShell Direct(VMBus),不依赖网络、不共享宿主机目录。
#>
$script:LabConfig = [ordered]@{
VmName = 'BakNRet-Lab'
LabRoot = 'D:\VMs\BakNRet-Lab'
VhdxPath = 'D:\VMs\BakNRet-Lab\vhdx\BakNRet-Lab.vhdx'
VhdxSizeGB = 80
IsoPath = 'F:\Images\Windows\Win11_25H2_Chinese_Simplified_x64_v2.iso'
ImageIndex = 4 # Windows 11 专业版
SwitchName = 'Default Switch'
MemoryStartupGB = 8
CpuCount = 8
GuestRepoPath = 'C:\BakNRet'
GuestLabPath = 'C:\BakNRet-Lab'
GuestUser = 'lab'
CheckpointName = 'clean-baseline'
RepoRoot = (Split-Path -Parent (Split-Path -Parent $PSScriptRoot))
}
function Get-LabConfig { return $script:LabConfig }
function Get-LabPath {
<# .SYNOPSIS 取宿主机侧实验目录下的路径(自动建父目录)。 #>
param([Parameter(Mandatory)][string]$Relative)
$full = Join-Path $script:LabConfig.LabRoot $Relative
$parent = Split-Path -Parent $full
if ($parent -and -not (Test-Path -LiteralPath $parent)) { New-Item -ItemType Directory -Force -Path $parent | Out-Null }
return $full
}
function Write-LabLog {
<# .SYNOPSIS 统一日志:同时进控制台与 $LabRoot\logs\lab.log。 #>
param([Parameter(Mandatory)][string]$Message, [ValidateSet('INFO', 'WARN', 'ERROR', 'STEP')][string]$Level = 'INFO')
$line = "[{0}] [{1,-5}] {2}" -f (Get-Date).ToString('HH:mm:ss'), $Level, $Message
switch ($Level) {
'STEP' { Write-Host $line -ForegroundColor Cyan }
'WARN' { Write-Host $line -ForegroundColor Yellow }
'ERROR' { Write-Host $line -ForegroundColor Red }
default { Write-Host $line }
}
Add-Content -LiteralPath (Get-LabPath 'logs\lab.log') -Value $line -Encoding UTF8 -ErrorAction SilentlyContinue
}
function Test-LabElevated {
param()
return ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Assert-LabElevated {
<# .SYNOPSIS 需要管理员:非同权限时给出可直接复制的提权命令。 #>
param([Parameter(Mandatory)][string]$Why)
if (Test-LabElevated) { return }
$gsudo = (Get-Command gsudo -ErrorAction SilentlyContinue | Select-Object -First 1).Source
$self = $MyInvocation.PSCommandPath
$hint = if ($gsudo) { "`n $gsudo pwsh -NoProfile -File `"$self`" $($MyInvocation.Line)" } else { '' }
throw "需要管理员权限:$Why$hint"
}
function Get-LabCredentialPath { return (Join-Path $script:LabConfig.LabRoot 'state\credentials.json') }
function Save-LabCredential {
<# .SYNOPSIS 把 VM 内 lab 账户的口令写进宿主机侧的 credentials.json(在仓库之外)。 #>
param([Parameter(Mandatory)][string]$Password)
$path = Get-LabCredentialPath
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $path) | Out-Null
[ordered]@{
VmName = $script:LabConfig.VmName
User = $script:LabConfig.GuestUser
Password = $Password
SavedAt = (Get-Date).ToString('s')
} | ConvertTo-Json | Set-Content -LiteralPath $path -Encoding UTF8
return $path
}
function Get-LabCredential {
<# .SYNOPSIS 读出 VM 凭据为 PSCredential。 #>
param()
$path = Get-LabCredentialPath
if (-not (Test-Path -LiteralPath $path)) { throw "找不到 VM 凭据:$path(先跑 New-BakNRetLab.ps1)" }
$j = Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json
$sec = ConvertTo-SecureString $j.Password -AsPlainText -Force
return [pscredential]::new("$($j.User)", $sec)
}
function New-LabPassword {
<# .SYNOPSIS 生成只含字母数字的口令(避免 XML / 命令行转义问题)。 #>
param([int]$Length = 24)
$chars = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'
return -join (1..$Length | ForEach-Object { $chars[(Get-Random -Minimum 0 -Maximum $chars.Length)] })
}
function Get-LabVm {
param()
return Get-VM -Name $script:LabConfig.VmName -ErrorAction SilentlyContinue
}
function Wait-LabVMRunning {
<# .SYNOPSIS 等 VM 进入 Running。 #>
param([int]$TimeoutSeconds = 300)
$sw = [Diagnostics.Stopwatch]::StartNew()
while ($sw.Elapsed.TotalSeconds -lt $TimeoutSeconds) {
$vm = Get-LabVm
if ($vm -and $vm.State -eq 'Running') { return $true }
Start-Sleep -Seconds 3
}
return $false
}
function New-LabSession {
<# .SYNOPSIS 建立 PowerShell Direct 会话(VMBus,不经网络)。 #>
param([int]$RetrySeconds = 600)
$cred = Get-LabCredential
$sw = [Diagnostics.Stopwatch]::StartNew()
$lastError = $null
while ($sw.Elapsed.TotalSeconds -lt $RetrySeconds) {
try {
$s = New-PSSession -VMName $script:LabConfig.VmName -Credential $cred -ErrorAction Stop
Write-LabLog "PowerShell Direct 会话已建立(等待 $([math]::Round($sw.Elapsed.TotalSeconds)) 秒)"
return $s
}
catch {
$lastError = $_.Exception.Message
Start-Sleep -Seconds 5
}
}
throw "无法建立 PowerShell Direct 会话:$lastError"
}
function Invoke-LabCommand {
<# .SYNOPSIS 在 VM 里跑一段脚本并回传结果(自动建/收会话)。 #>
param(
[Parameter(Mandatory)][scriptblock]$ScriptBlock,
[object[]]$ArgumentList = @(),
[int]$RetrySeconds = 600
)
$s = New-LabSession -RetrySeconds $RetrySeconds
try {
return Invoke-Command -Session $s -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList -ErrorAction Stop
}
finally {
Remove-PSSession -Session $s -ErrorAction SilentlyContinue
}
}
function Copy-LabFileToGuest {
<# .SYNOPSIS 宿主机 -> VM 传文件(Copy-VMFile,需要 Guest Service Interface)。 #>
param(
[Parameter(Mandatory)][string]$SourcePath,
[Parameter(Mandatory)][string]$DestinationPath
)
Copy-VMFile -VMName $script:LabConfig.VmName -SourcePath $SourcePath `
-DestinationPath $DestinationPath -CreateFullPath -FileSource Host -Force
}
function Get-HostSevenZip {
<# .SYNOPSIS 宿主机 7z 路径(用来打包仓库快照)。 #>
param()
$c = Get-Command 7z -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $c) { throw '宿主机找不到 7z' }
return $c.Source
}
function Test-LabGuestReady {
<# .SYNOPSIS 判断 VM 内供给是否完成(provision.ok)。 #>
param()
try {
$r = Invoke-LabCommand -ScriptBlock { Test-Path 'C:\BakNRet-Lab\state\provision.ok' } -RetrySeconds 60
return [bool]$r
}
catch { return $false }
}