Merge pull request #30 from curusarn/dev3

version 2.1.0
pull/58/head
Šimon Let 6 years ago committed by GitHub
commit d4a5ad8e6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 22
      Makefile
  3. 2
      VERSION
  4. 135
      cmd/collect/main.go
  5. 56
      cmd/config/main.go
  6. 10
      cmd/control/cmd/completion.go
  7. 14
      cmd/control/cmd/debug.go
  8. 28
      cmd/control/cmd/disable.go
  9. 105
      cmd/control/cmd/enable.go
  10. 10
      cmd/control/cmd/root.go
  11. 10
      cmd/control/status/status.go
  12. 14
      cmd/daemon/main.go
  13. 55
      cmd/daemon/recall.go
  14. 37
      cmd/daemon/run-server.go
  15. 84
      cmd/inspect/main.go
  16. 31
      cmd/sanitize/main.go
  17. 3
      conf/config.toml
  18. 3
      go.mod
  19. 8
      go.sum
  20. 3
      pkg/cfg/cfg.go
  21. 2
      pkg/collect/collect.go
  22. 150
      pkg/histfile/histfile.go
  23. 64
      pkg/histlist/histlist.go
  24. 12
      pkg/msg/msg.go
  25. 102
      pkg/records/records.go
  26. 105
      pkg/sesshist/sesshist.go
  27. 57
      pkg/signalhandler/signalhander.go
  28. 1
      scripts/hooks.sh
  29. 77
      scripts/reshctl.sh
  30. 13
      scripts/shellrc.sh
  31. 6
      scripts/test.sh
  32. 15
      scripts/util.sh
  33. 15
      scripts/widgets.sh
  34. 2
      submodules/bash-preexec
  35. 2
      submodules/bash-zsh-compat-widgets

1
.gitignore vendored

