diff --git a/go.mod b/go.mod index ae3565b7a9..df542246dd 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index e457e39c20..c4cb985f63 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/mcclient/modules/webconsole/mod_commandlog.go b/pkg/mcclient/modules/webconsole/mod_commandlog.go index fabc510b9f..fc6ec0432a 100644 --- a/pkg/mcclient/modules/webconsole/mod_commandlog.go +++ b/pkg/mcclient/modules/webconsole/mod_commandlog.go @@ -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", }, diff --git a/pkg/webconsole/models/commandlog.go b/pkg/webconsole/models/commandlog.go index 6c3ca693d9..e1d5907fb9 100644 --- a/pkg/webconsole/models/commandlog.go +++ b/pkg/webconsole/models/commandlog.go @@ -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 } diff --git a/pkg/webconsole/recorder/recorder.go b/pkg/webconsole/recorder/recorder.go index 3451741961..87555da879 100644 --- a/pkg/webconsole/recorder/recorder.go +++ b/pkg/webconsole/recorder/recorder.go @@ -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, } } diff --git a/pkg/webconsole/server/tty_server.go b/pkg/webconsole/server/tty_server.go index df5b55541d..aaa8ccf5bb 100644 --- a/pkg/webconsole/server/tty_server.go +++ b/pkg/webconsole/server/tty_server.go @@ -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, "") } }) diff --git a/vendor/github.com/LeeEirc/terminalparser/.gitignore b/vendor/github.com/LeeEirc/terminalparser/.gitignore new file mode 100644 index 0000000000..d1baad60c6 --- /dev/null +++ b/vendor/github.com/LeeEirc/terminalparser/.gitignore @@ -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 \ No newline at end of file diff --git a/vendor/github.com/LeeEirc/terminalparser/ascii_table.go b/vendor/github.com/LeeEirc/terminalparser/ascii_table.go new file mode 100644 index 0000000000..3e9ba6a3ef --- /dev/null +++ b/vendor/github.com/LeeEirc/terminalparser/ascii_table.go @@ -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 +) \ No newline at end of file diff --git a/vendor/github.com/LeeEirc/terminalparser/csi_func.go b/vendor/github.com/LeeEirc/terminalparser/csi_func.go new file mode 100644 index 0000000000..30e624d0c2 --- /dev/null +++ b/vendor/github.com/LeeEirc/terminalparser/csi_func.go @@ -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: + } + } + } + + }, +} diff --git a/vendor/github.com/LeeEirc/terminalparser/cursor.go b/vendor/github.com/LeeEirc/terminalparser/cursor.go new file mode 100644 index 0000000000..db574553fd --- /dev/null +++ b/vendor/github.com/LeeEirc/terminalparser/cursor.go @@ -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 + } +} diff --git a/vendor/github.com/LeeEirc/terminalparser/go.mod b/vendor/github.com/LeeEirc/terminalparser/go.mod new file mode 100644 index 0000000000..dacfccf792 --- /dev/null +++ b/vendor/github.com/LeeEirc/terminalparser/go.mod @@ -0,0 +1,5 @@ +module github.com/LeeEirc/terminalparser + +go 1.15 + +require github.com/mattn/go-runewidth v0.0.9 diff --git a/vendor/github.com/LeeEirc/terminalparser/go.sum b/vendor/github.com/LeeEirc/terminalparser/go.sum new file mode 100644 index 0000000000..4a94bf58b4 --- /dev/null +++ b/vendor/github.com/LeeEirc/terminalparser/go.sum @@ -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= diff --git a/vendor/github.com/LeeEirc/terminalparser/row.go b/vendor/github.com/LeeEirc/terminalparser/row.go new file mode 100644 index 0000000000..60c4bf5851 --- /dev/null +++ b/vendor/github.com/LeeEirc/terminalparser/row.go @@ -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) + } + +} diff --git a/vendor/github.com/LeeEirc/terminalparser/screen.go b/vendor/github.com/LeeEirc/terminalparser/screen.go new file mode 100644 index 0000000000..62bb8d3830 --- /dev/null +++ b/vendor/github.com/LeeEirc/terminalparser/screen.go @@ -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" diff --git a/vendor/github.com/LeeEirc/terminalparser/util.go b/vendor/github.com/LeeEirc/terminalparser/util.go new file mode 100644 index 0000000000..209aab57b1 --- /dev/null +++ b/vendor/github.com/LeeEirc/terminalparser/util.go @@ -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:] +} \ No newline at end of file diff --git a/vendor/modules.txt b/vendor/modules.txt index 985a08535d..733005ddf1 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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