Skip to content

fix(daemon): 修复 PM2 God 的 systemd cgroup 归属 - #919

Open
emosheeep wants to merge 1 commit into
deepcoldy:masterfrom
emosheeep:fix/pm2-cgroup-ownership
Open

fix(daemon): 修复 PM2 God 的 systemd cgroup 归属#919
emosheeep wants to merge 1 commit into
deepcoldy:masterfrom
emosheeep:fix/pm2-cgroup-ownership

Conversation

@emosheeep

Copy link
Copy Markdown
Contributor

背景

Closes #774

PM2 God 首次启动时会继承调用方所在的 cgroup;即使随后 daemonize,也不会自动迁出该进程组。因此,如果首次启动来自 IDE、Agent Runner 或其他 systemd service,PM2 God 和 Botmux daemon 的生命周期会意外绑定到该调用方,调用方重启或清理 cgroup 时会连带终止 Botmux。

本 PR 将 Linux 下的生命周期明确为:

botmux.service
└── PM2 God Daemon
    ├── botmux daemon
    ├── dashboard
    └── plugin services

修改思路

1. 让 systemd 正式接管 PM2 God

Linux user service 从 Type=oneshot + RemainAfterExit 调整为:

  • Type=forking
  • PIDFile=~/.botmux/pm2/pm2.pid
  • MainPID 指向 PM2 God
  • ExecStartExecStop 使用仅限 systemd service 内部调用的入口

PM2 God 由 botmux.service 内部创建,因此 God、daemon 和 dashboard 都进入独立的 botmux.service cgroup,不再继承执行 botmux start 的终端或其他 supervisor。

Service 使用 KillMode=process,并由经过 PID、进程启动代际和 cgroup 校验的 ExecStop 精确结束 PM2 God,避免 systemd 在重启时误杀需要跨 daemon 重启保留的 tmux、herd 或 zellij 会话。

2. 普通 restart 自动修复已有 Service

botmux restart 在 Linux 下会:

  1. 重写当前版本的 ~/.config/systemd/user/botmux.service
  2. 执行 systemctl --user daemon-reload
  3. 校验有效的 Type、PIDFile、ExecStart、ExecStop、PATH、WorkingDirectory 和 shutdown 策略
  4. 判断当前 PM2 God 是否已成为 botmux.service 的 MainPID
  5. 必要时通过 systemctl --user restart botmux.service 完成迁移
  6. 重启后重新校验 God ownership、service 状态和 MainPID

已有的旧版 Type=oneshot unit 不需要手动删除或重新执行 autostart enable,普通 restart 即可修复文件和运行态。

实现只维护一个主 botmux.service,不生成额外的 drop-in/conf 文件。

3. 拒绝复用其他 supervisor 的 God

新增 Linux PM2 lifecycle ownership 检查,通过 /proc 同时绑定 PM2_HOME、PID、进程启动代际和 systemd cgroup。

当已有 God 属于其他 service/cgroup 时,start、restart、插件服务 mutation 和 autostart 操作都会 fail closed,并提示先从原 owner 迁出。本 PR 不会自动结束外部 God,避免越权操作其他 supervisor 管理的进程。

4. 只读命令不隐式拉起 PM2

PM2 的公共 CLI/connect 路径在 socket 不存在时可能自动创建 God。新增只读 RPC 客户端用于 status、logs 和 Desktop 查询:

  • God 不存在时直接报告未运行
  • God 存在时只连接既有 RPC socket
  • 不调用 PM2 daemonize/start 路径
  • 查询期间绑定既有 God 的 PID、启动代际和 cgroup
  • socket 断开后不自动重连到替换 generation

5. 约束已有 God 上的 mutation

插件服务等必须修改现有 PM2 registry 的操作会在 mutation 前确认 God 唯一且属于 botmux.service,关闭 RPC socket 自动重连,并在发送请求前再次校验 PID、启动代际和 cgroup。God 消失或被替换时直接失败,不在调用方 cgroup 中创建新 God。

6. 保留插件服务与持久会话

  • 自动插件服务由正常启动流程重新 reconcile
  • 正在运行的 manual plugin service 在 systemd 迁移前记录并在成功后恢复
  • tmux、herd、zellij 等持久会话不会随 God 一起被 systemd 清理

