Files
mayfly-go/server/internal/machine/mcm/terminal_handler.go

215 lines
4.7 KiB
Go
Raw 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 mcm
import (
"bytes"
"fmt"
"mayfly-go/pkg/errorx"
"strings"
"time"
"github.com/veops/go-ansiterm"
)
// 命令过滤函数若返回error则不执行该命令
type CmdFilterFunc func(cmd string) error
const (
CR = 0x0d // 单个字节 13 通常表示发送一个 CRCarriage Return回车字符 \r
EOT = 0x03 // 通过向标准输入发送单个字节 3 通常表示发送一个 EOTEnd of Transmission信号。EOT 是一种控制字符,在通信中用于指示数据传输的结束。发送 EOT 信号可以被用来终止当前的交互或数据传输
)
type ExecutedCmd struct {
Cmd string `json:"cmd"` // 执行的命令
Time int64 `json:"time"` // 执行时间戳
}
// TerminalHandler 终端处理器
type TerminalHandler struct {
Filters []CmdFilterFunc
ExecutedCmds []*ExecutedCmd // 已执行的命令
Parser *Parser
}
// PreWriteHandle 写入数据至终端前的处理,可进行过滤等操作
func (tf *TerminalHandler) PreWriteHandle(p []byte) error {
tf.Parser.AppendInputData(p)
// 不是回车命令,则表示命令未结束
if bytes.LastIndex(p, []byte{CR}) != 0 {
return nil
}
// time.Sleep(time.Millisecond * 30)
command := tf.Parser.GetCmd()
// 重置终端输入输出
tf.Parser.Reset()
if command == "" {
return nil
}
// 执行命令过滤器
for _, filter := range tf.Filters {
if err := filter(command); err != nil {
msg := fmt.Sprintf("\r\n%s%s", tf.Parser.Ps1, GetErrorContent(err.Error()))
return errorx.NewBiz(msg)
}
}
// 记录执行命令
tf.ExecutedCmds = append(tf.ExecutedCmds, &ExecutedCmd{
Cmd: command,
Time: time.Now().Unix(),
})
return nil
}
// HandleRead 处理从终端读取的数据进行操作
func (tf *TerminalHandler) HandleRead(data []byte) error {
tf.Parser.AppendOutData(data)
return nil
}
type Parser struct {
Output *ansiterm.ByteStream
InputData []byte
OutputData []byte
Ps1 string
vimState bool
commandState bool
}
func NewParser(width, height int) *Parser {
return &Parser{
Output: NewParserByteStream(width, height),
vimState: false,
commandState: true,
}
}
func NewParserByteStream(width, height int) *ansiterm.ByteStream {
screen := ansiterm.NewScreen(width, height)
stream := ansiterm.InitByteStream(screen, false)
stream.Attach(screen)
return stream
}
var (
enterMarks = [][]byte{
[]byte("\x1b[?1049h"),
[]byte("\x1b[?1048h"),
[]byte("\x1b[?1047h"),
[]byte("\x1b[?47h"),
[]byte("\x1b[?25l"),
}
exitMarks = [][]byte{
[]byte("\x1b[?1049l"),
[]byte("\x1b[?1048l"),
[]byte("\x1b[?1047l"),
[]byte("\x1b[?47l"),
}
screenMarks = [][]byte{
{0x1b, 0x5b, 0x4b, 0x0d, 0x0a},
{0x1b, 0x5b, 0x34, 0x6c},
}
)
func (p *Parser) AppendInputData(data []byte) {
if len(p.InputData) == 0 {
// 如 "root@cloud-s0ervh-hh87:~# " 获取前一段用户名等提示内容
p.Ps1 = p.GetOutput()
}
p.InputData = append(p.InputData, data...)
}
func (p *Parser) AppendOutData(data []byte) {
// 非编辑等状态,则追加输出内容
if !p.State(data) {
p.OutputData = append(p.OutputData, data...)
}
}
// GetCmd 获取执行的命令
func (p *Parser) GetCmd() string {
// "root@cloud-s0ervh-hh87:~# ls"
s := p.GetOutput()
// Ps1 = "root@cloud-s0ervh-hh87:~# "
return strings.TrimPrefix(s, p.Ps1)
}
func (p *Parser) Reset() {
p.Output.Listener.Reset()
p.OutputData = nil
p.InputData = nil
}
func (p *Parser) GetOutput() string {
p.Output.Feed(p.OutputData)
res := parseOutput(p.Output.Listener.Display())
if len(res) == 0 {
return ""
}
return res[len(res)-1]
}
func parseOutput(data []string) (output []string) {
for _, line := range data {
if strings.TrimSpace(line) != "" {
output = append(output, line)
}
}
return output
}
func (p *Parser) State(b []byte) bool {
if !p.vimState && IsEditEnterMode(b) {
if !isNewScreen(b) {
p.vimState = true
p.commandState = false
}
}
if p.vimState && IsEditExitMode(b) {
// 重置终端输入输出
p.Reset()
p.vimState = false
p.commandState = true
}
return p.vimState
}
func isNewScreen(p []byte) bool {
return matchMark(p, screenMarks)
}
func IsEditEnterMode(p []byte) bool {
return matchMark(p, enterMarks)
}
func IsEditExitMode(p []byte) bool {
return matchMark(p, exitMarks)
}
func matchMark(p []byte, marks [][]byte) bool {
for _, item := range marks {
if bytes.Contains(p, item) {
return true
}
}
return false
}
// GetErrorContent 包装返回终端错误消息
func GetErrorContent(msg string) string {
return fmt.Sprintf("\033[1;31m%s\033[0m", msg)
}
// GetErrorContentRn 包装返回终端错误消息, 并自动回车换行
func GetErrorContentRn(msg string) string {
return fmt.Sprintf("\r\n%s", GetErrorContent(msg))
}