子进程组管理(INV-EXEC-PROC-01)
wesgine 通过 proc.Command 统一管理所有子进程的创建和销毁,确保取消时整个进程树被干净杀掉。
问题背景
exec.CommandContext 只向直接子进程发 SIGKILL:
wesgine → sh -c "npm ls -g"
↓
sh(被杀)
↓
node(活着,持有 stdout 管道写端)
↓
cmd.Wait() 永不返回
取消返回了,工作没有停。
解决方案:proc.Command
internal/infra/proc 是唯一的子进程创建原语:
cmd := proc.Command(ctx, "sh", "-c", "npm ls -g")
它做三件事:
Setpgid = true:创建新进程组Cancel:ctx 取消时 SIGKILL 整个进程组WaitDelay:给 Wait() 一个有限时间上界
禁止事项
| 禁止 | 原因 |
|---|---|
exec.Command(...) | 没有进程组管理 |
exec.CommandContext(ctx, ...) | 只杀直接子进程 |
exec.Cmd{} 复合字面量 | 最不像 fork 的拼写 |
修改 cmd.Cancel | 会关掉进程组杀死功能 |
修改 cmd.WaitDelay | WaitDelay=0 恢复原缺陷 |
豁免文件
只有 3 个文件被允许不使用 proc.Command,但各有义务:
| 文件 | 义务 |
|---|---|
tool/host_shell.go | 必须自设 Cancel + WaitDelay |
adapter/stt/tool.go | 脱离 ctx(转写活过请求),必须自设 SysProcAttr 建组 |
| 第三个豁免位 | 按需评估 |
闸门验证
scripts/audit-lock/39-proc-single-spawn.sh:
- 扫描全仓的
exec.Command/exec.CommandContext/exec.Cmd{} - 只允许出现在
proc.Command实现和豁免文件中 - 正向断言每条豁免的义务
进程超时后清理
前台 exec(INV-EXEC-11)
ctx 结束后必须调用 KillProcessGroup() 清理整个进程组。
管道排空(INV-EXEC-12)
进程退出后 stdout 排空最多等 5s:
- 超时强制
CloseOutput() - 解除 scanner 阻塞
- 处理 grandchild 持有管道写端的场景
管道消费(INV-EXEC-DRAIN-01)
输出读取必须无条件消费管道直到 EOF:
- 禁止
bufio.Scanner(1MB max-token 限制) - 使用
ReadSlice循环(64KB 块) cappedWriter限制内存
并发验证
规则的正确性不能只靠 grep:
- 进程组是否真被杀
- 持管道的孙进程会不会活过 cancel
WaitDelay是否限住了 Wait
go test ./internal/infra/proc/ 是不变量的实质验证。
相关文档
- Exec 安全模型 →
exec-security-model.md - 行式输入安全 →
line-input-safety.md - 治理管线 →
governance-pipeline.md