Files
BakNRet/tools/lab
Shuery e10503be76 fix: 让 5.1 真正可用(显式编码 + 原生 stderr 处理 + .psd1 夹具带 BOM)
上一提交让 5.1 能解析源码,但 Unit 与 Smoke 在 5.1 上仍然是红的。根因是三类彼此
无关的 5.1/7 行为差,全部实测确认:

1) 不写 -Encoding 时,5.1 的 Get-Content / Set-Content 默认是 ANSI,7 是 UTF-8。
   症状是 UTF-8 字节被按 GBK 解出「璇存槑」这类乱码。62 处补上显式 -Encoding UTF8。
   用 AST 而不是正则定位,避免把注释里的散文也改掉。

2) .psd1 夹具用无 BOM 写,而引擎的 .psd1 读取器(Import-PowerShellDataFile)只能靠
   BOM 判断编码、没有参数可传,于是 5.1 按 ANSI 解。40 处夹具改为带 BOM 写 —— 这正是
   .editorconfig 里 [*.psd1] charset = utf-8-bom 本来就要求的,是夹具违反了自己的约定。
   .cmd 批次文件刻意保持无 BOM:cmd.exe 会被 BOM 弄坏。

3) 5.1 在 $ErrorActionPreference = Stop 下会把原生命令写到 stderr 的内容升级成终止性
   NativeCommandError,7 改了这条。takeown/icacls 的 ACL 复位调用、以及 test.ps1 自己
   调子进程的地方,都需要在 Continue 下跑。

验收:test.ps1 9/9 全绿(Encode + Parse + Unit + Smoke + E2E,在 7 与 5.1 上各跑一遍)。

