From 3701a9c8cfc9115baa18e38e1310e80e16b0ed12 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Wed, 27 Nov 2019 16:21:27 +0100 Subject: [PATCH 01/18] handle signals - write pending records on shutdown --- cmd/daemon/main.go | 4 ++- cmd/daemon/run-server.go | 25 +++++++++---- pkg/histfile/histfile.go | 59 +++++++++++++++++++++---------- pkg/signalhandler/signalhander.go | 57 +++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 26 deletions(-) create mode 100644 pkg/signalhandler/signalhander.go diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index a0ff6df..8bc48f9 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -70,10 +70,12 @@ func main() { log.Fatal("Could not create pidfile", err) } runServer(config, historyPath) + log.Println("main: Removing pidfile ...") err = os.Remove(pidfilePath) if err != nil { log.Println("Could not delete pidfile", err) } + log.Println("main: Shutdown - bye") } func statusHandler(w http.ResponseWriter, r *http.Request) { @@ -92,7 +94,7 @@ func killDaemon(pidfile string) error { if err != nil { log.Fatal("Pidfile contents are malformed", err) } - cmd := exec.Command("kill", strconv.Itoa(pid)) + cmd := exec.Command("kill", "-s", "sigint", strconv.Itoa(pid)) err = cmd.Run() if err != nil { log.Printf("Command finished with error: %v", err) diff --git a/cmd/daemon/run-server.go b/cmd/daemon/run-server.go index 6eb44b7..34a530e 100644 --- a/cmd/daemon/run-server.go +++ b/cmd/daemon/run-server.go @@ -2,6 +2,7 @@ package main import ( "net/http" + "os" "strconv" "github.com/curusarn/resh/pkg/cfg" @@ -9,12 +10,16 @@ import ( "github.com/curusarn/resh/pkg/records" "github.com/curusarn/resh/pkg/sesshist" "github.com/curusarn/resh/pkg/sesswatch" + "github.com/curusarn/resh/pkg/signalhandler" ) func runServer(config cfg.Config, historyPath string) { var recordSubscribers []chan records.Record var sessionInitSubscribers []chan records.Record var sessionDropSubscribers []chan string + var signalSubscribers []chan os.Signal + + shutdown := make(chan string) // sessshist sesshistSessionsToInit := make(chan records.Record) @@ -29,7 +34,9 @@ func runServer(config cfg.Config, historyPath string) { recordSubscribers = append(recordSubscribers, histfileRecords) histfileSessionsToDrop := make(chan string) sessionDropSubscribers = append(sessionDropSubscribers, histfileSessionsToDrop) - histfileBox := histfile.New(histfileRecords, historyPath, 10000, histfileSessionsToDrop) + histfileSignals := make(chan os.Signal) + signalSubscribers = append(signalSubscribers, histfileSignals) + histfileBox := histfile.New(histfileRecords, historyPath, 10000, histfileSessionsToDrop, histfileSignals, shutdown) // sesshist New sesshistDispatch := sesshist.NewDispatch(sesshistSessionsToInit, sesshistSessionsToDrop, sesshistRecords, histfileBox, config.SesshistInitHistorySize) @@ -40,9 +47,15 @@ func runServer(config cfg.Config, historyPath string) { sesswatch.Go(sesswatchSessionsToWatch, sessionDropSubscribers, config.SesswatchPeriodSeconds) // handlers - http.HandleFunc("/status", statusHandler) - http.Handle("/record", &recordHandler{subscribers: recordSubscribers}) - http.Handle("/session_init", &sessionInitHandler{subscribers: sessionInitSubscribers}) - http.Handle("/recall", &recallHandler{sesshistDispatch: sesshistDispatch}) - http.ListenAndServe(":"+strconv.Itoa(config.Port), nil) + mux := http.NewServeMux() + mux.HandleFunc("/status", statusHandler) + mux.Handle("/record", &recordHandler{subscribers: recordSubscribers}) + mux.Handle("/session_init", &sessionInitHandler{subscribers: sessionInitSubscribers}) + mux.Handle("/recall", &recallHandler{sesshistDispatch: sesshistDispatch}) + + server := &http.Server{Addr: ":" + strconv.Itoa(config.Port), Handler: mux} + go server.ListenAndServe() + + // signalhandler - takes over the main goroutine so when signal handler exists the whole program exits + signalhandler.Run(signalSubscribers, shutdown, server) } diff --git a/pkg/histfile/histfile.go b/pkg/histfile/histfile.go index 5964603..7e1bcc2 100644 --- a/pkg/histfile/histfile.go +++ b/pkg/histfile/histfile.go @@ -23,14 +23,16 @@ type Histfile struct { } // New creates new histfile and runs two gorutines on it -func New(input chan records.Record, historyPath string, initHistSize int, sessionsToDrop chan string) *Histfile { +func New(input chan records.Record, historyPath string, initHistSize int, sessionsToDrop chan string, + signals chan os.Signal, shutdownDone chan string) *Histfile { + hf := Histfile{ sessions: map[string]records.Record{}, historyPath: historyPath, cmdLinesLastIndex: map[string]int{}, } go hf.loadHistory(initHistSize) - go hf.writer(input) + go hf.writer(input, signals, shutdownDone) go hf.sessionGC(sessionsToDrop) return &hf } @@ -61,33 +63,52 @@ func (h *Histfile) sessionGC(sessionsToDrop chan string) { } // writer reads records from channel, merges them and writes them to file -func (h *Histfile) writer(input chan records.Record) { +func (h *Histfile) writer(input chan records.Record, signals chan os.Signal, shutdownDone chan string) { for { func() { - record := <-input - h.sessionsMutex.Lock() - defer h.sessionsMutex.Unlock() + select { + case record := <-input: + h.sessionsMutex.Lock() + defer h.sessionsMutex.Unlock() - // allows nested sessions to merge records properly - mergeID := record.SessionID + "_" + strconv.Itoa(record.Shlvl) - if record.PartOne { - if _, found := h.sessions[mergeID]; found { - log.Println("histfile WARN: Got another first part of the records before merging the previous one - overwriting! " + - "(this happens in bash because bash-preexec runs when it's not supposed to)") - } - h.sessions[mergeID] = record - } else { - if part1, found := h.sessions[mergeID]; found == false { - log.Println("histfile ERROR: Got second part of records and nothing to merge it with - ignoring! (mergeID:", mergeID, ")") + // allows nested sessions to merge records properly + mergeID := record.SessionID + "_" + strconv.Itoa(record.Shlvl) + if record.PartOne { + if _, found := h.sessions[mergeID]; found { + log.Println("histfile WARN: Got another first part of the records before merging the previous one - overwriting! " + + "(this happens in bash because bash-preexec runs when it's not supposed to)") + } + h.sessions[mergeID] = record } else { - delete(h.sessions, mergeID) - go h.mergeAndWriteRecord(part1, record) + if part1, found := h.sessions[mergeID]; found == false { + log.Println("histfile ERROR: Got second part of records and nothing to merge it with - ignoring! (mergeID:", mergeID, ")") + } else { + delete(h.sessions, mergeID) + go h.mergeAndWriteRecord(part1, record) + } } + case sig := <-signals: + log.Println("histfile: Got signal " + sig.String()) + h.sessionsMutex.Lock() + defer h.sessionsMutex.Unlock() + log.Println("histfile DEBUG: Unlocked mutex") + + for sessID, record := range h.sessions { + log.Panicln("histfile WARN: Writing incomplete record for session " + sessID) + h.writeRecord(record) + } + log.Println("histfile DEBUG: Shutdown success") + shutdownDone <- "histfile" + return } }() } } +func (h *Histfile) writeRecord(part1 records.Record) { + writeRecord(part1, h.historyPath) +} + func (h *Histfile) mergeAndWriteRecord(part1, part2 records.Record) { err := part1.Merge(part2) if err != nil { diff --git a/pkg/signalhandler/signalhander.go b/pkg/signalhandler/signalhander.go new file mode 100644 index 0000000..c3c201b --- /dev/null +++ b/pkg/signalhandler/signalhander.go @@ -0,0 +1,57 @@ +package signalhandler + +import ( + "context" + "log" + "net/http" + "os" + "os/signal" + "strconv" + "syscall" + "time" +) + +func sendSignals(sig os.Signal, subscribers []chan os.Signal, done chan string) { + for _, sub := range subscribers { + sub <- sig + } + chanCount := len(subscribers) + start := time.Now() + delay := time.Millisecond * 100 + timeout := time.Millisecond * 2000 + + for { + select { + case _ = <-done: + chanCount-- + if chanCount == 0 { + log.Println("signalhandler: All boxes shut down successfully") + return + } + default: + time.Sleep(delay) + } + if time.Since(start) > timeout { + log.Println("signalhandler: Timouted while waiting for proper shutdown - " + strconv.Itoa(chanCount) + " boxes are up after " + timeout.String()) + return + } + } +} + +// Run catches and handles signals +func Run(subscribers []chan os.Signal, done chan string, server *http.Server) { + signals := make(chan os.Signal, 1) + + signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) + + sig := <-signals + log.Println("signalhandler: Got signal " + sig.String()) + + log.Println("signalhandler: Sending signals to Subscribers") + sendSignals(sig, subscribers, done) + + log.Println("signalhandler: Shutting down the server") + if err := server.Shutdown(context.Background()); err != nil { + log.Printf("HTTP server Shutdown: %v", err) + } +} From 4faf1a8832a102453995903ea5b6a596845030eb Mon Sep 17 00:00:00 2001 From: Simon Let Date: Tue, 10 Dec 2019 18:53:28 +0100 Subject: [PATCH 02/18] add debug option, estimate performance of /recall --- cmd/daemon/main.go | 4 ++++ cmd/daemon/recall.go | 10 ++++++++-- cmd/daemon/run-server.go | 4 +++- conf/config.toml | 1 + pkg/cfg/cfg.go | 1 + pkg/sesshist/sesshist.go | 11 +++++++---- 6 files changed, 24 insertions(+), 7 deletions(-) diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index 8bc48f9..5591399 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -48,6 +48,10 @@ func main() { log.Println("Error reading config", err) return } + if config.Debug { + log.SetFlags(log.LstdFlags | log.Lmicroseconds) + } + res, err := isDaemonRunning(config.Port) if err != nil { log.Println("Error while checking if the daemon is runnnig", err) diff --git a/cmd/daemon/recall.go b/cmd/daemon/recall.go index 7c5efeb..0f0ad83 100644 --- a/cmd/daemon/recall.go +++ b/cmd/daemon/recall.go @@ -16,6 +16,8 @@ type recallHandler struct { } func (h *recallHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log.Println("/recall START") + log.Println("/recall reading body ...") jsn, err := ioutil.ReadAll(r.Body) if err != nil { log.Println("Error reading the body", err) @@ -23,19 +25,22 @@ func (h *recallHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } rec := records.Record{} + log.Println("/recall unmarshaling record ...") err = json.Unmarshal(jsn, &rec) if err != nil { log.Println("Decoding error:", err) log.Println("Payload:", jsn) return } + log.Println("/recall recalling ...") cmd, err := h.sesshistDispatch.Recall(rec.SessionID, rec.RecallHistno, rec.RecallPrefix) if err != nil { log.Println("/recall - sess id:", rec.SessionID, " - histno:", rec.RecallHistno, " -> ERROR") log.Println("Recall error:", err) return } - resp := collect.SingleResponse{cmd} + resp := collect.SingleResponse{CmdLine: cmd} + log.Println("/recall marshaling response ...") jsn, err = json.Marshal(&resp) if err != nil { log.Println("Encoding error:", err) @@ -43,6 +48,7 @@ func (h *recallHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } log.Println(string(jsn)) + log.Println("/recall writing response ...") w.Write(jsn) - log.Println("/recall - sess id:", rec.SessionID, " - histno:", rec.RecallHistno, " -> ", cmd) + log.Println("/recall END - sess id:", rec.SessionID, " - histno:", rec.RecallHistno, " -> ", cmd) } diff --git a/cmd/daemon/run-server.go b/cmd/daemon/run-server.go index 34a530e..9d83e79 100644 --- a/cmd/daemon/run-server.go +++ b/cmd/daemon/run-server.go @@ -39,7 +39,9 @@ func runServer(config cfg.Config, historyPath string) { histfileBox := histfile.New(histfileRecords, historyPath, 10000, histfileSessionsToDrop, histfileSignals, shutdown) // sesshist New - sesshistDispatch := sesshist.NewDispatch(sesshistSessionsToInit, sesshistSessionsToDrop, sesshistRecords, histfileBox, config.SesshistInitHistorySize) + sesshistDispatch := sesshist.NewDispatch(sesshistSessionsToInit, sesshistSessionsToDrop, + sesshistRecords, histfileBox, + config.SesshistInitHistorySize) // sesswatch sesswatchSessionsToWatch := make(chan records.Record) diff --git a/conf/config.toml b/conf/config.toml index 1970f34..931767c 100644 --- a/conf/config.toml +++ b/conf/config.toml @@ -1,3 +1,4 @@ port = 2627 sesswatchPeriodSeconds = 120 sesshistInitHistorySize = 1000 +debug = true diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 16726bb..1ed3cb7 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -5,4 +5,5 @@ type Config struct { Port int SesswatchPeriodSeconds uint SesshistInitHistorySize int + Debug bool } diff --git a/pkg/sesshist/sesshist.go b/pkg/sesshist/sesshist.go index 50b79ec..0079b40 100644 --- a/pkg/sesshist/sesshist.go +++ b/pkg/sesshist/sesshist.go @@ -133,7 +133,9 @@ func (s *Dispatch) addRecentRecord(sessionID string, record records.Record) erro // Recall command from recent session history func (s *Dispatch) Recall(sessionID string, histno int, prefix string) (string, error) { + log.Println("sesshist - recall: RLocking main lock ...") s.mutex.RLock() + log.Println("sesshist - recall: Getting session history struct ...") session, found := s.sessions[sessionID] s.mutex.RUnlock() @@ -141,13 +143,14 @@ func (s *Dispatch) Recall(sessionID string, histno int, prefix string) (string, // go s.initSession(sessionID) return "", errors.New("sesshist ERROR: No session history for SessionID " + sessionID + " - should we create one?") } + log.Println("sesshist - recall: Locking session lock ...") + session.mutex.Lock() + defer session.mutex.Unlock() if prefix == "" { - session.mutex.Lock() - defer session.mutex.Unlock() + log.Println("sesshist - recall: Getting records by histno ...") return session.getRecordByHistno(histno) } - session.mutex.Lock() - defer session.mutex.Unlock() + log.Println("sesshist - recall: Searching for records by prefix ...") return session.searchRecordByPrefix(prefix, histno) } From 27121f904a1f358b01e4660ab098ac087a5a1933 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Tue, 10 Dec 2019 20:33:36 +0100 Subject: [PATCH 03/18] use slim record for /recall instead of regular one we went from about 0.15 ms of unmarshalling time to about 0.05 ms that takes simple recalls from 0.2 ms to 0.1 ms this all seems like useless optimizing but this improvement made recalling usable in zsh and probably tolerable in bash --- cmd/collect/main.go | 135 +++++++++++++++++++++-------------------- cmd/daemon/recall.go | 2 +- pkg/collect/collect.go | 2 +- pkg/records/records.go | 15 +++++ 4 files changed, 87 insertions(+), 67 deletions(-) diff --git a/cmd/collect/main.go b/cmd/collect/main.go index 5699ac6..49ccbde 100644 --- a/cmd/collect/main.go +++ b/cmd/collect/main.go @@ -169,74 +169,79 @@ func main() { // *osReleasePrettyName = "Linux" // } - rec := records.Record{ - // posix - Cols: *cols, - Lines: *lines, - // core - BaseRecord: records.BaseRecord{ - RecallHistno: *recallHistno, - - CmdLine: *cmdLine, - ExitCode: *exitCode, - Shell: *shell, - Uname: *uname, - SessionID: *sessionID, - - // posix - Home: *home, - Lang: *lang, - LcAll: *lcAll, - Login: *login, - // Path: *path, - Pwd: *pwd, - ShellEnv: *shellEnv, - Term: *term, - - // non-posix - RealPwd: realPwd, - Pid: *pid, - SessionPID: *sessionPid, - Host: *host, - Hosttype: *hosttype, - Ostype: *ostype, - Machtype: *machtype, - Shlvl: *shlvl, - - // before after - TimezoneBefore: *timezoneBefore, - - RealtimeBefore: realtimeBefore, - RealtimeBeforeLocal: realtimeBeforeLocal, - - RealtimeSinceSessionStart: realtimeSinceSessionStart, - RealtimeSinceBoot: realtimeSinceBoot, - - GitDir: gitDir, - GitRealDir: gitRealDir, - GitOriginRemote: *gitRemote, - MachineID: collect.ReadFileContent(machineIDPath), - - OsReleaseID: *osReleaseID, - OsReleaseVersionID: *osReleaseVersionID, - OsReleaseIDLike: *osReleaseIDLike, - OsReleaseName: *osReleaseName, - OsReleasePrettyName: *osReleasePrettyName, - - PartOne: true, - - ReshUUID: collect.ReadFileContent(reshUUIDPath), - ReshVersion: Version, - ReshRevision: Revision, - - RecallActionsRaw: *recallActions, - RecallPrefix: *recallPrefix, - RecallStrategy: *recallStrategy, - }, - } if *recall { + rec := records.SlimRecord{ + SessionID: *sessionID, + RecallHistno: *recallHistno, + RecallPrefix: *recallPrefix, + } fmt.Print(collect.SendRecallRequest(rec, strconv.Itoa(config.Port))) } else { + rec := records.Record{ + // posix + Cols: *cols, + Lines: *lines, + // core + BaseRecord: records.BaseRecord{ + RecallHistno: *recallHistno, + + CmdLine: *cmdLine, + ExitCode: *exitCode, + Shell: *shell, + Uname: *uname, + SessionID: *sessionID, + + // posix + Home: *home, + Lang: *lang, + LcAll: *lcAll, + Login: *login, + // Path: *path, + Pwd: *pwd, + ShellEnv: *shellEnv, + Term: *term, + + // non-posix + RealPwd: realPwd, + Pid: *pid, + SessionPID: *sessionPid, + Host: *host, + Hosttype: *hosttype, + Ostype: *ostype, + Machtype: *machtype, + Shlvl: *shlvl, + + // before after + TimezoneBefore: *timezoneBefore, + + RealtimeBefore: realtimeBefore, + RealtimeBeforeLocal: realtimeBeforeLocal, + + RealtimeSinceSessionStart: realtimeSinceSessionStart, + RealtimeSinceBoot: realtimeSinceBoot, + + GitDir: gitDir, + GitRealDir: gitRealDir, + GitOriginRemote: *gitRemote, + MachineID: collect.ReadFileContent(machineIDPath), + + OsReleaseID: *osReleaseID, + OsReleaseVersionID: *osReleaseVersionID, + OsReleaseIDLike: *osReleaseIDLike, + OsReleaseName: *osReleaseName, + OsReleasePrettyName: *osReleasePrettyName, + + PartOne: true, + + ReshUUID: collect.ReadFileContent(reshUUIDPath), + ReshVersion: Version, + ReshRevision: Revision, + + RecallActionsRaw: *recallActions, + RecallPrefix: *recallPrefix, + RecallStrategy: *recallStrategy, + }, + } collect.SendRecord(rec, strconv.Itoa(config.Port), "/record") } } diff --git a/cmd/daemon/recall.go b/cmd/daemon/recall.go index 0f0ad83..d5f831b 100644 --- a/cmd/daemon/recall.go +++ b/cmd/daemon/recall.go @@ -24,7 +24,7 @@ func (h *recallHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - rec := records.Record{} + rec := records.SlimRecord{} log.Println("/recall unmarshaling record ...") err = json.Unmarshal(jsn, &rec) if err != nil { diff --git a/pkg/collect/collect.go b/pkg/collect/collect.go index 7f26b4e..56ee374 100644 --- a/pkg/collect/collect.go +++ b/pkg/collect/collect.go @@ -19,7 +19,7 @@ type SingleResponse struct { } // SendRecallRequest to daemon -func SendRecallRequest(r records.Record, port string) string { +func SendRecallRequest(r records.SlimRecord, port string) string { recJSON, err := json.Marshal(r) if err != nil { log.Fatal("send err 1", err) diff --git a/pkg/records/records.go b/pkg/records/records.go index 3b4170a..319f0a0 100644 --- a/pkg/records/records.go +++ b/pkg/records/records.go @@ -129,6 +129,21 @@ type FallbackRecord struct { Lines int `json:"lines"` // notice the int type } +// SlimRecord used for recalling because unmarshalling record w/ 50+ fields is too slow +type SlimRecord struct { + SessionID string `json:"sessionId"` + RecallHistno int `json:"recallHistno,omitempty"` + RecallPrefix string `json:"recallPrefix,omitempty"` + + // extra recall - we might use these in the future + // Pwd string `json:"pwd"` + // RealPwd string `json:"realPwd"` + // GitDir string `json:"gitDir"` + // GitRealDir string `json:"gitRealDir"` + // GitOriginRemote string `json:"gitOriginRemote"` + +} + // Convert from FallbackRecord to Record func Convert(r *FallbackRecord) Record { return Record{ From 3c33b7195a3fea6d837b24750e59229fb4da10bc Mon Sep 17 00:00:00 2001 From: Simon Let Date: Thu, 12 Dec 2019 14:51:59 +0100 Subject: [PATCH 04/18] update submodules/bash-zsh-compat-widgets --- submodules/bash-zsh-compat-widgets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/bash-zsh-compat-widgets b/submodules/bash-zsh-compat-widgets index 7dde81e..22ed6c4 160000 --- a/submodules/bash-zsh-compat-widgets +++ b/submodules/bash-zsh-compat-widgets @@ -1 +1 @@ -Subproject commit 7dde81eaa09cbed11ebc70ea892bcae24ea1606c +Subproject commit 22ed6c4e553669f8c2d6e9b7c743a96741828ee0 From feb77ba4256f933aa404711afcaa0996941bd688 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Thu, 12 Dec 2019 15:40:08 +0100 Subject: [PATCH 05/18] update submodules/bash-preexec --- submodules/bash-preexec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/bash-preexec b/submodules/bash-preexec index 9811ba8..f558191 160000 --- a/submodules/bash-preexec +++ b/submodules/bash-preexec @@ -1 +1 @@ -Subproject commit 9811ba8b7694cdbd9debed931e922b67e439197a +Subproject commit f558191d74ae33654d63a0f091034ef27f4b44f9 From 4ba791151de2da4c3b5751b4984b8a20d7ed1ade Mon Sep 17 00:00:00 2001 From: Simon Let Date: Thu, 12 Dec 2019 18:40:54 +0100 Subject: [PATCH 06/18] minor fix --- scripts/test.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/test.sh b/scripts/test.sh index cbd1de6..34fa3e0 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,15 +1,17 @@ #!/usr/bin/env bash +# very simple tests to catch simple errors in scripts +# shellcheck disable=SC2016 [ "${BASH_SOURCE[0]}" != "scripts/test.sh" ] && echo 'Run this script using `make test`' && exit 1 for f in scripts/*.sh; do echo "Running shellcheck on $f ..." - shellcheck $f --shell=bash --severity=error || exit 1 + shellcheck "$f" --shell=bash --severity=error || exit 1 done for f in scripts/{shellrc,util,reshctl,hooks}.sh; do echo "Checking Zsh syntax of $f ..." - ! zsh -n scripts/shellrc.sh && echo "Zsh syntax check failed!" && exit 1 + ! zsh -n "$f" && echo "Zsh syntax check failed!" && exit 1 done for sh in bash zsh; do From aac6540d68bbdf339cf6af5b0c1958224ddec733 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Thu, 12 Dec 2019 18:41:32 +0100 Subject: [PATCH 07/18] minor fix in submodules/bash-zsh-compat-widgets --- submodules/bash-zsh-compat-widgets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/bash-zsh-compat-widgets b/submodules/bash-zsh-compat-widgets index 22ed6c4..8677b8a 160000 --- a/submodules/bash-zsh-compat-widgets +++ b/submodules/bash-zsh-compat-widgets @@ -1 +1 @@ -Subproject commit 22ed6c4e553669f8c2d6e9b7c743a96741828ee0 +Subproject commit 8677b8a6b87c6d44371e5aa38f9e480ade06e1a0 From 992178a9c8e7f4a5ef9933a46d6d52f1c2579390 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Thu, 12 Dec 2019 18:42:22 +0100 Subject: [PATCH 08/18] improve reshctl subcommands, add disable for arrow key binds --- Makefile | 2 +- cmd/control/cmd/disable.go | 26 +++++++++-------- cmd/control/cmd/enable.go | 26 +++++++++-------- cmd/control/cmd/root.go | 6 ++-- cmd/control/status/status.go | 8 +++--- scripts/reshctl.sh | 54 +++++++++++++++++++++++++++++------- scripts/shellrc.sh | 4 +-- 7 files changed, 85 insertions(+), 41 deletions(-) diff --git a/Makefile b/Makefile index 9b86acd..a63d529 100644 --- a/Makefile +++ b/Makefile @@ -131,7 +131,7 @@ uninstall: # Uninstalling ... -rm -rf ~/.resh/ -bin/resh-control: cmd/control/cmd/*.go +bin/resh-control: cmd/control/cmd/*.go cmd/control/status/*.go bin/resh-%: cmd/%/*.go pkg/*/*.go VERSION go build ${GOFLAGS} -o $@ cmd/$*/*.go diff --git a/cmd/control/cmd/disable.go b/cmd/control/cmd/disable.go index b0dead5..fbc72db 100644 --- a/cmd/control/cmd/disable.go +++ b/cmd/control/cmd/disable.go @@ -7,18 +7,22 @@ import ( var disableCmd = &cobra.Command{ Use: "disable", - Short: "disable RESH features", - Long: `Disables RESH bindings for arrows and C-R.`, + Short: "disable RESH features (arrow key bindings)", +} + +var disableArrowKeyBindingsCmd = &cobra.Command{ + Use: "arrow_key_bindings", + Short: "disable bindings for arrow keys (up/down) FOR THIS SHELL SESSION", Run: func(cmd *cobra.Command, args []string) { - exitCode = status.DisableAll + exitCode = status.DisableArrowKeyBindings }, } -// var disableRecallingCmd = &cobra.Command{ -// Use: "keybind", -// Short: "Disables RESH bindings for arrows and C-R.", -// Long: `Disables RESH bindings for arrows and C-R.`, -// Run: func(cmd *cobra.Command, args []string) { -// exitCode = status.DisableAll -// }, -// } +var disableArrowKeyBindingsGlobalCmd = &cobra.Command{ + Use: "arrow_key_bindings_global", + Short: "disable bindings for arrow keys (up/down) FOR THIS AND ALL FUTURE SHELL SESSIONS", + Run: func(cmd *cobra.Command, args []string) { + // TODO: config set arrow_key_bindings true + exitCode = status.DisableArrowKeyBindings + }, +} diff --git a/cmd/control/cmd/enable.go b/cmd/control/cmd/enable.go index ab4b144..0b62e78 100644 --- a/cmd/control/cmd/enable.go +++ b/cmd/control/cmd/enable.go @@ -7,18 +7,22 @@ import ( var enableCmd = &cobra.Command{ Use: "enable", - Short: "enable RESH features", - Long: `Enables RESH bindings for arrows and C-R.`, + Short: "enable RESH features (arrow key bindings)", +} + +var enableArrowKeyBindingsCmd = &cobra.Command{ + Use: "arrow_key_bindings", + Short: "enable bindings for arrow keys (up/down) FOR THIS SHELL SESSION", Run: func(cmd *cobra.Command, args []string) { - exitCode = status.EnableAll + exitCode = status.EnableArrowKeyBindings }, } -// var enableRecallingCmd = &cobra.Command{ -// Use: "keybind", -// Short: "Enables RESH bindings for arrows and C-R.", -// Long: `Enables RESH bindings for arrows and C-R.`, -// Run: func(cmd *cobra.Command, args []string) { -// exitCode = status.EnableAll -// }, -// } +var enableArrowKeyBindingsGlobalCmd = &cobra.Command{ + Use: "arrow_key_bindings_global", + Short: "enable bindings for arrow keys (up/down) FOR THIS AND ALL FUTURE SHELL SESSIONS", + Run: func(cmd *cobra.Command, args []string) { + // TODO: config set arrow_key_bindings true + exitCode = status.EnableArrowKeyBindings + }, +} diff --git a/cmd/control/cmd/root.go b/cmd/control/cmd/root.go index a00d342..a9e4c50 100644 --- a/cmd/control/cmd/root.go +++ b/cmd/control/cmd/root.go @@ -18,10 +18,12 @@ var rootCmd = &cobra.Command{ // Execute reshctl func Execute() status.Code { rootCmd.AddCommand(disableCmd) - // disableCmd.AddCommand(disableRecallingCmd) + disableCmd.AddCommand(disableArrowKeyBindingsCmd) + disableCmd.AddCommand(disableArrowKeyBindingsGlobalCmd) rootCmd.AddCommand(enableCmd) - // enableCmd.AddCommand(enableRecallingCmd) + enableCmd.AddCommand(enableArrowKeyBindingsCmd) + enableCmd.AddCommand(enableArrowKeyBindingsGlobalCmd) rootCmd.AddCommand(completionCmd) completionCmd.AddCommand(completionBashCmd) diff --git a/cmd/control/status/status.go b/cmd/control/status/status.go index 8b9eeec..97f9d68 100644 --- a/cmd/control/status/status.go +++ b/cmd/control/status/status.go @@ -8,10 +8,10 @@ const ( Success Code = 0 // Fail exit code Fail = 1 - // EnableAll exit code - tells reshctl() wrapper to enable_all - EnableAll = 100 - // DisableAll exit code - tells reshctl() wrapper to disable_all - DisableAll = 110 + // EnableArrowKeyBindings exit code - tells reshctl() wrapper to enable arrow key bindings + EnableArrowKeyBindings = 101 + // DisableArrowKeyBindings exit code - tells reshctl() wrapper to disable arrow key bindings + DisableArrowKeyBindings = 111 // ReloadRcFiles exit code - tells reshctl() wrapper to reload shellrc resh file ReloadRcFiles = 200 ) diff --git a/scripts/reshctl.sh b/scripts/reshctl.sh index 2656055..9f40bbc 100644 --- a/scripts/reshctl.sh +++ b/scripts/reshctl.sh @@ -5,22 +5,46 @@ . ~/.resh/widgets.sh __resh_bind_arrows() { - bindfunc '\e[A' __resh_widget_arrow_up_compat - bindfunc '\e[B' __resh_widget_arrow_down_compat + if [ "${__RESH_arrow_keys_bind_enabled-0}" != 0 ]; then + echo "Error: RESH arrow key bindings are already enabled!" + return 1 + fi + bindfunc --revert '\e[A' __resh_widget_arrow_up_compat + __RESH_bindfunc_revert_arrow_up_bind=$_bindfunc_revert + bindfunc --revert '\e[B' __resh_widget_arrow_down_compat + __RESH_bindfunc_revert_arrow_down_bind=$_bindfunc_revert + __RESH_arrow_keys_bind_enabled=1 return 0 } __resh_bind_control_R() { + # TODO echo "bindfunc __resh_widget_control_R_compat" return 0 } + __resh_unbind_arrows() { - echo "\ bindfunc __resh_widget_arrow_up_compat" - echo "\ bindfunc __resh_widget_arrow_down_compat" + if [ "${__RESH_arrow_keys_bind_enabled-0}" != 1 ]; then + echo "Error: Can't disable arrow key bindings because they are not enabled!" + return 1 + fi + if [ -z "${__RESH_bindfunc_revert_arrow_up_bind+x}" ]; then + echo "Warn: Couldn't revert arrow UP binding because 'revert command' is empty." + else + eval "$__RESH_bindfunc_revert_arrow_up_bind" + echo "RESH arrow up binding successfully disabled ✓" + fi + if [ -z "${__RESH_bindfunc_revert_arrow_down_bind+x}" ]; then + echo "Warn: Couldn't revert arrow DOWN binding because 'revert command' is empty." + else + eval "$__RESH_bindfunc_revert_arrow_down_bind" + echo "RESH arrow down binding successfully disabled ✓" + fi return 0 } __resh_unbind_control_R() { + # TODO echo "\ bindfunc __resh_widget_control_R_compat" return 0 } @@ -46,15 +70,25 @@ reshctl() { return "$_status" ;; # enable - 100) - # enable all - __resh_bind_all + # 100) + # # enable all + # __resh_bind_all + # return 0 + # ;; + 101) + # enable arrow keys + __resh_bind_arrows return 0 ;; # disable - 110) - # disable all - __resh_unbind_all + # 110) + # # disable all + # __resh_unbind_all + # return 0 + # ;; + 111) + # disable arrow keys + __resh_unbind_arrows return 0 ;; 200) diff --git a/scripts/shellrc.sh b/scripts/shellrc.sh index c4d3afc..e447fc7 100644 --- a/scripts/shellrc.sh +++ b/scripts/shellrc.sh @@ -23,13 +23,13 @@ else echo "resh PANIC unrecognized OS" fi -if [ -n "$ZSH_VERSION" ]; then +if [ -n "${ZSH_VERSION-}" ]; then # shellcheck disable=SC1009 __RESH_SHELL="zsh" __RESH_HOST="$HOST" __RESH_HOSTTYPE="$CPUTYPE" __resh_zsh_completion_init -elif [ -n "$BASH_VERSION" ]; then +elif [ -n "${BASH_VERSION-}" ]; then __RESH_SHELL="bash" __RESH_HOST="$HOSTNAME" __RESH_HOSTTYPE="$HOSTTYPE" From 53ab420013ac8bf9975b659300820e07f73c2de4 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Fri, 13 Dec 2019 01:01:04 +0100 Subject: [PATCH 09/18] enable/disable arrow key bindings - both local and global --- Makefile | 2 +- cmd/control/cmd/disable.go | 8 +-- cmd/control/cmd/enable.go | 85 ++++++++++++++++++++++++++++-- conf/config.toml | 2 + go.mod | 3 -- go.sum | 8 +-- pkg/cfg/cfg.go | 2 + scripts/reshctl.sh | 12 ++++- submodules/bash-zsh-compat-widgets | 2 +- 9 files changed, 106 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index a63d529..4249809 100644 --- a/Makefile +++ b/Makefile @@ -58,7 +58,7 @@ rebuild: make build clean: - rm resh-* + rm bin/resh-* install: build submodules/bash-preexec/bash-preexec.sh scripts/shellrc.sh conf/config.toml scripts/uuid.sh \ | $(HOME)/.resh $(HOME)/.resh/bin $(HOME)/.config $(HOME)/.resh/bash_completion.d $(HOME)/.resh/zsh_completion.d diff --git a/cmd/control/cmd/disable.go b/cmd/control/cmd/disable.go index fbc72db..f661585 100644 --- a/cmd/control/cmd/disable.go +++ b/cmd/control/cmd/disable.go @@ -20,9 +20,11 @@ var disableArrowKeyBindingsCmd = &cobra.Command{ var disableArrowKeyBindingsGlobalCmd = &cobra.Command{ Use: "arrow_key_bindings_global", - Short: "disable bindings for arrow keys (up/down) FOR THIS AND ALL FUTURE SHELL SESSIONS", + Short: "disable bindings for arrow keys (up/down) FOR FUTURE SHELL SESSIONS", + Long: "Disable bindings for arrow keys (up/down) FOR FUTURE SHELL SESSIONS.\n" + + "Note that this only affects sessions of the same shell.\n" + + "(e.g. running this in zsh will only affect future zsh sessions)", Run: func(cmd *cobra.Command, args []string) { - // TODO: config set arrow_key_bindings true - exitCode = status.DisableArrowKeyBindings + exitCode = enableDisableArrowKeyBindingsGlobally(false) }, } diff --git a/cmd/control/cmd/enable.go b/cmd/control/cmd/enable.go index 0b62e78..f2913a4 100644 --- a/cmd/control/cmd/enable.go +++ b/cmd/control/cmd/enable.go @@ -1,7 +1,14 @@ package cmd import ( + "fmt" + "os" + "os/user" + "path/filepath" + + "github.com/BurntSushi/toml" "github.com/curusarn/resh/cmd/control/status" + "github.com/curusarn/resh/pkg/cfg" "github.com/spf13/cobra" ) @@ -20,9 +27,81 @@ var enableArrowKeyBindingsCmd = &cobra.Command{ var enableArrowKeyBindingsGlobalCmd = &cobra.Command{ Use: "arrow_key_bindings_global", - Short: "enable bindings for arrow keys (up/down) FOR THIS AND ALL FUTURE SHELL SESSIONS", + Short: "enable bindings for arrow keys (up/down) FOR FUTURE SHELL SESSIONS", + Long: "Enable bindings for arrow keys (up/down) FOR FUTURE SHELL SESSIONS.\n" + + "Note that this only affects sessions of the same shell.\n" + + "(e.g. running this in zsh will only affect future zsh sessions)", Run: func(cmd *cobra.Command, args []string) { - // TODO: config set arrow_key_bindings true - exitCode = status.EnableArrowKeyBindings + exitCode = enableDisableArrowKeyBindingsGlobally(true) }, } + +func enableDisableArrowKeyBindingsGlobally(value bool) status.Code { + 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 { + fmt.Println("Error reading config", err) + return status.Fail + } + shell, found := os.LookupEnv("__RESH_ctl_shell") + // shell env variable must be set and must be equal to either bash or zsh + if found == false || (shell != "bash" && shell != "zsh") { + fmt.Println("Error while determining a shell you are using - your RESH instalation is probably broken. Please reinstall RESH - exiting!") + fmt.Println("found=", found, "shell=", shell) + return status.Fail + } + if shell == "bash" { + err := setConfigBindArrowKey(configPath, &config, &config.BindArrowKeysBash, shell, value) + if err != nil { + return status.Fail + } + } else if shell == "zsh" { + err := setConfigBindArrowKey(configPath, &config, &config.BindArrowKeysZsh, shell, value) + if err != nil { + return status.Fail + } + } else { + fmt.Println("FATAL ERROR while determining a shell you are using - your RESH instalation is probably broken. Please reinstall RESH - exiting!") + } + return status.Success +} + +// I don't like the interface this function has - passing both config structure and a part of it feels wrong +// It's ugly and could lead to future errors +func setConfigBindArrowKey(configPath string, config *cfg.Config, configField *bool, shell string, value bool) error { + if *configField == value { + if value { + fmt.Println("The RESH arrow key bindings are ALREADY GLOBALLY ENABLED for all future " + shell + " sessions - nothing to do - exiting.") + } else { + fmt.Println("The RESH arrow key bindings are ALREADY GLOBALLY DISABLED for all future " + shell + " sessions - nothing to do - exiting.") + } + return nil + } + if value { + fmt.Println("ENABLING the RESH arrow key bindings GLOBALLY (in " + shell + ") ...") + } else { + fmt.Println("DISABLING the RESH arrow key bindings GLOBALLY (in " + shell + ") ...") + } + *configField = value + + f, err := os.Create(configPath) + if err != nil { + fmt.Println("Error: Failed to create/open file:", configPath, "; error:", err) + return err + } + defer f.Close() + if err := toml.NewEncoder(f).Encode(config); err != nil { + fmt.Println("Error: Failed to encode and write the config values to hdd. error:", err) + return err + } + if value { + fmt.Println("SUCCESSFULLY ENABLED the RESH arrow key bindings GLOBALLY (in " + shell + ") " + + "- every new (" + shell + ") session will start with enabled RESH arrow key bindings!") + } else { + fmt.Println("SUCCESSFULLY DISABLED the RESH arrow key bindings GLOBALLY (in " + shell + ") " + + "- every new (" + shell + ") session will start with " + shell + " default arrow key bindings!") + } + return nil +} diff --git a/conf/config.toml b/conf/config.toml index 931767c..51c0d40 100644 --- a/conf/config.toml +++ b/conf/config.toml @@ -2,3 +2,5 @@ port = 2627 sesswatchPeriodSeconds = 120 sesshistInitHistorySize = 1000 debug = true +bindArrowKeysBash = true +bindArrowKeysZsh = true diff --git a/go.mod b/go.mod index 910abe6..75c5200 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,11 @@ go 1.12 require ( github.com/BurntSushi/toml v0.3.1 - github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629 github.com/mattn/go-shellwords v1.0.6 github.com/mb-14/gomarkov v0.0.0-20190125094512-044dd0dcb5e7 github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b github.com/schollz/progressbar v1.0.0 github.com/spf13/cobra v0.0.5 - github.com/wcharczuk/go-chart v2.0.1+incompatible github.com/whilp/git-urls v0.0.0-20160530060445-31bac0d230fa - golang.org/x/image v0.0.0-20190902063713-cb417be4ba39 // indirect ) diff --git a/go.sum b/go.sum index beb087d..3252295 100644 --- a/go.sum +++ b/go.sum @@ -7,9 +7,8 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629 h1:1dSBUfGlorLAua2CRx0zFN7kQsTpE2DQSmr7rrTNgY8= github.com/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629/go.mod h1:mb5nS4uRANwOJSZj8rlCWAfAcGi72GGMIXx+xGOjA7M= @@ -37,15 +36,12 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/wcharczuk/go-chart v2.0.1+incompatible h1:0pz39ZAycJFF7ju/1mepnk26RLVLBCWz1STcD3doU0A= -github.com/wcharczuk/go-chart v2.0.1+incompatible/go.mod h1:PF5tmL4EIx/7Wf+hEkpCqYi5He4u90sw+0+6FhrryuE= github.com/whilp/git-urls v0.0.0-20160530060445-31bac0d230fa h1:rW+Lu6281ed/4XGuVIa4/YebTRNvoUJlfJ44ktEVwZk= github.com/whilp/git-urls v0.0.0-20160530060445-31bac0d230fa/go.mod h1:2rx5KE5FLD0HRfkkpyn8JwbVLBdhgeiOb2D2D9LLKM4= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/image v0.0.0-20190902063713-cb417be4ba39 h1:4dQcAORh9oYBwVSBVIkP489LUPC+f1HBkTYXgmqfR+o= -golang.org/x/image v0.0.0-20190902063713-cb417be4ba39/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 1ed3cb7..2cfdbc2 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -6,4 +6,6 @@ type Config struct { SesswatchPeriodSeconds uint SesshistInitHistorySize int Debug bool + BindArrowKeysBash bool + BindArrowKeysZsh bool } diff --git a/scripts/reshctl.sh b/scripts/reshctl.sh index 9f40bbc..03b388a 100644 --- a/scripts/reshctl.sh +++ b/scripts/reshctl.sh @@ -6,7 +6,7 @@ __resh_bind_arrows() { if [ "${__RESH_arrow_keys_bind_enabled-0}" != 0 ]; then - echo "Error: RESH arrow key bindings are already enabled!" + echo "RESH arrow key bindings are already enabled!" return 1 fi bindfunc --revert '\e[A' __resh_widget_arrow_up_compat @@ -28,17 +28,21 @@ __resh_unbind_arrows() { echo "Error: Can't disable arrow key bindings because they are not enabled!" return 1 fi + if [ -z "${__RESH_bindfunc_revert_arrow_up_bind+x}" ]; then echo "Warn: Couldn't revert arrow UP binding because 'revert command' is empty." else eval "$__RESH_bindfunc_revert_arrow_up_bind" echo "RESH arrow up binding successfully disabled ✓" + __RESH_arrow_keys_bind_enabled=0 fi + if [ -z "${__RESH_bindfunc_revert_arrow_down_bind+x}" ]; then echo "Warn: Couldn't revert arrow DOWN binding because 'revert command' is empty." else eval "$__RESH_bindfunc_revert_arrow_down_bind" echo "RESH arrow down binding successfully disabled ✓" + __RESH_arrow_keys_bind_enabled=0 fi return 0 } @@ -60,10 +64,16 @@ __resh_unbind_all() { } reshctl() { + # local log=~/.resh/reshctl.log + # export current shell because resh-control needs to know + export __RESH_ctl_shell=$__RESH_SHELL # run resh-control aka the real reshctl resh-control "$@" + # modify current shell session based on exit status local _status=$? + # unexport current shell + unset __RESH_ctl_shell case "$_status" in 0|1) # success | fail diff --git a/submodules/bash-zsh-compat-widgets b/submodules/bash-zsh-compat-widgets index 8677b8a..c3077fc 160000 --- a/submodules/bash-zsh-compat-widgets +++ b/submodules/bash-zsh-compat-widgets @@ -1 +1 @@ -Subproject commit 8677b8a6b87c6d44371e5aa38f9e480ade06e1a0 +Subproject commit c3077fcdf2e20efb95dd27a53766b78533ab7bc4 From 4c7c1972e8416d1c47eb62aab6b3707542c70239 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Fri, 13 Dec 2019 02:10:41 +0100 Subject: [PATCH 10/18] enable arrow key bindings at the session start (if enabled in config) --- Makefile | 2 +- cmd/config/main.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++ conf/config.toml | 2 +- scripts/shellrc.sh | 9 ++++++++ 4 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 cmd/config/main.go diff --git a/Makefile b/Makefile index 4249809..da01a7a 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ 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 +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 test_go: # Running tests diff --git a/cmd/config/main.go b/cmd/config/main.go new file mode 100644 index 0000000..49fe9ee --- /dev/null +++ b/cmd/config/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "flag" + "fmt" + "os" + "os/user" + "path/filepath" + + "github.com/BurntSushi/toml" + "github.com/curusarn/resh/pkg/cfg" +) + +func main() { + usr, _ := user.Current() + dir := usr.HomeDir + configPath := filepath.Join(dir, ".config/resh.toml") + + var config cfg.Config + _, err := toml.DecodeFile(configPath, &config) + if err != nil { + fmt.Println("Error reading config", err) + os.Exit(1) + } + + configKey := flag.String("key", "", "Key of the requested config entry") + flag.Parse() + + if *configKey == "" { + fmt.Println("Error: expected option --key!") + os.Exit(1) + } + + switch *configKey { + case "BindArrowKeysBash": + fallthrough + case "bindArrowKeysBash": + printBoolNormalized(config.BindArrowKeysBash) + case "BindArrowKeysZsh": + fallthrough + case "bindArrowKeysZsh": + printBoolNormalized(config.BindArrowKeysZsh) + default: + fmt.Println("Error: illegal --key!") + os.Exit(1) + } +} + +// this might be unnecessary but I'm too lazy to look it up +func printBoolNormalized(x bool) { + if x { + fmt.Println("true") + } else { + fmt.Println("false") + } +} diff --git a/conf/config.toml b/conf/config.toml index 51c0d40..7dc647c 100644 --- a/conf/config.toml +++ b/conf/config.toml @@ -2,5 +2,5 @@ port = 2627 sesswatchPeriodSeconds = 120 sesshistInitHistorySize = 1000 debug = true -bindArrowKeysBash = true +bindArrowKeysBash = false bindArrowKeysZsh = true diff --git a/scripts/shellrc.sh b/scripts/shellrc.sh index e447fc7..a6b9ebc 100644 --- a/scripts/shellrc.sh +++ b/scripts/shellrc.sh @@ -86,5 +86,14 @@ if [ -z "${__RESH_INIT_DONE+x}" ]; then __resh_reset_variables + if [ "$__RESH_SHELL" = bash ] ; then + [ "$(resh-config --key BindArrowKeysBash)" = true ] && reshctl enable arrow_key_bindings + elif [ "$__RESH_SHELL" = zsh ] ; then + [ "$(resh-config --key BindArrowKeysZsh)" = true ] && reshctl enable arrow_key_bindings + else + echo "RESH error: unknown shell (init)" + echo "$__RESH_SHELL" + fi + __RESH_INIT_DONE=1 fi From ff8f182b751d91ba284f33aa2e554f86c8413a72 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Fri, 13 Dec 2019 15:50:29 +0100 Subject: [PATCH 11/18] fix issues with zsh and completions --- Makefile | 13 +++++++++---- cmd/control/cmd/completion.go | 10 +++++----- cmd/control/cmd/debug.go | 6 +++--- cmd/control/cmd/root.go | 3 +-- scripts/util.sh | 15 +++++++++++++-- 5 files changed, 31 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index da01a7a..d1e28f3 100644 --- a/Makefile +++ b/Makefile @@ -82,15 +82,20 @@ install: build submodules/bash-preexec/bash-preexec.sh scripts/shellrc.sh conf/c [ ! -f ~/.resh/history.json ] || mv ~/.resh/history.json ~/.resh_history.json # Adding resh shellrc to .bashrc ... grep '[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc' ~/.bashrc ||\ - echo '[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc' >> ~/.bashrc + echo -e '\n[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc' >> ~/.bashrc # Adding bash-preexec to .bashrc ... grep '[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh' ~/.bashrc ||\ - echo '[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh' >> ~/.bashrc + echo -e '\n[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh' >> ~/.bashrc # Adding resh shellrc to .zshrc ... grep '[ -f ~/.resh/shellrc ] && source ~/.resh/shellrc' ~/.zshrc ||\ - echo '[ -f ~/.resh/shellrc ] && source ~/.resh/shellrc' >> ~/.zshrc + echo -e '\n[ -f ~/.resh/shellrc ] && source ~/.resh/shellrc' >> ~/.zshrc + @# Deleting zsh completion cache - for future use + @# [ ! -e ~/.zcompdump ] || rm ~/.zcompdump # Restarting resh daemon ... - -[ ! -f ~/.resh/resh.pid ] || kill -SIGTERM $$(cat ~/.resh/resh.pid) + -if [ ! -f ~/.resh/resh.pid ]; then\ + kill -SIGTERM $$(cat ~/.resh/resh.pid);\ + rm ~/.resh/resh.pid;\ + fi nohup resh-daemon &>/dev/null & disown # Reloading rc files . ~/.resh/shellrc diff --git a/cmd/control/cmd/completion.go b/cmd/control/cmd/completion.go index bf09a67..e80fa56 100644 --- a/cmd/control/cmd/completion.go +++ b/cmd/control/cmd/completion.go @@ -10,20 +10,20 @@ import ( // completionCmd represents the completion command var completionCmd = &cobra.Command{ Use: "completion", - Short: "Generates bash/zsh completion scripts", + Short: "generate bash/zsh completion scripts", Long: `To load completion run . <(reshctl completion bash) OR -. <(reshctl completion zsh) +. <(reshctl completion zsh) && compdef _reshctl reshctl `, } var completionBashCmd = &cobra.Command{ Use: "bash", - Short: "Generates bash completion scripts", + Short: "generate bash completion scripts", Long: `To load completion run . <(reshctl completion bash) @@ -36,10 +36,10 @@ var completionBashCmd = &cobra.Command{ var completionZshCmd = &cobra.Command{ Use: "zsh", - Short: "Generates zsh completion scripts", + Short: "generate zsh completion scripts", Long: `To load completion run -. <(reshctl completion zsh) +. <(reshctl completion zsh) && compdef _reshctl reshctl `, Run: func(cmd *cobra.Command, args []string) { rootCmd.GenZshCompletion(os.Stdout) diff --git a/cmd/control/cmd/debug.go b/cmd/control/cmd/debug.go index fc10550..1b43c54 100644 --- a/cmd/control/cmd/debug.go +++ b/cmd/control/cmd/debug.go @@ -12,13 +12,13 @@ import ( var debugCmd = &cobra.Command{ Use: "debug", - Short: "Debug utils for resh", + Short: "debug utils for resh", Long: "Reloads resh rc files. Shows logs and output from last runs of resh", } var debugReloadCmd = &cobra.Command{ Use: "reload", - Short: "Reload resh rc files", + Short: "reload resh rc files", Long: "Reload resh rc files", Run: func(cmd *cobra.Command, args []string) { exitCode = status.ReloadRcFiles @@ -27,7 +27,7 @@ var debugReloadCmd = &cobra.Command{ var debugOutputCmd = &cobra.Command{ Use: "output", - Short: "Shows output from last runs of resh", + Short: "shows output from last runs of resh", Long: "Shows output from last runs of resh", Run: func(cmd *cobra.Command, args []string) { files := []string{ diff --git a/cmd/control/cmd/root.go b/cmd/control/cmd/root.go index a9e4c50..a9bd514 100644 --- a/cmd/control/cmd/root.go +++ b/cmd/control/cmd/root.go @@ -11,8 +11,7 @@ var exitCode status.Code var rootCmd = &cobra.Command{ Use: "reshctl", - Short: "Reshctl (RESH control) - enables you to enable/disable features and more.", - Long: `Enables you to enable/disable RESH bindings for arrows and C-R.`, + Short: "Reshctl (RESH control) - enable/disable RESH features and more.", } // Execute reshctl diff --git a/scripts/util.sh b/scripts/util.sh index bbdceb4..23e365b 100644 --- a/scripts/util.sh +++ b/scripts/util.sh @@ -63,8 +63,19 @@ __resh_bash_completion_init() { } __resh_zsh_completion_init() { - # shellcheck disable=SC2206 - fpath=(~/.resh/zsh_completion.d $fpath) + # NOTE: this is hacky - each completion needs to be added individually + # TODO: fix later + # fpath=(~/.resh/zsh_completion.d $fpath) + # we should be using fpath but that doesn't work well with oh-my-zsh + # so we are just adding it manually + # shellcheck disable=1090 + source ~/.resh/zsh_completion.d/_reshctl && compdef _reshctl reshctl + + # TODO: test and use this + # NOTE: this is not how globbing works + # for f in ~/.resh/zsh_completion.d/_*; do + # source ~/.resh/zsh_completion.d/_$f && compdef _$f $f + # done } __resh_session_init() { From b2b058e9e2d86faa0ae37256e61c3fc4a713fe36 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Fri, 13 Dec 2019 16:13:21 +0100 Subject: [PATCH 12/18] fix arrow key bindings in emacs mode --- scripts/reshctl.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/reshctl.sh b/scripts/reshctl.sh index 03b388a..b82cccb 100644 --- a/scripts/reshctl.sh +++ b/scripts/reshctl.sh @@ -9,10 +9,14 @@ __resh_bind_arrows() { echo "RESH arrow key bindings are already enabled!" return 1 fi - bindfunc --revert '\e[A' __resh_widget_arrow_up_compat + bindfunc --revert '\eOA' __resh_widget_arrow_up_compat __RESH_bindfunc_revert_arrow_up_bind=$_bindfunc_revert - bindfunc --revert '\e[B' __resh_widget_arrow_down_compat + bindfunc --revert '\e[A' __resh_widget_arrow_up_compat + __RESH_bindfunc_revert_arrow_up_bind_vim=$_bindfunc_revert + bindfunc --revert '\eOB' __resh_widget_arrow_down_compat __RESH_bindfunc_revert_arrow_down_bind=$_bindfunc_revert + bindfunc --revert '\e[B' __resh_widget_arrow_down_compat + __RESH_bindfunc_revert_arrow_down_bind_vim=$_bindfunc_revert __RESH_arrow_keys_bind_enabled=1 return 0 } @@ -33,6 +37,7 @@ __resh_unbind_arrows() { echo "Warn: Couldn't revert arrow UP binding because 'revert command' is empty." else eval "$__RESH_bindfunc_revert_arrow_up_bind" + [ -z "${__RESH_bindfunc_revert_arrow_up_bind_vim+x}" ] || eval "$__RESH_bindfunc_revert_arrow_up_bind_vim" echo "RESH arrow up binding successfully disabled ✓" __RESH_arrow_keys_bind_enabled=0 fi @@ -41,6 +46,7 @@ __resh_unbind_arrows() { echo "Warn: Couldn't revert arrow DOWN binding because 'revert command' is empty." else eval "$__RESH_bindfunc_revert_arrow_down_bind" + [ -z "${__RESH_bindfunc_revert_arrow_up_bind_vim+x}" ] || eval "$__RESH_bindfunc_revert_arrow_up_bind_vim" echo "RESH arrow down binding successfully disabled ✓" __RESH_arrow_keys_bind_enabled=0 fi From d251dc99f4c1f9b2675cdb0f573a8b6c16294eb6 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Fri, 13 Dec 2019 17:18:44 +0100 Subject: [PATCH 13/18] add sanitization for recallActions, minor binding fix --- cmd/sanitize/main.go | 31 +++++++++++++++++++++++++++++++ scripts/reshctl.sh | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/cmd/sanitize/main.go b/cmd/sanitize/main.go index a7415b1..e5630de 100644 --- a/cmd/sanitize/main.go +++ b/cmd/sanitize/main.go @@ -173,11 +173,42 @@ func (s *sanitizer) sanitizeRecord(record *records.Record) error { log.Fatal("Cmd:", record.CmdLine, "; sanitization error:", err) } + if len(record.RecallActionsRaw) > 0 { + record.RecallActionsRaw, err = s.sanitizeRecallActions(record.RecallActionsRaw) + if err != nil { + log.Fatal("RecallActionsRaw:", record.RecallActionsRaw, "; sanitization error:", err) + } + } // add a flag to signify that the record has been sanitized record.Sanitized = true return nil } +// sanitizes the recall actions by replacing the recall prefix with it's length +func (s *sanitizer) sanitizeRecallActions(str string) (string, error) { + sanStr := "" + for x, actionStr := range strings.Split(str, ";") { + if x == 0 { + continue + } + if len(actionStr) == 0 { + return str, errors.New("Action can't be empty; idx=" + strconv.Itoa(x)) + } + fields := strings.Split(actionStr, ":") + if len(fields) != 2 { + return str, errors.New("Action should have exactly one ':' - encountered:" + actionStr) + } + action := fields[0] + if action != "arrow_up" && action != "arrow_down" { + return str, errors.New("Action (part 1) should be either 'arrow_up' or 'arrow_down' - encountered:" + action) + } + prefix := fields[1] + sanPrefix := strconv.Itoa(len(prefix)) + sanStr += ";" + action + ":" + sanPrefix + } + return sanStr, nil +} + func (s *sanitizer) sanitizeCmdLine(cmdLine string) (string, error) { const optionEndingChars = "\"$'\\#[]!><|;{}()*,?~&=`:@^/+%." // all bash control characters, '=', ... const optionAllowedChars = "-_" // characters commonly found inside of options diff --git a/scripts/reshctl.sh b/scripts/reshctl.sh index b82cccb..a9c9376 100644 --- a/scripts/reshctl.sh +++ b/scripts/reshctl.sh @@ -46,7 +46,7 @@ __resh_unbind_arrows() { echo "Warn: Couldn't revert arrow DOWN binding because 'revert command' is empty." else eval "$__RESH_bindfunc_revert_arrow_down_bind" - [ -z "${__RESH_bindfunc_revert_arrow_up_bind_vim+x}" ] || eval "$__RESH_bindfunc_revert_arrow_up_bind_vim" + [ -z "${__RESH_bindfunc_revert_arrow_down_bind_vim+x}" ] || eval "$__RESH_bindfunc_revert_arrow_down_bind_vim" echo "RESH arrow down binding successfully disabled ✓" __RESH_arrow_keys_bind_enabled=0 fi From cfce56232d7bd09f515e650ddb9b55e639b34432 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Fri, 13 Dec 2019 17:19:35 +0100 Subject: [PATCH 14/18] bump version --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 227cea2..7ec1d6d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.0 +2.1.0 From 968c792f6945f81d0e159d12e583ef20ea495f9a Mon Sep 17 00:00:00 2001 From: Simon Let Date: Fri, 13 Dec 2019 17:20:47 +0100 Subject: [PATCH 15/18] add vscode files to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 36f971e..dc4d504 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ bin/* +.vscode/* \ No newline at end of file From 15a431cfc340e56e05a7a5f8f6fc817eaa2be54b Mon Sep 17 00:00:00 2001 From: Simon Let Date: Fri, 13 Dec 2019 22:01:46 +0100 Subject: [PATCH 16/18] add inspect command, fix deduplication issues, fixes --- Makefile | 9 ++-- cmd/control/cmd/debug.go | 8 ++++ cmd/control/cmd/root.go | 1 + cmd/control/status/status.go | 2 + cmd/daemon/recall.go | 43 ++++++++++++++++++ cmd/daemon/run-server.go | 1 + cmd/inspect/main.go | 84 ++++++++++++++++++++++++++++++++++++ pkg/histfile/histfile.go | 36 ++++++++++------ pkg/histlist/histlist.go | 27 ++++++++++++ pkg/msg/msg.go | 12 ++++++ pkg/records/records.go | 11 ++++- pkg/sesshist/sesshist.go | 75 ++++++++++++++++++++++++-------- scripts/reshctl.sh | 7 +++ 13 files changed, 276 insertions(+), 40 deletions(-) create mode 100644 cmd/inspect/main.go create mode 100644 pkg/histlist/histlist.go create mode 100644 pkg/msg/msg.go 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" From aba197561a2106bf706febed856e639cb8582444 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Sat, 14 Dec 2019 15:36:53 +0100 Subject: [PATCH 17/18] load bash/zsh history when resh hostory is too small, fix duplicates in sesshist, fix lag at the end of hist session --- cmd/daemon/main.go | 6 ++- cmd/daemon/run-server.go | 9 +++- pkg/histfile/histfile.go | 85 +++++++++++++++++++++++++++++--------- pkg/histlist/histlist.go | 37 +++++++++++++++++ pkg/records/records.go | 88 +++++++++++++++++++++++++++++++++++----- pkg/sesshist/sesshist.go | 26 +++--------- scripts/hooks.sh | 1 + scripts/widgets.sh | 15 +++++-- 8 files changed, 210 insertions(+), 57 deletions(-) diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index 5591399..8045c5c 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -31,7 +31,9 @@ func main() { dir := usr.HomeDir pidfilePath := filepath.Join(dir, ".resh/resh.pid") configPath := filepath.Join(dir, ".config/resh.toml") - historyPath := filepath.Join(dir, ".resh_history.json") + reshHistoryPath := filepath.Join(dir, ".resh_history.json") + bashHistoryPath := filepath.Join(dir, ".bash_history") + zshHistoryPath := filepath.Join(dir, ".zsh_history") logPath := filepath.Join(dir, ".resh/daemon.log") f, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) @@ -73,7 +75,7 @@ func main() { if err != nil { log.Fatal("Could not create pidfile", err) } - runServer(config, historyPath) + runServer(config, reshHistoryPath, bashHistoryPath, zshHistoryPath) log.Println("main: Removing pidfile ...") err = os.Remove(pidfilePath) if err != nil { diff --git a/cmd/daemon/run-server.go b/cmd/daemon/run-server.go index df25adf..0be0ec8 100644 --- a/cmd/daemon/run-server.go +++ b/cmd/daemon/run-server.go @@ -13,7 +13,7 @@ import ( "github.com/curusarn/resh/pkg/signalhandler" ) -func runServer(config cfg.Config, historyPath string) { +func runServer(config cfg.Config, reshHistoryPath, bashHistoryPath, zshHistoryPath string) { var recordSubscribers []chan records.Record var sessionInitSubscribers []chan records.Record var sessionDropSubscribers []chan string @@ -36,7 +36,12 @@ func runServer(config cfg.Config, historyPath string) { sessionDropSubscribers = append(sessionDropSubscribers, histfileSessionsToDrop) histfileSignals := make(chan os.Signal) signalSubscribers = append(signalSubscribers, histfileSignals) - histfileBox := histfile.New(histfileRecords, historyPath, 10000, histfileSessionsToDrop, histfileSignals, shutdown) + maxHistSize := 10000 // lines + minHistSizeKB := 100 // roughly lines + histfileBox := histfile.New(histfileRecords, histfileSessionsToDrop, + reshHistoryPath, bashHistoryPath, zshHistoryPath, + maxHistSize, minHistSizeKB, + histfileSignals, shutdown) // sesshist New sesshistDispatch := sesshist.NewDispatch(sesshistSessionsToInit, sesshistSessionsToDrop, diff --git a/pkg/histfile/histfile.go b/pkg/histfile/histfile.go index 74f125d..d2740d6 100644 --- a/pkg/histfile/histfile.go +++ b/pkg/histfile/histfile.go @@ -3,6 +3,7 @@ package histfile import ( "encoding/json" "log" + "math" "os" "strconv" "sync" @@ -20,30 +21,68 @@ type Histfile struct { recentMutex sync.Mutex recentRecords []records.Record - cmdLines histlist.Histlist + // NOTE: we have separate histories which only differ if there was not enough resh_history + // resh_history itself is common for both bash and zsh + bashCmdLines histlist.Histlist + zshCmdLines histlist.Histlist } -// New creates new histfile and runs two gorutines on it -func New(input chan records.Record, historyPath string, initHistSize int, sessionsToDrop chan string, +// New creates new histfile and runs its gorutines +func New(input chan records.Record, sessionsToDrop chan string, + reshHistoryPath string, bashHistoryPath string, zshHistoryPath string, + maxInitHistSize int, minInitHistSizeKB int, signals chan os.Signal, shutdownDone chan string) *Histfile { hf := Histfile{ - sessions: map[string]records.Record{}, - historyPath: historyPath, - cmdLines: histlist.New(), + sessions: map[string]records.Record{}, + historyPath: reshHistoryPath, + bashCmdLines: histlist.New(), + zshCmdLines: histlist.New(), } - go hf.loadHistory(initHistSize) + go hf.loadHistory(bashHistoryPath, zshHistoryPath, maxInitHistSize, minInitHistSizeKB) go hf.writer(input, signals, shutdownDone) go hf.sessionGC(sessionsToDrop) return &hf } -func (h *Histfile) loadHistory(initHistSize int) { +// loadsHistory from resh_history and if there is not enough of it also load native shell histories +func (h *Histfile) loadHistory(bashHistoryPath, zshHistoryPath string, maxInitHistSize, minInitHistSizeKB int) { h.recentMutex.Lock() defer h.recentMutex.Unlock() - 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)) + log.Println("histfile: Checking if resh_history is large enough ...") + fi, err := os.Stat(h.historyPath) + var size int + if err != nil { + log.Println("histfile ERROR: failed to stat resh_history file:", err) + } else { + size = int(fi.Size()) + } + useNativeHistories := false + if size/1024 < minInitHistSizeKB { + useNativeHistories = true + log.Println("histfile WARN: resh_history is too small - loading native bash and zsh history ...") + h.bashCmdLines = records.LoadCmdLinesFromBashFile(bashHistoryPath) + log.Println("histfile: bash history loaded - cmdLine count:", len(h.bashCmdLines.List)) + h.zshCmdLines = records.LoadCmdLinesFromZshFile(zshHistoryPath) + log.Println("histfile: zsh history loaded - cmdLine count:", len(h.zshCmdLines.List)) + // no maxInitHistSize when using native histories + maxInitHistSize = math.MaxInt32 + } + log.Println("histfile: Loading resh history from file ...") + reshCmdLines := histlist.New() + // NOTE: keeping this weird interface for now because we might use it in the future + // when we only load bash or zsh history + records.LoadCmdLinesFromFile(&reshCmdLines, h.historyPath, maxInitHistSize) + log.Println("histfile: resh history loaded - cmdLine count:", len(reshCmdLines.List)) + if useNativeHistories == false { + h.bashCmdLines = reshCmdLines + h.zshCmdLines = histlist.Copy(reshCmdLines) + return + } + h.bashCmdLines.AddHistlist(reshCmdLines) + log.Println("histfile: bash history + resh history - cmdLine count:", len(h.bashCmdLines.List)) + h.zshCmdLines.AddHistlist(reshCmdLines) + log.Println("histfile: zsh history + resh history - cmdLine count:", len(h.zshCmdLines.List)) } // sessionGC reads sessionIDs from channel and deletes them from histfile struct @@ -124,12 +163,8 @@ func (h *Histfile) mergeAndWriteRecord(part1, part2 records.Record) { defer h.recentMutex.Unlock() h.recentRecords = append(h.recentRecords, part1) cmdLine := part1.CmdLine - idx, found := h.cmdLines.LastIndex[cmdLine] - if found { - h.cmdLines.List = append(h.cmdLines.List[:idx], h.cmdLines.List[idx+1:]...) - } - h.cmdLines.LastIndex[cmdLine] = len(h.cmdLines.List) - h.cmdLines.List = append(h.cmdLines.List, cmdLine) + h.bashCmdLines.AddCmdLine(cmdLine) + h.zshCmdLines.AddCmdLine(cmdLine) }() writeRecord(part1, h.historyPath) @@ -156,11 +191,21 @@ func writeRecord(rec records.Record, outputPath string) { } // GetRecentCmdLines returns recent cmdLines -func (h *Histfile) GetRecentCmdLines(limit int) histlist.Histlist { +func (h *Histfile) GetRecentCmdLines(shell string, limit int) histlist.Histlist { + // NOTE: limit does nothing atm 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)) + var hl histlist.Histlist + if shell == "bash" { + hl = histlist.Copy(h.bashCmdLines) + log.Println("histfile: history copied (bash) - cmdLine count:", len(hl.List)) + return hl + } + if shell != "zsh" { + log.Println("histfile ERROR: Unknown shell: ", shell) + } + hl = histlist.Copy(h.zshCmdLines) + log.Println("histfile: history copied (zsh) - cmdLine count:", len(hl.List)) return hl } diff --git a/pkg/histlist/histlist.go b/pkg/histlist/histlist.go index fc6f9b0..0f5f621 100644 --- a/pkg/histlist/histlist.go +++ b/pkg/histlist/histlist.go @@ -1,5 +1,7 @@ package histlist +import "log" + // Histlist is a deduplicated list of cmdLines type Histlist struct { // list of commands lines (deduplicated) @@ -25,3 +27,38 @@ func Copy(hl Histlist) Histlist { } return newHl } + +// AddCmdLine to the histlist +func (h *Histlist) AddCmdLine(cmdLine string) { + lenBefore := len(h.List) + // lookup + idx, found := h.LastIndex[cmdLine] + if found { + // remove duplicate + if cmdLine != h.List[idx] { + log.Println("histlist ERROR: Adding cmdLine:", cmdLine, " != LastIndex[cmdLine]:", h.List[idx]) + } + h.List = append(h.List[:idx], h.List[idx+1:]...) + // idx++ + for idx < len(h.List) { + cmdLn := h.List[idx] + h.LastIndex[cmdLn]-- + if idx != h.LastIndex[cmdLn] { + log.Println("histlist ERROR: Shifting LastIndex idx:", idx, " != LastIndex[cmdLn]:", h.LastIndex[cmdLn]) + } + idx++ + } + } + // update last index + h.LastIndex[cmdLine] = len(h.List) + // append new cmdline + h.List = append(h.List, cmdLine) + log.Println("histlist: Added cmdLine:", cmdLine, "; history length:", lenBefore, "->", len(h.List)) +} + +// AddHistlist contents of another histlist to this histlist +func (h *Histlist) AddHistlist(h2 Histlist) { + for _, cmdLine := range h2.List { + h.AddCmdLine(cmdLine) + } +} diff --git a/pkg/records/records.go b/pkg/records/records.go index 4ff8c19..29477fe 100644 --- a/pkg/records/records.go +++ b/pkg/records/records.go @@ -455,9 +455,10 @@ func (r *EnrichedRecord) DistanceTo(r2 EnrichedRecord, p DistParams) float64 { return dist } -// LoadCmdLinesFromFile loads limit cmdlines from file -func LoadCmdLinesFromFile(fname string, limit int) histlist.Histlist { +// LoadCmdLinesFromFile loads cmdlines from file +func LoadCmdLinesFromFile(hl *histlist.Histlist, fname string, limit int) { recs := LoadFromFile(fname, limit*3) // assume that at least 1/3 of commands is unique + // go from bottom and deduplicate var cmdLines []string cmdLinesSet := map[string]bool{} for i := len(recs) - 1; i >= 0; i-- { @@ -471,24 +472,24 @@ func LoadCmdLinesFromFile(fname string, limit int) histlist.Histlist { break } } - hl := histlist.New() - hl.List = cmdLines - for idx, cmdLine := range cmdLines { - hl.LastIndex[cmdLine] = idx + // add everything to histlist + for _, cmdLine := range cmdLines { + hl.AddCmdLine(cmdLine) } - return hl } -// LoadFromFile loads at most 'limit' records from 'fname' file +// LoadFromFile loads records from 'fname' file func LoadFromFile(fname string, limit int) []Record { // NOTE: limit does nothing atm + var recs []Record file, err := os.Open(fname) if err != nil { - log.Fatal("Open() resh history file error:", err) + log.Println("Open() resh history file error:", err) + log.Println("WARN: Skipping reading resh history!") + return recs } defer file.Close() - var recs []Record scanner := bufio.NewScanner(file) for scanner.Scan() { record := Record{} @@ -507,3 +508,70 @@ func LoadFromFile(fname string, limit int) []Record { } return recs } + +// LoadCmdLinesFromZshFile loads cmdlines from zsh history file +func LoadCmdLinesFromZshFile(fname string) histlist.Histlist { + file, err := os.Open(fname) + if err != nil { + log.Fatal("Open() resh history file error:", err) + } + defer file.Close() + + hl := histlist.New() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + // trim newline + line = strings.TrimRight(line, "\n") + var cmd string + // zsh format EXTENDED_HISTORY + // : 1576270617:0;make install + // zsh format no EXTENDED_HISTORY + // make install + if len(line) == 0 { + // skip empty + continue + } + if strings.Contains(line, ":") && strings.Contains(line, ";") && + len(strings.Split(line, ":")) >= 3 && len(strings.Split(line, ";")) >= 2 { + // contains at least 2x ':' and 1x ';' => assume EXTENDED_HISTORY + cmd = strings.Split(line, ";")[1] + } else { + cmd = line + } + hl.AddCmdLine(cmd) + } + return hl +} + +// LoadCmdLinesFromBashFile loads cmdlines from bash history file +func LoadCmdLinesFromBashFile(fname string) histlist.Histlist { + file, err := os.Open(fname) + if err != nil { + log.Fatal("Open() resh history file error:", err) + } + defer file.Close() + + hl := histlist.New() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + // trim newline + line = strings.TrimRight(line, "\n") + // trim spaces from left + line = strings.TrimLeft(line, " ") + // bash format (two lines) + // #1576199174 + // make install + if strings.HasPrefix(line, "#") { + // is either timestamp or comment => skip + continue + } + if len(line) == 0 { + // skip empty + continue + } + hl.AddCmdLine(line) + } + return hl +} diff --git a/pkg/sesshist/sesshist.go b/pkg/sesshist/sesshist.go index 1d6b32b..0aefa91 100644 --- a/pkg/sesshist/sesshist.go +++ b/pkg/sesshist/sesshist.go @@ -40,7 +40,7 @@ func (s *Dispatch) sessionInitializer(sessionsToInit chan records.Record) { for { record := <-sessionsToInit log.Println("sesshist: got session to init - " + record.SessionID) - s.initSession(record.SessionID) + s.initSession(record.SessionID, record.Shell) } } @@ -64,7 +64,7 @@ func (s *Dispatch) recordAdder(recordsToAdd chan records.Record) { } // InitSession struct -func (s *Dispatch) initSession(sessionID string) error { +func (s *Dispatch) initSession(sessionID string, shell string) error { log.Println("sesshist: initializing session - " + sessionID) s.mutex.RLock() _, found := s.sessions[sessionID] @@ -75,7 +75,7 @@ func (s *Dispatch) initSession(sessionID string) error { } log.Println("sesshist: loading history to populate session - " + sessionID) - historyCmdLines := s.history.GetRecentCmdLines(s.historyInitSize) + historyCmdLines := s.history.GetRecentCmdLines(shell, s.historyInitSize) s.mutex.Lock() defer s.mutex.Unlock() @@ -113,29 +113,15 @@ func (s *Dispatch) addRecentRecord(sessionID string, record records.Record) erro s.mutex.RUnlock() if found == false { - log.Println("sesshist ERROR: addRecontRecord(): No session history for SessionID " + sessionID + " - creating session history.") - s.initSession(sessionID) + log.Println("sesshist ERROR: addRecentRecord(): No session history for SessionID " + sessionID + " - creating session history.") + s.initSession(sessionID, record.Shell) 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 - // trim spaces to have less duplicates in the sesshist - cmdLine = strings.TrimRight(cmdLine, " ") - idx, found := session.recentCmdLines.LastIndex[cmdLine] - if found { - 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:]...) - } - log.Println("sesshist: Updating last index ...") - session.recentCmdLines.LastIndex[cmdLine] = len(session.recentCmdLines.List) - // append new record - log.Println("sesshist: Appending cmdLine ...") - session.recentCmdLines.List = append(session.recentCmdLines.List, cmdLine) + session.recentCmdLines.AddCmdLine(record.CmdLine) log.Println("sesshist: record:", record.CmdLine, "; added to session:", sessionID, "; session len:", len(session.recentCmdLines.List), "; session len (records):", len(session.recentRecords)) return nil diff --git a/scripts/hooks.sh b/scripts/hooks.sh index 4963f95..8da69c3 100644 --- a/scripts/hooks.sh +++ b/scripts/hooks.sh @@ -1,6 +1,7 @@ __resh_reset_variables() { __RESH_HISTNO=0 + __RESH_HISTNO_MAX="" __RESH_HISTNO_ZERO_LINE="" __RESH_HIST_PREV_LINE="" __RESH_HIST_RECALL_ACTIONS="" diff --git a/scripts/widgets.sh b/scripts/widgets.sh index 82761bd..24143b8 100644 --- a/scripts/widgets.sh +++ b/scripts/widgets.sh @@ -37,8 +37,12 @@ __resh_widget_arrow_up() { __RESH_HIST_RECALL_ACTIONS="$__RESH_HIST_RECALL_ACTIONS;arrow_up:$__RESH_PREFIX" # increment histno __RESH_HISTNO=$((__RESH_HISTNO+1)) - # back at histno == 0 => restore original line - if [ "$__RESH_HISTNO" -eq 0 ]; then + if [ "${#__RESH_HISTNO_MAX}" -gt 0 ] && [ "${__RESH_HISTNO}" -gt "${__RESH_HISTNO_MAX}" ]; then + # end of the session -> don't recall, do nothing + # fix histno + __RESH_HISTNO=$((__RESH_HISTNO-1)) + elif [ "$__RESH_HISTNO" -eq 0 ]; then + # back at histno == 0 => restore original line BUFFER=$__RESH_HISTNO_ZERO_LINE else # run recall @@ -46,7 +50,12 @@ __resh_widget_arrow_up() { NEW_BUFFER="$(__resh_collect --recall --prefix-search "$__RESH_PREFIX" 2> ~/.resh/arrow_up_last_run_out.txt)" # IF new buffer in non-empty THEN use the new buffer ELSE revert histno change # shellcheck disable=SC2015 - [ "${#NEW_BUFFER}" -gt 0 ] && BUFFER=$NEW_BUFFER || __RESH_HISTNO=$((__RESH_HISTNO-1)) + if [ "${#NEW_BUFFER}" -gt 0 ]; then + BUFFER=$NEW_BUFFER + else + __RESH_HISTNO=$((__RESH_HISTNO-1)) + __RESH_HISTNO_MAX=$__RESH_HISTNO + fi fi # run post helper __resh_helper_arrow_post From 963fd9e9726f4d69fb16d5691f880bb843de5bc5 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Sat, 14 Dec 2019 15:47:33 +0100 Subject: [PATCH 18/18] init session when resh updates, cleanup --- cmd/daemon/run-server.go | 4 ++-- pkg/histlist/histlist.go | 4 ++-- pkg/sesshist/sesshist.go | 17 ++++++++++++++++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/cmd/daemon/run-server.go b/cmd/daemon/run-server.go index 0be0ec8..d1a87b7 100644 --- a/cmd/daemon/run-server.go +++ b/cmd/daemon/run-server.go @@ -36,8 +36,8 @@ func runServer(config cfg.Config, reshHistoryPath, bashHistoryPath, zshHistoryPa sessionDropSubscribers = append(sessionDropSubscribers, histfileSessionsToDrop) histfileSignals := make(chan os.Signal) signalSubscribers = append(signalSubscribers, histfileSignals) - maxHistSize := 10000 // lines - minHistSizeKB := 100 // roughly lines + maxHistSize := 10000 // lines + minHistSizeKB := 2000 // roughly lines histfileBox := histfile.New(histfileRecords, histfileSessionsToDrop, reshHistoryPath, bashHistoryPath, zshHistoryPath, maxHistSize, minHistSizeKB, diff --git a/pkg/histlist/histlist.go b/pkg/histlist/histlist.go index 0f5f621..a3f4334 100644 --- a/pkg/histlist/histlist.go +++ b/pkg/histlist/histlist.go @@ -30,7 +30,7 @@ func Copy(hl Histlist) Histlist { // AddCmdLine to the histlist func (h *Histlist) AddCmdLine(cmdLine string) { - lenBefore := len(h.List) + // lenBefore := len(h.List) // lookup idx, found := h.LastIndex[cmdLine] if found { @@ -53,7 +53,7 @@ func (h *Histlist) AddCmdLine(cmdLine string) { h.LastIndex[cmdLine] = len(h.List) // append new cmdline h.List = append(h.List, cmdLine) - log.Println("histlist: Added cmdLine:", cmdLine, "; history length:", lenBefore, "->", len(h.List)) + // log.Println("histlist: Added cmdLine:", cmdLine, "; history length:", lenBefore, "->", len(h.List)) } // AddHistlist contents of another histlist to this histlist diff --git a/pkg/sesshist/sesshist.go b/pkg/sesshist/sesshist.go index 0aefa91..ac8bd92 100644 --- a/pkg/sesshist/sesshist.go +++ b/pkg/sesshist/sesshist.go @@ -58,13 +58,28 @@ func (s *Dispatch) recordAdder(recordsToAdd chan records.Record) { if record.PartOne { log.Println("sesshist: got record to add - " + record.CmdLine) s.addRecentRecord(record.SessionID, record) + } else { + // this inits session on RESH update + s.checkSession(record.SessionID, record.Shell) } // TODO: we will need to handle part2 as well eventually } } +func (s *Dispatch) checkSession(sessionID, shell string) { + s.mutex.RLock() + _, found := s.sessions[sessionID] + s.mutex.RUnlock() + if found == false { + err := s.initSession(sessionID, shell) + if err != nil { + log.Println("sesshist: Error while checking session:", err) + } + } +} + // InitSession struct -func (s *Dispatch) initSession(sessionID string, shell string) error { +func (s *Dispatch) initSession(sessionID, shell string) error { log.Println("sesshist: initializing session - " + sessionID) s.mutex.RLock() _, found := s.sessions[sessionID]