Files
dsh_shenxian/dsh-server-docs/04-调整方案/79-插件启停的两处平台缺陷-uninstall缺await与无效pnpm-flag.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

79 lines
7.0 KiB
Markdown
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.
# 79 · 插件启停的两处平台缺陷:`uninstall` 缺 `await`(把整个 orchestrator 带崩)+ `pnpm remove` 用了无效 flag(2026-09-13 发现)
- 日期:2026-09-13
- 状态:✅ **已修复并部署**(2026-09-13 13:4x 逐条核实产物);仅剩**可选的**「加固 b 半应用态主动自愈」未做 —— 见 §七
- 触发:用户在 guest 的「功能管理」启用 `dsh-plugin-mcn-suite` → **安装失败**(2026-09-13 07:4x)
- 影响:**P0** —— 任一次插件启停失败都可能**终止整个 `dshs` 进程**(全体用户瞬断 + 所有实例被停),并把目标 profile 留在**半应用态**(该实例此后每次启动即崩)
> **TL;DR**:D1(缺 `await`)→ 未捕获 Promise 拒绝 → orchestrator 退出;D2(无效 flag)→ 让 D1 必然被触发;D3(内存不够)→ 让流程必然走进触发 D1 的分支。
## 一、现象链(journalctl 原文,逐条可核)
| 时刻 | 事件 |
|---|---|
| 07:41–07:43 | guest 装上 mcn-suite 后反复 `[dsh-plugin-mcn] 榜单查询失败: unable to open database file` |
| 07:43:28/46/56 | 实例侧 `FATAL ERROR: Reached heap limit`(`Mark-Compact 155.3(169.1) -> 153.9 MB` 等)→ `exitCode 134`;`[crash-restart]` 触发 |
| 07:43:58 | `Error: Command failed: … setpriv … pnpm remove dsh-plugin-mcn-suite --ignore-workspace-root-check …`;stderr 解出 **`ERROR Unknown option: 'ignore-workspace-root-check'`**;栈 = `runPnpmAs(…:406) ← uninstall(…:575) ← business-plugins.js:614` |
| 07:43:58 | **`dshs.service: Main process exited, code=exited, status=1/FAILURE`** → systemd 4 秒后自动重启(07:44:02) |
| 07:45–07:46 | 服务重启后 guest 继续被拉起 → 每次都 OOM → attempt 1..5 → **`crash-loop-circuit-open`**(熔断);两个实例均 inactive |
## 二、三个缺陷
| # | 级别 | 位置 | 内容 |
|---|---|---|---|
| **D1** | **P0** | `src/web/routes/business-plugins.ts:682`(部署版 `business-plugins.js:614`) | 隔离定位循环里写的是 `try { uninstall(sel.id) } catch { /* 尽力而为 */ }` —— **缺 `await`**。`uninstall` 是 `async`,其 rejection 逃出**同步** `try/catch` ⇒ **unhandled rejection ⇒ 进程退出**(Node 默认 `--unhandled-rejections=throw`) |
| **D2** | P1 | 同文件 `:644`(部署版 `:575`) | `runPnpmAs(user.id, ['remove', id, '--ignore-workspace-root-check', '--reporter', 'silent'], dir)` —— 该 flag **只存在于 `add` 子命令**,`remove` 不认(pnpm 9.15.9 报 `Unknown option`,提示 `ignore-workspace`)⇒ **禁用路径必然失败**,进而**必然触发 D1** |
| **D3** | P1 | 容量 | 实例 V8 堆限 160 MiB 装不下 `dsh-univer-office` + `dsh-plugin-mcn-suite`(隔离 cgroup 实测分别 **+63.7 / +38.5 MiB rss**)⇒ 探活失败 → 平台进「隔离定位」分支 → **必然走到 D1/D2** |
## 三、连带后果(本身也是缺陷)
- 因进程直接退出,平台兜底的 `restoreProfile()`(在 `try/catch` 里)**没跑** ⇒ profile 停在**半应用态**(mcn-suite 已在 `dependencies` + `bundles`)⇒ 该实例此后**每次启动都 OOM**(熔断循环),用户彻底不可用。
- 本次处置:手工回滚 guest profile(从 deps + bundles 移除 mcn-suite,备份 `package.json.bak-rollback-20260913T0748`)→ 触发一次 `enter` 验证实例恢复(`dsh web:` 正常、`active running`、cgroup **186.5 / 384 MiB**、无 crash-restart)。
## 四、修法(已备好,未落地)
1. **D1**:`try { uninstall(sel.id) } catch {}` → `try { await uninstall(sel.id) } catch {}`(**一行**)。
2. **D2**:`remove` 的参数改成 `['remove', id, '-w', '--reporter', 'silent']`(或直接去掉 `--ignore-workspace-root-check`)。
⚠️ `add` 的 `--ignore-workspace-root-check` 是**合法** flag,**不要一起改**。
3. **加固(建议同批)**:
- 路由最外层 `void (async () => {…})()` 补 `.catch()`(双保险,避免同类遗漏再崩进程);
- 启动/应用前的**半应用态自愈**:检测「`bundles` 里有包但 `node_modules` 装不全」→ 自动摘除或至少审计告警。
4. **验收**:修复后在任一 profile 上重现「启用一个注定失败的插件」→ 期望 = **任务 `failed` + profile 回滚 + 服务不退出**。
## 五、红线与影响
- 改码 + `npm run build` + `systemctl restart dshs` → **中断在线用户数秒** ⇒ 走 **R8 先知会**。
- 回滚:单文件 `git` 回退 + 重新 build + 重启;服务本身有 systemd 自动重启兜底。
## 六、相关
- 档案 65(功能插件启停 ↔ web provider 联动)|档案 16 §2.2(危险操作确认弹窗)|档案 75 维度 B(资源成本,与 D3 同源)|档案 74(V8 堆限 256→160,D3 的背景)。
---
## 七、修正(2026-09-13 13:4x)· **D1 / D2 / 加固 a 已落地并部署**
> 触发:用户问「还有待处理的事项吗」时复核本条,发现**状态已过期** —— 三个缺陷中有两个半已经修完并上线。
**逐条实测(部署产物 `lib/web/routes/business-plugins.js`,只读核对)**:
| 项 | 期望修法(§四) | 产物实测 | 结论 |
|---|---|---|---|
| **D1** 缺 `await` | `try { await uninstall(…) } catch {}` | `:587` 与 `:617` 均为 `await uninstall(sel.id)` | ✅ **已修** |
| **D2** 无效 flag | `remove` 去掉 `--ignore-workspace-root-check` | `:575` = `['remove', id, '-w', '--reporter', 'silent']`;全仓该 flag **仅剩 `add` 一处**(合法) | ✅ **已修** |
| **加固 a** 外层 `.catch()` | 路由最外层补 catch | `:613` `void (async () => {` … `:718` `})().catch((err: unknown) => {` | ✅ **已修** |
| **加固 b** 半应用态主动自愈 | 检测「`bundles` 有包但 `node_modules` 装不全」→ 自动摘除/告警 | 只有**失败路径**的 `restoreProfile()`(`:567` / `:671` / `:710`)回滚到快照 | ⚠️ **未做**(可选加固,非 P0) |
**D3 已消解(不再需要窗口)**:D3 的成因是「V8 堆限写死 160 MiB + `MemoryMax` 写死 384 MiB 装不下 univer + mcn-suite」。**档案 81 · R1-④ 已把两者改成「按插件集合动态计算」**(`instanceMemMb()` / `heapMbFor()`)。2026-09-13 13:3x 实测运行中实例:
```
guest(uid 100002) NODE_OPTIONS=--max-old-space-size=256 scope MemoryMax=544 MiB
admin(uid 114801) NODE_OPTIONS=--max-old-space-size=256 scope MemoryMax=384 MiB
平台进程 env 仍有 DSH_INSTANCE_NODE_OPTIONS=--max-old-space-size=160 ← 已被代码侧 withHeap() 覆盖,不再是生效值
```
⇒ **§五「需 R8 窗口重启」的前提已不成立**:不再需要为修复本条而重启。
**仍未闭环的一项**:§四.4 的端到端验收(**在某个 profile 上重现「启用一个注定失败的插件」→ 期望 任务 `failed` + profile 回滚 + 服务不退出**)**尚未跑过**。代码层面三个修法都已就位,但「服务真的不再被带崩」目前是**代码判据**、不是**实测判据**。建议与 T03 的 guest 启用(天然就是一次"可能失败的启用")合并验收,不必单独造场景。