diff --git a/Makefile b/Makefile index d1e28f3..cc67099 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,8 @@ sanitize: # # -build: submodules bin/resh-session-init bin/resh-collect bin/resh-postcollect bin/resh-daemon bin/resh-evaluate bin/resh-sanitize bin/resh-control bin/resh-config +build: submodules bin/resh-session-init bin/resh-collect bin/resh-postcollect bin/resh-daemon\ + bin/resh-evaluate bin/resh-sanitize bin/resh-control bin/resh-config bin/resh-inspect test_go: # Running tests @@ -92,7 +93,7 @@ install: build submodules/bash-preexec/bash-preexec.sh scripts/shellrc.sh conf/c @# Deleting zsh completion cache - for future use @# [ ! -e ~/.zcompdump ] || rm ~/.zcompdump # Restarting resh daemon ... - -if [ ! -f ~/.resh/resh.pid ]; then\ + -if [ -f ~/.resh/resh.pid ]; then\ kill -SIGTERM $$(cat ~/.resh/resh.pid);\ rm ~/.resh/resh.pid;\ fi @@ -136,9 +137,7 @@ uninstall: # Uninstalling ... -rm -rf ~/.resh/ -bin/resh-control: cmd/control/cmd/*.go cmd/control/status/*.go - -bin/resh-%: cmd/%/*.go pkg/*/*.go VERSION +bin/resh-%: cmd/%/*.go pkg/*/*.go VERSION cmd/control/cmd/*.go cmd/control/status/status.go go build ${GOFLAGS} -o $@ cmd/$*/*.go $(HOME)/.resh $(HOME)/.resh/bin $(HOME)/.config $(HOME)/.resh/bash_completion.d $(HOME)/.resh/zsh_completion.d: diff --git a/cmd/control/cmd/debug.go b/cmd/control/cmd/debug.go index 1b43c54..951cc66 100644 --- a/cmd/control/cmd/debug.go +++ b/cmd/control/cmd/debug.go @@ -25,6 +25,14 @@ var debugReloadCmd = &cobra.Command{ }, } +var debugInspectCmd = &cobra.Command{ + Use: "inspect", + Short: "inspect session history", + Run: func(cmd *cobra.Command, args []string) { + exitCode = status.InspectSessionHistory + }, +} + var debugOutputCmd = &cobra.Command{ Use: "output", Short: "shows output from last runs of resh", diff --git a/cmd/control/cmd/root.go b/cmd/control/cmd/root.go index a9bd514..518e1b2 100644 --- a/cmd/control/cmd/root.go +++ b/cmd/control/cmd/root.go @@ -30,6 +30,7 @@ func Execute() status.Code { rootCmd.AddCommand(debugCmd) debugCmd.AddCommand(debugReloadCmd) + debugCmd.AddCommand(debugInspectCmd) debugCmd.AddCommand(debugOutputCmd) if err := rootCmd.Execute(); err != nil { fmt.Println(err) diff --git a/cmd/control/status/status.go b/cmd/control/status/status.go index 97f9d68..2d3bf37 100644 --- a/cmd/control/status/status.go +++ b/cmd/control/status/status.go @@ -14,4 +14,6 @@ const ( DisableArrowKeyBindings = 111 // ReloadRcFiles exit code - tells reshctl() wrapper to reload shellrc resh file ReloadRcFiles = 200 + // InspectSessionHistory exit code - tells reshctl() wrapper to take current sessionID and send /inspect request to daemon + InspectSessionHistory = 201 ) diff --git a/cmd/daemon/recall.go b/cmd/daemon/recall.go index d5f831b..43e4e6b 100644 --- a/cmd/daemon/recall.go +++ b/cmd/daemon/recall.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/curusarn/resh/pkg/collect" + "github.com/curusarn/resh/pkg/msg" "github.com/curusarn/resh/pkg/records" "github.com/curusarn/resh/pkg/sesshist" ) @@ -52,3 +53,45 @@ func (h *recallHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write(jsn) log.Println("/recall END - sess id:", rec.SessionID, " - histno:", rec.RecallHistno, " -> ", cmd) } + +type inspectHandler struct { + sesshistDispatch *sesshist.Dispatch +} + +func (h *inspectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log.Println("/inspect START") + log.Println("/inspect reading body ...") + jsn, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Println("Error reading the body", err) + return + } + + mess := msg.InspectMsg{} + log.Println("/inspect unmarshaling record ...") + err = json.Unmarshal(jsn, &mess) + if err != nil { + log.Println("Decoding error:", err) + log.Println("Payload:", jsn) + return + } + log.Println("/inspect recalling ...") + cmds, err := h.sesshistDispatch.Inspect(mess.SessionID, int(mess.Count)) + if err != nil { + log.Println("/inspect - sess id:", mess.SessionID, " - count:", mess.Count, " -> ERROR") + log.Println("Inspect error:", err) + return + } + resp := msg.MultiResponse{CmdLines: cmds} + log.Println("/inspect marshaling response ...") + jsn, err = json.Marshal(&resp) + if err != nil { + log.Println("Encoding error:", err) + log.Println("Response:", resp) + return + } + // log.Println(string(jsn)) + log.Println("/inspect writing response ...") + w.Write(jsn) + log.Println("/inspect END - sess id:", mess.SessionID, " - count:", mess.Count) +} diff --git a/cmd/daemon/run-server.go b/cmd/daemon/run-server.go index 9d83e79..df25adf 100644 --- a/cmd/daemon/run-server.go +++ b/cmd/daemon/run-server.go @@ -54,6 +54,7 @@ func runServer(config cfg.Config, historyPath string) { mux.Handle("/record", &recordHandler{subscribers: recordSubscribers}) mux.Handle("/session_init", &sessionInitHandler{subscribers: sessionInitSubscribers}) mux.Handle("/recall", &recallHandler{sesshistDispatch: sesshistDispatch}) + mux.Handle("/inspect", &inspectHandler{sesshistDispatch: sesshistDispatch}) server := &http.Server{Addr: ":" + strconv.Itoa(config.Port), Handler: mux} go server.ListenAndServe() diff --git a/cmd/inspect/main.go b/cmd/inspect/main.go new file mode 100644 index 0000000..4edc41e --- /dev/null +++ b/cmd/inspect/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + + "github.com/BurntSushi/toml" + "github.com/curusarn/resh/pkg/cfg" + "github.com/curusarn/resh/pkg/msg" + + "os/user" + "path/filepath" + "strconv" +) + +// Version from git set during build +var Version string + +// Revision from git set during build +var Revision string + +func main() { + usr, _ := user.Current() + dir := usr.HomeDir + configPath := filepath.Join(dir, "/.config/resh.toml") + + var config cfg.Config + if _, err := toml.DecodeFile(configPath, &config); err != nil { + log.Fatal("Error reading config:", err) + } + + sessionID := flag.String("sessionID", "", "resh generated session id") + count := flag.Uint("count", 10, "Number of cmdLines to return") + flag.Parse() + + if *sessionID == "" { + fmt.Println("Error: you need to specify sessionId") + } + + m := msg.InspectMsg{SessionID: *sessionID, Count: *count} + resp := SendInspectMsg(m, strconv.Itoa(config.Port)) + for _, cmdLine := range resp.CmdLines { + fmt.Println("`" + cmdLine + "'") + } +} + +// SendInspectMsg to daemon +func SendInspectMsg(m msg.InspectMsg, port string) msg.MultiResponse { + recJSON, err := json.Marshal(m) + if err != nil { + log.Fatal("send err 1", err) + } + + req, err := http.NewRequest("POST", "http://localhost:"+port+"/inspect", + bytes.NewBuffer(recJSON)) + if err != nil { + log.Fatal("send err 2", err) + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Fatal("resh-daemon is not running :(") + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Fatal("read response error") + } + // log.Println(string(body)) + response := msg.MultiResponse{} + err = json.Unmarshal(body, &response) + if err != nil { + log.Fatal("unmarshal resp error: ", err) + } + return response +} diff --git a/pkg/histfile/histfile.go b/pkg/histfile/histfile.go index 7e1bcc2..74f125d 100644 --- a/pkg/histfile/histfile.go +++ b/pkg/histfile/histfile.go @@ -7,6 +7,7 @@ import ( "strconv" "sync" + "github.com/curusarn/resh/pkg/histlist" "github.com/curusarn/resh/pkg/records" ) @@ -16,10 +17,10 @@ type Histfile struct { sessions map[string]records.Record historyPath string - recentMutex sync.Mutex - recentRecords []records.Record - recentCmdLines []string // deduplicated - cmdLinesLastIndex map[string]int + recentMutex sync.Mutex + recentRecords []records.Record + + cmdLines histlist.Histlist } // New creates new histfile and runs two gorutines on it @@ -27,9 +28,9 @@ func New(input chan records.Record, historyPath string, initHistSize int, sessio signals chan os.Signal, shutdownDone chan string) *Histfile { hf := Histfile{ - sessions: map[string]records.Record{}, - historyPath: historyPath, - cmdLinesLastIndex: map[string]int{}, + sessions: map[string]records.Record{}, + historyPath: historyPath, + cmdLines: histlist.New(), } go hf.loadHistory(initHistSize) go hf.writer(input, signals, shutdownDone) @@ -40,7 +41,9 @@ func New(input chan records.Record, historyPath string, initHistSize int, sessio func (h *Histfile) loadHistory(initHistSize int) { h.recentMutex.Lock() defer h.recentMutex.Unlock() - h.recentCmdLines = records.LoadCmdLinesFromFile(h.historyPath, initHistSize) + log.Println("histfile: Loading history from file ...") + h.cmdLines = records.LoadCmdLinesFromFile(h.historyPath, initHistSize) + log.Println("histfile: History loaded - cmdLine count:", len(h.cmdLines.List)) } // sessionGC reads sessionIDs from channel and deletes them from histfile struct @@ -121,12 +124,12 @@ func (h *Histfile) mergeAndWriteRecord(part1, part2 records.Record) { defer h.recentMutex.Unlock() h.recentRecords = append(h.recentRecords, part1) cmdLine := part1.CmdLine - idx, found := h.cmdLinesLastIndex[cmdLine] + idx, found := h.cmdLines.LastIndex[cmdLine] if found { - h.recentCmdLines = append(h.recentCmdLines[:idx], h.recentCmdLines[idx+1:]...) + h.cmdLines.List = append(h.cmdLines.List[:idx], h.cmdLines.List[idx+1:]...) } - h.cmdLinesLastIndex[cmdLine] = len(h.recentCmdLines) - h.recentCmdLines = append(h.recentCmdLines, cmdLine) + h.cmdLines.LastIndex[cmdLine] = len(h.cmdLines.List) + h.cmdLines.List = append(h.cmdLines.List, cmdLine) }() writeRecord(part1, h.historyPath) @@ -153,6 +156,11 @@ func writeRecord(rec records.Record, outputPath string) { } // GetRecentCmdLines returns recent cmdLines -func (h *Histfile) GetRecentCmdLines(limit int) []string { - return h.recentCmdLines +func (h *Histfile) GetRecentCmdLines(limit int) histlist.Histlist { + h.recentMutex.Lock() + defer h.recentMutex.Unlock() + log.Println("histfile: History requested ...") + hl := histlist.Copy(h.cmdLines) + log.Println("histfile: History copied - cmdLine count:", len(hl.List)) + return hl } diff --git a/pkg/histlist/histlist.go b/pkg/histlist/histlist.go new file mode 100644 index 0000000..fc6f9b0 --- /dev/null +++ b/pkg/histlist/histlist.go @@ -0,0 +1,27 @@ +package histlist + +// Histlist is a deduplicated list of cmdLines +type Histlist struct { + // list of commands lines (deduplicated) + List []string + // lookup: cmdLine -> last index + LastIndex map[string]int +} + +// New Histlist +func New() Histlist { + return Histlist{LastIndex: make(map[string]int)} +} + +// Copy Histlist +func Copy(hl Histlist) Histlist { + newHl := New() + // copy list + newHl.List = make([]string, len(hl.List)) + copy(newHl.List, hl.List) + // copy map + for k, v := range hl.LastIndex { + newHl.LastIndex[k] = v + } + return newHl +} diff --git a/pkg/msg/msg.go b/pkg/msg/msg.go new file mode 100644 index 0000000..e402904 --- /dev/null +++ b/pkg/msg/msg.go @@ -0,0 +1,12 @@ +package msg + +// InspectMsg struct +type InspectMsg struct { + SessionID string `json:"sessionId"` + Count uint `json:"count"` +} + +// MultiResponse struct +type MultiResponse struct { + CmdLines []string `json:"cmdlines"` +} diff --git a/pkg/records/records.go b/pkg/records/records.go index 319f0a0..4ff8c19 100644 --- a/pkg/records/records.go +++ b/pkg/records/records.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" + "github.com/curusarn/resh/pkg/histlist" "github.com/mattn/go-shellwords" ) @@ -455,7 +456,7 @@ func (r *EnrichedRecord) DistanceTo(r2 EnrichedRecord, p DistParams) float64 { } // LoadCmdLinesFromFile loads limit cmdlines from file -func LoadCmdLinesFromFile(fname string, limit int) []string { +func LoadCmdLinesFromFile(fname string, limit int) histlist.Histlist { recs := LoadFromFile(fname, limit*3) // assume that at least 1/3 of commands is unique var cmdLines []string cmdLinesSet := map[string]bool{} @@ -470,11 +471,17 @@ func LoadCmdLinesFromFile(fname string, limit int) []string { break } } - return cmdLines + hl := histlist.New() + hl.List = cmdLines + for idx, cmdLine := range cmdLines { + hl.LastIndex[cmdLine] = idx + } + return hl } // LoadFromFile loads at most 'limit' records from 'fname' file func LoadFromFile(fname string, limit int) []Record { + // NOTE: limit does nothing atm file, err := os.Open(fname) if err != nil { log.Fatal("Open() resh history file error:", err) diff --git a/pkg/sesshist/sesshist.go b/pkg/sesshist/sesshist.go index 0079b40..1d6b32b 100644 --- a/pkg/sesshist/sesshist.go +++ b/pkg/sesshist/sesshist.go @@ -8,6 +8,7 @@ import ( "sync" "github.com/curusarn/resh/pkg/histfile" + "github.com/curusarn/resh/pkg/histlist" "github.com/curusarn/resh/pkg/records" ) @@ -80,8 +81,7 @@ func (s *Dispatch) initSession(sessionID string) error { defer s.mutex.Unlock() // init sesshist and populate it with history loaded from file s.sessions[sessionID] = &sesshist{ - recentCmdLines: historyCmdLines, - cmdLinesLastIndex: map[string]int{}, + recentCmdLines: historyCmdLines, } log.Println("sesshist: session init done - " + sessionID) return nil @@ -105,8 +105,11 @@ func (s *Dispatch) dropSession(sessionID string) error { // AddRecent record to session func (s *Dispatch) addRecentRecord(sessionID string, record records.Record) error { + log.Println("sesshist: Adding a record, RLocking main lock ...") s.mutex.RLock() + log.Println("sesshist: Getting a session ...") session, found := s.sessions[sessionID] + log.Println("sesshist: RUnlocking main lock ...") s.mutex.RUnlock() if found == false { @@ -114,20 +117,27 @@ func (s *Dispatch) addRecentRecord(sessionID string, record records.Record) erro s.initSession(sessionID) return s.addRecentRecord(sessionID, record) } + log.Println("sesshist: RLocking session lock (w/ defer) ...") session.mutex.Lock() defer session.mutex.Unlock() session.recentRecords = append(session.recentRecords, record) // remove previous occurance of record + log.Println("sesshist: Looking for duplicate cmdLine ...") cmdLine := record.CmdLine - idx, found := session.cmdLinesLastIndex[cmdLine] + // trim spaces to have less duplicates in the sesshist + cmdLine = strings.TrimRight(cmdLine, " ") + idx, found := session.recentCmdLines.LastIndex[cmdLine] if found { - session.recentCmdLines = append(session.recentCmdLines[:idx], session.recentCmdLines[idx+1:]...) + log.Println("sesshist: Removing duplicate cmdLine at index:", idx, " out of", len(session.recentCmdLines.List), "...") + session.recentCmdLines.List = append(session.recentCmdLines.List[:idx], session.recentCmdLines.List[idx+1:]...) } - session.cmdLinesLastIndex[cmdLine] = len(session.recentCmdLines) + log.Println("sesshist: Updating last index ...") + session.recentCmdLines.LastIndex[cmdLine] = len(session.recentCmdLines.List) // append new record - session.recentCmdLines = append(session.recentCmdLines, cmdLine) + log.Println("sesshist: Appending cmdLine ...") + session.recentCmdLines.List = append(session.recentCmdLines.List, cmdLine) log.Println("sesshist: record:", record.CmdLine, "; added to session:", sessionID, - "; session len:", len(session.recentCmdLines), "; session len w/ dups:", len(session.recentRecords)) + "; session len:", len(session.recentCmdLines.List), "; session len (records):", len(session.recentRecords)) return nil } @@ -154,11 +164,38 @@ func (s *Dispatch) Recall(sessionID string, histno int, prefix string) (string, return session.searchRecordByPrefix(prefix, histno) } +// Inspect commands in recent session history +func (s *Dispatch) Inspect(sessionID string, count int) ([]string, error) { + prefix := "" + log.Println("sesshist - inspect: RLocking main lock ...") + s.mutex.RLock() + log.Println("sesshist - inspect: Getting session history struct ...") + session, found := s.sessions[sessionID] + s.mutex.RUnlock() + + if found == false { + // go s.initSession(sessionID) + return nil, errors.New("sesshist ERROR: No session history for SessionID " + sessionID + " - should we create one?") + } + log.Println("sesshist - inspect: Locking session lock ...") + session.mutex.Lock() + defer session.mutex.Unlock() + if prefix == "" { + log.Println("sesshist - inspect: Getting records by histno ...") + idx := len(session.recentCmdLines.List) - count + if idx < 0 { + idx = 0 + } + return session.recentCmdLines.List[idx:], nil + } + log.Println("sesshist - inspect: Searching for records by prefix ... ERROR - Not implemented") + return nil, errors.New("sesshist ERROR: Inspect - Searching for records by prefix Not implemented yet") +} + type sesshist struct { - recentRecords []records.Record - recentCmdLines []string // deduplicated - cmdLinesLastIndex map[string]int - mutex sync.Mutex + mutex sync.Mutex + recentRecords []records.Record + recentCmdLines histlist.Histlist } func (s *sesshist) getRecordByHistno(histno int) (string, error) { @@ -170,11 +207,11 @@ func (s *sesshist) getRecordByHistno(histno int) (string, error) { if histno < 0 { return "", errors.New("sesshist ERROR: 'histno < 0' is a command from future (not supperted yet)") } - index := len(s.recentCmdLines) - histno + index := len(s.recentCmdLines.List) - histno if index < 0 { - return "", errors.New("sesshist ERROR: 'histno > number of commands in the session' (" + strconv.Itoa(len(s.recentCmdLines)) + ")") + return "", errors.New("sesshist ERROR: 'histno > number of commands in the session' (" + strconv.Itoa(len(s.recentCmdLines.List)) + ")") } - return s.recentCmdLines[index], nil + return s.recentCmdLines.List[index], nil } func (s *sesshist) searchRecordByPrefix(prefix string, histno int) (string, error) { @@ -184,14 +221,14 @@ func (s *sesshist) searchRecordByPrefix(prefix string, histno int) (string, erro if histno < 0 { return "", errors.New("sesshist ERROR: 'histno < 0' is a command from future (not supperted yet)") } - index := len(s.recentCmdLines) - histno + index := len(s.recentCmdLines.List) - histno if index < 0 { - return "", errors.New("sesshist ERROR: 'histno > number of commands in the session' (" + strconv.Itoa(len(s.recentCmdLines)) + ")") + return "", errors.New("sesshist ERROR: 'histno > number of commands in the session' (" + strconv.Itoa(len(s.recentCmdLines.List)) + ")") } cmdLines := []string{} - for i := len(s.recentCmdLines) - 1; i >= 0; i-- { - if strings.HasPrefix(s.recentCmdLines[i], prefix) { - cmdLines = append(cmdLines, s.recentCmdLines[i]) + for i := len(s.recentCmdLines.List) - 1; i >= 0; i-- { + if strings.HasPrefix(s.recentCmdLines.List[i], prefix) { + cmdLines = append(cmdLines, s.recentCmdLines.List[i]) if len(cmdLines) >= histno { break } diff --git a/scripts/reshctl.sh b/scripts/reshctl.sh index a9c9376..0eb9fa2 100644 --- a/scripts/reshctl.sh +++ b/scripts/reshctl.sh @@ -78,6 +78,7 @@ reshctl() { # modify current shell session based on exit status local _status=$? + # echo $_status # unexport current shell unset __RESH_ctl_shell case "$_status" in @@ -112,6 +113,12 @@ reshctl() { . ~/.resh/shellrc return 0 ;; + 201) + # inspect session history + # reshctl debug inspect N + resh-inspect --sessionID "$__RESH_SESSION_ID" --count "${3-10}" + return 0 + ;; *) echo "reshctl() FATAL ERROR: unknown status" >&2 return "$_status"