//go:build !windows package execx import ( "os/exec" "syscall" ) // SeparateGroup 让子进程自成进程组(pgid == 子进程 pid),必须在 Start 之前调用。 // 自成组之后 kill(-pgid) 才能把它自己起的孙子一并带走。 // // 用 Setpgid 而不是 Setsid:本包的工具子进程不需要控制终端,而 setsid 会顺带 // 脱离当前会话,交互性场景(终端里 Ctrl+C)行为会变。常驻 PTY 终端那条路径 // 要的是会话+控制终端,见 webui/terminal_unix.go 的三层回退。 func SeparateGroup(cmd *exec.Cmd) { if cmd.SysProcAttr == nil { cmd.SysProcAttr = &syscall.SysProcAttr{} } cmd.SysProcAttr.Setpgid = true } // KillTree 杀掉整个进程组。用于自己管生命周期的常驻子进程(MCP stdio server: // 只 Kill 包装进程会把真正的 server 留成孤儿,还占着 stdout 让读循环永不结束)。 // // 错误一律吞掉并返回 nil:ESRCH 在这里意味着"进程已经没了",正是我们想要的结果; // 把它抛给 exec 反而会盖掉真正的失败原因(signal: killed / 退出码), // 让上层看到的错误信息变成一句无意义的 "no such process"。 func KillTree(cmd *exec.Cmd) error { if cmd.Process == nil { return nil } pid := cmd.Process.Pid if err := syscall.Kill(-pid, syscall.SIGKILL); err != nil { _ = syscall.Kill(pid, syscall.SIGKILL) // 没成组(或组已空):退化为只杀本体 } return nil }