这一轮做的是交付一致性检查:把这一路改掉/移走/删掉的每个符号在全仓(含文档)扫一遍。42 处命中里 38 处是正当的 —— CHANGELOG 与 ADR 里讲“原来是什么”属于历史叙述,test 里的 Assert-FileExists 是因为我只抑制了误判而没有改名。剩下的 4 处是真陈旧: * README 还在配置表里写着 CatalogMaxDepth,而那个配置项上一轮已经整条移除; * Get-BakNRetItemArchiveName 的 param 里还留着一个内联的 [int]$MaxDepth = 5(上一轮的删除按行匹配,没覆盖到这种写在同一行的参数),它已经没有任何调用方传值; * tools\lab\README.md 与 docs\agents\domain.md 还写着模块叫 Common.psm1。 这正是这次改造从头到尾在抓的毛病:文档承诺的东西,代码已经不做了。区别是多了一个可执行的检查 —— 符号改名/删除之后,全仓扫一遍旧名字。 验收:test.ps1 9/9 全绿(7 与 5.1)、真实清单只读冒烟 4/4。
14 KiB
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(方便对拍)。
踩过的坑(照抄会踩)
SoftwareCatalog的相对路径是按仓库根解析的,不是按配置文件所在目录;而且路径 不存在时会静默回退到仓库真实的SoftwareCatalog.psd1。沙盒配置里必须写成仓库根 相对路径(tools\lab\payload\sandbox\SoftwareCatalog.psd1),否则软件名条目会悄悄用错名录。- 子进程被重定向的 stdout 是控制台代码页(中文 Windows 上是 GBK/936),按 UTF-8 读会
整片乱码;
Lab.ps1因此按「替换字符更少」的候选解码。脚本自己写的logs\backup\backup-*.log反而是 UTF-8。 ConvertFrom-Json把 JSON 数组当作一个对象写出,@(...)会套成嵌套数组;数组参数 经Invoke-Command -ArgumentList传到 VM 里再交给Start-Process -ArgumentList会报 「无法转换为 System.String」。Lab.ps1用 JSON 传参 + 显式枚举摊平。- Hyper-V 对新建 VM 默认开自动检查点,会不断堆叠差异盘。
New-BakNRetLab.ps1已关掉 (AutomaticCheckpointsEnabled = $false)。 - 中文 Windows 上集成服务名是本地的(「来宾服务接口」而不是
Guest Service Interface), 按名字启用会找不到;脚本改为「把所有未启用的集成服务启用」。 - 全新 Gen2 VM 的 NVRAM 是空的,固件会走 UEFI 回退路径
\EFI\Boot\bootx64.efi。bcdboot /f UEFI通常会写它;没写时脚本会从bootmgfw.efi补一份。 New-Partition -Size建出来的是普通数据分区,不是 ESP;要按 UEFI 规范用-GptType '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}'建,事后再用Set-Partition -GptType改类型可能被拒(尤其打错分区号时)。Initialize-Disk还会自带一个 MSR 分区。- exFAT 卷上写不了硬链接:DSH 的 write 工具用「临时目录 + 硬链接」做原子落盘,在 exFAT 上会
直接失败(EISDIR)。仓库已于 2026-09-26 迁到 NTFS(
D:\Workspace\Temp\BakNRet),不再受影响; 但 U 盘上的其它数据仍受此限制 —— 改那里的文件要么用 shell 重定向,要么先写 NTFS 再拷。 - VM 是未激活的 Windows:会有水印,个性化受限,功能测试不受影响。
- PowerShell Direct 的默认端点是 Windows PowerShell 5.1(不是 7)。要在 VM 里跑 7 的代码
必须显式
Start-Process pwsh.exe(Lab.ps1就是这么做的)。5.1 还读不了仓库里无 BOM 的 UTF-8 脚本(见下「已知问题」),Import-Module C:\BakNRet\BakNRet\BakNRet.psd1会报一串「缺少右 }」。
已知问题与规避
在 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-BakNRetLog 走 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 运行套件,不需要改动仓库里的测试代码。
若要在仓库里根治(三选一):
- 生成的
.cmd里先chcp 65001 >nul; - 子进程改成
pwsh -Command "[Console]::OutputEncoding=[Text.Encoding]::UTF8; & '<脚本>' <参数>"; - 读回时按控制台代码页解码,而不是写死
-Encoding UTF8。
名录改了、源没变时:归档与 manifest 会不一致
实测路径(在 VM 里真实撞到过):
- 名录里某个条目的 Slot 定义变了(当时是把沙盒名录的路径修对之后);
- 源目录一个字节没动;
- 下一次
Backup.ps1按「源未更新」跳过该条目 —— 归档保持旧内容; - 但 manifest 的
roots/layouts是按当前名录重新算的,于是它描述的内容比归档里实际有的多; - 恢复时才炸:
归档 AppFileSlot.7z 里既没有 'Profile',也没有旧布局的 '0'。
报错是清楚的(不是静默错误),修复办法就是重打一次:Lab.ps1 backup -Force
(实测重打后 Lab.ps1 restore 立刻变成 6/6 逐字节对拍通过)。
如果希望产品层面自动发现,可以在「源未更新」的判断里带上「本次解析出的 roots/layouts 是否与 manifest 记录的一致」,不一致就不要跳过。### 仓库里的 PowerShell 文件是「UTF-8 无 BOM」
Backup.ps1 / BakNRet\BakNRet.psd1 等都没有 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 下,便于事后排查;确认不要了再手工删该目录