如何在使用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.
相关产品推荐
相关产品推荐

