2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 05:31:44 +08:00
Files
panel/pkg/docker/turn.go
Copilot ddfcd3e45c fix: 修复 PTY Websocket 客户端断开连接后命令仍在后台运行的问题 (#1222)
* Initial plan

* fix: 修复 PTY Websocket 客户端断开连接后命令仍在后台运行的问题

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

* fix: 解决连接中断后命令不被杀死的问题

---------

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-12 04:13:04 +08:00

143 lines
3.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package docker
import (
"context"
"encoding/json"
"fmt"
"io"
"syscall"
"time"
"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
}
// 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() {
// 检查进程是否仍在运行
inspectResp, err := t.client.ExecInspect(t.ctx, t.execID, client.ExecInspectOptions{})
if err == nil && inspectResp.Running {
_ = syscall.Kill(inspectResp.PID, syscall.SIGTERM)
// 等待最多 10 秒
for i := 0; i < 10; i++ {
time.Sleep(1 * time.Second)
inspectResp, err = t.client.ExecInspect(t.ctx, t.execID, client.ExecInspectOptions{})
if err != nil || !inspectResp.Running {
break
}
}
// 如果仍在运行KILL
if err == nil && inspectResp.Running {
_ = syscall.Kill(inspectResp.PID, syscall.SIGKILL)
}
}
t.hijack.Close()
_ = t.hijack.CloseWrite()
_ = 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)
}