From ddfcd3e45cd9813038c73b2db20b4818e12fc479 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 04:13:04 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20PTY=20Websocket=20?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF=E6=96=AD=E5=BC=80=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E5=90=8E=E5=91=BD=E4=BB=A4=E4=BB=8D=E5=9C=A8=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E7=9A=84=E9=97=AE=E9=A2=98=20(#1222)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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: 耗子 --- pkg/docker/turn.go | 34 ++++++++++++++----- pkg/shell/pty.go | 20 ++++++++++- pkg/ssh/turn.go | 17 ++++++++++ .../components/common/PtyTerminalModal.vue | 5 +-- 4 files changed, 64 insertions(+), 12 deletions(-) diff --git a/pkg/docker/turn.go b/pkg/docker/turn.go index 167f3805..bf00a0f5 100644 --- a/pkg/docker/turn.go +++ b/pkg/docker/turn.go @@ -5,6 +5,8 @@ import ( "encoding/json" "fmt" "io" + "syscall" + "time" "github.com/coder/websocket" "github.com/moby/moby/client" @@ -19,12 +21,11 @@ type MessageResize struct { // Turn 容器终端转发器 type Turn struct { - ctx context.Context - ws *websocket.Conn - client *client.Client - execID string - hijack client.ExecAttachResult - closeOnce bool + ctx context.Context + ws *websocket.Conn + client *client.Client + execID string + hijack client.ExecAttachResult } // NewTurn 创建容器终端转发器 @@ -77,11 +78,26 @@ func (t *Turn) Write(p []byte) (n int, err error) { // Close 关闭连接 func (t *Turn) Close() { - if t.closeOnce { - return + // 检查进程是否仍在运行 + 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.closeOnce = true + t.hijack.Close() + _ = t.hijack.CloseWrite() _ = t.client.Close() } diff --git a/pkg/shell/pty.go b/pkg/shell/pty.go index 81cd98b8..c9339e79 100644 --- a/pkg/shell/pty.go +++ b/pkg/shell/pty.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "syscall" + "time" "github.com/coder/websocket" "github.com/creack/pty" @@ -65,8 +66,25 @@ func (t *Turn) Wait() { _ = t.cmd.Wait() } -// Close 关闭 PTY +// Close 关闭 PTY 并终止子进程 func (t *Turn) Close() { + _ = t.cmd.Process.Signal(syscall.SIGTERM) + + // 等待最多 10 秒 + done := make(chan struct{}) + go func() { + _ = t.cmd.Wait() + close(done) + }() + + select { + case <-done: + // 进程已退出 + case <-time.After(10 * time.Second): + // 超时,KILL + _ = t.cmd.Process.Kill() + } + _ = t.ptmx.Close() } diff --git a/pkg/ssh/turn.go b/pkg/ssh/turn.go index 046c9f4a..c2d644dd 100644 --- a/pkg/ssh/turn.go +++ b/pkg/ssh/turn.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "time" "github.com/coder/websocket" "golang.org/x/crypto/ssh" @@ -63,6 +64,22 @@ func (t *Turn) Write(p []byte) (n int, err error) { func (t *Turn) Close() { _ = t.stdin.Close() _ = t.session.Signal(ssh.SIGTERM) + + // 等待最多 10 秒 + done := make(chan struct{}) + go func() { + _ = t.session.Wait() + close(done) + }() + + select { + case <-done: + // 会话已退出 + case <-time.After(10 * time.Second): + // 超时,KILL + _ = t.session.Signal(ssh.SIGKILL) + } + _ = t.session.Close() } diff --git a/web/src/components/common/PtyTerminalModal.vue b/web/src/components/common/PtyTerminalModal.vue index d0eda9a4..c5ef286c 100644 --- a/web/src/components/common/PtyTerminalModal.vue +++ b/web/src/components/common/PtyTerminalModal.vue @@ -250,7 +250,8 @@ defineExpose({ v-model:show="show" preset="card" :title="title || $gettext('Terminal')" - style="width: 90vw; height: 80vh" + style="width: 90vw; height: 80vh; max-height: 80vh" + :content-style="{ display: 'flex', flexDirection: 'column', overflow: 'hidden', flex: 1, minHeight: 0 }" size="huge" :bordered="false" :segmented="false" @@ -263,7 +264,7 @@ defineExpose({