diff --git a/db/migrations/000001_init.up.sql b/db/migrations/000001_init.up.sql index af75bb6b..281b7bb0 100644 --- a/db/migrations/000001_init.up.sql +++ b/db/migrations/000001_init.up.sql @@ -114,7 +114,11 @@ CREATE TABLE history ( screenid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, lineid int NOT NULL, + remoteownerid varchar(36) NOT NULL, + remoteid varchar(36) NOT NULL, + remotename varchar(50) NOT NULL, haderror boolean NOT NULL, cmdid varchar(36) NOT NULL, - cmdstr text NOT NULL + cmdstr text NOT NULL, + ismetacmd boolean ); diff --git a/db/schema.sql b/db/schema.sql index 470f8e36..503a7246 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -107,7 +107,11 @@ CREATE TABLE history ( screenid varchar(36) NOT NULL, windowid varchar(36) NOT NULL, lineid int NOT NULL, + remoteownerid varchar(36) NOT NULL, + remoteid varchar(36) NOT NULL, + remotename varchar(50) NOT NULL, haderror boolean NOT NULL, cmdid varchar(36) NOT NULL, - cmdstr text NOT NULL + cmdstr text NOT NULL, + ismetacmd boolean ); diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 352bd59a..c6825cb3 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -63,6 +63,9 @@ func init() { registerCmdAlias("remote", RemoteCommand) registerCmdFn("remote:show", RemoteShowCommand) + registerCmdFn("remote:showall", RemoteShowAllCommand) + + registerCmdFn("history", HistoryCommand) } func getValidCommands() []string { @@ -126,6 +129,17 @@ func resolveBool(arg string, def bool) bool { return true } +func resolveInt(arg string, def int) (int, error) { + if arg == "" { + return def, nil + } + ival, err := strconv.Atoi(arg) + if err != nil { + return 0, err + } + return ival, nil +} + 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 { @@ -159,13 +173,13 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U return sstore.ModelUpdate{Line: rtnLine, Cmd: cmd}, nil } -func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, update sstore.UpdatePacket, hadError bool) error { +func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, update sstore.UpdatePacket, isMetaCmd bool, hadError bool) error { cmdStr := firstArg(pk) ids, err := resolveIds(ctx, pk, R_Session|R_Screen|R_Window) if err != nil { return err } - lineId, cmdId := sstore.ReadLineCmdIdFromUpdate(update) + lineId, cmdId, rptr := sstore.ReadHistoryDataFromUpdate(update) hitem := &sstore.HistoryItemType{ HistoryId: uuid.New().String(), Ts: time.Now().UnixMilli(), @@ -177,6 +191,10 @@ func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, update HadError: hadError, CmdId: cmdId, CmdStr: cmdStr, + IsMetaCmd: isMetaCmd, + } + if !isMetaCmd && rptr != nil { + hitem.Remote = *rptr } err = sstore.InsertHistoryItem(ctx, hitem) if err != nil { @@ -195,7 +213,7 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. } update, err := HandleCommand(ctx, newPk) if !resolveBool(pk.Kwargs["nohist"], false) { - err := addToHistory(ctx, pk, update, (err != nil)) + err := addToHistory(ctx, pk, update, (newPk.MetaCmd != "run"), (err != nil)) if err != nil { fmt.Printf("[error] adding to history: %v\n", err) // continue... @@ -369,11 +387,30 @@ func RemoteShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s if ids.RemoteState != nil { buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cwd", ids.RemoteState.Cwd)) } - output := buf.String() return sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoTitle: fmt.Sprintf("show remote '%s' info", ids.RemoteDisplayName), - InfoLines: splitLinesForInfo(output), + InfoLines: splitLinesForInfo(buf.String()), + }, + }, nil +} + +func RemoteShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + stateArr := remote.GetAllRemoteState() + var buf bytes.Buffer + for _, rstate := range stateArr { + var name string + if rstate.RemoteAlias == "" { + name = rstate.RemoteCanonicalName + } else { + name = fmt.Sprintf("%s (%s)", rstate.RemoteCanonicalName, rstate.RemoteAlias) + } + buf.WriteString(fmt.Sprintf("%-12s %-5s %8s %s\n", rstate.Status, rstate.RemoteType, rstate.RemoteId[0:8], name)) + } + return sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoTitle: fmt.Sprintf("show all remote info"), + InfoLines: splitLinesForInfo(buf.String()), }, }, nil } @@ -903,6 +940,55 @@ func ClearCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore return update, nil } +const DefaultMaxHistoryItems = 10000 + +func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveIds(ctx, pk, R_Session|R_Screen|R_Window|R_Remote) + if err != nil { + return nil, err + } + maxItems, err := resolveInt(pk.Kwargs["maxitems"], DefaultMaxHistoryItems) + if err != nil { + return nil, fmt.Errorf("invalid maxitems value '%s' (must be a number): %v", pk.Kwargs["maxitems"], err) + } + if maxItems < 0 { + return nil, fmt.Errorf("invalid maxitems value '%s' (cannot be negative)", maxItems) + } + if maxItems == 0 { + maxItems = DefaultMaxHistoryItems + } + hitems, err := sstore.GetSessionHistoryItems(ctx, ids.SessionId, maxItems) + if err != nil { + return nil, err + } + var filteredItems []*sstore.HistoryItemType + for _, hitem := range hitems { + if hitem.ScreenId == ids.ScreenId && hitem.WindowId == ids.WindowId && (hitem.Remote == ids.RemotePtr || hitem.IsMetaCmd) { + filteredItems = append(filteredItems, hitem) + } + } + var buf bytes.Buffer + if len(filteredItems) == 0 { + buf.WriteString("(no history)") + } else { + for idx := len(filteredItems) - 1; idx >= 0; idx-- { + hitem := filteredItems[idx] + hnumStr := hitem.HistoryNum + if hitem.IsMetaCmd { + hnumStr = "*" + hnumStr + } + hstr := fmt.Sprintf("%6s %s\n", hnumStr, hitem.CmdStr) + buf.WriteString(hstr) + } + } + update := &sstore.ModelUpdate{} + update.Info = &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("history, limited to current session, screen, window, and remote (maxitems=%d)", maxItems), + InfoLines: splitLinesForInfo(buf.String()), + } + return update, nil +} + func splitLinesForInfo(str string) []string { rtn := strings.Split(str, "\n") if rtn[len(rtn)-1] == "" { diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index bd28184d..d0a6386c 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -155,6 +155,9 @@ func EvalMetaCommand(ctx context.Context, origPk *scpacket.FeCommandPacketType) if len(origPk.Args) == 0 { return nil, fmt.Errorf("empty command (no fields)") } + if strings.TrimSpace(origPk.Args[0]) == "" { + return nil, fmt.Errorf("empty command") + } metaCmd, metaSubCmd, commandArgs := parseMetaCmd(origPk.Args[0]) rtnPk := scpacket.MakeFeCommandPacket() rtnPk.MetaCmd = metaCmd diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 3a073b0a..3c993157 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "strconv" "strings" "github.com/google/uuid" @@ -107,9 +108,9 @@ func InsertHistoryItem(ctx context.Context, hitem *HistoryItemType) error { if err != nil { return err } - query := `INSERT INTO history ( historyid, ts, userid, sessionid, screenid, windowid, lineid, cmdid, haderror, cmdstr) VALUES - (:historyid,:ts,:userid,:sessionid,:screenid,:windowid,:lineid,:cmdid,:haderror,:cmdstr)` - _, err = db.NamedExec(query, hitem) + query := `INSERT INTO history ( historyid, ts, userid, sessionid, screenid, windowid, lineid, cmdid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd) VALUES + (:historyid,:ts,:userid,:sessionid,:screenid,:windowid,:lineid,:cmdid,:haderror,:cmdstr,:remoteownerid,:remoteid,:remotename,:ismetacmd)` + _, err = db.NamedExec(query, hitem.ToMap()) if err != nil { return err } @@ -119,8 +120,16 @@ func InsertHistoryItem(ctx context.Context, hitem *HistoryItemType) error { func GetSessionHistoryItems(ctx context.Context, sessionId string, maxItems int) ([]*HistoryItemType, error) { var rtn []*HistoryItemType err := WithTx(ctx, func(tx *TxWrap) error { - query := `SELECT * FROM history WHERE sessionid = ? ORDER BY ts DESC LIMIT ?` - tx.SelectWrap(&rtn, query, sessionId, maxItems) + query := `SELECT count(*) FROM history WHERE sessionid = ?` + totalNum := tx.GetInt(query, sessionId) + query = `SELECT * FROM history WHERE sessionid = ? ORDER BY ts DESC, historyid LIMIT ?` + marr := tx.SelectMaps(query, sessionId, maxItems) + for idx, m := range marr { + hnum := totalNum - idx + hitem := HistoryItemFromMap(m) + hitem.HistoryNum = strconv.Itoa(hnum) + rtn = append(rtn, hitem) + } return nil }) if err != nil { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 075cf85f..12606d56 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -206,6 +206,47 @@ func WindowFromMap(m map[string]interface{}) *WindowType { return &w } +func (h *HistoryItemType) ToMap() map[string]interface{} { + rtn := make(map[string]interface{}) + rtn["historyid"] = h.HistoryId + rtn["ts"] = h.Ts + rtn["userid"] = h.UserId + rtn["sessionid"] = h.SessionId + rtn["screenid"] = h.ScreenId + rtn["windowid"] = h.WindowId + rtn["lineid"] = h.LineId + rtn["haderror"] = h.HadError + rtn["cmdid"] = h.CmdId + rtn["cmdstr"] = h.CmdStr + rtn["remoteownerid"] = h.Remote.OwnerId + rtn["remoteid"] = h.Remote.RemoteId + rtn["remotename"] = h.Remote.Name + rtn["ismetacmd"] = h.IsMetaCmd + return rtn +} + +func HistoryItemFromMap(m map[string]interface{}) *HistoryItemType { + if len(m) == 0 { + return nil + } + var h HistoryItemType + quickSetStr(&h.HistoryId, m, "historyid") + quickSetInt64(&h.Ts, m, "ts") + quickSetStr(&h.UserId, m, "userid") + quickSetStr(&h.SessionId, m, "sessionid") + quickSetStr(&h.ScreenId, m, "screenid") + quickSetStr(&h.WindowId, m, "windowid") + quickSetStr(&h.LineId, m, "lineid") + quickSetBool(&h.HadError, m, "haderror") + quickSetStr(&h.CmdId, m, "cmdid") + quickSetStr(&h.CmdStr, m, "cmdstr") + quickSetStr(&h.Remote.OwnerId, m, "remoteownerid") + quickSetStr(&h.Remote.RemoteId, m, "remoteid") + quickSetStr(&h.Remote.Name, m, "remotename") + quickSetBool(&h.IsMetaCmd, m, "ismetacmd") + return &h +} + type ScreenOptsType struct { TabColor string `json:"tabcolor,omitempty"` } @@ -271,19 +312,24 @@ type ScreenWindowType struct { } type HistoryItemType struct { - HistoryId string `json:"historyid"` - Ts int64 `json:"ts"` - UserId string `json:"userid"` - SessionId string `json:"sessionid"` - ScreenId string `json:"screenid"` - WindowId string `json:"windowid"` - LineId string `json:"lineid"` - HadError bool `json:"haderror"` - CmdId string `json:"cmdid"` - CmdStr string `json:"cmdstr"` + HistoryId string `json:"historyid"` + Ts int64 `json:"ts"` + UserId string `json:"userid"` + SessionId string `json:"sessionid"` + ScreenId string `json:"screenid"` + WindowId string `json:"windowid"` + LineId string `json:"lineid"` + HadError bool `json:"haderror"` + CmdId string `json:"cmdid"` + CmdStr string `json:"cmdstr"` + Remote RemotePtrType `json:"remote"` + IsMetaCmd bool `json:"ismetacmd"` // only for updates Remove bool `json:"remove"` + + // transient (string because of different history orderings) + HistoryNum string `json:"historynum"` } type RemoteState struct { diff --git a/pkg/sstore/updatebus.go b/pkg/sstore/updatebus.go index 47057778..f9e6a461 100644 --- a/pkg/sstore/updatebus.go +++ b/pkg/sstore/updatebus.go @@ -49,15 +49,19 @@ func MakeSingleSessionUpdate(sessionId string) (ModelUpdate, *SessionType) { return update, session } -func ReadLineCmdIdFromUpdate(update UpdatePacket) (string, string) { +func ReadHistoryDataFromUpdate(update UpdatePacket) (string, string, *RemotePtrType) { modelUpdate, ok := update.(ModelUpdate) if !ok { - return "", "" + return "", "", nil } if modelUpdate.Line == nil { - return "", "" + return "", "", nil } - return modelUpdate.Line.LineId, modelUpdate.Line.CmdId + var rptr *RemotePtrType + if modelUpdate.Cmd != nil { + rptr = &modelUpdate.Cmd.Remote + } + return modelUpdate.Line.LineId, modelUpdate.Line.CmdId, rptr } type InfoMsgType struct {