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
更多关于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设置终端原始模式,并妥善处理信号和窗口大小调整。