@ -1 +1,2 @@
bin/*
.vscode/*

@ -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
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
@ -58,7 +59,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
@ -82,15 +83,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
@ -131,9 +137,7 @@ uninstall:
# Uninstalling ...
-rm -rf ~/.resh/
bin/resh-control: cmd/control/cmd/*.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:

@ -1 +1 @@
2.0.0
2.1.0

@ -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")
}
}

@ -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")
}
}

@ -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)

@ -12,22 +12,30 @@ 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
},
}
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",
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{

@ -7,18 +7,24 @@ 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 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) {
exitCode = enableDisableArrowKeyBindingsGlobally(false)
},
}

@ -1,24 +1,107 @@
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"
)
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 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) {
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
}

@ -11,17 +11,18 @@ 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
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)
@ -29,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)

@ -8,10 +8,12 @@ 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
// InspectSessionHistory exit code - tells reshctl() wrapper to take current sessionID and send /inspect request to daemon
InspectSessionHistory = 201
)

@ -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)
@ -48,6 +50,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)
@ -69,11 +75,13 @@ 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 {
log.Println("Could not delete pidfile", err)
}
log.Println("main: Shutdown - bye")
}
func statusHandler(w http.ResponseWriter, r *http.Request) {
@ -92,7 +100,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)

@ -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"
)
@ -16,26 +17,31 @@ 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)
return
}
rec := records.Record{}
rec := records.SlimRecord{}
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 +49,49 @@ 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)
}
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)
}

@ -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) {
func runServer(config cfg.Config, reshHistoryPath, bashHistoryPath, zshHistoryPath 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,10 +34,19 @@ 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)
maxHistSize := 10000 // lines
minHistSizeKB := 2000 // roughly lines
histfileBox := histfile.New(histfileRecords, histfileSessionsToDrop,
reshHistoryPath, bashHistoryPath, zshHistoryPath,
maxHistSize, minHistSizeKB,
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)
@ -40,9 +54,16 @@ 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})
mux.Handle("/inspect", &inspectHandler{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)
}

@ -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
}

@ -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

@ -1,3 +1,6 @@
port = 2627
sesswatchPeriodSeconds = 120
sesshistInitHistorySize = 1000
debug = true
bindArrowKeysBash = false
bindArrowKeysZsh = true

@ -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
)

@ -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=

@ -5,4 +5,7 @@ type Config struct {
Port int
SesswatchPeriodSeconds uint
SesshistInitHistorySize int
Debug bool
BindArrowKeysBash bool
BindArrowKeysZsh bool
}

@ -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)

@ -3,10 +3,12 @@ package histfile
import (
"encoding/json"
"log"
"math"
"os"
"strconv"
"sync"
"github.com/curusarn/resh/pkg/histlist"
"github.com/curusarn/resh/pkg/records"
)
@ -16,29 +18,71 @@ 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
// 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) *Histfile {
// 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,
cmdLinesLastIndex: map[string]int{},
sessions: map[string]records.Record{},
historyPath: reshHistoryPath,
bashCmdLines: histlist.New(),
zshCmdLines: histlist.New(),
}
go hf.loadHistory(initHistSize)
go hf.writer(input)
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()
h.recentCmdLines = records.LoadCmdLinesFromFile(h.historyPath, initHistSize)
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
@ -61,33 +105,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 {
@ -100,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.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)
h.bashCmdLines.AddCmdLine(cmdLine)
h.zshCmdLines.AddCmdLine(cmdLine)
}()
writeRecord(part1, h.historyPath)
@ -132,6 +191,21 @@ 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(shell string, limit int) histlist.Histlist {
// NOTE: limit does nothing atm
h.recentMutex.Lock()
defer h.recentMutex.Unlock()
log.Println("histfile: History requested ...")
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
}

@ -0,0 +1,64 @@
package histlist
import "log"
// 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
}
// 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)
}
}

@ -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"`
}

@ -10,6 +10,7 @@ import (
"strconv"
"strings"
"github.com/curusarn/resh/pkg/histlist"
"github.com/mattn/go-shellwords"
)
@ -129,6 +130,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{
@ -439,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) []string {
// 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-- {
@ -455,18 +472,24 @@ func LoadCmdLinesFromFile(fname string, limit int) []string {
break
}
}
return cmdLines
// add everything to histlist
for _, cmdLine := range cmdLines {
hl.AddCmdLine(cmdLine)
}
}
// 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{}
@ -485,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
}

@ -8,6 +8,7 @@ import (
"sync"
"github.com/curusarn/resh/pkg/histfile"
"github.com/curusarn/resh/pkg/histlist"
"github.com/curusarn/resh/pkg/records"
)
@ -39,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)
}
}
@ -57,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) error {
func (s *Dispatch) initSession(sessionID, shell string) error {
log.Println("sesshist: initializing session - " + sessionID)
s.mutex.RLock()
_, found := s.sessions[sessionID]
@ -74,14 +90,13 @@ 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()
// 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,35 +120,33 @@ 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 {
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
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)
session.recentCmdLines.AddCmdLine(record.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
}
// 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,21 +154,49 @@ 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)
}
log.Println("sesshist - recall: Searching for records by prefix ...")
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()
return session.searchRecordByPrefix(prefix, histno)
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) {
@ -167,11 +208,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) {
@ -181,14 +222,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
}

@ -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)
}
}

@ -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=""

@ -5,22 +5,56 @@
. ~/.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 "RESH arrow key bindings are already enabled!"
return 1
fi
bindfunc --revert '\eOA' __resh_widget_arrow_up_compat
__RESH_bindfunc_revert_arrow_up_bind=$_bindfunc_revert
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
}
__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"
[ -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
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"
[ -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
return 0
}
__resh_unbind_control_R() {
# TODO
echo "\ bindfunc __resh_widget_control_R_compat"
return 0
}
@ -36,25 +70,42 @@ __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=$?
# echo $_status
# unexport current shell
unset __RESH_ctl_shell
case "$_status" in
0|1)
# success | fail
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)
@ -62,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"

@ -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"
@ -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

@ -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

@ -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() {

@ -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

@ -1 +1 @@
Subproject commit 9811ba8b7694cdbd9debed931e922b67e439197a
Subproject commit f558191d74ae33654d63a0f091034ef27f4b44f9

@ -1 +1 @@
Subproject commit 7dde81eaa09cbed11ebc70ea892bcae24ea1606c
Subproject commit c3077fcdf2e20efb95dd27a53766b78533ab7bc4
Loading…
Cancel
Save