2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 06:47:20 +08:00
Files
panel/pkg/shell/pty.go
Copilot 8031e53852 feat: 添加容器编排实时进度显示功能 (#1217)
* Initial plan

* feat: 添加容器编排实时进度显示功能

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

* fix: 修复代码审查问题,使用动态路径和安全的字体大小操作

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

* docs: 完成容器编排实时进度显示功能

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

* feat: 容器优化

* fix: 用户关闭命令窗口时杀死正在运行的进程

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

* refactor: 改进 WebSocket 断开检测,支持用户输入转发到 PTY

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

* feat: 启用终端用户输入支持,转发键盘输入到 PTY

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

* feat: 容器优化

* fix: lint

* feat: 添加 PTY 窗口大小调整支持

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

* 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 18:37:01 +08:00

150 lines
3.2 KiB
Go

package shell
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"syscall"
"github.com/coder/websocket"
"github.com/creack/pty"
)
// MessageResize 终端大小调整消息
type MessageResize struct {
Resize bool `json:"resize"`
Columns uint `json:"columns"`
Rows uint `json:"rows"`
}
// Turn PTY 终端
type Turn struct {
ctx context.Context
ws *websocket.Conn
ptmx *os.File
cmd *exec.Cmd
}
// NewPTYTurn 使用 PTY 执行命令,返回 Turn 用于流式读取输出
// 调用方需要负责调用 Close() 和 Wait()
func NewPTYTurn(ctx context.Context, ws *websocket.Conn, shell string, args ...any) (*Turn, error) {
if !preCheckArg(args) {
return nil, errors.New("command contains illegal characters")
}
if len(args) > 0 {
shell = fmt.Sprintf(shell, args...)
}
_ = os.Setenv("LC_ALL", "C")
cmd := exec.CommandContext(ctx, "bash", "-c", shell)
ptmx, err := pty.Start(cmd)
if err != nil {
return nil, fmt.Errorf("failed to start pty: %w", err)
}
return &Turn{
ctx: ctx,
ws: ws,
ptmx: ptmx,
cmd: cmd,
}, nil
}
// Write 写入 PTY 输入
func (t *Turn) Write(data []byte) (int, error) {
return t.ptmx.Write(data)
}
// Wait 等待命令完成
func (t *Turn) Wait() {
_ = t.cmd.Wait()
}
// Close 关闭 PTY
func (t *Turn) Close() {
_ = t.ptmx.Close()
}
// Handle 从 WebSocket 读取输入写入 PTY
func (t *Turn) Handle(ctx context.Context) error {
var resize MessageResize
go func() { _ = t.Pipe(ctx) }()
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.Resize(uint16(resize.Rows), uint16(resize.Columns)); err != nil {
return fmt.Errorf("failed to resize terminal: %w", err)
}
}
continue
}
if _, err = t.Write(data); err != nil {
return fmt.Errorf("failed to write to pty stdin: %w", err)
}
}
}
}
// Pipe 从 PTY 读取输出写入 WebSocket
func (t *Turn) Pipe(ctx context.Context) error {
buf := make([]byte, 8192)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
n, err := t.ptmx.Read(buf)
if err != nil {
if err = IsPTYError(err); err != nil {
return fmt.Errorf("failed to read from pty: %w", err)
}
return nil
}
if n > 0 {
if err = t.ws.Write(ctx, websocket.MessageBinary, buf[:n]); err != nil {
return fmt.Errorf("failed to write to ws: %w", err)
}
}
}
}
}
// Resize 调整 PTY 窗口大小
func (t *Turn) Resize(rows, cols uint16) error {
return pty.Setsize(t.ptmx, &pty.Winsize{
Rows: rows,
Cols: cols,
})
}
// IsPTYError Linux kernel return EIO when attempting to read from a master pseudo
// terminal which no longer has an open slave. So ignore error here.
// See https://github.com/creack/pty/issues/21
func IsPTYError(err error) error {
var pathErr *os.PathError
if !errors.As(err, &pathErr) || !errors.Is(pathErr.Err, syscall.EIO) || !errors.Is(err, io.EOF) {
return err
}
return nil
}