mirror of
https://github.com/wavetermdev/backup.git
synced 2026-08-05 13:57:07 -07:00
implement simple command completions
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
+200
-50
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+24
-7
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user