Luo-root/PRism · opened by Luo-root
本次 PR 将 process.go 中的 Windows 特定代码(进程管理、进程树 kill)拆分到 build tag 隔离的 platform 文件中,实现了跨平台进程管理的重构。核心思路是将 setupProcess() 和 killProcessTree() 抽象为平台特定函数,通过 //go:build tag 在编译期选择正确实现。
原代码中 DefaultLangs() 对 runtime.GOOS == "windows" 有独立的语言配置分支,使用 `python`(非 python3)、`cmd /C`(非 bash -c)。重构时只移除了 Windows 分支,但未将 DefaultLangs 也通过 build tag 做跨平台分离。当前 Windows 编译后将使用 Unix 配置:`python3`(Windows 通常只有 `python`)和 `bash -c`(Windows 默认无 bash),导致 Python 和 Shell 执行在 Windows 上完全不可用。这是一个回归 Bug——原代码能正确运行于 Windows,重构后反而破坏了。
func DefaultLangs() map[string]LangConfig {
return map[string]LangConfig{
"python": {Command: "python3", Ext: ".py"},
"node": {Command: "node", Ext: ".js"},
"go": {
Command: "go", Args: []string{"run"}, Ext: ".go",
InitFiles: map[string]string{"go.mod": "module sandbox\ngo 1.21\n"},
},
"shell": {Command: "bash", Args: []string{"-c"}},
}
}
// 在 process.go 中保留为 build tag 中立版本(如果各平台一致部分)
// 推荐方案:将 DefaultLangs 也拆分为平台特定文件
// process_langs_unix.go (//go:build linux || darwin)
func DefaultLangs() map[string]LangConfig {
return map[string]LangConfig{
"python": {Command: "python3", Ext: ".py"},
"node": {Command: "node", Ext: ".js"},
"go": {
Command: "go", Args: []string{"run"}, Ext: ".go",
InitFiles: map[string]string{"go.mod": "module sandbox\ngo 1.21\n"},
},
"shell": {Command: "bash", Args: []string{"-c"}},
}
}
// process_langs_windows.go (//go:build windows)
func DefaultLangs() map[string]LangConfig {
return map[string]LangConfig{
"python": {Command: "python", Ext: ".py"},
"node": {Command: "node", Ext: ".js"},
"go": {
Command: "go", Args: []string{"run"}, Ext: ".go",
InitFiles: map[string]string{"go.mod": "module sandbox\ngo 1.21\n"},
},
"shell": {Command: "cmd", Args: []string{"/C"}},
}
}
process_unix.go 的 setupProcess() 是空函数,未设置 cmd.Cancel 和 cmd.WaitDelay。当 context 被取消时,Go 默认行为仅发送 os.Kill 给主进程(不保证杀子进程),且没有 WaitDelay 兜底。如果子进程的 stdout/stderr 管道仍被其他子进程持有(例如管道泄漏),Wait() 将永远阻塞。原 Windows 实现中设置了 `WaitDelay: 3 * time.Second` 来防御此场景,Unix 端应同等处理。此外,空的 killProcessTree 仅 p.Kill() 单进程,不处理子进程树,对执行 shell 脚本等会 fork 子进程的场景无效。
func setupProcess(cmd *exec.Cmd) {
// Unix 系统不需要特殊设置
}
func killProcessTree(p *os.Process) {
if p != nil {
p.Kill()
}
}
func setupProcess(cmd *exec.Cmd) {
// 设置进程组,便于后续整组 kill
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
cmd.Cancel = func() error {
killProcessTree(cmd.Process)
return nil
}
// 防止 I/O 阻塞导致 Wait() 永远不返回
cmd.WaitDelay = 3 * time.Second
}
func killProcessTree(p *os.Process) {
if p == nil {
return
}
// 向进程组发送 SIGKILL,确保子进程也被终止
pgid, err := syscall.Getpgid(p.Pid)
if err == nil {
syscall.Kill(-pgid, syscall.SIGKILL)
} else {
p.Kill()
}
}
setupProcess 的 Cancel 函数先调用 killProcessTree(cmd.Process)(内部执行 taskkill /F /T /PID,已杀死整棵进程树),随后又调用 cmd.Process.Kill()(再次 kill 已死亡的主进程)。虽然 Go 运行时会忽略 Cancel 返回的 error,但这种写法会让代码读者困惑——两次 kill 的意图不明确。建议去掉冗余的 cmd.Process.Kill(),或者在注释中说明兜底意图。
cmd.Cancel = func() error {
killProcessTree(cmd.Process)
return cmd.Process.Kill()
}
cmd.Cancel = func() error {
// taskkill /T 已杀掉整棵进程树,此处返回 nil 即可
killProcessTree(cmd.Process)
return nil
}
当前使用 `//go:build linux || darwin` 语法(Go 1.17+),这是正确的。但缺少 package 注释说明此文件的职责,建议补充文件头注释以保持一致性并提升可读性。这是纯风格建议,不影响功能。
//go:build linux || darwin
package sandbox
//go:build linux || darwin
// process_unix.go 提供 Unix (Linux/macOS) 平台的进程管理实现。
// 包括进程初始化 (setupProcess) 和进程树终止 (killProcessTree)。
package sandbox
与 process_unix.go 相同,缺少文件头注释描述文件的职责和平台约束。建议添加以保持文档一致性。
//go:build windows
package sandbox
//go:build windows
// process_windows.go 提供 Windows 平台的进程管理实现。
// 使用 taskkill /T 进行进程树终止,CREATE_NEW_PROCESS_GROUP 进行进程组管理。
package sandbox