fix(webconsole): record command input and ps1

This commit is contained in:
Zexi Li
2022-04-23 13:22:28 +08:00
parent ea7c6cf213
commit a5a098b751
16 changed files with 969 additions and 27 deletions

1
go.mod
View File

@@ -12,6 +12,7 @@ require (
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
github.com/DataDog/dd-trace-go v0.6.1 // indirect
github.com/DataDog/zstd v1.3.4 // indirect
github.com/LeeEirc/terminalparser v0.0.0-20220328021224-de16b7643ea4
github.com/Masterminds/goutils v1.1.0 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible

3
go.sum
View File

@@ -77,6 +77,8 @@ github.com/DataDog/dd-trace-go v0.6.1 h1:nsZ2lohbSw1CKtfNRu3wPh1jFirv6XSz8vqNpuI
github.com/DataDog/dd-trace-go v0.6.1/go.mod h1:SmQTTcC37XMyEm75HV0AWiZIYxDiaNhRi49zorIpW+o=
github.com/DataDog/zstd v1.3.4 h1:LAGHkXuvC6yky+C2CUG2tD7w8QlrUwpue8XwIh0X4AY=
github.com/DataDog/zstd v1.3.4/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/LeeEirc/terminalparser v0.0.0-20220328021224-de16b7643ea4 h1:Gk7m4Nu2jqVqJAJqNlTYqkiq96WkANAtB4fVi+t7Xv8=
github.com/LeeEirc/terminalparser v0.0.0-20220328021224-de16b7643ea4/go.mod h1:tiLv6VBLH4Z3KdBSe2qIKRwQDGCVQ9/F5fOKpQGvyoA=
github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg=
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
@@ -482,6 +484,7 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=

View File

@@ -37,7 +37,7 @@ func NewCommandLogManager() *CommandLogManager {
modulebase.ResourceManager{
BaseManager: *modulebase.NewBaseManager("webconsole", "", "webconsole", []string{
"id", "ops_time", "obj_id", "obj_type", "obj_name", "user", "user_id", "tenant", "tenant_id", "owner_tenant_id", "action", "notes",
"session_id", "accessed_at", "type", "login_user", "start_time", "command",
"session_id", "accessed_at", "type", "login_user", "start_time", "ps1", "command",
}, nil),
Keyword: "commandlog", KeywordPlural: "commandlogs",
},

View File

@@ -74,6 +74,7 @@ type SCommandLog struct {
Type string `width:"32" charset:"utf8" nullable:"true" list:"user" create:"required"`
LoginUser string `charset:"utf8" list:"user" create:"required"`
StartTime time.Time `list:"user" create:"required"`
Ps1 string `charset:"utf8" list:"user" create:"optional" json:"ps1"`
Command CommandType `charset:"utf8" list:"user" create:"required"`
}
@@ -96,6 +97,7 @@ type CommandLogCreateInput struct {
Type CommandType
LoginUser string
StartTime time.Time
Ps1 string `json:"ps1"`
Command string
Notes jsonutils.JSONObject
}

View File

@@ -16,8 +16,11 @@ package recorder
import (
"strings"
"sync"
"time"
"github.com/LeeEirc/terminalparser"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
@@ -28,7 +31,7 @@ import (
type Recoder interface {
Start()
Write(data string)
Write(userInput string, ptyOutput string)
}
type Object struct {
@@ -50,44 +53,90 @@ func NewObject(id, name, oType, loginUser string, notes jsonutils.JSONObject) *O
}
type cmdRecoder struct {
cs *mcclient.ClientSession
sessionId string
accessedAt time.Time
buffer []string
curCmd string
cmdCh chan string
object *Object
cs *mcclient.ClientSession
sessionId string
accessedAt time.Time
userInputStart bool
userInputBuff string
ptyInitialOutput string
ptyOutputBuff string
ps1Parsed bool
ps1 string
cmdCh chan string
object *Object
wLock *sync.Mutex
}
func NewCmdRecorder(s *mcclient.ClientSession, obj *Object, sessionId string, accessedAt time.Time) Recoder {
return &cmdRecoder{
cs: s,
sessionId: sessionId,
accessedAt: accessedAt,
buffer: make([]string, 0),
curCmd: "",
cmdCh: make(chan string),
object: obj,
cs: s,
sessionId: sessionId,
accessedAt: accessedAt,
userInputStart: false,
userInputBuff: "",
ptyOutputBuff: "",
cmdCh: make(chan string),
object: obj,
wLock: new(sync.Mutex),
}
}
func (r *cmdRecoder) Write(data string) {
if data == "\n" || data == "\r" {
r.sendMessage(r.curCmd)
func (r *cmdRecoder) Write(userInput string, ptyOutput string) {
r.wLock.Lock()
defer r.wLock.Unlock()
if len(userInput) != 0 {
r.userInputStart = true
}
if !r.userInputStart && userInput == "" && len(ptyOutput) > 0 {
r.ptyInitialOutput += ptyOutput
}
// try parse PS1
if !r.ps1Parsed && r.userInputBuff != "" && userInput == "\r" && r.ptyInitialOutput != "" {
outs := r.parsePtyOutputs(r.ptyInitialOutput)
if len(outs) != 0 && len(outs) > 1 {
r.ps1 = outs[len(outs)-1]
}
r.ps1Parsed = true
}
// user enter command
if userInput == "\r" && r.userInputBuff != "" {
r.sendMessage(r.ptyOutputBuff)
return
}
r.curCmd += data
r.userInputBuff += userInput
r.ptyOutputBuff += ptyOutput
}
func (r *cmdRecoder) cleanCmd() *cmdRecoder {
r.curCmd = ""
r.userInputBuff = ""
r.ptyOutputBuff = ""
return r
}
func (r *cmdRecoder) sendMessage(msg string) {
r.cmdCh <- msg
func (r *cmdRecoder) parsePtyOutputs(data string) []string {
s := terminalparser.Screen{
Rows: make([]*terminalparser.Row, 0, 1024),
Cursor: &terminalparser.Cursor{},
}
return s.Parse([]byte(data))
}
func (r *cmdRecoder) sendMessage(ptyOutputBuff string) {
ptyOuts := r.parsePtyOutputs(ptyOutputBuff)
if len(ptyOuts) == 0 {
return
}
cmd := ptyOuts[len(ptyOuts)-1]
if r.ps1 != "" {
cmd = strings.TrimPrefix(cmd, r.ps1)
}
r.cleanCmd()
log.Infof("Message %q sended", msg)
r.cmdCh <- cmd
log.Debugf("sendMessage ps1: %q, ptyOuts: %#v, cmd: %q", r.ps1, ptyOuts, cmd)
}
func (r *cmdRecoder) Start() {
@@ -105,6 +154,9 @@ func (r *cmdRecoder) save(command string) error {
if r.object == nil {
return nil
}
if command == "" {
return nil
}
userCred := r.cs.GetToken()
input := r.newModelInput(userCred, command)
_, err := models.GetCommandLogManager().Create(r.cs.GetContext(), userCred, input)
@@ -135,6 +187,7 @@ func (r *cmdRecoder) newModelInput(userCred mcclient.TokenCredential, command st
LoginUser: r.object.LoginUser,
Type: models.CommandTypeSSH,
StartTime: time.Now(),
Ps1: r.ps1,
Command: command,
}
}

View File

@@ -67,7 +67,7 @@ func (server *TTYServer) initEventHandler(s *session.SSession) {
}
func initSocketHandler(so socketio.Socket, p *session.Pty) {
// handle read
// handle command output
go func() {
for !p.Exit {
if p.IsInShellMode() {
@@ -82,7 +82,9 @@ func initSocketHandler(so socketio.Socket, p *session.Pty) {
*/
cleanUp(so, p)
} else {
// log.Errorf("--p.Pty.output data: %q", data)
so.Emit(OUTPUT_EVENT, string(data))
go p.Session.GetRecorder().Write("", string(data))
}
continue
}
@@ -95,7 +97,7 @@ func initSocketHandler(so socketio.Socket, p *session.Pty) {
}
}()
// handle write
// handle user input write
so.On(INPUT_EVENT, func(data string) {
if !p.IsInShellMode() {
for _, d := range []byte(data) {
@@ -120,7 +122,7 @@ func initSocketHandler(so socketio.Socket, p *session.Pty) {
}
} else {
p.Pty.Write([]byte(data))
go p.Session.GetRecorder().Write(data)
go p.Session.GetRecorder().Write(data, "")
}
})

16
vendor/github.com/LeeEirc/terminalparser/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,16 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
.idea

View File

@@ -0,0 +1,76 @@
package terminalparser
/*
Control Character - A single character with an ASCII code with the range
of 000 to 037 and 200 to 237 octal, 00 to 1F and 80 to 9F hex.
*/
/*
C0 Control 000-037 octal, 00-1F hex (G0 is 041-176 octal, 21-7E hex)
SPACE 040+240 octal, 20+A0 hex Always and everywhere a blank space
Intermediate 040-057 octal, 20-2F hex !"#$%&'()*+,-./
Parameters 060-077 octal, 30-3F hex 0123456789:;<=>?
Uppercase 100-137 octal, 40-5F hex @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
Lowercase 140-176 octal, 60-7E hex `abcdefghijlkmnopqrstuvwxyz{|}~
Alphabetic 100-176 octal, 40-7E hex (all of upper and lower case)
Delete 177 octal, 7F hex Always and everywhere ignored
C1 Control 200-237 octal, 80-9F hex 32 additional control characters
G1 Displayable 241-376 octal, A1-FE hex 94 additional displayable characters
Special 240+377 octal, A0+FF hex Same as SPACE and DELETE
*/
var ASCIITable = [128]rune{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f,
0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f,
}
var C0Control = ASCIITable[0:32]
var Spaces = []rune{0x20, 0xa0}
var Intermediate = ASCIITable[32:48]
var Parameters = ASCIITable[48:64]
var Uppercase = ASCIITable[64:96]
var Lowercase = ASCIITable[96:128]
var Alphabetic = ASCIITable[64:128]
// Always and everywhere ignored
var Delete rune = 0x7f
var C1Control = []rune{
0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f,
0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f,
}
var G1Display = []rune{
0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf,
0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf,
0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf,
0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf,
0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef,
0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe,
}
var SpecialSpace = 0xa0
var SpecialDelete = 0xff
const (
NUL rune = 0x00 + iota
SOH
STX
ETX
EOT
ENQ
ACK
BEL
BS
ST rune = 0x9c
ESCKey = 0x1b
)

327
vendor/github.com/LeeEirc/terminalparser/csi_func.go generated vendored Normal file
View File

@@ -0,0 +1,327 @@
package terminalparser
import (
"log"
"strconv"
"strings"
)
type screenCsiFunc func(screen *Screen, params []rune)
var CSIFuncMap = map[rune]screenCsiFunc{
'@': func(s *Screen, params []rune) {
currentRow := s.GetCursorRow()
switch len(params) {
case 1:
if ps, err := strconv.Atoi(string(params)); err == nil {
insertData := make([]rune, ps)
for i := 0; i < ps; i++ {
insertData[i] = Spaces[0]
}
currentRow.changeCursorToX(s.Cursor.X)
currentRow.insertCharacters(insertData)
}
case 2:
if params[len(params)-1] == Spaces[0] {
if _, err := strconv.Atoi(string(params[0])); err == nil {
log.Printf("Screen 不支持解析 CSI `%s` @\n", string(params))
}
}
default:
currentRow.changeCursorToX(s.Cursor.X)
currentRow.insertCharacters([]rune{Spaces[0]})
}
},
'A': func(s *Screen, params []rune) {
/*
CSI Ps A Cursor Up Ps Times (default = 1) (CUU).
CSI Ps SP A
Shift right Ps columns(s) (default = 1) (SR), ECMA-48.
*/
switch len(params) {
case 0:
s.Cursor.MoveUp(1)
case 1:
if ps, err := strconv.Atoi(string(params)); err == nil {
s.Cursor.MoveUp(ps)
}
case 2:
if params[len(params)-1] == Spaces[0] {
if ps, err := strconv.Atoi(string(params[0])); err == nil {
s.Cursor.MoveRight(ps)
log.Printf("Shift right %d columns(s) \n", ps)
}
}
default:
if params[len(params)-1] == Spaces[0] {
if ps, err := strconv.Atoi(string(params[0])); err == nil {
s.Cursor.MoveRight(ps)
log.Printf("Shift right %d columns(s) \n", ps)
}
}
}
},
'B': func(s *Screen, params []rune) {
/*
CSI Ps B Cursor Down Ps Times (default = 1) (CUD).
*/
switch len(params) {
case 0:
s.Cursor.MoveDown(1)
default:
if ps, err := strconv.Atoi(string(params)); err == nil {
s.Cursor.MoveDown(ps)
}
}
},
'C': func(s *Screen, params []rune) {
/*
CSI Ps C Cursor Forward Ps Times (default = 1) (CUF).
*/
switch len(params) {
case 0:
s.Cursor.MoveRight(1)
default:
if ps, err := strconv.Atoi(string(params)); err == nil {
s.Cursor.MoveRight(ps)
}
}
},
'D': func(s *Screen, params []rune) {
/*
CSI Ps D Cursor Backward Ps Times (default = 1) (CUB).
*/
switch len(params) {
case 0:
s.Cursor.MoveLeft(1)
default:
if ps, err := strconv.Atoi(string(params)); err == nil {
s.Cursor.MoveLeft(ps)
}
}
},
'E': func(s *Screen, params []rune) {
/*
CSI Ps E Cursor Next Line Ps Times (default = 1) (CNL).
*/
switch len(params) {
case 0:
s.Cursor.MoveDown(1)
default:
if ps, err := strconv.Atoi(string(params)); err == nil {
s.Cursor.MoveDown(ps)
}
}
},
'F': func(s *Screen, params []rune) {
/*
CSI Ps F Cursor Preceding Line Ps Times (default = 1) (CPL).
*/
switch len(params) {
case 0:
s.Cursor.MoveUp(1)
case 1:
if ps, err := strconv.Atoi(string(params)); err == nil {
s.Cursor.MoveUp(ps)
}
default:
if ps, err := strconv.Atoi(string(params)); err == nil {
s.Cursor.MoveUp(ps)
}
}
},
'I': func(screen *Screen, params []rune) {
log.Println("Screen 不支持 I")
},
'G': func(s *Screen, params []rune) {
switch len(params) {
case 1:
if ps, err := strconv.Atoi(string(params)); err == nil {
s.Cursor.X = ps
}
}
},
'H': func(s *Screen, params []rune) {
if len(params) == 3 && params[1] == ';' {
if row, err := strconv.Atoi(string(params[0])); err == nil {
s.Cursor.Y = row
}
if column, err := strconv.Atoi(string(params[2])); err == nil {
s.Cursor.X = column
}
}
if len(params) == 0 {
s.Cursor.MoveHome()
}
},
'J': func(s *Screen, params []rune) {
/*
ESC J Erase from the cursor to the end of the screen.
CSI Ps J Erase in Display (ED), VT100.
Ps = 0 ⇒ Erase Below (default).
Ps = 1 ⇒ Erase Above.
Ps = 2 ⇒ Erase All.
Ps = 3 ⇒ Erase Saved Lines, xterm.
*/
switch len(params) {
case 0:
s.eraseFromCursor()
case 1:
if ps, err := strconv.Atoi(string(params[0])); err == nil {
switch ps {
case 0:
s.eraseBelow()
case 1:
s.eraseAbove()
case 2:
s.eraseAll()
case 3:
log.Println("screen 未处理Erase Saved Lines, xterm.")
default:
log.Printf("screen 未处理 Erase.%d \n", ps)
}
}
case 2:
log.Printf("screen 未处理 Erase in Display (DECSED), VT220. %s\n", string(params))
default:
log.Printf("screen 未处理 Erase %s\n", string(params))
}
},
'K': func(s *Screen, params []rune) {
/*
ESC K Erase from the cursor to the end of the line.
CSI Ps K Erase in Line (EL), VT100.
Ps = 0 ⇒ Erase to Right (default).
Ps = 1 ⇒ Erase to Left.
Ps = 2 ⇒ Erase All.
CSI ? Ps K
Erase in Line (DECSEL), VT220.
Ps = 0 ⇒ Selective Erase to Right (default).
Ps = 1 ⇒ Selective Erase to Left.
Ps = 2 ⇒ Selective Erase All.
*/
switch len(params) {
case 0:
s.eraseEndToLine()
default:
paramsS := string(params)
if strings.HasPrefix(paramsS, "?") {
log.Printf("Screen不支持解析 CSI `%s` K\n", paramsS)
return
}
if ps, err := strconv.Atoi(paramsS); err == nil {
switch ps {
case 0:
s.eraseRight()
case 1:
s.eraseLeft()
case 2:
s.eraseAll()
default:
log.Printf("未处理erase %d\n", ps)
}
}
}
},
'L': func(o *Screen, params []rune) {
/*
CSI Ps L Insert Ps Line(s) (default = 1) (IL).
*/
log.Println("Screen不支持解析L")
},
'M': func(o *Screen, params []rune) {
/*
CSI Ps M Delete Ps Line(s) (default = 1) (DL).
*/
log.Println("Screen不支持解析M")
},
'P': func(s *Screen, params []rune) {
/*
CSI Ps P Delete Ps Character(s) (default = 1) (DCH).
*/
switch len(params) {
case 0:
s.deleteChars(1)
default:
if ps, err := strconv.Atoi(string(params)); err == nil {
s.deleteChars(ps)
}
}
},
'X': func(s *Screen, params []rune) {
switch len(params) {
case 0:
s.deleteChars(1)
default:
if ps, err := strconv.Atoi(string(params)); err == nil {
s.deleteChars(ps)
}
}
},
'd': func(s *Screen, params []rune) {
switch len(params) {
case 0:
s.Cursor.Y = 1
default:
if ps, err := strconv.Atoi(string(params)); err == nil {
s.Cursor.Y = ps
}
}
},
'l': func(screen *Screen, params []rune) {
log.Println("Screen不支持解析l")
screen.pasteMode = false
},
'h': func(screen *Screen, params []rune) {
log.Println("Screen不支持解析h")
screen.pasteMode = true
},
'm': func(s *Screen, params []rune) {
switch len(params) {
case 0:
default:
if ps, err := strconv.Atoi(string(params)); err == nil {
switch ps {
case 30:
/*
针对fish的环境特殊处理
*/
if s.Cursor.Y >= 1 && len(s.Rows) > 0 {
index := s.Cursor.Y - 1
if index >= len(s.Rows) {
index = len(s.Rows) - 1
}
s.Rows[index].stopRecord()
}
case 90:
if s.Cursor.Y >= 1 && len(s.Rows) > 0 {
index := s.Cursor.Y - 1
if index >= len(s.Rows) {
index = len(s.Rows) - 1
}
s.Rows[index].startRecord()
}
default:
}
}
}
},
}

32
vendor/github.com/LeeEirc/terminalparser/cursor.go generated vendored Normal file
View File

@@ -0,0 +1,32 @@
package terminalparser
type Cursor struct {
X, Y int
}
func (c *Cursor) MoveHome() {
c.X = 0
c.Y = 0
}
func (c *Cursor) MoveUp(ps int) {
c.Y -= ps
if c.Y < 0 {
c.Y = 0
}
}
func (c *Cursor) MoveDown(ps int) {
c.Y += ps
}
func (c *Cursor) MoveRight(ps int) {
c.X += ps
}
func (c *Cursor) MoveLeft(ps int) {
c.X -= ps
if c.X < 0 {
c.X = 0
}
}

5
vendor/github.com/LeeEirc/terminalparser/go.mod generated vendored Normal file
View File

@@ -0,0 +1,5 @@
module github.com/LeeEirc/terminalparser
go 1.15
require github.com/mattn/go-runewidth v0.0.9

2
vendor/github.com/LeeEirc/terminalparser/go.sum generated vendored Normal file
View File

@@ -0,0 +1,2 @@
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=

107
vendor/github.com/LeeEirc/terminalparser/row.go generated vendored Normal file
View File

@@ -0,0 +1,107 @@
package terminalparser
import (
"strings"
"github.com/mattn/go-runewidth"
)
type Row struct {
dataRune []rune
currentX int
currentRuneIndex int
// fish shell 补全提示
tipRune []rune
tipRecord bool
}
func (r *Row) String() string {
return strings.TrimSuffix(string(r.dataRune), string(r.tipRune))
}
func (r *Row) appendCharacter(code rune) {
width := runewidth.StringWidth(string(code))
if r.currentRuneIndex < len(r.dataRune) {
r.dataRune[r.currentRuneIndex] = code
} else {
r.dataRune = append(r.dataRune, code)
}
r.currentRuneIndex++
r.currentX += width
r.addTipRune(code)
}
func (r *Row) insertCharacters(data []rune) {
result := make([]rune, len(r.dataRune)+len(data))
copy(result, r.dataRune[:r.currentRuneIndex])
copy(result[r.currentRuneIndex:], data)
copy(result[r.currentRuneIndex+len(data):], r.dataRune[r.currentRuneIndex:])
for i := range data {
r.currentRuneIndex++
r.currentX += runewidth.StringWidth(string(data[i]))
}
r.dataRune = result
}
func (r *Row) eraseRight() {
r.dataRune = r.dataRune[:r.currentRuneIndex]
}
func (r *Row) deleteChars(ps int) {
result := make([]rune, r.currentRuneIndex, len(r.dataRune))
copy(result, r.dataRune[:r.currentRuneIndex])
rest := r.dataRune[r.currentRuneIndex:]
inits := ps
for i := range rest {
inits -= runewidth.StringWidth(string(rest[i]))
if inits == 0 {
result = append(result, rest[i+1:]...)
break
}
}
r.dataRune = result
}
func (r *Row) changeCurrentRuneIndex() {
if r.currentX < 0 {
r.currentX = 0
}
currentRuneIndex := 0
for i := range r.dataRune {
currentRuneIndex += runewidth.StringWidth(string(r.dataRune[i]))
if currentRuneIndex > r.currentX {
r.currentRuneIndex = i
return
}
}
r.currentRuneIndex = len(r.dataRune)
}
func (r *Row) changeCursorToX(x int) {
if r.currentX == x {
return
}
r.currentX = x
r.changeCurrentRuneIndex()
}
func (r *Row) startRecord() {
r.tipRecord = true
r.tipRune = make([]rune, 0, 100)
}
func (r *Row) stopRecord() {
if !r.tipRecord {
r.tipRune = make([]rune, 0, 100)
}
r.tipRecord = false
}
func (r *Row) addTipRune(code rune) {
if r.tipRecord {
r.tipRune = append(r.tipRune, code)
}
}

277
vendor/github.com/LeeEirc/terminalparser/screen.go generated vendored Normal file
View File

@@ -0,0 +1,277 @@
package terminalparser
import (
"bytes"
"log"
"strconv"
"unicode/utf8"
"github.com/mattn/go-runewidth"
)
type Screen struct {
Rows []*Row
Cursor *Cursor
pasteMode bool // Set bracketed paste mode, xterm. ?2004h reset ?2004l
title string
}
func (s *Screen) Parse(data []byte) []string {
s.Cursor.Y = 1
s.Rows = append(s.Rows, &Row{
dataRune: make([]rune, 0, 1024),
})
rest := data
for len(rest) > 0 {
code, size := utf8.DecodeRune(rest)
rest = rest[size:]
switch code {
case ESCKey:
code, size = utf8.DecodeRune(rest)
rest = rest[size:]
switch code {
case '[':
// CSI
rest = s.parseCSISequence(rest)
continue
case ']':
// OSC
rest = s.parseOSCSequence(rest)
continue
default:
if existIndex := bytes.IndexRune([]byte(string(Intermediate)), code); existIndex >= 0 {
// ESC
rest = s.parseIntermediate(code, rest)
continue
}
if existIndex := bytes.IndexRune([]byte(string(Parameters)), code); existIndex >= 0 {
log.Printf("Screen 未解析 ESC `%q` %xParameters字符\n", code, code)
continue
}
if existIndex := bytes.IndexRune([]byte(string(Uppercase)), code); existIndex >= 0 {
log.Printf("Screen 未解析 ESC `%q` %x Uppercase字符\n", code, code)
continue
}
if existIndex := bytes.IndexRune([]byte(string(Lowercase)), code); existIndex >= 0 {
log.Printf("Screen 未解析 ESC `%q` %x Lowercase字符\n", code, code)
continue
}
log.Printf("Screen 未解析 ESC `%q` %x\n", code, code)
}
continue
case Delete:
continue
default:
if existIndex := bytes.IndexRune([]byte(string(C0Control)), code); existIndex >= 0 {
s.parseC0Sequence(code)
} else {
if len(s.Rows) == 0 && s.Cursor.Y == 0 {
s.Rows = append(s.Rows, &Row{
dataRune: make([]rune, 0, 1024),
})
s.Cursor.Y++
}
s.appendCharacter(code)
}
continue
}
}
result := make([]string, len(s.Rows))
for i := range s.Rows {
result[i] = s.Rows[i].String()
}
return result
}
func (s *Screen) parseC0Sequence(code rune) {
switch code {
case 0x07:
//bell 忽略
case 0x08:
// 后退1光标
s.Cursor.MoveLeft(1)
case 0x0d:
/*
\r
*/
s.Cursor.X = 0
s.Cursor.Y++
if s.Cursor.Y > len(s.Rows) {
s.Rows = append(s.Rows, &Row{
dataRune: make([]rune, 0, 1024),
})
}
case 0x0a:
/*
\n
*/
s.Cursor.Y++
if s.Cursor.Y > len(s.Rows) {
s.Rows = append(s.Rows, &Row{
dataRune: make([]rune, 0, 1024),
})
}
default:
log.Printf("未处理的字符 %q %v\n", code, code)
}
}
func (s *Screen) parseCSISequence(p []byte) []byte {
endIndex := bytes.IndexFunc(p, IsAlphabetic)
params := []rune(string(p[:endIndex]))
switch rune(p[endIndex]) {
case 'Y':
// /*
// ESC Y Ps Ps
// Move the cursor to given row and column.
// */
if len(p[endIndex+1:]) < 2 {
return p[endIndex+1:]
}
if row, err := strconv.Atoi(string(p[endIndex+1])); err == nil {
s.Cursor.Y = row
}
if col, err := strconv.Atoi(string(p[endIndex+2])); err == nil {
s.Cursor.X = col
}
return p[endIndex+3:]
}
funcName, ok := CSIFuncMap[rune(p[endIndex])]
if ok {
funcName(s, params)
} else {
log.Printf("screen未处理的CSI %s %q\n", DebugString(string(params)), p[endIndex])
}
return p[endIndex+1:]
}
func (s *Screen) parseIntermediate(code rune, p []byte) []byte {
switch code {
case '(':
terminationIndex := bytes.IndexFunc(p, func(r rune) bool {
if insideIndex := bytes.IndexRune([]byte(string(Alphabetic)), r); insideIndex < 0 {
return false
}
return true
})
params := p[:terminationIndex+1]
switch string(params) {
case "B":
/*
ESC ( C Designate G0 Character Set, VT100, ISO 2022.
C = B ⇒ United States (USASCII), VT100.
*/
}
p = p[terminationIndex+1:]
return p
case ')':
terminationIndex := bytes.IndexFunc(p, func(r rune) bool {
if insideIndex := bytes.IndexRune([]byte(string(Alphabetic)), r); insideIndex < 0 {
return false
}
return true
})
p = p[terminationIndex+1:]
default:
log.Printf("Screen 未解析 ESC `%q` %x Intermediate字符\n", code, code)
}
return p
}
func (s *Screen) parseOSCSequence(p []byte) []byte {
if endIndex := bytes.IndexRune(p, BEL); endIndex >= 0 {
return p[endIndex+1:]
}
if endIndex := bytes.IndexRune(p, ST); endIndex >= 0 {
return p[endIndex+1:]
}
log.Println("未处理的 parseOSCSequence")
return p
}
func (s *Screen) appendCharacter(code rune) {
currentRow := s.GetCursorRow()
currentRow.changeCursorToX(s.Cursor.X)
currentRow.appendCharacter(code)
width := runewidth.StringWidth(string(code))
s.Cursor.X += width
}
func (s *Screen) eraseEndToLine() {
currentRow := s.GetCursorRow()
currentRow.changeCursorToX(s.Cursor.X)
currentRow.eraseRight()
}
func (s *Screen) eraseRight() {
currentRow := s.GetCursorRow()
currentRow.changeCursorToX(s.Cursor.X)
currentRow.eraseRight()
}
func (s *Screen) eraseLeft() {
log.Printf("Screen %s Erase Left cursor(%d%d) 总Row数量 %d",
UnsupportedMsg, s.Cursor.X, s.Cursor.Y, len(s.Rows))
}
func (s *Screen) eraseAbove() {
s.Rows = s.Rows[s.Cursor.Y-1:]
}
func (s *Screen) eraseBelow() {
s.Rows = s.Rows[:s.Cursor.Y]
}
func (s *Screen) eraseAll() {
s.Rows = s.Rows[:0]
//htop?
s.Cursor.X = 0
s.Cursor.Y = 0
}
func (s *Screen) eraseFromCursor() {
if s.Cursor.Y > len(s.Rows) {
s.Cursor.Y = len(s.Rows)
}
s.Rows = s.Rows[:s.Cursor.Y]
currentRow := s.GetCursorRow()
currentRow.changeCursorToX(s.Cursor.X)
currentRow.eraseRight()
}
func (s *Screen) deleteChars(ps int) {
currentRow := s.GetCursorRow()
currentRow.changeCursorToX(s.Cursor.X)
currentRow.deleteChars(ps)
}
func (s *Screen) GetCursorRow() *Row {
if s.Cursor.Y == 0 {
s.Cursor.Y++
}
if len(s.Rows) == 0 {
s.Rows = append(s.Rows, &Row{
dataRune: make([]rune, 0, 1024),
})
}
index := s.Cursor.Y - 1
if index >= len(s.Rows) {
log.Printf("总行数 %d 比当前行 %d 小,可能存在解析错误 \n", len(s.Rows), s.Cursor.Y)
return s.Rows[len(s.Rows)-1]
}
return s.Rows[s.Cursor.Y-1]
}
const UnsupportedMsg = "Unsupported"

37
vendor/github.com/LeeEirc/terminalparser/util.go generated vendored Normal file
View File

@@ -0,0 +1,37 @@
package terminalparser
import (
"bytes"
"fmt"
"strings"
"unicode"
"unicode/utf8"
)
func DebugString(p string) string {
var s strings.Builder
for _, v := range []rune(p) {
if unicode.IsPrint(v) {
s.WriteRune(v)
} else {
s.WriteString(fmt.Sprintf("%q", v))
}
}
return s.String()
}
func IsAlphabetic(r rune) bool {
index := bytes.IndexRune([]byte(string(Alphabetic)), r)
if index < 0 {
return false
}
return true
}
func ReadRunePacket(p []byte) (code rune, rest []byte) {
r, l := utf8.DecodeRune(p)
if r == utf8.RuneError {
return utf8.RuneError, p
}
return r, p[l:]
}

2
vendor/modules.txt vendored
View File

@@ -47,6 +47,8 @@ github.com/DataDog/dd-trace-go/tracer
github.com/DataDog/dd-trace-go/tracer/ext
# github.com/DataDog/zstd v1.3.4
github.com/DataDog/zstd
# github.com/LeeEirc/terminalparser v0.0.0-20220328021224-de16b7643ea4
github.com/LeeEirc/terminalparser
# github.com/Masterminds/goutils v1.1.0
github.com/Masterminds/goutils
# github.com/Masterminds/semver v1.5.0