diff --git a/.gitmodules b/.gitmodules index ea75497..4d42b4f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "submodules/bash-preexec"] path = submodules/bash-preexec url = https://github.com/rcaloras/bash-preexec.git +[submodule "submodules/bash-zsh-compat-widgets"] + path = submodules/bash-zsh-compat-widgets + url = git@github.com:curusarn/bash-zsh-compat-widgets.git diff --git a/Makefile b/Makefile index 353f9c1..9b86acd 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ sanitize: # # -build: test_go submodules bin/resh-collect bin/resh-daemon bin/resh-evaluate bin/resh-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 test_go: # Running tests @@ -50,7 +50,7 @@ test_go: go test $$dir/*.go ; \ done -test: +test: test_go scripts/test.sh rebuild: @@ -60,15 +60,24 @@ rebuild: clean: rm 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 +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 # Copying files to resh directory ... cp -f submodules/bash-preexec/bash-preexec.sh ~/.bash-preexec.sh + cp -f submodules/bash-zsh-compat-widgets/bindfunc.sh ~/.resh/bindfunc.sh + cp -f conf/config.toml ~/.config/resh.toml + cp -f scripts/shellrc.sh ~/.resh/shellrc + cp -f scripts/reshctl.sh scripts/widgets.sh scripts/hooks.sh scripts/util.sh ~/.resh/ + + bin/resh-control completion bash > ~/.resh/bash_completion.d/_reshctl + bin/resh-control completion zsh > ~/.resh/zsh_completion.d/_reshctl + cp -f scripts/uuid.sh ~/.resh/bin/resh-uuid cp -f bin/* ~/.resh/bin/ cp -f scripts/resh-evaluate-plot.py ~/.resh/bin/ - cp -fr data/sanitizer ~/.resh/ + cp -fr data/sanitizer ~/.resh/sanitizer_data # backward compatibility: We have a new location for resh history file [ ! -f ~/.resh/history.json ] || mv ~/.resh/history.json ~/.resh_history.json # Adding resh shellrc to .bashrc ... @@ -81,8 +90,14 @@ install: build submodules/bash-preexec/bash-preexec.sh scripts/shellrc.sh conf/c grep '[ -f ~/.resh/shellrc ] && source ~/.resh/shellrc' ~/.zshrc ||\ echo '[ -f ~/.resh/shellrc ] && source ~/.resh/shellrc' >> ~/.zshrc # Restarting resh daemon ... - [ ! -f ~/.resh/resh.pid ] || kill -SIGTERM $$(cat ~/.resh/resh.pid) + -[ ! -f ~/.resh/resh.pid ] || kill -SIGTERM $$(cat ~/.resh/resh.pid) nohup resh-daemon &>/dev/null & disown + # Reloading rc files + . ~/.resh/shellrc + # Generating resh-uuid + [ -e "$(HOME)/.resh/resh-uuid" ] \ + || cat /proc/sys/kernel/random/uuid > "$(HOME)/.resh/resh-uuid" 2>/dev/null \ + || ./uuid.sh > "$(HOME)/.resh/resh-uuid" 2>/dev/null # Final touch touch ~/.resh_history.json # @@ -116,10 +131,12 @@ uninstall: # Uninstalling ... -rm -rf ~/.resh/ -bin/resh-%: cmd/%/main.go pkg/*/*.go VERSION +bin/resh-control: cmd/control/cmd/*.go + +bin/resh-%: cmd/%/*.go pkg/*/*.go VERSION go build ${GOFLAGS} -o $@ cmd/$*/*.go -$(HOME)/.resh $(HOME)/.resh/bin $(HOME)/.config: +$(HOME)/.resh $(HOME)/.resh/bin $(HOME)/.config $(HOME)/.resh/bash_completion.d $(HOME)/.resh/zsh_completion.d: # Creating dirs ... mkdir -p $@ @@ -129,7 +146,8 @@ $(HOME)/.resh/resh-uuid: .PHONY: submodules build install rebuild uninstall clean autoinstall -submodules: | submodules/bash-preexec/bash-preexec.sh + +submodules: | submodules/bash-preexec/bash-preexec.sh submodules/bash-zsh-compat-widgets/bindfunc.sh @# sets submodule.recurse to true if unset @# sets status.submoduleSummary to true if unset @git config --get submodule.recurse >/dev/null || git config --global submodule.recurse true diff --git a/VERSION b/VERSION index 781dcb0..6085e94 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.3 +1.2.1 diff --git a/cmd/collect/main.go b/cmd/collect/main.go index 75cc61d..5699ac6 100644 --- a/cmd/collect/main.go +++ b/cmd/collect/main.go @@ -1,24 +1,20 @@ package main import ( - "bytes" - "encoding/json" "flag" "fmt" - "io/ioutil" "log" - "net/http" "os" "github.com/BurntSushi/toml" "github.com/curusarn/resh/pkg/cfg" + "github.com/curusarn/resh/pkg/collect" "github.com/curusarn/resh/pkg/records" // "os/exec" "os/user" "path/filepath" "strconv" - "strings" ) // Version from git set during build @@ -39,18 +35,29 @@ func main() { if _, err := toml.DecodeFile(configPath, &config); err != nil { log.Fatal("Error reading config:", err) } + // recall command + recall := flag.Bool("recall", false, "Recall command on position --histno") + recallHistno := flag.Int("histno", 0, "Recall command on position --histno") + recallPrefix := flag.String("prefix-search", "", "Recall command based on prefix --prefix-search") + + // version showVersion := flag.Bool("version", false, "Show version and exit") showRevision := flag.Bool("revision", false, "Show git revision and exit") requireVersion := flag.String("requireVersion", "", "abort if version doesn't match") requireRevision := flag.String("requireRevision", "", "abort if revision doesn't match") + // core cmdLine := flag.String("cmdLine", "", "command line") exitCode := flag.Int("exitCode", -1, "exit code") shell := flag.String("shell", "", "actual shell") uname := flag.String("uname", "", "uname") sessionID := flag.String("sessionId", "", "resh generated session id") + // recall metadata + recallActions := flag.String("recall-actions", "", "recall actions that took place before executing the command") + recallStrategy := flag.String("recall-strategy", "", "recall strategy used during recall actions") + // posix variables cols := flag.String("cols", "-1", "$COLUMNS") lines := flag.String("lines", "-1", "$LINES") @@ -60,7 +67,6 @@ func main() { login := flag.String("login", "", "$LOGIN") // path := flag.String("path", "", "$PATH") pwd := flag.String("pwd", "", "$PWD - present working directory") - pwdAfter := flag.String("pwdAfter", "", "$PWD after command") shellEnv := flag.String("shellEnv", "", "$SHELL") term := flag.String("term", "", "$TERM") @@ -81,7 +87,6 @@ func main() { // before after timezoneBefore := flag.String("timezoneBefore", "", "") - timezoneAfter := flag.String("timezoneAfter", "", "") osReleaseID := flag.String("osReleaseId", "", "/etc/os-release ID") osReleaseVersionID := flag.String("osReleaseVersionId", "", @@ -92,7 +97,6 @@ func main() { "/etc/os-release ID") rtb := flag.String("realtimeBefore", "-1", "before $EPOCHREALTIME") - rta := flag.String("realtimeAfter", "-1", "after $EPOCHREALTIME") rtsess := flag.String("realtimeSession", "-1", "on session start $EPOCHREALTIME") rtsessboot := flag.String("realtimeSessSinceBoot", "-1", @@ -121,10 +125,11 @@ func main() { ")") os.Exit(3) } - realtimeAfter, err := strconv.ParseFloat(*rta, 64) - if err != nil { - log.Fatal("Flag Parsing error (rta):", err) + if *recallPrefix != "" && *recall == false { + log.Println("Option '--prefix-search' only works with '--recall' option - exiting!") + os.Exit(4) } + realtimeBefore, err := strconv.ParseFloat(*rtb, 64) if err != nil { log.Fatal("Flag Parsing error (rtb):", err) @@ -137,40 +142,32 @@ func main() { if err != nil { log.Fatal("Flag Parsing error (rt sess boot):", err) } - realtimeDuration := realtimeAfter - realtimeBefore realtimeSinceSessionStart := realtimeBefore - realtimeSessionStart realtimeSinceBoot := realtimeSessSinceBoot + realtimeSinceSessionStart - timezoneBeforeOffset := getTimezoneOffsetInSeconds(*timezoneBefore) - timezoneAfterOffset := getTimezoneOffsetInSeconds(*timezoneAfter) + timezoneBeforeOffset := collect.GetTimezoneOffsetInSeconds(*timezoneBefore) realtimeBeforeLocal := realtimeBefore + timezoneBeforeOffset - realtimeAfterLocal := realtimeAfter + timezoneAfterOffset realPwd, err := filepath.EvalSymlinks(*pwd) if err != nil { log.Println("err while handling pwd realpath:", err) realPwd = "" } - realPwdAfter, err := filepath.EvalSymlinks(*pwdAfter) - if err != nil { - log.Println("err while handling pwdAfter realpath:", err) - realPwd = "" - } - gitDir, gitRealDir := getGitDirs(*gitCdup, *gitCdupExitCode, *pwd) + gitDir, gitRealDir := collect.GetGitDirs(*gitCdup, *gitCdupExitCode, *pwd) if *gitRemoteExitCode != 0 { *gitRemote = "" } - if *osReleaseID == "" { - *osReleaseID = "linux" - } - if *osReleaseName == "" { - *osReleaseName = "Linux" - } - if *osReleasePrettyName == "" { - *osReleasePrettyName = "Linux" - } + // if *osReleaseID == "" { + // *osReleaseID = "linux" + // } + // if *osReleaseName == "" { + // *osReleaseName = "Linux" + // } + // if *osReleasePrettyName == "" { + // *osReleasePrettyName = "Linux" + // } rec := records.Record{ // posix @@ -178,6 +175,8 @@ func main() { Lines: *lines, // core BaseRecord: records.BaseRecord{ + RecallHistno: *recallHistno, + CmdLine: *cmdLine, ExitCode: *exitCode, Shell: *shell, @@ -191,38 +190,32 @@ func main() { Login: *login, // Path: *path, Pwd: *pwd, - PwdAfter: *pwdAfter, ShellEnv: *shellEnv, Term: *term, // non-posix - RealPwd: realPwd, - RealPwdAfter: realPwdAfter, - Pid: *pid, - SessionPid: *sessionPid, - Host: *host, - Hosttype: *hosttype, - Ostype: *ostype, - Machtype: *machtype, - Shlvl: *shlvl, + RealPwd: realPwd, + Pid: *pid, + SessionPID: *sessionPid, + Host: *host, + Hosttype: *hosttype, + Ostype: *ostype, + Machtype: *machtype, + Shlvl: *shlvl, // before after TimezoneBefore: *timezoneBefore, - TimezoneAfter: *timezoneAfter, RealtimeBefore: realtimeBefore, - RealtimeAfter: realtimeAfter, RealtimeBeforeLocal: realtimeBeforeLocal, - RealtimeAfterLocal: realtimeAfterLocal, - RealtimeDuration: realtimeDuration, RealtimeSinceSessionStart: realtimeSinceSessionStart, RealtimeSinceBoot: realtimeSinceBoot, GitDir: gitDir, GitRealDir: gitRealDir, GitOriginRemote: *gitRemote, - MachineID: readFileContent(machineIDPath), + MachineID: collect.ReadFileContent(machineIDPath), OsReleaseID: *osReleaseID, OsReleaseVersionID: *osReleaseVersionID, @@ -230,109 +223,20 @@ func main() { OsReleaseName: *osReleaseName, OsReleasePrettyName: *osReleasePrettyName, - ReshUUID: readFileContent(reshUUIDPath), + PartOne: true, + + ReshUUID: collect.ReadFileContent(reshUUIDPath), ReshVersion: Version, ReshRevision: Revision, - }, - } - sendRecord(rec, strconv.Itoa(config.Port)) -} - -func sendRecord(r records.Record, port string) { - recJSON, err := json.Marshal(r) - if err != nil { - log.Fatal("send err 1", err) - } - - req, err := http.NewRequest("POST", "http://localhost:"+port+"/record", - bytes.NewBuffer(recJSON)) - if err != nil { - log.Fatal("send err 2", err) - } - req.Header.Set("Content-Type", "application/json") - - client := &http.Client{} - _, err = client.Do(req) - if err != nil { - log.Fatal("resh-daemon is not running :(") - } -} - -func readFileContent(path string) string { - dat, err := ioutil.ReadFile(path) - if err != nil { - return "" - //log.Fatal("failed to open " + path) - } - return strings.TrimSuffix(string(dat), "\n") -} -func getGitDirs(cdup string, exitCode int, pwd string) (string, string) { - if exitCode != 0 { - return "", "" - } - abspath := filepath.Clean(filepath.Join(pwd, cdup)) - realpath, err := filepath.EvalSymlinks(abspath) - if err != nil { - log.Println("err while handling git dir paths:", err) - return "", "" - } - return abspath, realpath -} - -func getTimezoneOffsetInSeconds(zone string) float64 { - // date +%z -> "+0200" - hoursStr := zone[:3] - minsStr := zone[3:] - hours, err := strconv.Atoi(hoursStr) - if err != nil { - log.Println("err while parsing hours in timezone offset:", err) - return -1 + RecallActionsRaw: *recallActions, + RecallPrefix: *recallPrefix, + RecallStrategy: *recallStrategy, + }, } - mins, err := strconv.Atoi(minsStr) - if err != nil { - log.Println("err while parsing mins in timezone offset:", err) - return -1 + if *recall { + fmt.Print(collect.SendRecallRequest(rec, strconv.Itoa(config.Port))) + } else { + collect.SendRecord(rec, strconv.Itoa(config.Port), "/record") } - secs := ((hours * 60) + mins) * 60 - return float64(secs) } - -// func getGitRemote() string { -// out, err := exec.Command("git", "remote", "get-url", "origin").Output() -// if err != nil { -// if exitError, ok := err.(*exec.ExitError); ok { -// if exitError.ExitCode() == 128 { -// return "" -// } -// log.Fatal("git remote cmd failed") -// } else { -// log.Fatal("git remote cmd failed w/o exit code") -// } -// } -// return strings.TrimSuffix(string(out), "\n") -// } -// -// func getGitDir() string { -// // assume we are in pwd -// gitWorkTree := os.Getenv("GIT_WORK_TREE") -// -// if gitWorkTree != "" { -// return gitWorkTree -// } -// // we should look up the git directory ourselves -// // OR leave it to resh daemon to not slow down user -// out, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() -// if err != nil { -// if exitError, ok := err.(*exec.ExitError); ok { -// if exitError.ExitCode() == 128 { -// return "" -// } -// log.Fatal("git rev-parse cmd failed") -// } else { -// log.Fatal("git rev-parse cmd failed w/o exit code") -// } -// } -// return strings.TrimSuffix(string(out), "\n") -// } -// } diff --git a/cmd/control/cmd/completion.go b/cmd/control/cmd/completion.go new file mode 100644 index 0000000..bf09a67 --- /dev/null +++ b/cmd/control/cmd/completion.go @@ -0,0 +1,48 @@ +package cmd + +import ( + "os" + + "github.com/curusarn/resh/cmd/control/status" + "github.com/spf13/cobra" +) + +// completionCmd represents the completion command +var completionCmd = &cobra.Command{ + Use: "completion", + Short: "Generates bash/zsh completion scripts", + Long: `To load completion run + +. <(reshctl completion bash) + +OR + +. <(reshctl completion zsh) +`, +} + +var completionBashCmd = &cobra.Command{ + Use: "bash", + Short: "Generates bash completion scripts", + Long: `To load completion run + +. <(reshctl completion bash) +`, + Run: func(cmd *cobra.Command, args []string) { + rootCmd.GenBashCompletion(os.Stdout) + exitCode = status.Success + }, +} + +var completionZshCmd = &cobra.Command{ + Use: "zsh", + Short: "Generates zsh completion scripts", + Long: `To load completion run + +. <(reshctl completion zsh) +`, + Run: func(cmd *cobra.Command, args []string) { + rootCmd.GenZshCompletion(os.Stdout) + exitCode = status.Success + }, +} diff --git a/cmd/control/cmd/debug.go b/cmd/control/cmd/debug.go new file mode 100644 index 0000000..fc10550 --- /dev/null +++ b/cmd/control/cmd/debug.go @@ -0,0 +1,61 @@ +package cmd + +import ( + "fmt" + "io/ioutil" + "os/user" + "path/filepath" + + "github.com/curusarn/resh/cmd/control/status" + "github.com/spf13/cobra" +) + +var debugCmd = &cobra.Command{ + Use: "debug", + 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", + Long: "Reload resh rc files", + Run: func(cmd *cobra.Command, args []string) { + exitCode = status.ReloadRcFiles + }, +} + +var debugOutputCmd = &cobra.Command{ + Use: "output", + 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{ + "daemon_last_run_out.txt", + "collect_last_run_out.txt", + "postcollect_last_run_out.txt", + "session_init_last_run_out.txt", + "arrow_up_last_run_out.txt", + "arrow_down_last_run_out.txt", + } + usr, _ := user.Current() + dir := usr.HomeDir + reshdir := filepath.Join(dir, ".resh") + for _, fpath := range files { + fpath := filepath.Join(reshdir, fpath) + debugReadFile(fpath) + } + exitCode = status.Success + }, +} + +func debugReadFile(path string) { + fmt.Println("============================================================") + fmt.Println(" filepath:", path) + fmt.Println("============================================================") + dat, err := ioutil.ReadFile(path) + if err != nil { + fmt.Println("ERROR while reading file:", err) + } + fmt.Println(string(dat)) +} diff --git a/cmd/control/cmd/disable.go b/cmd/control/cmd/disable.go new file mode 100644 index 0000000..b0dead5 --- /dev/null +++ b/cmd/control/cmd/disable.go @@ -0,0 +1,24 @@ +package cmd + +import ( + "github.com/curusarn/resh/cmd/control/status" + "github.com/spf13/cobra" +) + +var disableCmd = &cobra.Command{ + Use: "disable", + Short: "disable RESH features", + Long: `Disables RESH bindings for arrows and C-R.`, + Run: func(cmd *cobra.Command, args []string) { + exitCode = status.DisableAll + }, +} + +// 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 +// }, +// } diff --git a/cmd/control/cmd/enable.go b/cmd/control/cmd/enable.go new file mode 100644 index 0000000..ab4b144 --- /dev/null +++ b/cmd/control/cmd/enable.go @@ -0,0 +1,24 @@ +package cmd + +import ( + "github.com/curusarn/resh/cmd/control/status" + "github.com/spf13/cobra" +) + +var enableCmd = &cobra.Command{ + Use: "enable", + Short: "enable RESH features", + Long: `Enables RESH bindings for arrows and C-R.`, + Run: func(cmd *cobra.Command, args []string) { + exitCode = status.EnableAll + }, +} + +// 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 +// }, +// } diff --git a/cmd/control/cmd/root.go b/cmd/control/cmd/root.go new file mode 100644 index 0000000..a00d342 --- /dev/null +++ b/cmd/control/cmd/root.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "fmt" + + "github.com/curusarn/resh/cmd/control/status" + "github.com/spf13/cobra" +) + +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.`, +} + +// Execute reshctl +func Execute() status.Code { + rootCmd.AddCommand(disableCmd) + // disableCmd.AddCommand(disableRecallingCmd) + + rootCmd.AddCommand(enableCmd) + // enableCmd.AddCommand(enableRecallingCmd) + + rootCmd.AddCommand(completionCmd) + completionCmd.AddCommand(completionBashCmd) + completionCmd.AddCommand(completionZshCmd) + + rootCmd.AddCommand(debugCmd) + debugCmd.AddCommand(debugReloadCmd) + debugCmd.AddCommand(debugOutputCmd) + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + return status.Fail + } + return exitCode +} diff --git a/cmd/control/main.go b/cmd/control/main.go new file mode 100644 index 0000000..7b03c2f --- /dev/null +++ b/cmd/control/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "os" + + "github.com/curusarn/resh/cmd/control/cmd" +) + +// Version from git set during build +var Version string + +// Revision from git set during build +var Revision string + +func main() { + os.Exit(int(cmd.Execute())) +} diff --git a/cmd/control/status/status.go b/cmd/control/status/status.go new file mode 100644 index 0000000..8b9eeec --- /dev/null +++ b/cmd/control/status/status.go @@ -0,0 +1,17 @@ +package status + +// Code - exit code of the resh-control command +type Code int + +const ( + // Success exit code + 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 + // ReloadRcFiles exit code - tells reshctl() wrapper to reload shellrc resh file + ReloadRcFiles = 200 +) diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index 9411350..a0ff6df 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -1,7 +1,7 @@ package main import ( - "encoding/json" + //"flag" "io/ioutil" "log" @@ -15,7 +15,6 @@ import ( "github.com/BurntSushi/toml" "github.com/curusarn/resh/pkg/cfg" - "github.com/curusarn/resh/pkg/records" ) // Version from git set during build @@ -32,7 +31,7 @@ func main() { dir := usr.HomeDir pidfilePath := filepath.Join(dir, ".resh/resh.pid") configPath := filepath.Join(dir, ".config/resh.toml") - outputPath := filepath.Join(dir, ".resh_history.json") + historyPath := filepath.Join(dir, ".resh_history.json") logPath := filepath.Join(dir, ".resh/daemon.log") f, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) @@ -70,7 +69,7 @@ func main() { if err != nil { log.Fatal("Could not create pidfile", err) } - runServer(config.Port, outputPath) + runServer(config, historyPath) err = os.Remove(pidfilePath) if err != nil { log.Println("Could not delete pidfile", err) @@ -83,52 +82,6 @@ func statusHandler(w http.ResponseWriter, r *http.Request) { log.Println("Status OK") } -type recordHandler struct { - OutputPath string -} - -func (h *recordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("OK\n")) - record := records.Record{} - - jsn, err := ioutil.ReadAll(r.Body) - if err != nil { - log.Println("Error reading the body", err) - return - } - - err = json.Unmarshal(jsn, &record) - if err != nil { - log.Println("Decoding error: ", err) - log.Println("Payload: ", jsn) - return - } - f, err := os.OpenFile(h.OutputPath, - os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - log.Println("Could not open file", err) - return - } - defer f.Close() - _, err = f.Write(append(jsn, []byte("\n")...)) - if err != nil { - log.Printf("Error while writing: %v, %s\n", record, err) - return - } - log.Println("Received: ", record.CmdLine) - - // fmt.Println("cmd:", r.CmdLine) - // fmt.Println("pwd:", r.Pwd) - // fmt.Println("git:", r.GitWorkTree) - // fmt.Println("exit_code:", r.ExitCode) -} - -func runServer(port int, outputPath string) { - http.HandleFunc("/status", statusHandler) - http.Handle("/record", &recordHandler{OutputPath: outputPath}) - http.ListenAndServe(":"+strconv.Itoa(port), nil) -} - func killDaemon(pidfile string) error { dat, err := ioutil.ReadFile(pidfile) if err != nil { @@ -158,25 +111,4 @@ func isDaemonRunning(port int) (bool, error) { } defer resp.Body.Close() return true, nil - //body, err := ioutil.ReadAll(resp.Body) - - // dat, err := ioutil.ReadFile(pidfile) - // if err != nil { - // log.Println("Reading pid file failed", err) - // return false, err - // } - // log.Print(string(dat)) - // pid, err := strconv.ParseInt(string(dat), 10, 64) - // if err != nil { - // log.Fatal(err) - // } - // process, err := os.FindProcess(int(pid)) - // if err != nil { - // log.Printf("Failed to find process: %s\n", err) - // return false, err - // } else { - // err := process.Signal(syscall.Signal(0)) - // log.Printf("process.Signal on pid %d returned: %v\n", pid, err) - // } - // return true, nil } diff --git a/cmd/daemon/recall.go b/cmd/daemon/recall.go new file mode 100644 index 0000000..7c5efeb --- /dev/null +++ b/cmd/daemon/recall.go @@ -0,0 +1,48 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + + "github.com/curusarn/resh/pkg/collect" + "github.com/curusarn/resh/pkg/records" + "github.com/curusarn/resh/pkg/sesshist" +) + +type recallHandler struct { + sesshistDispatch *sesshist.Dispatch +} + +func (h *recallHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + jsn, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Println("Error reading the body", err) + return + } + + rec := records.Record{} + err = json.Unmarshal(jsn, &rec) + if err != nil { + log.Println("Decoding error:", err) + log.Println("Payload:", jsn) + return + } + 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} + jsn, err = json.Marshal(&resp) + if err != nil { + log.Println("Encoding error:", err) + log.Println("Response:", resp) + return + } + log.Println(string(jsn)) + w.Write(jsn) + log.Println("/recall - sess id:", rec.SessionID, " - histno:", rec.RecallHistno, " -> ", cmd) +} diff --git a/cmd/daemon/record.go b/cmd/daemon/record.go new file mode 100644 index 0000000..ee403f8 --- /dev/null +++ b/cmd/daemon/record.go @@ -0,0 +1,47 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + + "github.com/curusarn/resh/pkg/records" +) + +type recordHandler struct { + subscribers []chan records.Record +} + +func (h *recordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("OK\n")) + jsn, err := ioutil.ReadAll(r.Body) + // run rest of the handler as goroutine to prevent any hangups + go func() { + if err != nil { + log.Println("Error reading the body", err) + return + } + + record := records.Record{} + err = json.Unmarshal(jsn, &record) + if err != nil { + log.Println("Decoding error: ", err) + log.Println("Payload: ", jsn) + return + } + for _, sub := range h.subscribers { + sub <- record + } + part := "2" + if record.PartOne { + part = "1" + } + log.Println("/record - ", record.CmdLine, " - part", part) + }() + + // fmt.Println("cmd:", r.CmdLine) + // fmt.Println("pwd:", r.Pwd) + // fmt.Println("git:", r.GitWorkTree) + // fmt.Println("exit_code:", r.ExitCode) +} diff --git a/cmd/daemon/run-server.go b/cmd/daemon/run-server.go new file mode 100644 index 0000000..6eb44b7 --- /dev/null +++ b/cmd/daemon/run-server.go @@ -0,0 +1,48 @@ +package main + +import ( + "net/http" + "strconv" + + "github.com/curusarn/resh/pkg/cfg" + "github.com/curusarn/resh/pkg/histfile" + "github.com/curusarn/resh/pkg/records" + "github.com/curusarn/resh/pkg/sesshist" + "github.com/curusarn/resh/pkg/sesswatch" +) + +func runServer(config cfg.Config, historyPath string) { + var recordSubscribers []chan records.Record + var sessionInitSubscribers []chan records.Record + var sessionDropSubscribers []chan string + + // sessshist + sesshistSessionsToInit := make(chan records.Record) + sessionInitSubscribers = append(sessionInitSubscribers, sesshistSessionsToInit) + sesshistSessionsToDrop := make(chan string) + sessionDropSubscribers = append(sessionDropSubscribers, sesshistSessionsToDrop) + sesshistRecords := make(chan records.Record) + recordSubscribers = append(recordSubscribers, sesshistRecords) + + // histfile + histfileRecords := make(chan records.Record) + recordSubscribers = append(recordSubscribers, histfileRecords) + histfileSessionsToDrop := make(chan string) + sessionDropSubscribers = append(sessionDropSubscribers, histfileSessionsToDrop) + histfileBox := histfile.New(histfileRecords, historyPath, 10000, histfileSessionsToDrop) + + // sesshist New + sesshistDispatch := sesshist.NewDispatch(sesshistSessionsToInit, sesshistSessionsToDrop, sesshistRecords, histfileBox, config.SesshistInitHistorySize) + + // sesswatch + sesswatchSessionsToWatch := make(chan records.Record) + sessionInitSubscribers = append(sessionInitSubscribers, sesswatchSessionsToWatch) + 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) +} diff --git a/cmd/daemon/session-init.go b/cmd/daemon/session-init.go new file mode 100644 index 0000000..27a1b27 --- /dev/null +++ b/cmd/daemon/session-init.go @@ -0,0 +1,38 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + + "github.com/curusarn/resh/pkg/records" +) + +type sessionInitHandler struct { + subscribers []chan records.Record +} + +func (h *sessionInitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("OK\n")) + jsn, err := ioutil.ReadAll(r.Body) + // run rest of the handler as goroutine to prevent any hangups + go func() { + if err != nil { + log.Println("Error reading the body", err) + return + } + + record := records.Record{} + err = json.Unmarshal(jsn, &record) + if err != nil { + log.Println("Decoding error: ", err) + log.Println("Payload: ", jsn) + return + } + for _, sub := range h.subscribers { + sub <- record + } + log.Println("/session_init - id:", record.SessionID, " - pid:", record.SessionPID) + }() +} diff --git a/cmd/event/main.go b/cmd/event/main.go new file mode 100644 index 0000000..fe3cb72 --- /dev/null +++ b/cmd/event/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("Hell world") +} diff --git a/cmd/postcollect/main.go b/cmd/postcollect/main.go new file mode 100644 index 0000000..6029bff --- /dev/null +++ b/cmd/postcollect/main.go @@ -0,0 +1,149 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + + "github.com/BurntSushi/toml" + "github.com/curusarn/resh/pkg/cfg" + "github.com/curusarn/resh/pkg/collect" + "github.com/curusarn/resh/pkg/records" + + // "os/exec" + "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") + reshUUIDPath := filepath.Join(dir, "/.resh/resh-uuid") + + machineIDPath := "/etc/machine-id" + + var config cfg.Config + if _, err := toml.DecodeFile(configPath, &config); err != nil { + log.Fatal("Error reading config:", err) + } + showVersion := flag.Bool("version", false, "Show version and exit") + showRevision := flag.Bool("revision", false, "Show git revision and exit") + + requireVersion := flag.String("requireVersion", "", "abort if version doesn't match") + requireRevision := flag.String("requireRevision", "", "abort if revision doesn't match") + + cmdLine := flag.String("cmdLine", "", "command line") + exitCode := flag.Int("exitCode", -1, "exit code") + sessionID := flag.String("sessionId", "", "resh generated session id") + shlvl := flag.Int("shlvl", -1, "$SHLVL") + + // posix variables + pwdAfter := flag.String("pwdAfter", "", "$PWD after command") + + // non-posix + // sessionPid := flag.Int("sessionPid", -1, "$$ at session start") + + gitCdupAfter := flag.String("gitCdupAfter", "", "git rev-parse --show-cdup") + gitRemoteAfter := flag.String("gitRemoteAfter", "", "git remote get-url origin") + + gitCdupExitCodeAfter := flag.Int("gitCdupExitCodeAfter", -1, "... $?") + gitRemoteExitCodeAfter := flag.Int("gitRemoteExitCodeAfter", -1, "... $?") + + // before after + timezoneAfter := flag.String("timezoneAfter", "", "") + + rtb := flag.String("realtimeBefore", "-1", "before $EPOCHREALTIME") + rta := flag.String("realtimeAfter", "-1", "after $EPOCHREALTIME") + flag.Parse() + + if *showVersion == true { + fmt.Println(Version) + os.Exit(0) + } + if *showRevision == true { + fmt.Println(Revision) + os.Exit(0) + } + if *requireVersion != "" && *requireVersion != Version { + fmt.Println("Please restart/reload this terminal session " + + "(resh version: " + Version + + "; resh version of this terminal session: " + *requireVersion + + ")") + os.Exit(3) + } + if *requireRevision != "" && *requireRevision != Revision { + fmt.Println("Please restart/reload this terminal session " + + "(resh revision: " + Revision + + "; resh revision of this terminal session: " + *requireRevision + + ")") + os.Exit(3) + } + realtimeAfter, err := strconv.ParseFloat(*rta, 64) + if err != nil { + log.Fatal("Flag Parsing error (rta):", err) + } + realtimeBefore, err := strconv.ParseFloat(*rtb, 64) + if err != nil { + log.Fatal("Flag Parsing error (rtb):", err) + } + realtimeDuration := realtimeAfter - realtimeBefore + + timezoneAfterOffset := collect.GetTimezoneOffsetInSeconds(*timezoneAfter) + realtimeAfterLocal := realtimeAfter + timezoneAfterOffset + + realPwdAfter, err := filepath.EvalSymlinks(*pwdAfter) + if err != nil { + log.Println("err while handling pwdAfter realpath:", err) + realPwdAfter = "" + } + + gitDirAfter, gitRealDirAfter := collect.GetGitDirs(*gitCdupAfter, *gitCdupExitCodeAfter, *pwdAfter) + if *gitRemoteExitCodeAfter != 0 { + *gitRemoteAfter = "" + } + + rec := records.Record{ + // core + BaseRecord: records.BaseRecord{ + CmdLine: *cmdLine, + ExitCode: *exitCode, + SessionID: *sessionID, + Shlvl: *shlvl, + + PwdAfter: *pwdAfter, + + // non-posix + RealPwdAfter: realPwdAfter, + + // before after + TimezoneAfter: *timezoneAfter, + + RealtimeBefore: realtimeBefore, + RealtimeAfter: realtimeAfter, + RealtimeAfterLocal: realtimeAfterLocal, + + RealtimeDuration: realtimeDuration, + + GitDirAfter: gitDirAfter, + GitRealDirAfter: gitRealDirAfter, + GitOriginRemoteAfter: *gitRemoteAfter, + MachineID: collect.ReadFileContent(machineIDPath), + + PartOne: false, + + ReshUUID: collect.ReadFileContent(reshUUIDPath), + ReshVersion: Version, + ReshRevision: Revision, + }, + } + collect.SendRecord(rec, strconv.Itoa(config.Port), "/record") +} diff --git a/cmd/sanitize/main.go b/cmd/sanitize/main.go index 763f29a..a7415b1 100644 --- a/cmd/sanitize/main.go +++ b/cmd/sanitize/main.go @@ -89,7 +89,7 @@ func main() { log.Println("Line:", line) log.Fatal("Decoding error:", err) } - record = records.ConvertRecord(&fallbackRecord) + record = records.Convert(&fallbackRecord) } err = sanitizer.sanitizeRecord(&record) if err != nil { diff --git a/cmd/session-init/main.go b/cmd/session-init/main.go new file mode 100644 index 0000000..b7c4516 --- /dev/null +++ b/cmd/session-init/main.go @@ -0,0 +1,186 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + + "github.com/BurntSushi/toml" + "github.com/curusarn/resh/pkg/cfg" + "github.com/curusarn/resh/pkg/collect" + "github.com/curusarn/resh/pkg/records" + + "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") + reshUUIDPath := filepath.Join(dir, "/.resh/resh-uuid") + + machineIDPath := "/etc/machine-id" + + var config cfg.Config + if _, err := toml.DecodeFile(configPath, &config); err != nil { + log.Fatal("Error reading config:", err) + } + showVersion := flag.Bool("version", false, "Show version and exit") + showRevision := flag.Bool("revision", false, "Show git revision and exit") + + requireVersion := flag.String("requireVersion", "", "abort if version doesn't match") + requireRevision := flag.String("requireRevision", "", "abort if revision doesn't match") + + shell := flag.String("shell", "", "actual shell") + uname := flag.String("uname", "", "uname") + sessionID := flag.String("sessionId", "", "resh generated session id") + + // posix variables + cols := flag.String("cols", "-1", "$COLUMNS") + lines := flag.String("lines", "-1", "$LINES") + home := flag.String("home", "", "$HOME") + lang := flag.String("lang", "", "$LANG") + lcAll := flag.String("lcAll", "", "$LC_ALL") + login := flag.String("login", "", "$LOGIN") + shellEnv := flag.String("shellEnv", "", "$SHELL") + term := flag.String("term", "", "$TERM") + + // non-posix + pid := flag.Int("pid", -1, "$$") + sessionPid := flag.Int("sessionPid", -1, "$$ at session start") + shlvl := flag.Int("shlvl", -1, "$SHLVL") + + host := flag.String("host", "", "$HOSTNAME") + hosttype := flag.String("hosttype", "", "$HOSTTYPE") + ostype := flag.String("ostype", "", "$OSTYPE") + machtype := flag.String("machtype", "", "$MACHTYPE") + + // before after + timezoneBefore := flag.String("timezoneBefore", "", "") + + osReleaseID := flag.String("osReleaseId", "", "/etc/os-release ID") + osReleaseVersionID := flag.String("osReleaseVersionId", "", + "/etc/os-release ID") + osReleaseIDLike := flag.String("osReleaseIdLike", "", "/etc/os-release ID") + osReleaseName := flag.String("osReleaseName", "", "/etc/os-release ID") + osReleasePrettyName := flag.String("osReleasePrettyName", "", + "/etc/os-release ID") + + rtb := flag.String("realtimeBefore", "-1", "before $EPOCHREALTIME") + rtsess := flag.String("realtimeSession", "-1", + "on session start $EPOCHREALTIME") + rtsessboot := flag.String("realtimeSessSinceBoot", "-1", + "on session start $EPOCHREALTIME") + flag.Parse() + + if *showVersion == true { + fmt.Println(Version) + os.Exit(0) + } + if *showRevision == true { + fmt.Println(Revision) + os.Exit(0) + } + if *requireVersion != "" && *requireVersion != Version { + fmt.Println("Please restart/reload this terminal session " + + "(resh version: " + Version + + "; resh version of this terminal session: " + *requireVersion + + ")") + os.Exit(3) + } + if *requireRevision != "" && *requireRevision != Revision { + fmt.Println("Please restart/reload this terminal session " + + "(resh revision: " + Revision + + "; resh revision of this terminal session: " + *requireRevision + + ")") + os.Exit(3) + } + realtimeBefore, err := strconv.ParseFloat(*rtb, 64) + if err != nil { + log.Fatal("Flag Parsing error (rtb):", err) + } + realtimeSessionStart, err := strconv.ParseFloat(*rtsess, 64) + if err != nil { + log.Fatal("Flag Parsing error (rt sess):", err) + } + realtimeSessSinceBoot, err := strconv.ParseFloat(*rtsessboot, 64) + if err != nil { + log.Fatal("Flag Parsing error (rt sess boot):", err) + } + realtimeSinceSessionStart := realtimeBefore - realtimeSessionStart + realtimeSinceBoot := realtimeSessSinceBoot + realtimeSinceSessionStart + + timezoneBeforeOffset := collect.GetTimezoneOffsetInSeconds(*timezoneBefore) + realtimeBeforeLocal := realtimeBefore + timezoneBeforeOffset + + if *osReleaseID == "" { + *osReleaseID = "linux" + } + if *osReleaseName == "" { + *osReleaseName = "Linux" + } + if *osReleasePrettyName == "" { + *osReleasePrettyName = "Linux" + } + + rec := records.Record{ + // posix + Cols: *cols, + Lines: *lines, + // core + BaseRecord: records.BaseRecord{ + Shell: *shell, + Uname: *uname, + SessionID: *sessionID, + + // posix + Home: *home, + Lang: *lang, + LcAll: *lcAll, + Login: *login, + // Path: *path, + ShellEnv: *shellEnv, + Term: *term, + + // non-posix + 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, + + MachineID: collect.ReadFileContent(machineIDPath), + + OsReleaseID: *osReleaseID, + OsReleaseVersionID: *osReleaseVersionID, + OsReleaseIDLike: *osReleaseIDLike, + OsReleaseName: *osReleaseName, + OsReleasePrettyName: *osReleasePrettyName, + + ReshUUID: collect.ReadFileContent(reshUUIDPath), + ReshVersion: Version, + ReshRevision: Revision, + }, + } + collect.SendRecord(rec, strconv.Itoa(config.Port), "/session_init") +} diff --git a/conf/config.toml b/conf/config.toml index c015120..1970f34 100644 --- a/conf/config.toml +++ b/conf/config.toml @@ -1 +1,3 @@ port = 2627 +sesswatchPeriodSeconds = 120 +sesshistInitHistorySize = 1000 diff --git a/go.mod b/go.mod index b13b13d..910abe6 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,9 @@ require ( 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 b5684fc..beb087d 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,51 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +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/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= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-shellwords v1.0.6 h1:9Jok5pILi5S1MnDirGVTufYGtksUs/V2BWUP3ZkeUUI= github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mb-14/gomarkov v0.0.0-20190125094512-044dd0dcb5e7 h1:VsJjhYhufMGXICLwLYr8mFVMp8/A+YqmagMHnG/BA/4= github.com/mb-14/gomarkov v0.0.0-20190125094512-044dd0dcb5e7/go.mod h1:zQmHoMvvVJb7cxyt1wGT77lqUaeOFXlogOppOr4uHVo= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b h1:9+ke9YJ9KGWw5ANXK6ozjoK47uI3uNbXv4YVINBnGm8= +github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/schollz/progressbar v1.0.0 h1:gbyFReLHDkZo8mxy/dLWMr+Mpb1MokGJ1FqCiqacjZM= github.com/schollz/progressbar v1.0.0/go.mod h1:/l9I7PC3L3erOuz54ghIRKUEFcosiWfLvJv+Eq26UMs= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 8373306..16726bb 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -2,5 +2,7 @@ package cfg // Config struct type Config struct { - Port int + Port int + SesswatchPeriodSeconds uint + SesshistInitHistorySize int } diff --git a/pkg/collect/collect.go b/pkg/collect/collect.go new file mode 100644 index 0000000..7f26b4e --- /dev/null +++ b/pkg/collect/collect.go @@ -0,0 +1,118 @@ +package collect + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "log" + "net/http" + "path/filepath" + "strconv" + "strings" + + "github.com/curusarn/resh/pkg/records" +) + +// SingleResponse json struct +type SingleResponse struct { + CmdLine string `json:"cmdline"` +} + +// SendRecallRequest to daemon +func SendRecallRequest(r records.Record, port string) string { + recJSON, err := json.Marshal(r) + if err != nil { + log.Fatal("send err 1", err) + } + + req, err := http.NewRequest("POST", "http://localhost:"+port+"/recall", + 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 := SingleResponse{} + err = json.Unmarshal(body, &response) + if err != nil { + log.Fatal("unmarshal resp error: ", err) + } + log.Println(response) + return response.CmdLine +} + +// SendRecord to daemon +func SendRecord(r records.Record, port, path string) { + recJSON, err := json.Marshal(r) + if err != nil { + log.Fatal("send err 1", err) + } + + req, err := http.NewRequest("POST", "http://localhost:"+port+path, + bytes.NewBuffer(recJSON)) + if err != nil { + log.Fatal("send err 2", err) + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + _, err = client.Do(req) + if err != nil { + log.Fatal("resh-daemon is not running :(") + } +} + +// ReadFileContent and return it as a string +func ReadFileContent(path string) string { + dat, err := ioutil.ReadFile(path) + if err != nil { + return "" + //log.Fatal("failed to open " + path) + } + return strings.TrimSuffix(string(dat), "\n") +} + +// GetGitDirs based on result of git "cdup" command +func GetGitDirs(cdup string, exitCode int, pwd string) (string, string) { + if exitCode != 0 { + return "", "" + } + abspath := filepath.Clean(filepath.Join(pwd, cdup)) + realpath, err := filepath.EvalSymlinks(abspath) + if err != nil { + log.Println("err while handling git dir paths:", err) + return "", "" + } + return abspath, realpath +} + +// GetTimezoneOffsetInSeconds based on zone returned by date command +func GetTimezoneOffsetInSeconds(zone string) float64 { + // date +%z -> "+0200" + hoursStr := zone[:3] + minsStr := zone[3:] + hours, err := strconv.Atoi(hoursStr) + if err != nil { + log.Println("err while parsing hours in timezone offset:", err) + return -1 + } + mins, err := strconv.Atoi(minsStr) + if err != nil { + log.Println("err while parsing mins in timezone offset:", err) + return -1 + } + secs := ((hours * 60) + mins) * 60 + return float64(secs) +} diff --git a/pkg/histanal/histload.go b/pkg/histanal/histload.go index 313c7ff..17497e5 100644 --- a/pkg/histanal/histload.go +++ b/pkg/histanal/histload.go @@ -162,7 +162,7 @@ func (e *HistLoad) loadHistoryRecords(fname string) []records.EnrichedRecord { log.Println("Line:", line) log.Fatal("Decoding error:", err) } - record = records.ConvertRecord(&fallbackRecord) + record = records.Convert(&fallbackRecord) } if e.sanitizedInput == false { if record.CmdLength != 0 { diff --git a/pkg/histfile/histfile.go b/pkg/histfile/histfile.go new file mode 100644 index 0000000..5964603 --- /dev/null +++ b/pkg/histfile/histfile.go @@ -0,0 +1,137 @@ +package histfile + +import ( + "encoding/json" + "log" + "os" + "strconv" + "sync" + + "github.com/curusarn/resh/pkg/records" +) + +// Histfile writes records to histfile +type Histfile struct { + sessionsMutex sync.Mutex + sessions map[string]records.Record + historyPath string + + recentMutex sync.Mutex + recentRecords []records.Record + recentCmdLines []string // deduplicated + cmdLinesLastIndex map[string]int +} + +// New creates new histfile and runs two gorutines on it +func New(input chan records.Record, historyPath string, initHistSize int, sessionsToDrop 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.sessionGC(sessionsToDrop) + return &hf +} + +func (h *Histfile) loadHistory(initHistSize int) { + h.recentMutex.Lock() + defer h.recentMutex.Unlock() + h.recentCmdLines = records.LoadCmdLinesFromFile(h.historyPath, initHistSize) +} + +// sessionGC reads sessionIDs from channel and deletes them from histfile struct +func (h *Histfile) sessionGC(sessionsToDrop chan string) { + for { + func() { + session := <-sessionsToDrop + log.Println("histfile: got session to drop", session) + h.sessionsMutex.Lock() + defer h.sessionsMutex.Unlock() + if part1, found := h.sessions[session]; found == true { + log.Println("histfile: Dropping session:", session) + delete(h.sessions, session) + go writeRecord(part1, h.historyPath) + } else { + log.Println("histfile: No hanging parts for session:", session) + } + }() + } +} + +// writer reads records from channel, merges them and writes them to file +func (h *Histfile) writer(input chan records.Record) { + for { + func() { + 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, ")") + } else { + delete(h.sessions, mergeID) + go h.mergeAndWriteRecord(part1, record) + } + } + }() + } +} + +func (h *Histfile) mergeAndWriteRecord(part1, part2 records.Record) { + err := part1.Merge(part2) + if err != nil { + log.Println("Error while merging", err) + return + } + + func() { + h.recentMutex.Lock() + defer h.recentMutex.Unlock() + h.recentRecords = append(h.recentRecords, part1) + cmdLine := part1.CmdLine + idx, found := h.cmdLinesLastIndex[cmdLine] + if found { + h.recentCmdLines = append(h.recentCmdLines[:idx], h.recentCmdLines[idx+1:]...) + } + h.cmdLinesLastIndex[cmdLine] = len(h.recentCmdLines) + h.recentCmdLines = append(h.recentCmdLines, cmdLine) + }() + + writeRecord(part1, h.historyPath) +} + +func writeRecord(rec records.Record, outputPath string) { + recJSON, err := json.Marshal(rec) + if err != nil { + log.Println("Marshalling error", err) + return + } + f, err := os.OpenFile(outputPath, + os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + log.Println("Could not open file", err) + return + } + defer f.Close() + _, err = f.Write(append(recJSON, []byte("\n")...)) + if err != nil { + log.Printf("Error while writing: %v, %s\n", rec, err) + return + } +} + +// GetRecentCmdLines returns recent cmdLines +func (h *Histfile) GetRecentCmdLines(limit int) []string { + return h.recentCmdLines +} diff --git a/pkg/records/records.go b/pkg/records/records.go index f15c717..3b4170a 100644 --- a/pkg/records/records.go +++ b/pkg/records/records.go @@ -1,10 +1,12 @@ package records import ( + "bufio" "encoding/json" "errors" "log" "math" + "os" "strconv" "strings" @@ -35,7 +37,7 @@ type BaseRecord struct { RealPwd string `json:"realPwd"` RealPwdAfter string `json:"realPwdAfter"` Pid int `json:"pid"` - SessionPid int `json:"sessionPid"` + SessionPID int `json:"sessionPid"` Host string `json:"host"` Hosttype string `json:"hosttype"` Ostype string `json:"ostype"` @@ -56,10 +58,13 @@ type BaseRecord struct { RealtimeSinceBoot float64 `json:"realtimeSinceBoot"` //Logs []string `json: "logs"` - GitDir string `json:"gitDir"` - GitRealDir string `json:"gitRealDir"` - GitOriginRemote string `json:"gitOriginRemote"` - MachineID string `json:"machineId"` + GitDir string `json:"gitDir"` + GitRealDir string `json:"gitRealDir"` + GitOriginRemote string `json:"gitOriginRemote"` + GitDirAfter string `json:"gitDirAfter"` + GitRealDirAfter string `json:"gitRealDirAfter"` + GitOriginRemoteAfter string `json:"gitOriginRemoteAfter"` + MachineID string `json:"machineId"` OsReleaseID string `json:"osReleaseId"` OsReleaseVersionID string `json:"osReleaseVersionId"` @@ -71,6 +76,22 @@ type BaseRecord struct { ReshVersion string `json:"reshVersion"` ReshRevision string `json:"reshRevision"` + // records come in two parts (collect and postcollect) + PartOne bool `json:"partOne,omitempty"` // false => part two + PartsMerged bool `json:"partsMerged"` + // special flag -> not an actual record but an session end + SessionExit bool `json:"sessionExit,omitempty"` + + // recall metadata + Recalled bool `json:"recalled"` + RecallHistno int `json:"recallHistno,omitempty"` + RecallStrategy string `json:"recallStrategy,omitempty"` + RecallActionsRaw string `json:"recallActionsRaw,omitempty"` + RecallActions []string `json:"recallActions,omitempty"` + + // recall command + RecallPrefix string `json:"recallPrefix,omitempty"` + // added by sanitizatizer Sanitized bool `json:"sanitized,omitempty"` CmdLength int `json:"cmdLength,omitempty"` @@ -108,8 +129,8 @@ type FallbackRecord struct { Lines int `json:"lines"` // notice the int type } -// ConvertRecord from FallbackRecord to Record -func ConvertRecord(r *FallbackRecord) Record { +// Convert from FallbackRecord to Record +func Convert(r *FallbackRecord) Record { return Record{ BaseRecord: r.BaseRecord, // these two lines are the only reason we are doing this @@ -151,6 +172,34 @@ func Enriched(r Record) EnrichedRecord { // TODO: Detect and mark simple commands r.Simple } +// Merge two records (part1 - collect + part2 - postcollect) +func (r *Record) Merge(r2 Record) error { + if r.PartOne == false || r2.PartOne { + return errors.New("Expected part1 and part2 of the same record - usage: part1.Merge(part2)") + } + if r.SessionID != r2.SessionID { + return errors.New("Records to merge are not from the same sesion - r1:" + r.SessionID + " r2:" + r2.SessionID) + } + if r.CmdLine != r2.CmdLine { + return errors.New("Records to merge are not parts of the same records - r1:" + r.CmdLine + " r2:" + r2.CmdLine) + } + // r.RealtimeBefore != r2.RealtimeBefore - can't be used because of bash-preexec runs when it's not supposed to + r.ExitCode = r2.ExitCode + r.PwdAfter = r2.PwdAfter + r.RealPwdAfter = r2.RealPwdAfter + r.GitDirAfter = r2.GitDirAfter + r.GitRealDirAfter = r2.GitRealDirAfter + r.RealtimeAfter = r2.RealtimeAfter + r.GitOriginRemoteAfter = r2.GitOriginRemoteAfter + r.TimezoneAfter = r2.TimezoneAfter + r.RealtimeAfterLocal = r2.RealtimeAfterLocal + r.RealtimeDuration = r2.RealtimeDuration + + r.PartsMerged = true + r.PartOne = false + return nil +} + // Validate - returns error if the record is invalid func (r *Record) Validate() error { if r.CmdLine == "" { @@ -389,3 +438,50 @@ func (r *EnrichedRecord) DistanceTo(r2 EnrichedRecord, p DistParams) float64 { return dist } + +// LoadCmdLinesFromFile loads limit cmdlines from file +func LoadCmdLinesFromFile(fname string, limit int) []string { + recs := LoadFromFile(fname, limit*3) // assume that at least 1/3 of commands is unique + var cmdLines []string + cmdLinesSet := map[string]bool{} + for i := len(recs) - 1; i >= 0; i-- { + cmdLine := recs[i].CmdLine + if cmdLinesSet[cmdLine] { + continue + } + cmdLinesSet[cmdLine] = true + cmdLines = append([]string{cmdLine}, cmdLines...) + if len(cmdLines) > limit { + break + } + } + return cmdLines +} + +// LoadFromFile loads at most 'limit' records from 'fname' file +func LoadFromFile(fname string, limit int) []Record { + file, err := os.Open(fname) + if err != nil { + log.Fatal("Open() resh history file error:", err) + } + defer file.Close() + + var recs []Record + scanner := bufio.NewScanner(file) + for scanner.Scan() { + record := Record{} + fallbackRecord := FallbackRecord{} + line := scanner.Text() + err = json.Unmarshal([]byte(line), &record) + if err != nil { + err = json.Unmarshal([]byte(line), &fallbackRecord) + if err != nil { + log.Println("Line:", line) + log.Fatal("Decoding error:", err) + } + record = Convert(&fallbackRecord) + } + recs = append(recs, record) + } + return recs +} diff --git a/pkg/sess/sess.go b/pkg/sess/sess.go new file mode 100644 index 0000000..f2e0fb8 --- /dev/null +++ b/pkg/sess/sess.go @@ -0,0 +1,7 @@ +package sess + +// Session represents a session, used for sennding through channels when more than just ID is needed +type Session struct { + ID string + PID int +} diff --git a/pkg/sesshist/sesshist.go b/pkg/sesshist/sesshist.go new file mode 100644 index 0000000..50b79ec --- /dev/null +++ b/pkg/sesshist/sesshist.go @@ -0,0 +1,201 @@ +package sesshist + +import ( + "errors" + "log" + "strconv" + "strings" + "sync" + + "github.com/curusarn/resh/pkg/histfile" + "github.com/curusarn/resh/pkg/records" +) + +// Dispatch Recall() calls to an apropriate session history (sesshist) +type Dispatch struct { + sessions map[string]*sesshist + mutex sync.RWMutex + + history *histfile.Histfile + historyInitSize int +} + +// NewDispatch creates a new sesshist.Dispatch and starts necessary gorutines +func NewDispatch(sessionsToInit chan records.Record, sessionsToDrop chan string, + recordsToAdd chan records.Record, history *histfile.Histfile, historyInitSize int) *Dispatch { + + s := Dispatch{ + sessions: map[string]*sesshist{}, + history: history, + historyInitSize: historyInitSize, + } + go s.sessionInitializer(sessionsToInit) + go s.sessionDropper(sessionsToDrop) + go s.recordAdder(recordsToAdd) + return &s +} + +func (s *Dispatch) sessionInitializer(sessionsToInit chan records.Record) { + for { + record := <-sessionsToInit + log.Println("sesshist: got session to init - " + record.SessionID) + s.initSession(record.SessionID) + } +} + +func (s *Dispatch) sessionDropper(sessionsToDrop chan string) { + for { + sessionID := <-sessionsToDrop + log.Println("sesshist: got session to drop - " + sessionID) + s.dropSession(sessionID) + } +} + +func (s *Dispatch) recordAdder(recordsToAdd chan records.Record) { + for { + record := <-recordsToAdd + if record.PartOne { + log.Println("sesshist: got record to add - " + record.CmdLine) + s.addRecentRecord(record.SessionID, record) + } + // TODO: we will need to handle part2 as well eventually + } +} + +// InitSession struct +func (s *Dispatch) initSession(sessionID string) error { + log.Println("sesshist: initializing session - " + sessionID) + s.mutex.RLock() + _, found := s.sessions[sessionID] + s.mutex.RUnlock() + + if found == true { + return errors.New("sesshist ERROR: Can't INIT already existing session " + sessionID) + } + + log.Println("sesshist: loading history to populate session - " + sessionID) + historyCmdLines := s.history.GetRecentCmdLines(s.historyInitSize) + + s.mutex.Lock() + defer s.mutex.Unlock() + // init sesshist and populate it with history loaded from file + s.sessions[sessionID] = &sesshist{ + recentCmdLines: historyCmdLines, + cmdLinesLastIndex: map[string]int{}, + } + log.Println("sesshist: session init done - " + sessionID) + return nil +} + +// DropSession struct +func (s *Dispatch) dropSession(sessionID string) error { + s.mutex.RLock() + _, found := s.sessions[sessionID] + s.mutex.RUnlock() + + if found == false { + return errors.New("sesshist ERROR: Can't DROP not existing session " + sessionID) + } + + s.mutex.Lock() + defer s.mutex.Unlock() + delete(s.sessions, sessionID) + return nil +} + +// AddRecent record to session +func (s *Dispatch) addRecentRecord(sessionID string, record records.Record) error { + s.mutex.RLock() + session, found := s.sessions[sessionID] + s.mutex.RUnlock() + + if found == false { + log.Println("sesshist ERROR: addRecontRecord(): No session history for SessionID " + sessionID + " - creating session history.") + s.initSession(sessionID) + return s.addRecentRecord(sessionID, record) + } + session.mutex.Lock() + defer session.mutex.Unlock() + session.recentRecords = append(session.recentRecords, record) + // remove previous occurance of record + cmdLine := record.CmdLine + idx, found := session.cmdLinesLastIndex[cmdLine] + if found { + session.recentCmdLines = append(session.recentCmdLines[:idx], session.recentCmdLines[idx+1:]...) + } + session.cmdLinesLastIndex[cmdLine] = len(session.recentCmdLines) + // append new record + session.recentCmdLines = append(session.recentCmdLines, cmdLine) + log.Println("sesshist: record:", record.CmdLine, "; added to session:", sessionID, + "; session len:", len(session.recentCmdLines), "; session len w/ dups:", len(session.recentRecords)) + return nil +} + +// Recall command from recent session history +func (s *Dispatch) Recall(sessionID string, histno int, prefix string) (string, error) { + s.mutex.RLock() + session, found := s.sessions[sessionID] + s.mutex.RUnlock() + + if found == false { + // go s.initSession(sessionID) + return "", errors.New("sesshist ERROR: No session history for SessionID " + sessionID + " - should we create one?") + } + if prefix == "" { + session.mutex.Lock() + defer session.mutex.Unlock() + return session.getRecordByHistno(histno) + } + session.mutex.Lock() + defer session.mutex.Unlock() + return session.searchRecordByPrefix(prefix, histno) +} + +type sesshist struct { + recentRecords []records.Record + recentCmdLines []string // deduplicated + cmdLinesLastIndex map[string]int + mutex sync.Mutex +} + +func (s *sesshist) getRecordByHistno(histno int) (string, error) { + // addRecords() appends records to the end of the slice + // -> this func handles the indexing + if histno == 0 { + return "", errors.New("sesshist ERROR: 'histno == 0' is not a record from history") + } + if histno < 0 { + return "", errors.New("sesshist ERROR: 'histno < 0' is a command from future (not supperted yet)") + } + index := len(s.recentCmdLines) - histno + if index < 0 { + return "", errors.New("sesshist ERROR: 'histno > number of commands in the session' (" + strconv.Itoa(len(s.recentCmdLines)) + ")") + } + return s.recentCmdLines[index], nil +} + +func (s *sesshist) searchRecordByPrefix(prefix string, histno int) (string, error) { + if histno == 0 { + return "", errors.New("sesshist ERROR: 'histno == 0' is not a record from history") + } + if histno < 0 { + return "", errors.New("sesshist ERROR: 'histno < 0' is a command from future (not supperted yet)") + } + index := len(s.recentCmdLines) - histno + if index < 0 { + return "", errors.New("sesshist ERROR: 'histno > number of commands in the session' (" + strconv.Itoa(len(s.recentCmdLines)) + ")") + } + cmdLines := []string{} + for i := len(s.recentCmdLines) - 1; i >= 0; i-- { + if strings.HasPrefix(s.recentCmdLines[i], prefix) { + cmdLines = append(cmdLines, s.recentCmdLines[i]) + if len(cmdLines) >= histno { + break + } + } + } + if len(cmdLines) < histno { + return "", errors.New("sesshist ERROR: 'histno > number of commands matching with given prefix' (" + strconv.Itoa(len(cmdLines)) + ")") + } + return cmdLines[histno-1], nil +} diff --git a/pkg/sesswatch/sesswatch.go b/pkg/sesswatch/sesswatch.go new file mode 100644 index 0000000..cb3d41e --- /dev/null +++ b/pkg/sesswatch/sesswatch.go @@ -0,0 +1,64 @@ +package sesswatch + +import ( + "log" + "sync" + "time" + + "github.com/curusarn/resh/pkg/records" + "github.com/mitchellh/go-ps" +) + +type sesswatch struct { + sessionsToDrop []chan string + sleepSeconds uint + + watchedSessions map[string]bool + mutex sync.Mutex +} + +// Go runs the session watcher - watches sessions and sends +func Go(sessionsToWatch chan records.Record, sessionsToDrop []chan string, sleepSeconds uint) { + sw := sesswatch{sessionsToDrop: sessionsToDrop, sleepSeconds: sleepSeconds, watchedSessions: map[string]bool{}} + go sw.waiter(sessionsToWatch) +} + +func (s *sesswatch) waiter(sessionsToWatch chan records.Record) { + for { + func() { + record := <-sessionsToWatch + id := record.SessionID + pid := record.SessionPID + s.mutex.Lock() + defer s.mutex.Unlock() + if s.watchedSessions[id] == false { + log.Println("sesswatch: start watching NEW session ~ pid:", id, "~", pid) + s.watchedSessions[id] = true + go s.watcher(id, pid) + } + }() + } +} + +func (s *sesswatch) watcher(sessionID string, sessionPID int) { + for { + time.Sleep(time.Duration(s.sleepSeconds) * time.Second) + proc, err := ps.FindProcess(sessionPID) + if err != nil { + log.Println("sesswatch ERROR: error while finding process:", sessionPID) + } else if proc == nil { + log.Println("sesswatch: Dropping session ~ pid:", sessionID, "~", sessionPID) + func() { + s.mutex.Lock() + defer s.mutex.Unlock() + s.watchedSessions[sessionID] = false + }() + for _, ch := range s.sessionsToDrop { + log.Println("sesswatch: sending 'drop session' message ...") + ch <- sessionID + log.Println("sesswatch: sending 'drop session' message DONE") + } + break + } + } +} diff --git a/scripts/hooks.sh b/scripts/hooks.sh new file mode 100644 index 0000000..4963f95 --- /dev/null +++ b/scripts/hooks.sh @@ -0,0 +1,161 @@ + +__resh_reset_variables() { + __RESH_HISTNO=0 + __RESH_HISTNO_ZERO_LINE="" + __RESH_HIST_PREV_LINE="" + __RESH_HIST_RECALL_ACTIONS="" + __RESH_HIST_NO_PREFIX_MODE=0 + __RESH_HIST_RECALL_STRATEGY="" +} + +__resh_preexec() { + # core + __RESH_COLLECT=1 + __RESH_CMDLINE="$1" # not local to preserve it for postcollect (useful as sanity check) + __resh_collect --cmdLine "$__RESH_CMDLINE" \ + --recall-actions "$__RESH_HIST_RECALL_ACTIONS" \ + --recall-strategy "$__RESH_HIST_RECALL_STRATEGY" \ + &>~/.resh/collect_last_run_out.txt || echo "resh-collect ERROR: $(head -n 1 ~/.resh/collect_last_run_out.txt)" +} + +# used for collect and collect --recall +__resh_collect() { + # posix + local __RESH_COLS="$COLUMNS" + local __RESH_LANG="$LANG" + local __RESH_LC_ALL="$LC_ALL" + # other LC ? + local __RESH_LINES="$LINES" + # __RESH_PATH="$PATH" + local __RESH_PWD="$PWD" + + # non-posix + local __RESH_SHLVL="$SHLVL" + local __RESH_GIT_CDUP; __RESH_GIT_CDUP="$(git rev-parse --show-cdup 2>/dev/null)" + local __RESH_GIT_CDUP_EXIT_CODE=$? + local __RESH_GIT_REMOTE; __RESH_GIT_REMOTE="$(git remote get-url origin 2>/dev/null)" + local __RESH_GIT_REMOTE_EXIT_CODE=$? + #__RESH_GIT_TOPLEVEL="$(git rev-parse --show-toplevel)" + #__RESH_GIT_TOPLEVEL_EXIT_CODE=$? + + if [ -n "$ZSH_VERSION" ]; then + # assume Zsh + local __RESH_PID="$$" # current pid + elif [ -n "$BASH_VERSION" ]; then + # assume Bash + local __RESH_PID="$BASHPID" # current pid + fi + # time + local __RESH_TZ_BEFORE; __RESH_TZ_BEFORE=$(date +%z) + # __RESH_RT_BEFORE="$EPOCHREALTIME" + __RESH_RT_BEFORE=$(__resh_get_epochrealtime) + + if [ "$__RESH_VERSION" != "$(resh-collect -version)" ]; then + # shellcheck source=shellrc.sh + source ~/.resh/shellrc + if [ "$__RESH_VERSION" != "$(resh-collect -version)" ]; then + echo "RESH WARNING: You probably just updated RESH - PLEASE RESTART OR RELOAD THIS TERMINAL SESSION (resh version: $(resh-collect -version); resh version of this terminal session: ${__RESH_VERSION})" + else + echo "RESH INFO: New RESH shellrc script was loaded - if you encounter any issues please restart this terminal session." + fi + elif [ "$__RESH_REVISION" != "$(resh-collect -revision)" ]; then + # shellcheck source=shellrc.sh + source ~/.resh/shellrc + if [ "$__RESH_REVISION" != "$(resh-collect -revision)" ]; then + echo "RESH WARNING: You probably just updated RESH - PLEASE RESTART OR RELOAD THIS TERMINAL SESSION (resh revision: $(resh-collect -revision); resh revision of this terminal session: ${__RESH_REVISION})" + fi + fi + if [ "$__RESH_VERSION" = "$(resh-collect -version)" ] && [ "$__RESH_REVISION" = "$(resh-collect -revision)" ]; then + resh-collect -requireVersion "$__RESH_VERSION" \ + -requireRevision "$__RESH_REVISION" \ + -shell "$__RESH_SHELL" \ + -uname "$__RESH_UNAME" \ + -sessionId "$__RESH_SESSION_ID" \ + -cols "$__RESH_COLS" \ + -home "$__RESH_HOME" \ + -lang "$__RESH_LANG" \ + -lcAll "$__RESH_LC_ALL" \ + -lines "$__RESH_LINES" \ + -login "$__RESH_LOGIN" \ + -pwd "$__RESH_PWD" \ + -shellEnv "$__RESH_SHELL_ENV" \ + -term "$__RESH_TERM" \ + -pid "$__RESH_PID" \ + -sessionPid "$__RESH_SESSION_PID" \ + -host "$__RESH_HOST" \ + -hosttype "$__RESH_HOSTTYPE" \ + -ostype "$__RESH_OSTYPE" \ + -machtype "$__RESH_MACHTYPE" \ + -shlvl "$__RESH_SHLVL" \ + -gitCdup "$__RESH_GIT_CDUP" \ + -gitCdupExitCode "$__RESH_GIT_CDUP_EXIT_CODE" \ + -gitRemote "$__RESH_GIT_REMOTE" \ + -gitRemoteExitCode "$__RESH_GIT_REMOTE_EXIT_CODE" \ + -realtimeBefore "$__RESH_RT_BEFORE" \ + -realtimeSession "$__RESH_RT_SESSION" \ + -realtimeSessSinceBoot "$__RESH_RT_SESS_SINCE_BOOT" \ + -timezoneBefore "$__RESH_TZ_BEFORE" \ + -osReleaseId "$__RESH_OS_RELEASE_ID" \ + -osReleaseVersionId "$__RESH_OS_RELEASE_VERSION_ID" \ + -osReleaseIdLike "$__RESH_OS_RELEASE_ID_LIKE" \ + -osReleaseName "$__RESH_OS_RELEASE_NAME" \ + -osReleasePrettyName "$__RESH_OS_RELEASE_PRETTY_NAME" \ + -histno "$__RESH_HISTNO" \ + "$@" + fi +} + +__resh_precmd() { + local __RESH_EXIT_CODE=$? + local __RESH_RT_AFTER + local __RESH_TZ_AFTER + local __RESH_PWD_AFTER + local __RESH_GIT_CDUP_AFTER + local __RESH_GIT_CDUP_EXIT_CODE_AFTER + local __RESH_GIT_REMOTE_AFTER + local __RESH_GIT_REMOTE_EXIT_CODE_AFTER + local __RESH_SHLVL="$SHLVL" + __RESH_RT_AFTER=$(__resh_get_epochrealtime) + __RESH_TZ_AFTER=$(date +%z) + __RESH_PWD_AFTER="$PWD" + __RESH_GIT_CDUP_AFTER="$(git rev-parse --show-cdup 2>/dev/null)" + __RESH_GIT_CDUP_EXIT_CODE_AFTER=$? + __RESH_GIT_REMOTE_AFTER="$(git remote get-url origin 2>/dev/null)" + __RESH_GIT_REMOTE_EXIT_CODE_AFTER=$? + if [ -n "${__RESH_COLLECT}" ]; then + if [ "$__RESH_VERSION" != "$(resh-postcollect -version)" ]; then + # shellcheck source=shellrc.sh + source ~/.resh/shellrc + if [ "$__RESH_VERSION" != "$(resh-postcollect -version)" ]; then + echo "RESH WARNING: You probably just updated RESH - PLEASE RESTART OR RELOAD THIS TERMINAL SESSION (resh version: $(resh-collect -version); resh version of this terminal session: ${__RESH_VERSION})" + else + echo "RESH INFO: New RESH shellrc script was loaded - if you encounter any issues please restart this terminal session." + fi + elif [ "$__RESH_REVISION" != "$(resh-postcollect -revision)" ]; then + # shellcheck source=shellrc.sh + source ~/.resh/shellrc + if [ "$__RESH_REVISION" != "$(resh-postcollect -revision)" ]; then + echo "RESH WARNING: You probably just updated RESH - PLEASE RESTART OR RELOAD THIS TERMINAL SESSION (resh revision: $(resh-collect -revision); resh revision of this terminal session: ${__RESH_REVISION})" + fi + fi + if [ "$__RESH_VERSION" = "$(resh-postcollect -version)" ] && [ "$__RESH_REVISION" = "$(resh-postcollect -revision)" ]; then + resh-postcollect -requireVersion "$__RESH_VERSION" \ + -requireRevision "$__RESH_REVISION" \ + -cmdLine "$__RESH_CMDLINE" \ + -realtimeBefore "$__RESH_RT_BEFORE" \ + -exitCode "$__RESH_EXIT_CODE" \ + -sessionId "$__RESH_SESSION_ID" \ + -shlvl "$__RESH_SHLVL" \ + -pwdAfter "$__RESH_PWD_AFTER" \ + -gitCdupAfter "$__RESH_GIT_CDUP_AFTER" \ + -gitCdupExitCodeAfter "$__RESH_GIT_CDUP_EXIT_CODE_AFTER" \ + -gitRemoteAfter "$__RESH_GIT_REMOTE_AFTER" \ + -gitRemoteExitCodeAfter "$__RESH_GIT_REMOTE_EXIT_CODE_AFTER" \ + -realtimeAfter "$__RESH_RT_AFTER" \ + -timezoneAfter "$__RESH_TZ_AFTER" \ + &>~/.resh/postcollect_last_run_out.txt || echo "resh-postcollect ERROR: $(head -n 1 ~/.resh/postcollect_last_run_out.txt)" + fi + __resh_reset_variables + fi + unset __RESH_COLLECT +} diff --git a/scripts/reshctl.sh b/scripts/reshctl.sh new file mode 100644 index 0000000..2656055 --- /dev/null +++ b/scripts/reshctl.sh @@ -0,0 +1,70 @@ + +# shellcheck source=../submodules/bash-zsh-compat-widgets/bindfunc.sh +. ~/.resh/bindfunc.sh +# shellcheck source=widgets.sh +. ~/.resh/widgets.sh + +__resh_bind_arrows() { + bindfunc '\e[A' __resh_widget_arrow_up_compat + bindfunc '\e[B' __resh_widget_arrow_down_compat + return 0 +} + +__resh_bind_control_R() { + 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" + return 0 +} + +__resh_unbind_control_R() { + echo "\ bindfunc __resh_widget_control_R_compat" + return 0 +} + +__resh_bind_all() { + __resh_bind_arrows + __resh_bind_control_R +} + +__resh_unbind_all() { + __resh_unbind_arrows + __resh_unbind_control_R +} + +reshctl() { + # run resh-control aka the real reshctl + resh-control "$@" + # modify current shell session based on exit status + local _status=$? + case "$_status" in + 0|1) + # success | fail + return "$_status" + ;; + # enable + 100) + # enable all + __resh_bind_all + return 0 + ;; + # disable + 110) + # disable all + __resh_unbind_all + return 0 + ;; + 200) + # reload rc files + . ~/.resh/shellrc + return 0 + ;; + *) + echo "reshctl() FATAL ERROR: unknown status" >&2 + return "$_status" + ;; + esac +} \ No newline at end of file diff --git a/scripts/shellrc.sh b/scripts/shellrc.sh index 126008b..c4d3afc 100644 --- a/scripts/shellrc.sh +++ b/scripts/shellrc.sh @@ -4,41 +4,12 @@ PATH=$PATH:~/.resh/bin # zmodload zsh/datetime # fi -__resh_get_uuid() { - cat /proc/sys/kernel/random/uuid 2>/dev/null || resh-uuid -} - -__resh_get_epochrealtime() { - if date +%s.%N | grep -vq 'N'; then - # GNU date - date +%s.%N - elif gdate --version >/dev/null && gdate +%s.%N | grep -vq 'N'; then - # GNU date take 2 - gdate +%s.%N - elif [ -n "$ZSH_VERSION" ]; then - # zsh fallback using $EPOCHREALTIME - if [ -z "${__RESH_ZSH_LOADED_DATETIME+x}" ]; then - zmodload zsh/datetime - __RESH_ZSH_LOADED_DATETIME=1 - fi - echo "$EPOCHREALTIME" - else - # dumb date - # XXX: we lost precison beyond seconds - date +%s - if [ -z "${__RESH_DATE_WARN+x}" ]; then - echo "resh WARN: can't get precise time - consider installing GNU date!" - __RESH_DATE_WARN=1 - fi - fi -} - -__resh_run_daemon() { - if [ -n "$ZSH_VERSION" ]; then - setopt LOCAL_OPTIONS NO_NOTIFY NO_MONITOR - fi - nohup resh-daemon &>/dev/null & disown -} +# shellcheck source=hooks.sh +. ~/.resh/hooks.sh +# shellcheck source=util.sh +. ~/.resh/util.sh +# shellcheck source=reshctl.sh +. ~/.resh/reshctl.sh __RESH_MACOS=0 __RESH_LINUX=0 @@ -53,23 +24,20 @@ else fi 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 __RESH_SHELL="bash" __RESH_HOST="$HOSTNAME" __RESH_HOSTTYPE="$HOSTTYPE" + __resh_bash_completion_init else echo "resh PANIC unrecognized shell" fi -if [ -z "${__RESH_SESSION_ID+x}" ]; then - export __RESH_SESSION_ID=$(__resh_get_uuid) - export __RESH_SESSION_PID="$$" - # TODO add sesson time -fi - # posix __RESH_HOME="$HOME" __RESH_LOGIN="$LOGNAME" @@ -101,116 +69,22 @@ __RESH_REVISION=$(resh-collect -revision) __resh_run_daemon -__resh_preexec() { - # core - __RESH_COLLECT=1 - __RESH_CMDLINE="$1" - - # posix - __RESH_COLS="$COLUMNS" - __RESH_LANG="$LANG" - __RESH_LC_ALL="$LC_ALL" - # other LC ? - __RESH_LINES="$LINES" - # __RESH_PATH="$PATH" - __RESH_PWD="$PWD" - - # non-posix - __RESH_SHLVL="$SHLVL" - __RESH_GIT_CDUP="$(git rev-parse --show-cdup 2>/dev/null)" - __RESH_GIT_CDUP_EXIT_CODE=$? - __RESH_GIT_REMOTE="$(git remote get-url origin 2>/dev/null)" - __RESH_GIT_REMOTE_EXIT_CODE=$? - #__RESH_GIT_TOPLEVEL="$(git rev-parse --show-toplevel)" - #__RESH_GIT_TOPLEVEL_EXIT_CODE=$? - - if [ -n "$ZSH_VERSION" ]; then - # assume Zsh - __RESH_PID="$$" # current pid - elif [ -n "$BASH_VERSION" ]; then - # assume Bash - __RESH_PID="$BASHPID" # current pid - fi - # time - __RESH_TZ_BEFORE=$(date +%z) - # __RESH_RT_BEFORE="$EPOCHREALTIME" - __RESH_RT_BEFORE=$(__resh_get_epochrealtime) - - # TODO: we should evaluate symlinks in preexec - # -> maybe create resh-precollect that could handle most of preexec - # maybe even move resh-collect here and send data to daemon and - # send rest of the data ($?, timeAfter) to daemon in precmd - # daemon will combine the data and save the record - # and save the unfinnished record even if it never finishes - # detect if the command died with the parent ps and save it then +# block for anything we only want to do once per session +# NOTE: nested shells are still the same session +if [ -z "${__RESH_SESSION_ID+x}" ]; then + export __RESH_SESSION_ID; __RESH_SESSION_ID=$(__resh_get_uuid) + export __RESH_SESSION_PID="$$" + # TODO add sesson time + __resh_reset_variables + __resh_session_init +fi -} +# block for anything we only want to do once per shell +if [ -z "${__RESH_INIT_DONE+x}" ]; then + preexec_functions+=(__resh_preexec) + precmd_functions+=(__resh_precmd) -__resh_precmd() { - __RESH_EXIT_CODE=$? - __RESH_RT_AFTER=$(__resh_get_epochrealtime) - __RESH_TZ_AFTER=$(date +%z) - __RESH_PWD_AFTER="$PWD" - if [ -n "${__RESH_COLLECT}" ]; then - if [ "$__RESH_VERSION" != $(resh-collect -version) ]; then - source ~/.resh/shellrc - if [ "$__RESH_VERSION" != $(resh-collect -version) ]; then - echo "RESH WARNING: You probably just updated RESH - PLEASE RESTART OR RELOAD THIS TERMINAL SESSION (resh version: $(resh-collect -version); resh version of this terminal session: ${__RESH_VERSION})" - else - echo "RESH INFO: New RESH shellrc script was loaded - if you encounter any issues please restart this terminal session." - fi - elif [ "$__RESH_REVISION" != $(resh-collect -revision) ]; then - source ~/.resh/shellrc - if [ "$__RESH_REVISION" != $(resh-collect -revision) ]; then - echo "RESH WARNING: You probably just updated RESH - PLEASE RESTART OR RELOAD THIS TERMINAL SESSION (resh revision: $(resh-collect -revision); resh revision of this terminal session: ${__RESH_REVISION})" - fi - fi - if [ "$__RESH_VERSION" = $(resh-collect -version) ] && [ "$__RESH_REVISION" = $(resh-collect -revision) ]; then - resh-collect -requireVersion "$__RESH_VERSION" \ - -requireRevision "$__RESH_REVISION" \ - -cmdLine "$__RESH_CMDLINE" \ - -exitCode "$__RESH_EXIT_CODE" \ - -shell "$__RESH_SHELL" \ - -uname "$__RESH_UNAME" \ - -sessionId "$__RESH_SESSION_ID" \ - -cols "$__RESH_COLS" \ - -home "$__RESH_HOME" \ - -lang "$__RESH_LANG" \ - -lcAll "$__RESH_LC_ALL" \ - -lines "$__RESH_LINES" \ - -login "$__RESH_LOGIN" \ - -pwd "$__RESH_PWD" \ - -pwdAfter "$__RESH_PWD_AFTER" \ - -shellEnv "$__RESH_SHELL_ENV" \ - -term "$__RESH_TERM" \ - -pid "$__RESH_PID" \ - -sessionPid "$__RESH_SESSION_PID" \ - -host "$__RESH_HOST" \ - -hosttype "$__RESH_HOSTTYPE" \ - -ostype "$__RESH_OSTYPE" \ - -machtype "$__RESH_MACHTYPE" \ - -shlvl "$__RESH_SHLVL" \ - -gitCdup "$__RESH_GIT_CDUP" \ - -gitCdupExitCode "$__RESH_GIT_CDUP_EXIT_CODE" \ - -gitRemote "$__RESH_GIT_REMOTE" \ - -gitRemoteExitCode "$__RESH_GIT_REMOTE_EXIT_CODE" \ - -realtimeBefore "$__RESH_RT_BEFORE" \ - -realtimeAfter "$__RESH_RT_AFTER" \ - -realtimeSession "$__RESH_RT_SESSION" \ - -realtimeSessSinceBoot "$__RESH_RT_SESS_SINCE_BOOT" \ - -timezoneBefore "$__RESH_TZ_BEFORE" \ - -timezoneAfter "$__RESH_TZ_AFTER" \ - -osReleaseId "$__RESH_OS_RELEASE_ID" \ - -osReleaseVersionId "$__RESH_OS_RELEASE_VERSION_ID" \ - -osReleaseIdLike "$__RESH_OS_RELEASE_ID_LIKE" \ - -osReleaseName "$__RESH_OS_RELEASE_NAME" \ - -osReleasePrettyName "$__RESH_OS_RELEASE_PRETTY_NAME" \ - &>~/.resh/client_last_run_out.txt || echo "resh ERROR: $(head -n 1 ~/.resh/client_last_run_out.txt)" - # -path "$__RESH_PATH" \ - fi - fi - unset __RESH_COLLECT -} + __resh_reset_variables -preexec_functions+=(__resh_preexec) -precmd_functions+=(__resh_precmd) + __RESH_INIT_DONE=1 +fi diff --git a/scripts/test.sh b/scripts/test.sh index 3eca974..cbd1de6 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -7,8 +7,10 @@ for f in scripts/*.sh; do shellcheck $f --shell=bash --severity=error || exit 1 done -echo "Checking Zsh syntax of scripts/shellrc.sh ..." -! zsh -n scripts/shellrc.sh && echo "Zsh syntax check failed!" && exit 1 +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 +done for sh in bash zsh; do echo "Running functions in scripts/shellrc.sh using $sh ..." diff --git a/scripts/util.sh b/scripts/util.sh new file mode 100644 index 0000000..bbdceb4 --- /dev/null +++ b/scripts/util.sh @@ -0,0 +1,136 @@ +# util.sh - resh utility functions +__resh_get_uuid() { + cat /proc/sys/kernel/random/uuid 2>/dev/null || resh-uuid +} + +__resh_get_pid() { + if [ -n "$ZSH_VERSION" ]; then + # assume Zsh + local __RESH_PID="$$" # current pid + elif [ -n "$BASH_VERSION" ]; then + # assume Bash + local __RESH_PID="$BASHPID" # current pid + fi + echo "$__RESH_PID" +} + +__resh_get_epochrealtime() { + if date +%s.%N | grep -vq 'N'; then + # GNU date + date +%s.%N + elif gdate --version >/dev/null && gdate +%s.%N | grep -vq 'N'; then + # GNU date take 2 + gdate +%s.%N + elif [ -n "$ZSH_VERSION" ]; then + # zsh fallback using $EPOCHREALTIME + if [ -z "${__RESH_ZSH_LOADED_DATETIME+x}" ]; then + zmodload zsh/datetime + __RESH_ZSH_LOADED_DATETIME=1 + fi + echo "$EPOCHREALTIME" + else + # dumb date + # XXX: we lost precison beyond seconds + date +%s + if [ -z "${__RESH_DATE_WARN+x}" ]; then + echo "resh WARN: can't get precise time - consider installing GNU date!" + __RESH_DATE_WARN=1 + fi + fi +} + +__resh_run_daemon() { + if [ -n "$ZSH_VERSION" ]; then + setopt LOCAL_OPTIONS NO_NOTIFY NO_MONITOR + fi + nohup resh-daemon &>~/.resh/daemon_last_run_out.txt & disown +} + +__resh_bash_completion_init() { + local bash_completion_dir=~/.resh/bash_completion.d + # source user completion directory definitions + # taken from /usr/share/bash-completion/bash_completion + if [[ -d $bash_completion_dir && -r $bash_completion_dir && \ + -x $bash_completion_dir ]]; then + for i in $(LC_ALL=C command ls "$bash_completion_dir"); do + i=$bash_completion_dir/$i + # shellcheck disable=SC2154 + # shellcheck source=/dev/null + [[ ${i##*/} != @($_backup_glob|Makefile*|$_blacklist_glob) \ + && -f $i && -r $i ]] && . "$i" + done + fi +} + +__resh_zsh_completion_init() { + # shellcheck disable=SC2206 + fpath=(~/.resh/zsh_completion.d $fpath) +} + +__resh_session_init() { + # posix + local __RESH_COLS="$COLUMNS" + local __RESH_LANG="$LANG" + local __RESH_LC_ALL="$LC_ALL" + # other LC ? + local __RESH_LINES="$LINES" + local __RESH_PWD="$PWD" + + # non-posix + local __RESH_SHLVL="$SHLVL" + + # pid + local __RESH_PID; __RESH_PID=$(__resh_get_pid) + + # time + local __RESH_TZ_BEFORE; __RESH_TZ_BEFORE=$(date +%z) + local __RESH_RT_BEFORE; __RESH_RT_BEFORE=$(__resh_get_epochrealtime) + + if [ "$__RESH_VERSION" != "$(resh-session-init -version)" ]; then + # shellcheck source=shellrc.sh + source ~/.resh/shellrc + if [ "$__RESH_VERSION" != "$(resh-session-init -version)" ]; then + echo "RESH WARNING: You probably just updated RESH - PLEASE RESTART OR RELOAD THIS TERMINAL SESSION (resh version: $(resh-session-init -version); resh version of this terminal session: ${__RESH_VERSION})" + else + echo "RESH INFO: New RESH shellrc script was loaded - if you encounter any issues please restart this terminal session." + fi + elif [ "$__RESH_REVISION" != "$(resh-session-init -revision)" ]; then + # shellcheck source=shellrc.sh + source ~/.resh/shellrc + if [ "$__RESH_REVISION" != "$(resh-session-init -revision)" ]; then + echo "RESH WARNING: You probably just updated RESH - PLEASE RESTART OR RELOAD THIS TERMINAL SESSION (resh revision: $(resh-session-init -revision); resh revision of this terminal session: ${__RESH_REVISION})" + fi + fi + if [ "$__RESH_VERSION" = "$(resh-session-init -version)" ] && [ "$__RESH_REVISION" = "$(resh-session-init -revision)" ]; then + resh-session-init -requireVersion "$__RESH_VERSION" \ + -requireRevision "$__RESH_REVISION" \ + -shell "$__RESH_SHELL" \ + -uname "$__RESH_UNAME" \ + -sessionId "$__RESH_SESSION_ID" \ + -cols "$__RESH_COLS" \ + -home "$__RESH_HOME" \ + -lang "$__RESH_LANG" \ + -lcAll "$__RESH_LC_ALL" \ + -lines "$__RESH_LINES" \ + -login "$__RESH_LOGIN" \ + -shellEnv "$__RESH_SHELL_ENV" \ + -term "$__RESH_TERM" \ + -pid "$__RESH_PID" \ + -sessionPid "$__RESH_SESSION_PID" \ + -host "$__RESH_HOST" \ + -hosttype "$__RESH_HOSTTYPE" \ + -ostype "$__RESH_OSTYPE" \ + -machtype "$__RESH_MACHTYPE" \ + -shlvl "$__RESH_SHLVL" \ + -realtimeBefore "$__RESH_RT_BEFORE" \ + -realtimeSession "$__RESH_RT_SESSION" \ + -realtimeSessSinceBoot "$__RESH_RT_SESS_SINCE_BOOT" \ + -timezoneBefore "$__RESH_TZ_BEFORE" \ + -osReleaseId "$__RESH_OS_RELEASE_ID" \ + -osReleaseVersionId "$__RESH_OS_RELEASE_VERSION_ID" \ + -osReleaseIdLike "$__RESH_OS_RELEASE_ID_LIKE" \ + -osReleaseName "$__RESH_OS_RELEASE_NAME" \ + -osReleasePrettyName "$__RESH_OS_RELEASE_PRETTY_NAME" \ + &>~/.resh/session_init_last_run_out.txt || echo "resh-session-init ERROR: $(head -n 1 ~/.resh/session_init_last_run_out.txt)" + fi +} diff --git a/scripts/widgets.sh b/scripts/widgets.sh new file mode 100644 index 0000000..82761bd --- /dev/null +++ b/scripts/widgets.sh @@ -0,0 +1,93 @@ + +# shellcheck source=hooks.sh +. ~/.resh/hooks.sh + +__resh_helper_arrow_pre() { + # this is a very bad workaround + # force bash-preexec to run repeatedly because otherwise premature run of bash-preexec overshadows the next poper run + # I honestly think that it's impossible to make widgets work in bash without hacks like this + # shellcheck disable=2034 + __bp_preexec_interactive_mode="on" + # set recall strategy + __RESH_HIST_RECALL_STRATEGY="bash_recent - history-search-{backward,forward}" + # set prefix + __RESH_PREFIX=${BUFFER:0:$CURSOR} + # cursor not at the end of the line => end "NO_PREFIX_MODE" + [ "$CURSOR" -ne "${#BUFFER}" ] && __RESH_HIST_NO_PREFIX_MODE=0 + # if user made any edits from last recall action => restart histno AND deactivate "NO_PREFIX_MODE" + [ "$BUFFER" != "$__RESH_HIST_PREV_LINE" ] && __RESH_HISTNO=0 && __RESH_HIST_NO_PREFIX_MODE=0 + # "NO_PREFIX_MODE" => set prefix to empty string + [ "$__RESH_HIST_NO_PREFIX_MODE" -eq 1 ] && __RESH_PREFIX="" + # histno == 0 => save current line + [ "$__RESH_HISTNO" -eq 0 ] && __RESH_HISTNO_ZERO_LINE=$BUFFER +} +__resh_helper_arrow_post() { + # cursor at the beginning of the line => activate "NO_PREFIX_MODE" + [ "$CURSOR" -eq 0 ] && __RESH_HIST_NO_PREFIX_MODE=1 + # "NO_PREFIX_MODE" => move cursor to the end of the line + [ "$__RESH_HIST_NO_PREFIX_MODE" -eq 1 ] && CURSOR=${#BUFFER} + # save current line so we can spot user edits next time + __RESH_HIST_PREV_LINE=$BUFFER +} + +__resh_widget_arrow_up() { + # run helper function + __resh_helper_arrow_pre + # append curent recall action + __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 + BUFFER=$__RESH_HISTNO_ZERO_LINE + else + # run recall + local NEW_BUFFER + 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)) + fi + # run post helper + __resh_helper_arrow_post +} +__resh_widget_arrow_down() { + # run helper function + __resh_helper_arrow_pre + # append curent recall action + __RESH_HIST_RECALL_ACTIONS="$__RESH_HIST_RECALL_ACTIONS;arrow_down:$__RESH_PREFIX" + # increment histno + __RESH_HISTNO=$((__RESH_HISTNO-1)) + # prevent HISTNO from getting negative (for now) + [ "$__RESH_HISTNO" -lt 0 ] && __RESH_HISTNO=0 + # back at histno == 0 => restore original line + if [ "$__RESH_HISTNO" -eq 0 ]; then + BUFFER=$__RESH_HISTNO_ZERO_LINE + else + # run recall + local NEW_BUFFER + NEW_BUFFER="$(__resh_collect --recall --prefix-search "$__RESH_PREFIX" 2> ~/.resh/arrow_down_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++ )) + fi + __resh_helper_arrow_post +} +__resh_widget_control_R() { + local __RESH_PREFIX=${BUFFER:0:CURSOR} + __RESH_HIST_RECALL_ACTIONS="$__RESH_HIST_RECALL_ACTIONS;control_R:$__RESH_PREFIX" + # resh-collect --hstr + hstr +} + +__resh_widget_arrow_up_compat() { + __bindfunc_compat_wrapper __resh_widget_arrow_up +} + +__resh_widget_arrow_down_compat() { + __bindfunc_compat_wrapper __resh_widget_arrow_down +} + +__resh_widget_control_R_compat() { + __bindfunc_compat_wrapper __resh_widget_control_R +} diff --git a/submodules/bash-zsh-compat-widgets b/submodules/bash-zsh-compat-widgets new file mode 160000 index 0000000..7dde81e --- /dev/null +++ b/submodules/bash-zsh-compat-widgets @@ -0,0 +1 @@ +Subproject commit 7dde81eaa09cbed11ebc70ea892bcae24ea1606c