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

如何在使用Go Docker SDK执行进程时处理Ctrl+C中断?

问题:通过Go Docker SDK执行容器内Python脚本无法用Ctrl+C中断

我通过Go Docker SDK创建并启动了一个以tail -f /dev/null为默认命令、持续运行的Docker容器,代码如下:

...
config := &container.Config{
    Image:        "test",
    Tty:          true,
    OpenStdin:    true,
    AttachStdin:  true,
    AttachStdout: true,
    AttachStderr: true,
}

hostConfig := &container.HostConfig{
}

createResp, err := cli.ContainerCreate(context.Background(), config, hostConfig, nil, nil, "test")
if err != nil {
    return err
}

err := cli.ContainerStart(context.Background(), createResp.ID, container.StartOptions{})
if err != nil {
    return err
}
...

现在我希望在该容器内运行一个可通过Ctrl+C中断的Python脚本,示例脚本如下:

import time

while True:
    print("Press Ctrl+C to exit")
    time.sleep(1)

使用Docker CLI执行该脚本时,可正常被中断:

docker exec -it test python3 main.py
Press Ctrl+C to exit
Press Ctrl+C to exit
^CTraceback (most recent call last):
  File "/.../.../main.py", line 25, in <module>
    time.sleep(1)
KeyboardInterrupt

但使用Go Docker SDK复现该行为时遇到问题,目前我的代码如下:

...
execConfig := container.ExecOptions{
    Cmd:          []string{"python3", "main.py"},
    Tty:          true,
    AttachStdin:  true,
    AttachStdout: true,
    AttachStderr: true,
}

execCreateResp, err := cli.ContainerExecCreate(ctx, containerID, execConfig)
if err != nil {
    return err
}

execOpts := container.ExecStartOptions{
    Tty: true,
}
execResp, err := cli.ContainerExecAttach(ctx, execCreateResp.ID, execOpts)
if err != nil {
    return err
}
defer execResp.Close()

signalChannel := make(chan os.Signal, 1)
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM)

go func() {
    _, err = io.Copy(os.Stdout, execResp.Reader)
    if err != nil {
        fmt.Println("Error reading output:", err)
    }
}()

<-signalChannel
fmt.Println("Received Ctrl+C, stopping command...")
...

此时Ctrl+C信号已被捕获,但脚本执行并未中断,现象如下:

Press Ctrl+C to exit
Press Ctrl+C to exit
Press Ctrl+C to exit
^CReceived Ctrl+C, stopping command...
Press Ctrl+C to exit
Press Ctrl+C to exit
Press Ctrl+C to exit
Press Ctrl+C to exit
Press Ctrl+C to exit

解决方案

问题核心是:本地捕获的SIGINT信号没有传递到容器内的exec进程,且缺少本地stdin到容器的双向转发逻辑(Docker CLI的-it参数自动处理了这两点)。修复步骤如下:

1. 补充stdin双向转发

当启用Tty: true时,终端的Ctrl+C信号会通过stdin传递给容器内进程,因此需要添加本地stdin到容器exec stdin的转发goroutine:

// 新增:转发本地stdin到容器exec的stdin
go func() {
    _, err = io.Copy(execResp.Conn, os.Stdin)
    if err != nil && !strings.Contains(err.Error(), "closed") {
        fmt.Println("Error writing to stdin:", err)
    }
}()

2. 手动发送SIGINT信号到exec进程

作为兜底方案,在捕获到本地SIGINT后,调用SDK的ContainerExecSignal方法直接向exec进程发送SIGINT:

<-signalChannel
fmt.Println("Received Ctrl+C, stopping command...")

// 向容器内的exec进程发送SIGINT信号
err = cli.ContainerExecSignal(ctx, execCreateResp.ID, "SIGINT")
if err != nil {
    fmt.Println("Failed to send SIGINT to exec process:", err)
    return err
}

3. 完整修复后代码片段

import (
    "context"
    "fmt"
    "io"
    "os"
    "strings"
    "syscall"
    "time"

    "github.com/docker/docker/api/types/container"
    "github.com/docker/docker/client"
)

// ... 其他代码 ...

execConfig := container.ExecOptions{
    Cmd:          []string{"python3", "main.py"},
    Tty:          true,
    AttachStdin:  true,
    AttachStdout: true,
    AttachStderr: true,
}

execCreateResp, err := cli.ContainerExecCreate(ctx, containerID, execConfig)
if err != nil {
    return err
}

execOpts := container.ExecStartOptions{
    Tty: true,
}
execResp, err := cli.ContainerExecAttach(ctx, execCreateResp.ID, execOpts)
if err != nil {
    return err
}
defer execResp.Close()

signalChannel := make(chan os.Signal, 1)
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM)

// 转发容器输出到本地stdout
go func() {
    _, err = io.Copy(os.Stdout, execResp.Reader)
    if err != nil && !strings.Contains(err.Error(), "closed") {
        fmt.Println("Error reading output:", err)
    }
}()

// 转发本地stdin到容器exec的stdin
go func() {
    _, err = io.Copy(execResp.Conn, os.Stdin)
    if err != nil && !strings.Contains(err.Error(), "closed") {
        fmt.Println("Error writing to stdin:", err)
    }
}()

<-signalChannel
fmt.Println("Received Ctrl+C, stopping command...")

// 发送SIGINT信号终止exec进程
err = cli.ContainerExecSignal(ctx, execCreateResp.ID, "SIGINT")
if err != nil {
    fmt.Println("Failed to send SIGINT:", err)
    return err
}

// 可选:等待exec进程退出并确认
var execInspect container.ExecInspect
for {
    execInspect, err = cli.ContainerExecInspect(ctx, execCreateResp.ID)
    if err != nil {
        return err
    }
    if !execInspect.Running {
        break
    }
    time.Sleep(100 * time.Millisecond)
}
fmt.Println("Exec process exited successfully")

// ... 其他代码 ...

关键说明

  • 启用Tty: true时,终端信号依赖stdin通道传递,因此必须实现双向IO转发才能让Ctrl+C信号正确触达容器内的Python脚本。
  • ContainerExecSignal是兜底机制,确保即使stdin转发出现异常,也能强制终止进程。
  • 需要导入strings和time包来处理IO错误过滤和进程退出等待逻辑。

内容的提问来源于stack exchange,提问作者Mister D.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 04:27:28