aps-agent/docs/development/nsis-smoke.md

124 lines
10 KiB
Markdown
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.

# NSIS 安装包临时目录安装冒烟
> round-44 并行 sub agent 方向 GG(矩阵 99 / 118 本地衔接部分)。
> 执行日期:2026-08-02;环境:Windows(PowerShell),Node v22.13.0。
## 1. 验收矩阵与本方向范围
| 矩阵行 | 条目 | 本地可测部分 | 剩余(外部项) |
|--------|------|--------------|----------------|
| 99 | Defender 和真实 APS 求解通过 | 衔接 round-43 CC:Defender 静态扫描 4/4 clean(见 packaging-smoke.md);真实 APS 求解由四引擎黄金测试覆盖(既有体系,非本方向交付) | Defender for Endpoint 门户/API 门禁;实时防护真实拦截演练(需受控样本与策略配合) |
| 118 | 干净离线机安装冒烟 | 本方向补"真装冒烟":NSIS 静默安装到随机临时目录 + 布局/内容检查 + 卸载清理(本地实测 2 次 PASS,见 §6) | 真实用户机安装(含 %LOCALAPPDATA%\Programs 落点、开始菜单/桌面快捷方式体验);干净离线机实机安装 + 冷启动 + 升级/回滚验收,需外部验证机 |
硬性纪律遵守情况:未修改 server/**、apps/desktop/**、packaging/**(仅只读引用);未 commit/push/merge;未重启/占用 8003/5173。
## 2. 涉及文件
新增(本方向写范围):
- `scripts/nsis-install-smoke.mjs` —— NSIS 临时目录安装冒烟脚本
- `tests/node/nsis-smoke.test.mjs` —— 可测逻辑的 node 单测
- `docs/development/nsis-smoke.md` —— 本文档
只读引用:
- `scripts/offline-install-check.mjs` —— 复用 `sha256File` / `readDesktopConfig` / `resolveInstallerName` / `formatBytes`(round-43 CC)
- `build/offline-check/offline-install-manifest.json` —— 期望 sha256 来源(nsis-installer 条目)
- `apps/desktop/updater.cjs` —— `nsisInstall` 参数约定(/S 静默;/D= 安装目录须为最后参数、不带引号),本轮真装实测验证该约定可行
- `apps/desktop/release/aps-agent-desktop-0.1.0.exe` —— 本轮实测的 NSIS 安装包(163.1 MB,sha256 `044c4a83aaf28476a1e2405a7a08185b5aff0dfab27f0299b8bcf16c8d32bf21`)
## 3. 脚本原理与用法
原理:在系统临时目录下 `fs.mkdtemp` 随机目录,用 NSIS 参数 `/S /D=<临时目录>` 静默安装(/D 是最后参数、不带引号),校验安装后布局(主 exe + resources/app.asar + resources/sidecar/aps-sidecar.exe + 卸载器),可选启动主 exe 数秒做健康冒烟,最后卸载/删除临时目录。全程不触碰真实安装位置。
用法:
node scripts/nsis-install-smoke.mjs [installer.exe] [options]
node scripts/nsis-install-smoke.mjs --installer <path> --run-uninstaller
node scripts/nsis-install-smoke.mjs --self-test
| 选项 | 说明 |
|------|------|
| 位置参数 / `--installer <path>` | 显式指定安装包路径(显式优先,不再自动发现) |
| `--expected-sha256 <hex>` | 期望 sha256(覆盖清单) |
| `--manifest <path>` | 期望 sha256 清单(默认 `build/offline-check/offline-install-manifest.json`) |
| `--timeout <ms>` | 安装器超时(默认 600000 = 10 min) |
| `--run-uninstaller` | 用安装产物自带的卸载器(`Uninstall <productName>.exe /S`)卸载后再清理;默认直接删除临时目录 |
| `--launch` / `--no-launch` | 启动主 exe 数秒健康冒烟(默认**不启动**,`--launch-seconds` 默认 8s,启动后强制 taskkill /T /F 树杀) |
| `--temp-root <dir>` | 临时根(默认 `os.tmpdir()`);非 ASCII %TEMP% 环境建议显式 ASCII 目录 |
| `--keep-temp-dir` | 保留临时安装目录供人工检查(打印路径,不自动删除) |
| `--top-n <n>` | 内容清单打印条数(默认 10) |
| `--json` | 机器可读 JSON 报告 |
| `--self-test` | 内部逻辑冒烟(不安装、不清理) |
产物定位顺序:显式参数 → 清单 nsis-installer 条目 → `apps/desktop/release/<artifactName>` → `build/offline-check/<artifactName>` → release 目录下 ≥50MB 的 exe。缺产物时明确报错。
退出码:`0` = PASS(校验、安装、布局、清理全部通过);`1` = FAIL(sha256 不匹配 / 安装失败 / 布局无效 / 清理失败 / 非 Windows);`2` = NO_INSTALLER(找不到安装包)。
## 4. 安全边界与取舍
- **绝不写 %LOCALAPPDATA%\Programs 或系统目录**:安装目录 = `fs.mkdtemp` 于系统临时目录,`/D=` 把 NSIS 指到那里;脚本自身不写注册表、不写开始菜单/桌面快捷方式。
- **安装器自身的副作用**:electron-builder NSIS 安装器(即便 `/S` 且 `/D=` 指向临时目录)会为本次临时安装写 per-user 卸载注册表项 + 开始菜单快捷方式(指向临时目录)。这是安装器自带逻辑,不是本脚本的行为。
- `--run-uninstaller`:运行卸载器,删除文件并**清掉这些注册表项与快捷方式**(实测卸载后 HKCU 无 aps 卸载键、开始菜单无残留快捷方式)。
- 默认直接删除临时目录:不执行卸载器,安装器自己的注册表项/快捷方式会残留并指向已删除路径 —— 取舍说明:CI 沙箱或不想跑卸载器的场景选默认;想彻底干净选 `--run-uninstaller`。
- **删除前路径校验**(Windows 安全规则):`assertSafeTempDir()` 先 `path.resolve`,确认目标位于系统临时目录下、不是临时目录根、basename 以 `aps-nsis-smoke-` 开头,才允许 `fs.rmSync(recursive)`;守卫逻辑有单测覆盖。
- **不驻留进程**:默认不启动主 exe;`--launch` 启动后强制 `taskkill /T /F` 杀进程树,并把 `--user-data-dir` 指到临时安装目录内(应用用户数据不出沙箱)。卸载/删除前还会用 `Get-CimInstance Win32_Process` 枚举可执行路径在临时目录下的残留进程并杀掉(防安装器 runAfterFinish 自动拉起残留)。
- **卸载器异步收尾**:卸载器进程退出(exit 0)后文件删除是异步的,脚本会轮询等待目录消失(最长 25s),仍未消失才回退直接删除(重试 8 次、最长约 17s)。
## 5. 测试
- 脚本自检:`node scripts/nsis-install-smoke.mjs --self-test` → 14/14 PASS(参数解析、路径守卫、布局检测、产物定位、清单哈希、内容清单、卸载器定位)。
- node 单测:`node --test tests/node/nsis-smoke.test.mjs` → 13/13 PASS(复用 packaging-smoke.test.mjs 的 import 风格;覆盖路径安全守卫、布局校验、sha256 确定性、清单期望哈希、产物定位、内容清单排序;全部在临时目录内,不触发真安装)。
- 真实安装路径不在单测内(避免每轮跑 1 分钟+写盘),由 §6 本地实测覆盖。
## 6. 本地实测记录(2026-08-02)
产物:`apps/desktop/release/aps-agent-desktop-0.1.0.exe`(171,025,378 B = 163.1 MB,sha256 `044c4a83aaf28476a1e2405a7a08185b5aff0dfab27f0299b8bcf16c8d32bf21`,与 build/offline-check 清单一致)。
**运行 1:安装 + 布局/内容检查 + 卸载器清理(--run-uninstaller)→ PASS**
node scripts/nsis-install-smoke.mjs --installer apps/desktop/release/aps-agent-desktop-0.1.0.exe --run-uninstaller
- 校验:sha256 PASS(与清单 044c4a83…d32bf21 一致)
- 安装:exit 0,耗时 **33.6s**(163MB → 解包 567MB,Defender 实时防护按需扫描下)
- 布局:PASS(mainExe / resources / app.asar / sidecar / Uninstall 工业智核 APS.exe 全部存在)
- 内容:1158 个文件 / 567.1 MB,前 10 项:
- 215.2 MB `工业智核 APS.exe`
- 32.9 MB `resources/sidecar/_internal/ortools/.libs/ortools.dll`
- 24.4 MB `dxcompiler.dll`
- 19.5 MB `resources/sidecar/_internal/numpy.libs/libscipy_openblas64_-b788215d9d47792bcba3a2e2a7114320.dll`
- 19.4 MB `LICENSES.chromium.html`
- 17.5 MB `resources/sidecar/aps-sidecar.exe`
- 12.9 MB `resources/sidecar/_internal/ortools/.libs/libprotobuf.dll`
- 12.5 MB `resources/sidecar/_internal/libprotobuf.dll`
- 12.2 MB `resources/sidecar/_internal/ortools/.libs/libscip.dll`
- 10.4 MB `icudtl.dat`
- 清理:卸载器 exit 0,临时目录 removed=true
- 机器状态复查:%LOCALAPPDATA%\Programs 下无安装;无残留 aps-nsis-smoke-* 临时目录;无残留进程;HKCU 卸载注册表无 aps 键;开始菜单无残留快捷方式
**运行 2:+ --launch(启动主 exe 8s 健康冒烟 + 树杀 + 卸载清理)→ PASS**
node scripts/nsis-install-smoke.mjs --installer apps/desktop/release/aps-agent-desktop-0.1.0.exe --run-uninstaller --launch --launch-seconds 8
- 安装:exit 0,耗时 **30.9s**;布局/内容与运行 1 一致(PASS)
- 启动健康冒烟:主 exe 存活 9.1s(≥8s 窗口),进程树强制结束(killed=1),无驻留
- 清理:卸载器 exit 0,removed=true;机器状态复查同样干净
**过程中发现并修复**:首次实跑时卸载器 exit 0 后目录仍被短暂占用(EBUSY,卸载器异步收尾删除文件),清理判定为 FAIL;已加"轮询等待目录消失(最长 25s)+ 直接删除重试 8 次"后复跑 PASS。EBUSY 属卸载器自身时序,非安装缺陷。
## 7. 矩阵达成情况与剩余风险
矩阵 99(Defender + 真实 APS 求解):
- 本方向为衔接:Defender 静态扫描已在 round-43 CC 达成(4/4 clean);真实求解由黄金测试覆盖(既有体系)。
- 剩余(外部):Defender for Endpoint 门户/API 门禁、CI 门禁;实时防护真实拦截演练(本机实时防护已开启,但真实拦截需受控样本与策略配合)。
矩阵 118(干净离线机安装冒烟):
- 本方向达成:**真实 NSIS 静默安装到随机临时目录 + 布局/内容检查 + 卸载/清理** 本地 2 次 PASS;同时验证了 updater.cjs `nsisInstall` 的 `/S` + `/D=` 参数约定在真实安装包上可行(之前只做单元/校验和,未实跑)。
- 剩余(外部):真实用户机安装(%LOCALAPPDATA%\Programs 落点、快捷方式、升级/回滚、冷启动);干净离线机(无 Python、无网络)实机安装 + 冷启动 + 升级/回滚验收,需外部验证机,可用 §4 的 portable 流程随 U 盘执行。
已知限制:
- 安装器自身的 per-user 卸载注册表项与开始菜单快捷方式只有 `--run-uninstaller` 才会清掉;默认直接删除会残留指向已删路径的条目。
- 本机非管理员,未测 perMachine(HKLM / 系统级)安装路径。
- Defender 实时防护按需扫描会让安装耗时波动(实测 30.9s / 33.6s 两档);本次安装包静态扫描已在 CC 轮次 clean。
- `--launch` 健康冒烟仅验证进程存活数秒(不驻留),完整渲染器/Sidecar 校验归 `scripts/smoke-packaged-desktop.mjs`。