mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 13:47:15 +08:00
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>
This commit is contained in:
@@ -10,7 +10,6 @@ import (
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/creack/pty"
|
||||
@@ -208,7 +207,7 @@ func ExecfWithTTY(shell string, args ...any) (string, error) {
|
||||
}
|
||||
defer func(f *os.File) { _ = f.Close() }(f)
|
||||
|
||||
if _, err = io.Copy(&out, f); ptyError(err) != nil {
|
||||
if _, err = io.Copy(&out, f); IsPTYError(err) != nil {
|
||||
return "", fmt.Errorf("run %s failed, out: %s, err: %w", shell, strings.TrimSpace(out.String()), err)
|
||||
}
|
||||
if stderr.Len() > 0 {
|
||||
@@ -228,15 +227,3 @@ func preCheckArg(args []any) bool {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 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 ptyError(err error) error {
|
||||
var pathErr *os.PathError
|
||||
if !errors.As(err, &pathErr) || !errors.Is(pathErr.Err, syscall.EIO) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
149
pkg/shell/pty.go
Normal file
149
pkg/shell/pty.go
Normal file
@@ -0,0 +1,149 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user