mirror of
https://github.com/fankes/komari-agent.git
synced 2025-10-18 18:49:23 +08:00
feat: 实现跨平台终端启动功能,整合Unix和Windows终端逻辑
This commit is contained in:
113
terminal/terminal.go
Normal file
113
terminal/terminal.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Terminal 接口定义平台特定的终端操作
|
||||||
|
type Terminal interface {
|
||||||
|
Close() error
|
||||||
|
Read(p []byte) (int, error)
|
||||||
|
Write(p []byte) (int, error)
|
||||||
|
Resize(cols, rows int) error
|
||||||
|
Wait() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// terminalImpl 封装终端和平台特定逻辑
|
||||||
|
type terminalImpl struct {
|
||||||
|
shell string
|
||||||
|
workingDir string
|
||||||
|
term Terminal
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartTerminal 启动终端并处理 WebSocket 通信
|
||||||
|
func StartTerminal(conn *websocket.Conn) {
|
||||||
|
impl, err := newTerminalImpl()
|
||||||
|
if err != nil {
|
||||||
|
conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("Error: %v\r\n", err)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
defer impl.term.Close()
|
||||||
|
// 从 WebSocket 读取消息并写入终端
|
||||||
|
go handleWebSocketInput(conn, impl.term, errChan)
|
||||||
|
|
||||||
|
// 从终端读取输出并写入 WebSocket
|
||||||
|
go handleTerminalOutput(conn, impl.term, errChan)
|
||||||
|
|
||||||
|
// 错误处理和清理
|
||||||
|
go func() {
|
||||||
|
err := <-errChan
|
||||||
|
if err != nil && conn != nil {
|
||||||
|
conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("Error: %v\r\n", err)))
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
impl.term.Close()
|
||||||
|
}()
|
||||||
|
// 等待终端进程结束
|
||||||
|
if err := impl.term.Wait(); err != nil {
|
||||||
|
select {
|
||||||
|
case errChan <- err:
|
||||||
|
// 错误已发送
|
||||||
|
default:
|
||||||
|
// 错误通道已满或已关闭
|
||||||
|
conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("Terminal exited with error: %v\r\n", err)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleWebSocketInput 处理 WebSocket 输入
|
||||||
|
func handleWebSocketInput(conn *websocket.Conn, term Terminal, errChan chan<- error) {
|
||||||
|
for {
|
||||||
|
t, p, err := conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
errChan <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if t == websocket.TextMessage {
|
||||||
|
var cmd struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Cols int `json:"cols,omitempty"`
|
||||||
|
Rows int `json:"rows,omitempty"`
|
||||||
|
Input string `json:"input,omitempty"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(p, &cmd); err == nil {
|
||||||
|
switch cmd.Type {
|
||||||
|
case "resize":
|
||||||
|
if cmd.Cols > 0 && cmd.Rows > 0 {
|
||||||
|
term.Resize(cmd.Cols, cmd.Rows)
|
||||||
|
}
|
||||||
|
case "input":
|
||||||
|
if cmd.Input != "" {
|
||||||
|
term.Write([]byte(cmd.Input))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
term.Write(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t == websocket.BinaryMessage {
|
||||||
|
term.Write(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleTerminalOutput 处理终端输出
|
||||||
|
func handleTerminalOutput(conn *websocket.Conn, term Terminal, errChan chan<- error) {
|
||||||
|
buf := make([]byte, 4096)
|
||||||
|
for {
|
||||||
|
n, err := term.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
errChan <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := conn.WriteMessage(websocket.BinaryMessage, buf[:n]); err != nil {
|
||||||
|
errChan <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -3,117 +3,73 @@
|
|||||||
package terminal
|
package terminal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/creack/pty"
|
"github.com/creack/pty"
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// StartTerminal 在Unix/Linux系统上启动终端
|
func newTerminalImpl() (*terminalImpl, error) {
|
||||||
func StartTerminal(conn *websocket.Conn) {
|
// 查找可用 shell
|
||||||
// 获取shell
|
defaultShells := []string{"zsh", "bash", "sh"}
|
||||||
defalut_shell := []string{"zsh", "bash", "sh"}
|
|
||||||
shell := ""
|
shell := ""
|
||||||
for _, s := range defalut_shell {
|
for _, s := range defaultShells {
|
||||||
if _, err := exec.LookPath(s); err == nil {
|
if _, err := exec.LookPath(s); err == nil {
|
||||||
shell = s
|
shell = s
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if shell == "" {
|
if shell == "" {
|
||||||
conn.WriteMessage(websocket.TextMessage, []byte("No supported shell found."))
|
return nil, fmt.Errorf("no supported shell found")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建进程
|
// 创建进程
|
||||||
cmd := exec.Command(shell)
|
cmd := exec.Command(shell)
|
||||||
cmd.Env = append(cmd.Env, "TERM=xterm-256color")
|
cmd.Env = append(cmd.Env, "TERM=xterm-256color")
|
||||||
tty, err := pty.Start(cmd)
|
tty, err := pty.Start(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("Error: %v\r\n", err)))
|
return nil, fmt.Errorf("failed to start pty: %v", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
defer tty.Close()
|
|
||||||
// 设置终端大小
|
|
||||||
pty.Setsize(tty, &pty.Winsize{
|
|
||||||
Rows: 24,
|
|
||||||
Cols: 80,
|
|
||||||
X: 0,
|
|
||||||
Y: 0,
|
|
||||||
})
|
|
||||||
terminateConn := func() {
|
|
||||||
pgid, err := syscall.Getpgid(cmd.Process.Pid)
|
|
||||||
if err != nil {
|
|
||||||
cmd.Process.Kill()
|
|
||||||
}
|
|
||||||
syscall.Kill(-pgid, syscall.SIGKILL)
|
|
||||||
if conn != nil {
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err_chan := make(chan error, 1)
|
|
||||||
// 从WebSocket读取数据并写入pty
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
t, p, err := conn.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
err_chan <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if t == websocket.TextMessage {
|
|
||||||
var cmd struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Cols int `json:"cols,omitempty"`
|
|
||||||
Rows int `json:"rows,omitempty"`
|
|
||||||
Input string `json:"input,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(p, &cmd); err == nil {
|
// 设置初始终端大小
|
||||||
switch cmd.Type {
|
pty.Setsize(tty, &pty.Winsize{Rows: 24, Cols: 80})
|
||||||
case "resize":
|
|
||||||
if cmd.Cols > 0 && cmd.Rows > 0 {
|
|
||||||
pty.Setsize(tty, &pty.Winsize{
|
|
||||||
Rows: uint16(cmd.Rows),
|
|
||||||
Cols: uint16(cmd.Cols),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
case "input":
|
|
||||||
if cmd.Input != "" {
|
|
||||||
tty.Write([]byte(cmd.Input))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tty.Write(p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if t == websocket.BinaryMessage {
|
|
||||||
tty.Write(p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
return &terminalImpl{
|
||||||
buf := make([]byte, 4096)
|
shell: shell,
|
||||||
for {
|
term: &unixTerminal{
|
||||||
n, err := tty.Read(buf)
|
tty: tty,
|
||||||
if err != nil {
|
cmd: cmd,
|
||||||
err_chan <- err
|
},
|
||||||
return
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = conn.WriteMessage(websocket.BinaryMessage, buf[:n])
|
type unixTerminal struct {
|
||||||
if err != nil {
|
tty *os.File
|
||||||
err_chan <- err
|
cmd *exec.Cmd
|
||||||
return
|
}
|
||||||
}
|
|
||||||
}
|
func (t *unixTerminal) Close() error {
|
||||||
}()
|
pgid, err := syscall.Getpgid(t.cmd.Process.Pid)
|
||||||
|
if err != nil {
|
||||||
err = <-err_chan
|
return t.cmd.Process.Kill()
|
||||||
if err != nil && conn != nil {
|
}
|
||||||
conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("Error: %v\r\n", err)))
|
return syscall.Kill(-pgid, syscall.SIGKILL)
|
||||||
}
|
}
|
||||||
terminateConn()
|
|
||||||
|
func (t *unixTerminal) Read(p []byte) (int, error) {
|
||||||
|
return t.tty.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *unixTerminal) Write(p []byte) (int, error) {
|
||||||
|
return t.tty.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *unixTerminal) Resize(cols, rows int) error {
|
||||||
|
return pty.Setsize(t.tty, &pty.Winsize{Rows: uint16(rows), Cols: uint16(cols)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *unixTerminal) Wait() error {
|
||||||
|
return t.cmd.Wait()
|
||||||
}
|
}
|
||||||
|
@@ -4,105 +4,77 @@ package terminal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/UserExistsError/conpty"
|
"github.com/UserExistsError/conpty"
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// StartTerminal 在Windows系统上启动终端
|
func newTerminalImpl() (*terminalImpl, error) {
|
||||||
func StartTerminal(conn *websocket.Conn) {
|
// 查找 shell
|
||||||
// 创建进程
|
|
||||||
shell, err := exec.LookPath("powershell.exe")
|
shell, err := exec.LookPath("powershell.exe")
|
||||||
if err != nil || shell == "" {
|
if err != nil || shell == "" {
|
||||||
shell = "cmd.exe"
|
shell = "cmd.exe"
|
||||||
}
|
}
|
||||||
current_dir := "."
|
if shell == "" {
|
||||||
executable, err := os.Executable()
|
return nil, fmt.Errorf("no supported shell found")
|
||||||
if err == nil {
|
|
||||||
current_dir = filepath.Dir(executable)
|
|
||||||
}
|
|
||||||
if shell == "" || current_dir == "" {
|
|
||||||
conn.WriteMessage(websocket.TextMessage, []byte("No supported shell found."))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tty, err := conpty.Start(shell, conpty.ConPtyWorkDir(current_dir))
|
// 获取工作目录
|
||||||
if err != nil {
|
workingDir := "."
|
||||||
conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("Error: %v\r\n", err)))
|
if executable, err := os.Executable(); err == nil {
|
||||||
return
|
workingDir = filepath.Dir(executable)
|
||||||
}
|
}
|
||||||
defer tty.Close()
|
|
||||||
err_chan := make(chan error, 1)
|
// 启动 ConPTY
|
||||||
// 设置终端大小
|
tty, err := conpty.Start(shell, conpty.ConPtyWorkDir(workingDir))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to start conpty: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置初始终端大小
|
||||||
tty.Resize(80, 24)
|
tty.Resize(80, 24)
|
||||||
|
|
||||||
go func() {
|
return &terminalImpl{
|
||||||
for {
|
shell: shell,
|
||||||
t, p, err := conn.ReadMessage()
|
workingDir: workingDir,
|
||||||
if err != nil {
|
term: &windowsTerminal{
|
||||||
err_chan <- err
|
tty: tty,
|
||||||
return
|
},
|
||||||
}
|
}, nil
|
||||||
if t == websocket.TextMessage {
|
}
|
||||||
var cmd struct {
|
|
||||||
Type string `json:"type"`
|
type windowsTerminal struct {
|
||||||
Cols int `json:"cols,omitempty"`
|
tty *conpty.ConPty
|
||||||
Rows int `json:"rows,omitempty"`
|
closed bool
|
||||||
Input string `json:"input,omitempty"`
|
}
|
||||||
}
|
|
||||||
|
func (t *windowsTerminal) Close() error {
|
||||||
if err := json.Unmarshal(p, &cmd); err == nil {
|
if t.closed {
|
||||||
switch cmd.Type {
|
return nil
|
||||||
case "resize":
|
}
|
||||||
if cmd.Cols > 0 && cmd.Rows > 0 {
|
if err := t.tty.Close(); err != nil {
|
||||||
tty.Resize(cmd.Cols, cmd.Rows)
|
return err
|
||||||
}
|
}
|
||||||
case "input":
|
t.closed = true
|
||||||
if cmd.Input != "" {
|
return nil
|
||||||
tty.Write([]byte(cmd.Input))
|
}
|
||||||
}
|
|
||||||
}
|
func (t *windowsTerminal) Read(p []byte) (int, error) {
|
||||||
} else {
|
return t.tty.Read(p)
|
||||||
tty.Write(p)
|
}
|
||||||
}
|
|
||||||
}
|
func (t *windowsTerminal) Write(p []byte) (int, error) {
|
||||||
if t == websocket.BinaryMessage {
|
return t.tty.Write(p)
|
||||||
tty.Write(p)
|
}
|
||||||
}
|
|
||||||
}
|
func (t *windowsTerminal) Resize(cols, rows int) error {
|
||||||
}()
|
return t.tty.Resize(cols, rows)
|
||||||
|
}
|
||||||
go func() {
|
|
||||||
buf := make([]byte, 4096)
|
func (t *windowsTerminal) Wait() error {
|
||||||
for {
|
_, err := t.tty.Wait(context.Background())
|
||||||
n, err := tty.Read(buf)
|
return err
|
||||||
if err != nil {
|
|
||||||
err_chan <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = conn.WriteMessage(websocket.BinaryMessage, buf[:n])
|
|
||||||
if err != nil {
|
|
||||||
err_chan <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
err := <-err_chan
|
|
||||||
if err != nil && tty != nil {
|
|
||||||
conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("Error: %v\r\n", err)))
|
|
||||||
}
|
|
||||||
conn.Close()
|
|
||||||
tty.Close()
|
|
||||||
}()
|
|
||||||
tty.Wait(context.Background())
|
|
||||||
tty.Close()
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user