影响范围

  • Linux:daemon 首次启动和必要的 Service 修复统一经过 user systemd;外部 cgroup 的 God 不再隐式复用;旧版主 unit 可通过普通 restart 自动修复。
  • macOS / Windows:launchd 和 Task Scheduler 注册方式不变;PM2 查询改为不 daemonize 的只读 RPC,不引入 Linux ownership 判断。
  • CLI / 后端 / 会话类型:未修改 AI CLI adapter 和飞书消息处理路径;PtyBackend 与持久会话后端启动协议不变;插件服务继续使用 Botmux 专用 PM2_HOME。

设计边界

本 PR 聚焦 Issue 要求的基本生命周期问题:建立明确的 botmux.service owner、校验并拒绝外部 God、自动修复旧版主 Service、阻止只读命令隐式 daemonize。

没有引入额外 drop-in 配置、持久迁移 journal 或跨崩溃恢复事务。

验证

构建

pnpm build

通过,包括 TypeScript 编译、dashboard bundle 和 dist audit。

定向测试与真实 systemd E2E

pnpm exec vitest run \
  test/autostart-existing-unit-repair.test.ts \
  test/autostart-pm2-ownership.test.ts \
  test/autostart-refresh-retry.test.ts \
  test/plugin-service-manual-snapshot.test.ts \
  test/pm2-existing-client.test.ts \
  test/pm2-readonly-cli.test.ts \
  test/desktop/desktop-pm2-apps.test.ts \
  test/plugin-pm2-env.test.ts \
  test/shutdown-supervisor-contract.test.ts \
  test/systemd-pm2-migration.e2e.ts

结果:10 个测试文件、62 个用例全部通过。

真实 systemd E2E 覆盖:

  • Type=oneshot unit 迁移到 Type=forking
  • PM2 God 换代后成为 MainPID
  • God 和核心进程进入 Botmux service cgroup
  • 持久子进程跨 service restart 存活
  • ExecStop 失败时不会由 systemd fallback 强杀 God

本地运行

pnpm switch:here
pnpm daemon:restart

实际读回:

  • botmux.serviceactive/running
  • Type=forking
  • MainPID 与唯一 PM2 God PID 一致
  • PM2 God、botmux-0、dashboard 均属于 botmux.service cgroup
  • daemon 与 dashboard 均为 online
  • 已有 tmux 会话在 restart 后仍然存在

@emosheeep
emosheeep marked this pull request as ready for review August 18, 2026 14:02
@emosheeep
emosheeep requested a review from deepcoldy as a code owner August 18, 2026 14:02

Copy link
Copy Markdown
Owner

这是一条自动化初步评审意见,最终以维护者审阅为准。

整体看,Type=forking + PIDFile/MainPID、现有 God 的代际绑定、mutation 前重校验、以及只读查询不隐式 daemonize 的实现较扎实;拒绝普通命令复用 external cgroup God 也符合 #774 已确认的边界。定向测试与真实 systemd E2E 均通过。

有几项非阻断的升级体验建议:

  1. 当前 same-PM2_HOME God 位于 shell / IDE / Runner 的 session.scope 时,start/restart/status/logs/stop 会直接进入 external reject。拒绝复用本身合理,但错误目前由顶层未捕获异常呈现为完整 Node stack;尤其 status / logs 建议收敛为清晰的 stderr、稳定退出码和可执行迁移提示。
  2. 对 session-scope 的存量状态,建议补一套安全、精确的人工迁移说明,明确先确认 Session/Riff workload idle、绑定唯一且同用户/同 PM2_HOME 的 God,再退休旧 generation,并说明 manual plugin service 的恢复方式。若直接建议 pm2 kill,需明确它会绕过 Botmux 的 attested shutdown 边界;更理想的是提供显式确认式的一次性 retirement/handoff 入口。
  3. PR 描述第 3 节只列出 start/restart/plugin/autostart 会拒绝 external,而第 4 节写“God 存在时只连接既有 RPC socket”;当前实现中 status/logs 也会拒绝 external,建议统一文档与实际行为。
  4. 建议增加一条 external same-home God → 人工退休/新 botmux.service owned generation 的迁移用例,避免后续提示或迁移流程再次失效。

以上不否定核心生命周期方案,主要是让受 #774 影响最直接的存量安装在升级时有清楚、安全的出路。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: prevent Botmux PM2 daemon from inheriting caller cgroup

2 participants