2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 14:57:16 +08:00
Files
panel/pkg/docker/turn.go
Copilot 3d8623a9f9 feat: 添加容器终端功能 (#1216)
* Initial plan

* feat: 添加容器终端功能 - 通过 WebSocket 进入容器执行命令

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* fix: 将 fmt.Errorf 错误信息改为英语

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* feat: 前端优化

* feat: 容器优化

* feat: 前端优化

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>
Co-authored-by: 耗子 <haozi@loli.email>
2026-01-11 05:30:52 +08:00

127 lines
2.8 KiB
Go

package docker
import (
"context"
"encoding/json"
"fmt"
"io"
"github.com/coder/websocket"
"github.com/moby/moby/client"
)
// MessageResize 终端大小调整消息
type MessageResize struct {
Resize bool `json:"resize"`
Columns uint `json:"columns"`
Rows uint `json:"rows"`
}
// Turn 容器终端转发器
type Turn struct {
ctx context.Context
ws *websocket.Conn
client *client.Client
execID string
hijack client.ExecAttachResult
closeOnce bool
}
// NewTurn 创建容器终端转发器
func NewTurn(ctx context.Context, ws *websocket.Conn, containerID string, command []string) (*Turn, error) {
apiClient, err := client.New(client.WithHost("unix:///var/run/docker.sock"))
if err != nil {
return nil, fmt.Errorf("failed to create docker client: %w", err)
}
// 创建 exec 实例
execCreateResp, err := apiClient.ExecCreate(ctx, containerID, client.ExecCreateOptions{
Cmd: command,
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
TTY: true,
})
if err != nil {
_ = apiClient.Close()
return nil, fmt.Errorf("failed to create exec instance: %w", err)
}
// 附加到 exec 实例
hijack, err := apiClient.ExecAttach(ctx, execCreateResp.ID, client.ExecAttachOptions{
TTY: true,
})
if err != nil {
_ = apiClient.Close()
return nil, fmt.Errorf("failed to attach to exec instance: %w", err)
}
turn := &Turn{
ctx: ctx,
ws: ws,
client: apiClient,
execID: execCreateResp.ID,
hijack: hijack,
}
return turn, nil
}
// Write 实现 io.Writer 接口,将容器输出写入 WebSocket
func (t *Turn) Write(p []byte) (n int, err error) {
if err = t.ws.Write(t.ctx, websocket.MessageText, p); err != nil {
return 0, err
}
return len(p), nil
}
// Close 关闭连接
func (t *Turn) Close() {
if t.closeOnce {
return
}
t.closeOnce = true
t.hijack.Close()
_ = t.client.Close()
}
// Handle 处理 WebSocket 消息
func (t *Turn) Handle(ctx context.Context) error {
var resize MessageResize
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
_, data, err := t.ws.Read(ctx)
if err != nil {
// 通常是客户端关闭连接
return fmt.Errorf("failed to read ws message: %w", err)
}
// 判断是否是 resize 消息
if err = json.Unmarshal(data, &resize); err == nil {
if resize.Resize && resize.Columns > 0 && resize.Rows > 0 {
if _, err = t.client.ExecResize(ctx, t.execID, client.ExecResizeOptions{
Height: resize.Rows,
Width: resize.Columns,
}); err != nil {
return fmt.Errorf("failed to resize terminal: %w", err)
}
}
continue
}
if _, err = t.hijack.Conn.Write(data); err != nil {
return fmt.Errorf("failed to write to container stdin: %w", err)
}
}
}
}
// Wait 等待容器输出并转发到 WebSocket
func (t *Turn) Wait() {
_, _ = io.Copy(t, t.hijack.Reader)
}