Golang中终端处理的实现方法

Golang中终端处理的实现方法 我正在使用Docker的SDK构建这个Docker客户端软件,但在我的docker exec版本(我的工具名为dtools,所以是dtools exec)中遇到了多个问题。

每当我运行dtools exec [flags] $container bash时,会发生以下情况:

  • 按下回车后,标准输入也会被发送到标准输出
  • CTRL+C会退出容器,而不是返回到容器内shell的提示符
  • TAB键不起作用

代码位于GitHub - jeanfrancoisgratton/dtools at exec_run,具体在src/container/exec.go下。

对于如何正确运行这个有什么建议吗?

基本上,我需要在一个运行类似docker exec -ti命令的上下文中,获得关于上述问题的指导。我的工具已接近完成,除了Docker卷处理(尚未编码)和Docker推送不工作(进行中)。有关docker和dtools之间的兼容性对照表,请参阅MAPPINGS.md


更多关于Golang中终端处理的实现方法的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang中终端处理的实现方法的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Golang中实现终端处理时,需要正确配置TTY和终端原始模式。以下是针对您问题的具体解决方案:

1. 标准输入回显问题

您需要禁用终端的本地回显,并正确设置终端模式:

import (
    "github.com/docker/docker/api/types"
    "github.com/docker/docker/pkg/term"
    "golang.org/x/sys/unix"
)

func setupTerminal(fd uintptr) (*term.State, error) {
    // 获取当前终端状态
    oldState, err := term.SetRawTerminal(fd)
    if err != nil {
        return nil, err
    }
    
    // 禁用本地回显
    ws, err := unix.IoctlGetWinsize(int(fd), unix.TIOCGWINSZ)
    if err == nil {
        // 设置终端大小
        term.SetWinsize(fd, ws)
    }
    
    return oldState, nil
}

2. CTRL+C处理问题

需要正确处理信号并配置终端控制字符:

import (
    "os"
    "os/signal"
    "syscall"
)

func handleSignals(execID string, cli *client.Client) {
    sigs := make(chan os.Signal, 1)
    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
    
    go func() {
        sig := <-sigs
        switch sig {
        case syscall.SIGINT:
            // 发送CTRL+C到容器而不是退出程序
            sendSignalToContainer(execID, cli, "SIGINT")
        case syscall.SIGTERM:
            // 清理资源
            cleanupTerminal()
        }
    }()
}

func sendSignalToContainer(execID string, cli *client.Client, signal string) {
    // 使用Docker API发送信号到容器内的进程
    inspect, err := cli.ContainerExecInspect(context.Background(), execID)
    if err == nil && inspect.Pid > 0 {
        // 发送信号到容器内的进程
        cli.ContainerKill(context.Background(), inspect.ContainerID, signal)
    }
}

3. TAB键补全问题

需要正确配置终端输入模式以启用行编辑功能:

import (
    "github.com/creack/pty"
    "github.com/docker/docker/api/types"
    "io"
)

func createPtyAndHandleIO(hijacked types.HijackedResponse) error {
    // 创建伪终端
    ptyMaster, ptySlave, err := pty.Open()
    if err != nil {
        return err
    }
    defer ptyMaster.Close()
    defer ptySlave.Close()
    
    // 设置终端模式以启用行编辑
    term.MakeRaw(int(ptyMaster.Fd()))
    
    // 处理输入输出
    go func() {
        io.Copy(ptyMaster, hijacked.Reader)
    }()
    
    go func() {
        io.Copy(hijacked.Conn, ptyMaster)
    }()
    
    // 从标准输入复制到pty
    go func() {
        io.Copy(ptySlave, os.Stdin)
    }()
    
    return nil
}

4. 完整的exec实现示例

package container

import (
    "context"
    "fmt"
    "io"
    "os"
    
    "github.com/docker/docker/api/types"
    "github.com/docker/docker/client"
    "github.com/docker/docker/pkg/term"
    "github.com/creack/pty"
)

func ExecContainer(cli *client.Client, containerID string, cmd []string) error {
    // 创建exec配置
    execConfig := types.ExecConfig{
        AttachStdin:  true,
        AttachStdout: true,
        AttachStderr: true,
        Tty:          true,
        Cmd:          cmd,
    }
    
    // 创建exec实例
    execResp, err := cli.ContainerExecCreate(context.Background(), containerID, execConfig)
    if err != nil {
        return err
    }
    
    // 附加到exec
    hijackedResp, err := cli.ContainerExecAttach(context.Background(), execResp.ID, types.ExecStartCheck{
        Tty: true,
    })
    if err != nil {
        return err
    }
    defer hijackedResp.Close()
    
    // 设置终端
    fd := os.Stdin.Fd()
    if term.IsTerminal(fd) {
        oldState, err := term.SetRawTerminal(fd)
        if err != nil {
            return err
        }
        defer term.RestoreTerminal(fd, oldState)
        
        // 获取并设置窗口大小
        ws, err := term.GetWinsize(fd)
        if err == nil {
            err = cli.ContainerExecResize(context.Background(), execResp.ID, types.ResizeOptions{
                Height: uint(ws.Height),
                Width:  uint(ws.Width),
            })
            if err != nil {
                return err
            }
        }
    }
    
    // 启动exec
    err = cli.ContainerExecStart(context.Background(), execResp.ID, types.ExecStartCheck{})
    if err != nil {
        return err
    }
    
    // 处理IO
    done := make(chan error, 1)
    
    go func() {
        defer close(done)
        _, err = io.Copy(os.Stdout, hijackedResp.Reader)
        done <- err
    }()
    
    go func() {
        io.Copy(hijackedResp.Conn, os.Stdin)
        hijackedResp.CloseWrite()
    }()
    
    // 等待完成
    select {
    case err := <-done:
        if err != nil && err != io.EOF {
            return err
        }
    }
    
    // 检查退出代码
    inspect, err := cli.ContainerExecInspect(context.Background(), execResp.ID)
    if err != nil {
        return err
    }
    
    if inspect.ExitCode != 0 {
        return fmt.Errorf("exit code %d", inspect.ExitCode)
    }
    
    return nil
}

5. 窗口大小调整处理

func handleResize(cli *client.Client, execID string, fd uintptr) {
    sigchan := make(chan os.Signal, 1)
    signal.Notify(sigchan, syscall.SIGWINCH)
    
    go func() {
        for range sigchan {
            ws, err := term.GetWinsize(fd)
            if err != nil {
                continue
            }
            
            cli.ContainerExecResize(context.Background(), execID, types.ResizeOptions{
                Height: uint(ws.Height),
                Width:  uint(ws.Width),
            })
        }
    }()
}

这些代码示例解决了您提到的三个主要问题:禁用本地回显、正确处理CTRL+C信号、以及启用TAB键补全功能。关键是要正确使用term.SetRawTerminal设置终端原始模式,并妥善处理信号和窗口大小调整。

回到顶部