You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Go中exec.Command执行含nohup的脚本时出现挂起问题

问题:Go执行包含nohup的脚本时进程挂起

场景复现

使用如下Go代码执行系统命令或脚本:

var shell, shellArg string
if runtime.GOOS == "windows" {
    shell = "powershell.exe"
    shellArg = "-Command"
    // 确保命令出错时退出
    command = fmt.Sprintf("try { %s } catch { exit 1 }", command)
} else {
    shell = "/bin/sh"
    shellArg = "-c"
    // 用set -e确保任何错误都会导致退出
    command = fmt.Sprintf("set -e; %s", command)
}

cmd := exec.Command(shell, shellArg, command)

普通命令/脚本执行正常,但遇到特殊场景时会挂起:

  • 存在控制脚本main.sh,其中调用的services.sh使用nohup ./startService.sh &启动WebLogic服务
  • 执行main.sh后,WebLogic服务已启动,main.sh进程也已结束,但Go的exec.Command始终处于挂起状态
  • 当把nohup命令改为nohup ./startService.sh > /dev/null 2>&1 &时,Go执行恢复正常

完整的Go执行代码如下:

// 捕获stdout和stderr
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr

Logger.Printf("Executing command: %s", strings.Join(cmd.Args, " "))

// 启动命令
if err := cmd.Start(); err != nil {
    result.Stderr = fmt.Sprintf("Failed to start command: %v", err)
    result.ExitCode = 1
    return result
}

done := make(chan error)
go func() {
    done <- cmd.Wait()
}()

select {
case err := <-done:
    // 命令完成
    fmt.Println("Command Completed")
    if err != nil {
        if exitError, ok := err.(*exec.ExitError); ok {
            result.ExitCode = exitError.ExitCode()
        } else {
            result.ExitCode = 1 // 非ExitError时用通用错误码
        }
        result.Stderr = stderr.String()
    } else {
        result.ExitCode = 0 // 成功
        result.Stdout = stdout.String()
        result.Stderr = stderr.String()
    }
case <-time.After(864000 * time.Second): // 可调整超时时间
    // 命令超时
    if err := cmd.Process.Kill(); err != nil {
        result.Stderr = fmt.Sprintf("Failed to kill process: %v", err)
        result.ExitCode = 1
        return result
    }
    result.Stderr = "Command timed out"
    result.ExitCode = 1
    return result
}

result.Stdout = stdout.String()
result.Stderr = stderr.String()

原因分析

nohup默认会将子进程的stdout和stderr重定向到nohup.out,但当Go进程通过exec.Command启动shell时,shell会将自身的stdout/stderr管道传递给所有子进程(包括startService.sh)。即使main.sh进程退出,startService.sh作为后台进程仍会继续向这个管道输出内容,而Go代码中用bytes.Buffer绑定了stdout/stderr,会一直等待读取管道中的数据,导致cmd.Wait()阻塞,表现为Go进程挂起。

当添加> /dev/null 2>&1后,子进程的输出被定向到空设备,不再往Go持有的管道写数据,管道会被正常关闭,cmd.Wait()就能正常返回。

解决方案

方案1:脚本层重定向输出(已验证有效)

在services.sh中修改nohup命令,显式将所有输出定向到空设备:

nohup ./startService.sh > /dev/null 2>&1 &

方案2:Go代码中忽略子进程输出

如果不需要捕获脚本的stdout/stderr,可以将cmd.Stdout和cmd.Stderr设置为os.DevNull,避免持有管道:

cmd.Stdout = os.DevNull
cmd.Stderr = os.DevNull

方案3:让子进程脱离当前会话

通过syscall让子进程创建新会话,脱离父进程的管道关联(仅适用于类Unix系统):

cmd.SysProcAttr = &syscall.SysProcAttr{
    Setsid: true,
}

这样子进程的输出不会继承父进程的stdout/stderr管道,即使不重定向也不会导致Go进程挂起。

方案4:修改shell命令,强制后台进程输出重定向

在Go拼接shell命令时,统一处理后台进程的输出,比如:

command = fmt.Sprintf("set -e; %s 2>&1 >/dev/null &", command)

但该方案仅适用于不需要捕获输出的场景。

内容的提问来源于stack exchange,提问作者Karthi1234

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 23:46:18