From 81c564aca93dd146e6f40899ef9561b6e8d6f08a Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 10 Aug 2022 18:33:32 -0700 Subject: [PATCH] implement simple command completions --- go.mod | 1 + go.sum | 1 + pkg/cmdrunner/cmdrunner.go | 250 +++++++++++++++++++++++++++++-------- pkg/sstore/dbops.go | 13 ++ pkg/sstore/sstore.go | 5 +- pkg/sstore/updatebus.go | 31 +++-- 6 files changed, 243 insertions(+), 58 deletions(-) diff --git a/go.mod b/go.mod index 511d18ff..567032ef 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/mattn/go-shellwords v1.0.12 // indirect go.uber.org/atomic v1.7.0 // indirect golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect ) diff --git a/go.sum b/go.sum index f5a32cd6..809fecbb 100644 --- a/go.sum +++ b/go.sum @@ -787,6 +787,7 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 11f6e6cc..d5c3eed4 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -5,6 +5,8 @@ import ( "fmt" "path" "path/filepath" + "regexp" + "sort" "strconv" "strings" @@ -55,6 +57,44 @@ func SubMetaCmd(cmd string) string { } } +var ValidCommands = []string{ + "/run", + "/eval", + "/screen", "/screen:open", "/screen:close", + "/session", "/session:open", "/session:close", + "/comment", + "/cd", + "/compgen", +} + +func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + switch SubMetaCmd(pk.MetaCmd) { + case "run": + return RunCommand(ctx, pk) + + case "eval": + return EvalCommand(ctx, pk) + + case "screen": + return ScreenCommand(ctx, pk) + + case "session": + return SessionCommand(ctx, pk) + + case "comment": + return CommentCommand(ctx, pk) + + case "cd": + return CdCommand(ctx, pk) + + case "compgen": + return CompGenCommand(ctx, pk) + + default: + return nil, fmt.Errorf("invalid command '/%s', no handler", pk.MetaCmd) + } +} + func firstArg(pk *scpacket.FeCommandPacketType) string { if len(pk.Args) == 0 { return "" @@ -212,34 +252,6 @@ func resolveIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int return rtn, nil } -func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - switch SubMetaCmd(pk.MetaCmd) { - case "run": - return RunCommand(ctx, pk) - - case "eval": - return EvalCommand(ctx, pk) - - case "screen": - return ScreenCommand(ctx, pk) - - case "session": - return SessionCommand(ctx, pk) - - case "comment": - return CommentCommand(ctx, pk) - - case "cd": - return CdCommand(ctx, pk) - - case "compgen": - return CompGenCommand(ctx, pk) - - default: - return nil, fmt.Errorf("invalid command '/%s', no handler", pk.MetaCmd) - } -} - func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) if err != nil { @@ -300,18 +312,24 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. if metaCmd == "" { metaCmd = "run" } - var args []string - if metaCmd == "run" || metaCmd == "comment" { - args = []string{commandStr} - } else { - args = strings.Fields(commandStr) - } newPk := &scpacket.FeCommandPacketType{ MetaCmd: metaCmd, MetaSubCmd: metaSubCmd, - Args: args, Kwargs: pk.Kwargs, } + if metaCmd == "run" || metaCmd == "comment" { + newPk.Args = []string{commandStr} + } else { + allArgs := strings.Fields(commandStr) + for _, arg := range allArgs { + if strings.Index(arg, "=") == -1 { + newPk.Args = append(newPk.Args, arg) + continue + } + fields := strings.SplitN(arg, "=", 2) + newPk.Kwargs[fields[0]] = fields[1] + } + } return HandleCommand(ctx, newPk) } @@ -388,10 +406,14 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up if curRemote == nil { return nil, fmt.Errorf("invalid remote, cannot execute command") } - _, err = curRemote.PacketRpc(ctx, cdPacket) + resp, err := curRemote.PacketRpc(ctx, cdPacket) if err != nil { return nil, err } + if err = resp.Err(); err != nil { + return nil, err + } + fmt.Printf("cd-resp %#v\n", resp) remote, err := sstore.UpdateRemoteCwd(ctx, ids.RemoteName, ids.SessionId, ids.WindowId, ids.RemoteId, newDir) if err != nil { return nil, err @@ -402,6 +424,10 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up WindowId: ids.WindowId, Remotes: []*sstore.RemoteInstance{remote}, }, + Info: &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("[%s] current directory = %s", ids.RemoteName, newDir), + TimeoutMs: 2000, + }, } return update, nil } @@ -431,38 +457,162 @@ func getStrArr(v interface{}, field string) []string { return sarr } -func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) - if err != nil { - return nil, fmt.Errorf("/compgen error: %w", err) +func getBool(v interface{}, field string) bool { + if v == nil { + return false + } + m, ok := v.(map[string]interface{}) + if !ok { + return false + } + fieldVal := m[field] + if fieldVal == nil { + return false + } + bval, ok := fieldVal.(bool) + if !ok { + return false + } + return bval +} + +func makeInfoFromComps(compType string, comps []string, hasMore bool) sstore.UpdatePacket { + sort.Strings(comps) + update := sstore.InfoUpdate{ + Info: &sstore.InfoMsgType{ + InfoTitle: fmt.Sprintf("%s completions", compType), + InfoStrings: comps, + InfoStringsMore: hasMore, + }, + } + return update +} + +func makeInsertUpdateFromComps(pos int64, prefix string, comps []string, hasMore bool) sstore.UpdatePacket { + if hasMore { + return nil + } + lcp := longestPrefix(prefix, comps) + if lcp == prefix || len(lcp) < len(prefix) || !strings.HasPrefix(lcp, prefix) { + return nil + } + insertChars := lcp[len(prefix):] + clu := &sstore.CmdLineType{InsertChars: insertChars, InsertPos: pos} + return sstore.InfoUpdate{CmdLine: clu} +} + +func longestPrefix(root string, comps []string) string { + if len(comps) == 0 { + return root + } + if len(comps) == 1 { + comp := comps[0] + if len(comp) >= len(root) && strings.HasPrefix(comp, root) { + return comps[0] + " " + } + } + lcp := comps[0] + for i := 1; i < len(comps); i++ { + s := comps[i] + for j := 0; j < len(lcp); j++ { + if j >= len(s) || lcp[j] != s[j] { + lcp = lcp[0:j] + break + } + } + } + if len(lcp) < len(root) || !strings.HasPrefix(lcp, root) { + return root + } + return lcp +} + +var wsRe = regexp.MustCompile("\\s+") + +func doMetaCompGen(ctx context.Context, ids resolvedIds, prefix string) ([]string, bool, error) { + var comps []string + for _, cmd := range ValidCommands { + if strings.HasPrefix(cmd, prefix) { + comps = append(comps, cmd) + } + } + return comps, false, nil +} + +func doCompGen(ctx context.Context, ids resolvedIds, prefix string, compType string) ([]string, bool, error) { + if compType == "metacommand" { + return doMetaCompGen(ctx, ids, prefix) } - compType := argN(pk, 0) - prefix := argN(pk, 1) if !packet.IsValidCompGenType(compType) { - return nil, fmt.Errorf("/compgen invalid type '%s'", compType) + return nil, false, fmt.Errorf("/compgen invalid type '%s'", compType) } cgPacket := packet.MakeCompGenPacket() cgPacket.ReqId = uuid.New().String() cgPacket.CompType = compType cgPacket.Prefix = prefix if ids.RemoteState == nil { - return nil, fmt.Errorf("/compgen invalid remote state") + return nil, false, fmt.Errorf("/compgen invalid remote state") } cgPacket.Cwd = ids.RemoteState.Cwd curRemote := remote.GetRemoteById(ids.RemoteId) if curRemote == nil { - return nil, fmt.Errorf("invalid remote, cannot execute command") + return nil, false, fmt.Errorf("invalid remote, cannot execute command") } resp, err := curRemote.PacketRpc(ctx, cgPacket) if err != nil { - return nil, err + return nil, false, err + } + if err = resp.Err(); err != nil { + return nil, false, err } comps := getStrArr(resp.Data, "comps") - update := sstore.InfoUpdate{ - InfoTitle: fmt.Sprintf("%s completions", compType), - InfoStrings: comps, + hasMore := getBool(resp.Data, "hasmore") + return comps, hasMore, nil +} + +func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote) + if err != nil { + return nil, fmt.Errorf("/compgen error: %w", err) } - return update, nil + cmdLine := firstArg(pk) + pos := len(cmdLine) + if pk.Kwargs["comppos"] != "" { + posArg, err := strconv.Atoi(pk.Kwargs["comppos"]) + if err != nil { + return nil, fmt.Errorf("/compgen invalid comppos '%s': %w", pk.Kwargs["comppos"], err) + } + pos = posArg + } + if pos < 0 { + pos = 0 + } + if pos > len(cmdLine) { + pos = len(cmdLine) + } + showComps := resolveBool(pk.Kwargs["compshow"], false) + prefix := cmdLine[:pos] + parts := strings.Split(prefix, " ") + compType := "file" + if len(parts) > 0 && strings.HasPrefix(parts[0], "/") { + compType = "metacommand" + } else if len(parts) == 2 && (parts[0] == "cd" || parts[0] == "/cd") { + compType = "directory" + } else if len(parts) <= 1 { + compType = "command" + } + lastPart := "" + if len(parts) > 0 { + lastPart = parts[len(parts)-1] + } + comps, hasMore, err := doCompGen(ctx, ids, lastPart, compType) + if err != nil { + return nil, err + } + if showComps { + return makeInfoFromComps(compType, comps, hasMore), nil + } + return makeInsertUpdateFromComps(int64(pos), lastPart, comps, hasMore), nil } func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 8b763c9e..a27b7acf 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -103,7 +103,9 @@ func GetAllSessions(ctx context.Context) ([]*SessionType, error) { err := WithTx(ctx, func(tx *TxWrap) error { query := `SELECT * FROM session` tx.SelectWrap(&rtn, query) + sessionMap := make(map[string]*SessionType) for _, session := range rtn { + sessionMap[session.SessionId] = session session.Full = true } var screens []*ScreenType @@ -132,6 +134,15 @@ func GetAllSessions(ctx context.Context) ([]*SessionType, error) { } screen.Windows = append(screen.Windows, sw) } + query = `SELECT * FROM remote_instance WHERE sessionscope` + var ris []*RemoteInstance + tx.SelectWrap(&ris, query) + for _, ri := range ris { + s := sessionMap[ri.SessionId] + if s != nil { + s.Remotes = append(s.Remotes, ri) + } + } return nil }) return rtn, err @@ -154,6 +165,8 @@ func GetWindowById(ctx context.Context, sessionId string, windowId string) (*Win for _, m := range cmdMaps { window.Cmds = append(window.Cmds, CmdFromMap(m)) } + query = `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND NOT sessionscope` + tx.SelectWrap(&window.Remotes, query, sessionId, windowId) return nil }) return rtnWindow, err diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index b8997fdb..1ba2bd85 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -194,9 +194,12 @@ type RemoteInstance struct { Name string `json:"name"` SessionId string `json:"sessionid"` WindowId string `json:"windowid"` - RemoteId string `json"remoteid"` + RemoteId string `json:"remoteid"` SessionScope bool `json:"sessionscope"` State RemoteState `json:"state"` + + // only for updates + Remove bool `json:"remove,omitempty"` } type LineType struct { diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index f6d55294..9a969148 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -10,6 +10,7 @@ const WindowUpdateStr = "window" const CmdUpdateStr = "cmd" const LineCmdUpdateStr = "line+cmd" const InfoUpdateStr = "info" +const CompGenUpdateStr = "compgen" type UpdatePacket interface { UpdateType() string @@ -33,8 +34,8 @@ func (PtyDataUpdate) UpdateType() string { } type WindowUpdate struct { - Window WindowType `json:"window"` - Remove bool `json:"remove,omitempty"` + Window WindowType `json:"window"` + Info *InfoMsgType `json:"info,omitempty"` } func (WindowUpdate) UpdateType() string { @@ -44,6 +45,7 @@ func (WindowUpdate) UpdateType() string { type SessionUpdate struct { Sessions []*SessionType `json:"sessions"` ActiveSessionId string `json:"activesessionid,omitempty"` + Info *InfoMsgType `json:"info,omitempty"` } func (SessionUpdate) UpdateType() string { @@ -62,18 +64,33 @@ func MakeSingleSessionUpdate(sessionId string) (*SessionUpdate, *SessionType) { } type LineUpdate struct { - Line *LineType `json:"line"` - Cmd *CmdType `json:"cmd,omitempty"` - Remove bool `json:"remove,omitempty"` + Line *LineType `json:"line"` + Cmd *CmdType `json:"cmd,omitempty"` + Remove bool `json:"remove,omitempty"` + Info *InfoMsgType `json:"info,omitempty"` } func (LineUpdate) UpdateType() string { return LineCmdUpdateStr } +type InfoMsgType struct { + InfoTitle string `json:"infotitle"` + InfoError string `json:"infoerror,omitempty"` + InfoMsg string `json:"infomsg,omitempty"` + InfoStrings []string `json:"infostrings"` + InfoStringsMore bool `json:"infostringsmore"` + TimeoutMs int64 `json:"timeoutms,omitempty"` +} + +type CmdLineType struct { + InsertChars string `json:"insertchars"` + InsertPos int64 `json:"insertpos"` +} + type InfoUpdate struct { - InfoTitle string `json:"infotitle"` - InfoStrings []string `json:"infostrings"` + Info *InfoMsgType `json:"info,omitempty"` + CmdLine *CmdLineType `json:"cmdline,omitempty"` } func (InfoUpdate) UpdateType() string {