Golang Docker SDK编写`docker exec`类似功能时遇到的问题

Golang Docker SDK编写docker exec类似功能时遇到的问题 (抱歉,如果看起来像是重复发帖。我最初将此问题发布在“获取帮助”板块,后来发现了这个子论坛,因此删除了原帖,但不知为何主题标题仍然保留。)

我正在使用官方SDK编写一个Docker客户端;到目前为止它运行良好,但我在自己的docker exec克隆功能上遇到了问题。 假设我运行等效于docker exec -tiuroot $CONTAINERNAME bash的命令,然后继续向分配的tty发送一些命令(如ls、ps等),我会得到重复的输出(参见第二个代码片段)。另一个问题是转义序列:如果我在容器内运行tail -f $SOME_FILE,然后按下CTRL+C,它不会停止tail命令,而是会将我从容器中踢出。

我不知道如何改变这两种行为。

第一个代码片段是我实际使用的方法,附带的图片是一个示例,展示了运行dtools exec时出现的“重复输出”现象。

这两个问题(重复输出和转义字符)让我有些抓狂,我希望能在这里得到一些建议。 哦,还有另一个问题是我在上传图片时刚刚发现的(图片中未显示):TAB补全在这个实现中无法工作;在同一个容器中使用“真正的”docker exec是可以的,所以这是我的实现中stdin/stdout的问题。

关于DockerExecConfig变量:它是types.ExecConfig类型,并在其他地方预先填充。

func ContainerExec (uri string, args []string) string {
	cli, err := client.NewClientWithOpts(client.WithHost(uri), client.WithAPIVersionNegotiation())
	if err != nil {
		fmt.Printf("Unable to create docker client: %s\n", err)
		os.Exit(-1)
	}

	DockerExecConfig.AttachStderr = DockerExecConfig.AttachStdout
	DockerExecConfig.AttachStdin = DockerExecConfig.AttachStdout
	DockerExecConfig.Cmd = args[1:]

	execResponse, err := cli.ContainerExecCreate(ctx, args[0], DockerExecConfig)
	if err != nil {
		return err.Error()
	}

	execAttachConfig := types.ExecStartCheck{
		Tty: !DockerExecConfig.Tty,
	}

	resp, err := cli.ContainerExecAttach(ctx, execResponse.ID, execAttachConfig)
	if err != nil {
		return err.Error()
	}

	defer resp.Close()

	errChan := make(chan error, 1)

	go func() {
		_, errorInContainer := stdcopy.StdCopy(os.Stdout, os.Stderr, resp.Reader)
		errChan <- errorInContainer
	}()

	go func() {
		_, err := io.Copy(resp.Conn, os.Stdin)
		errChan <- err
	}()

	select {
	case errorInContainer := <-errChan:
		if errorInContainer != nil {
			fmt.Printf("dtools exec error: %s\n", errorInContainer.Error())
			os.Exit(-1)
		}
		return ""
	}
}

在这张图片中,您可以看到在CLI提示符下输入的每个命令也会被发送到stdout,同时请注意$PS1提示符,它显示dtools命令是从主机“bergen”发送到容器“nginx”的,而CTRL+C将控制权返回给了bergen,而不是终止tail -1 -f。随后的docker exec显示tail -1 -f仍在运行。 image


更多关于Golang Docker SDK编写`docker exec`类似功能时遇到的问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

好的,我甚至尝试了ChatGPT,但得到的答案更糟(!):在容器内我有标准输入,但没有标准输出或标准错误。

任何帮助都将不胜感激,我被难住了。

更多关于Golang Docker SDK编写`docker exec`类似功能时遇到的问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我在文件顶部附近添加了这行代码: DockerExecConfig.DetachKeys = "ctrl-a,ctrl-d" … 但 CTRL+C 仍然会让我退出容器。

所以,总结一下我的问题: . 标准输入同时被复制到标准输出 . CTRL+C 会让我退出容器,而它本应针对的命令(例如 tail -f)却得以继续执行(即没有被中断) . 容器内的 Tab 键自动补全功能完全不起作用

问题出在ExecStartCheckTty设置上。你错误地将Tty设置为!DockerExecConfig.Tty,这会导致终端模式不匹配。正确的做法是保持TtyDockerExecConfig.Tty一致。

以下是修正后的代码片段:

func ContainerExec(uri string, args []string) string {
    cli, err := client.NewClientWithOpts(client.WithHost(uri), client.WithAPIVersionNegotiation())
    if err != nil {
        fmt.Printf("Unable to create docker client: %s\n", err)
        os.Exit(-1)
    }

    DockerExecConfig.AttachStderr = DockerExecConfig.AttachStdout
    DockerExecConfig.AttachStdin = DockerExecConfig.AttachStdout
    DockerExecConfig.Cmd = args[1:]

    execResponse, err := cli.ContainerExecCreate(ctx, args[0], DockerExecConfig)
    if err != nil {
        return err.Error()
    }

    // 关键修正:保持Tty设置一致
    execAttachConfig := types.ExecStartCheck{
        Tty: DockerExecConfig.Tty,  // 这里使用相同的Tty值
    }

    resp, err := cli.ContainerExecAttach(ctx, execResponse.ID, execAttachConfig)
    if err != nil {
        return err.Error()
    }
    defer resp.Close()

    // 根据Tty模式选择不同的处理方式
    if DockerExecConfig.Tty {
        // 终端模式处理
        errChan := make(chan error, 1)
        
        go func() {
            _, err := io.Copy(os.Stdout, resp.Reader)
            errChan <- err
        }()

        go func() {
            _, err := io.Copy(resp.Conn, os.Stdin)
            errChan <- err
        }()

        select {
        case err := <-errChan:
            if err != nil && err != io.EOF {
                return fmt.Sprintf("exec error: %v", err)
            }
        }
    } else {
        // 非终端模式处理
        errChan := make(chan error, 1)
        
        go func() {
            _, err := stdcopy.StdCopy(os.Stdout, os.Stderr, resp.Reader)
            errChan <- err
        }()

        go func() {
            _, err := io.Copy(resp.Conn, os.Stdin)
            errChan <- err
        }()

        select {
        case err := <-errChan:
            if err != nil && err != io.EOF {
                return fmt.Sprintf("exec error: %v", err)
            }
        }
    }
    
    // 检查执行状态
    inspectResp, err := cli.ContainerExecInspect(ctx, execResponse.ID)
    if err != nil {
        return fmt.Sprintf("inspect error: %v", err)
    }
    
    if inspectResp.ExitCode != 0 {
        return fmt.Sprintf("exited with code: %d", inspectResp.ExitCode)
    }
    
    return ""
}

对于转义序列和TAB补全问题,还需要确保终端设置正确。可以添加终端原始模式设置:

import "golang.org/x/term"

func ContainerExec(uri string, args []string) string {
    // ... 前面的代码保持不变 ...
    
    if DockerExecConfig.Tty {
        // 保存原始终端状态
        oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
        if err != nil {
            return fmt.Sprintf("terminal make raw error: %v", err)
        }
        defer term.Restore(int(os.Stdin.Fd()), oldState)
        
        // 设置终端大小
        if err := setupTerminalResize(ctx, cli, execResponse.ID, os.Stdin); err != nil {
            return fmt.Sprintf("terminal resize error: %v", err)
        }
        
        // ... 其余终端模式代码 ...
    }
    
    // ... 其余代码 ...
}

func setupTerminalResize(ctx context.Context, cli *client.Client, execID string, stdin *os.File) error {
    ch := make(chan os.Signal, 1)
    signal.Notify(ch, syscall.SIGWINCH)
    
    go func() {
        for range ch {
            size, err := term.GetSize(int(stdin.Fd()))
            if err != nil {
                continue
            }
            cli.ContainerExecResize(ctx, execID, types.ResizeOptions{
                Height: uint(size.Height),
                Width:  uint(size.Width),
            })
        }
    }()
    
    // 初始调整大小
    size, err := term.GetSize(int(stdin.Fd()))
    if err != nil {
        return err
    }
    
    return cli.ContainerExecResize(ctx, execID, types.ResizeOptions{
        Height: uint(size.Height),
        Width:  uint(size.Width),
    })
}

主要修正点:

  1. ExecStartCheck.Tty 必须与 DockerExecConfig.Tty 保持一致
  2. 根据Tty模式选择不同的数据流处理方式
  3. 添加终端原始模式设置以正确处理转义序列
  4. 实现终端大小调整以支持TAB补全等功能
  5. 添加执行状态检查

这些修正应该能解决重复输出、CTRL+C转义序列和TAB补全的问题。

回到顶部