已知未处理(留待后续提交):tools/lab/** 里还有若干「原生命令 + 2>&1 + Stop」的同类
写法(takeown / icacls / scoop / code / Get-WimInfo)。它们要 Hyper-V 实验机才跑得到,
不在验收门槛内。
2026-09-26 22:16:04 +08:00
..
2026-09-26 21:46:55 +08:00
2026-09-26 21:46:55 +08:00

tools\lab —— BakNRet 的隔离测试环境(Hyper-V 真机级 VM)

在宿主机之外的 Windows 虚拟机里跑 BakNRet 的备份 / 恢复 / 测试。宿主机仓库、Backups\、 logs\ 在本环境里只被读取,从不写入;VM 内也没有挂载宿主机的任何目录(一切交互走 PowerShell Direct,也就是 VMBus,不需要网络共享)。

宿主机                                    隔离 VM(BakNRet-Lab)
──────────────────────────────            ─────────────────────────────────────────
D:\Workspace\Temp\BakNRet  ← 仓库(只读)  ──sync──▶  C:\BakNRet   仓库副本(每次覆盖)
D:\VMs\BakNRet-Lab                        C:\BakNRet-Lab      工具负载 + 沙盒 + 日志
  ├─ vhdx\BakNRet-Lab.vhdx  系统盘          ├─ payload\        7-Zip 26.03 / pwsh 7 / Pester 5.9.1
  ├─ state\credentials.json lab 口令        ├─ sources\        带刺的假数据(见下)
  ├─ logs\                  全流程日志      ├─ Backups\        沙盒归档 + manifest.json
  └─ stage\repo.zip         仓库快照        └─ logs\           脚本日志与重定向输出

为什么用它

真机语义是单元测试造不出来的。这套环境里能真正跑到:

形态 说明
真 NTFS 连接点(junction) sources\JunctionToData 指向 AppMultiSlot\Data;真实源目录里不该造这种东西,VM 内的沙盒源可以随便折腾
被占用文件 AppLocked\locked.bin 由后台进程持句柄,用来压「有文件没打进归档」的告警路径
长路径 / 深目录 10 层嵌套、112 字符路径
中文 + 空格 + 点的路径 sources\软件 目录.甲,归档名同样是中文
多 Slot / 单文件 Slot 一个软件多个 Slot(<Slot>\<内容>)与文件 Slot(包内是名为 Slot 的文件)
排除与追加 :- 的 Slot 前缀形式与 ! 任意层级形式;:+ Modules:<路径> 追加映射
覆盖 Path 清单里的 :: <路径> 覆盖名录里故意写错的 Path
源不存在的条目 记 missing-source、退出码仍为 0
方向标记 行首 +(仅备份)与 -(仅恢复)
增量判断 第二次备份对未变更的源报「源目录未更新」并跳过,-Force 强制重打
计划任务 / 重启持久性 真机环境,可注册计划任务、可重启后继续验证

搭建

前提:Windows 10/11 专业版或更高(需要 Hyper-V)、管理员权限、一个 Windows 安装 ISO。 默认读 F:\Images\Windows\Win11_25H2_Chinese_Simplified_x64_v2.iso(可在 Lab-Common.ps1 的 $LabConfig 里改)。

# 0. 先看 ISO 里有哪些版本(记住要装的索引,默认 4 = 专业版)
gsudo pwsh -NoProfile -File .\tools\lab\New-BakNRetLab.ps1 -ListImages

# 1. 一次搭完:建 VHDX -> 分区 -> DISM 展开 -> 注入负载与无人值守文件 -> bcdboot
#              -> 建 VM -> 首启无人值守 -> 等供给完成 -> 打 clean-baseline 检查点
gsudo pwsh -NoProfile -File .\tools\lab\New-BakNRetLab.ps1

全程不需要点任何安装向导,也不需要 VM 的图形界面:Windows 是用 DISM 离线展开进 VHDX 的, 首次启动由 payload\unattend.xml(放进 C:\Windows\Panther\)无人值守走完 specialize + OOBE, 再由 payload\provision.ps1 把 7-Zip / PowerShell 7 / Pester 装好并写上 PATH。

分阶段重跑(排查用):-Stage disk / -Stage vm / -Stage provision。

VM 规格:Gen2、8 vCPU、12 GB 静态内存、Default Switch(NAT,可联网)、80 GB 动态 VHDX (实际占用约 15 GB,另有检查点差异盘)。lab 账户口令随机生成,只写在 D:\VMs\BakNRet-Lab\state\credentials.json(仓库之外),不进程版本库。

日常使用

gsudo pwsh -NoProfile -File .\tools\lab\Lab.ps1 status     # 一眼看状态
gsudo pwsh -NoProfile -File .\tools\lab\Lab.ps1 sync       # 把当前工作树推给 VM
gsudo pwsh -NoProfile -File .\tools\lab\Lab.ps1 seed -Force # 重建带刺假数据
gsudo pwsh -NoProfile -File .\tools\lab\Lab.ps1 backup     # VM 内真跑 Backup.ps1(沙盒清单+配置)
gsudo pwsh -NoProfile -File .\tools\lab\Lab.ps1 restore    # 用真实归档做恢复演练(逐字节对拍)
gsudo pwsh -NoProfile -File .\tools\lab\Lab.ps1 acl-test   # 安全描述符演练(scoop/vscode + ProgramData 属主)
gsudo pwsh -NoProfile -File .\tools\lab\Lab.ps1 acl-test -SkipScoop   # 只跑 ProgramData 那段(不下载 vscode)
gsudo pwsh -NoProfile -File .\tools\lab\Lab.ps1 test -Suite all   # 三套仓库自带测试
gsudo pwsh -NoProfile -File .\tools\lab\Lab.ps1 shell      # 进去自己敲(exit 出来)
gsudo pwsh -NoProfile -File .\tools\lab\Lab.ps1 reset      # 秒回 clean-baseline

动词一览:status / start / stop / wait / sync / seed / backup / restore / acl-test / test / shell / console / checkpoint / reset / destroy。

backup 支持 -DryRun / -Force / -AcceptWarnings;restore 支持 -Entries @('AppMultiSlot','C:\BakNRet-Lab\sources\AppBig') 指定条目;test 支持 -Suite pester|zero|e2e;acl-test 支持 -SkipScoop / -KeepWork。

acl-test:安全描述符演练(payload\run-acl-scenario.ps1)

两段,都在 VM 里真跑(不是模拟),宿主侧退出码由动词 throw 回传:

  • A. 用户级真实场景:默认方式装 scoop(提权会话按官方写法加 -RunAsAdmin,目录仍是 %USERPROFILE%\scoop)→ scoop install git → bucket add extras → scoop install vscode → 改 vscode 的 settings.json → 备份 → 删源 → 恢复 → 断言:CLI 仍可执行、改过的配置原样 读得回、数据目录可写、app/persist 的安全指纹与备份前一致。 两个实测坑写在脚本注释里:extras 的 vscode 清单没有 bin 条目(所以没有 shims\code.cmd, CLI 在 apps\vscode\current\bin\code.cmd);code --version 拉起的 Code.exe 会锁住文件, 删源前必须先清进程。
  • B. 权限现场:C:\ProgramData\baknret-acl-lab\data,属主设成 SYSTEM、DACL 是 protected 且只有 (A;OICIIO;GA;;;CO) + SYSTEM/Administrators/Users —— 就是 ProgramData 下那些目录的形态。备份 / 删源 / 恢复后断言:属主仍是 SYSTEM、CREATOR OWNER 的 inherit-only ACE 还在、逐对象安全指纹与备份前一致;外加一条负对照(只搬文件、不回放 安全描述符)证明属主会落到"跑脚本的账户"头上。

沙盒清单 / 名录 / 配置

三个文件都在 tools\lab\payload\sandbox\,随 sync 进 VM:

  • BackupList.txt —— 沙盒清单,覆盖上面表里的各种形态;
  • SoftwareCatalog.psd1 —— 软件名 → Slot 组,全部指向 C:\BakNRet-Lab\sources;
  • BackupConfig.psd1 —— 归档/日志/快照都落在 VM 内(C:\BakNRet-Lab\Backups), 压缩级别 1(跑得快),ComputeHash = $true(方便对拍)。

踩过的坑(照抄会踩)

  1. SoftwareCatalog 的相对路径是按仓库根解析的,不是按配置文件所在目录;而且路径 不存在时会静默回退到仓库真实的 SoftwareCatalog.psd1。沙盒配置里必须写成仓库根 相对路径(tools\lab\payload\sandbox\SoftwareCatalog.psd1),否则软件名条目会悄悄用错名录。
  2. 子进程被重定向的 stdout 是控制台代码页(中文 Windows 上是 GBK/936),按 UTF-8 读会 整片乱码;Lab.ps1 因此按「替换字符更少」的候选解码。脚本自己写的 logs\backup\backup-*.log 反而是 UTF-8。
  3. ConvertFrom-Json 把 JSON 数组当作一个对象写出,@(...) 会套成嵌套数组;数组参数 经 Invoke-Command -ArgumentList 传到 VM 里再交给 Start-Process -ArgumentList 会报 「无法转换为 System.String」。Lab.ps1 用 JSON 传参 + 显式枚举摊平。
  4. Hyper-V 对新建 VM 默认开自动检查点,会不断堆叠差异盘。New-BakNRetLab.ps1 已关掉 (AutomaticCheckpointsEnabled = $false)。
  5. 中文 Windows 上集成服务名是本地的(「来宾服务接口」而不是 Guest Service Interface), 按名字启用会找不到;脚本改为「把所有未启用的集成服务启用」。
  6. 全新 Gen2 VM 的 NVRAM 是空的,固件会走 UEFI 回退路径 \EFI\Boot\bootx64.efi。 bcdboot /f UEFI 通常会写它;没写时脚本会从 bootmgfw.efi 补一份。
  7. New-Partition -Size 建出来的是普通数据分区,不是 ESP;要按 UEFI 规范用 -GptType '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}' 建,事后再用 Set-Partition -GptType 改类型可能被拒(尤其打错分区号时)。Initialize-Disk 还会自带一个 MSR 分区。
  8. exFAT 卷上写不了硬链接:DSH 的 write 工具用「临时目录 + 硬链接」做原子落盘,在 exFAT 上会 直接失败(EISDIR)。仓库已于 2026-09-26 迁到 NTFS(D:\Workspace\Temp\BakNRet),不再受影响; 但 U 盘上的其它数据仍受此限制 —— 改那里的文件要么用 shell 重定向,要么先写 NTFS 再拷。
  9. VM 是未激活的 Windows:会有水印,个性化受限,功能测试不受影响。
  10. PowerShell Direct 的默认端点是 Windows PowerShell 5.1(不是 7)。要在 VM 里跑 7 的代码 必须显式 Start-Process pwsh.exe(Lab.ps1 就是这么做的)。5.1 还读不了仓库里无 BOM 的 UTF-8 脚本(见下「已知问题」),Import-Module C:\BakNRet\Common.psm1 会报一串「缺少右 }」。

已知问题与规避

在 VM 里直接跑 tests\Run-Pester.ps1 会红 8 项(都是中文断言)

tests\BakNRet*.Tests.ps1 里的 Invoke-BaknretScript 这样抓子进程输出:

cmd /c pwsh -File Backup.ps1 ... > out.txt 2>&1
Get-Content -LiteralPath out.txt -Encoding UTF8

而 Backup.ps1 / Restore.ps1 的 Write-Log 走 Write-Host,写进 out.txt 的字节编码取自 [Console]::OutputEncoding:

环境 [Console]::OutputEncoding 结果
宿主机(日常会话) utf-8 文件是 UTF-8,按 UTF-8 读回正确 → 150/150 绿
全新 Windows VM(中文系统) gb2312(936) 文件是 GBK 字节,按 UTF-8 读回得到替换字符 → 8 项中文断言失败

实测:VM 里直接跑是 142 通过 / 8 失败;把控制台输出编码先钉成 UTF-8 后是 150/150。

这是测试环境的编码假设问题,不是产品缺陷(产品行为在两边完全一致)。 Lab.ps1 test 因此会经 payload\run-suite-utf8.ps1 运行套件,不需要改动仓库里的测试代码。

若要在仓库里根治(三选一):

  1. 生成的 .cmd 里先 chcp 65001 >nul;
  2. 子进程改成 pwsh -Command "[Console]::OutputEncoding=[Text.Encoding]::UTF8; & '<脚本>' <参数>";
  3. 读回时按控制台代码页解码,而不是写死 -Encoding UTF8。

名录改了、源没变时:归档与 manifest 会不一致

实测路径(在 VM 里真实撞到过):

  1. 名录里某个条目的 Slot 定义变了(当时是把沙盒名录的路径修对之后);
  2. 源目录一个字节没动;
  3. 下一次 Backup.ps1 按「源未更新」跳过该条目 —— 归档保持旧内容;
  4. 但 manifest 的 roots / layouts 是按当前名录重新算的,于是它描述的内容比归档里实际有的多;
  5. 恢复时才炸:归档 AppFileSlot.7z 里既没有 'Profile',也没有旧布局的 '0'。

报错是清楚的(不是静默错误),修复办法就是重打一次:Lab.ps1 backup -Force (实测重打后 Lab.ps1 restore 立刻变成 6/6 逐字节对拍通过)。

如果希望产品层面自动发现,可以在「源未更新」的判断里带上「本次解析出的 roots/layouts 是否与 manifest 记录的一致」,不一致就不要跳过。### 仓库里的 PowerShell 文件是「UTF-8 无 BOM」

Backup.ps1 / Common.psm1 等都没有 BOM(开头字节是 3C 23 0A = <# + 换行)。 PowerShell 7 默认按 UTF-8 读,没问题;Windows PowerShell 5.1 会把无 BOM 文件按 ANSI(GBK) 读, 中文注释会被拆出错字节,甚至报「语句块或类型定义中缺少右 }」这类假解析错误。 要么给这些文件加 BOM,要么在文档里明确只支持 PowerShell 7。

仓库位置(2026-09-26 已从 U 盘迁到 NTFS)

仓库原在 F:\Backup\BakNRet(exFAT 的 Ventoy U 盘),为了减少 U 盘读写、并且拿回 NTFS 的 ACL / 硬链接支持,已整体搬到 D:\Workspace\Temp\BakNRet(NTFS,561 个文件 / 7.45 GB, 搬迁后做了逐文件 SHA256 对拍,全部一致)。

对这套 lab 没有影响:Lab-Common.ps1 用 $PSScriptRoot 推导 RepoRoot, 搬迁后实测自动指向新路径,脚本无需改动。唯一仍在 U 盘上的是默认安装 ISO (F:\Images\Windows\...),只在重新 -Stage disk 时只读用一次;想彻底不读 U 盘, 把它复制一份到 D: 再改 Lab-Common.ps1 的 IsoPath 即可。

仓库在 NTFS 上还顺带修好了 git:原先 exFAT 不记录属主,git 报 dubious ownership 全部命令失败; 搬迁后 git status / git log 直接可用(不需要 safe.directory 白名单)。## 拆掉

gsudo pwsh -NoProfile -File .\tools\lab\Lab.ps1 destroy -Confirm   # 删 VM 与系统盘
# 日志、凭据、仓库快照留在 D:\VMs\BakNRet-Lab 下,便于事后排查;确认不要了再手工删该目录