From 583224527f30decbf91dc0f50d5fca06b33bf9d0 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Thu, 3 Oct 2019 02:15:53 +0200 Subject: [PATCH 01/14] Add submodule bash-zsh-compat-widgets --- .gitmodules | 3 +++ Makefile | 3 ++- submodules/bash-zsh-compat-widgets | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) create mode 160000 submodules/bash-zsh-compat-widgets 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..69a79be 100644 --- a/Makefile +++ b/Makefile @@ -129,7 +129,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/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 From 82a40f7f29535ec04c9b805c2aea952ad05afe68 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Thu, 3 Oct 2019 21:14:25 +0200 Subject: [PATCH 02/14] Add reshctl to enable/disable RESH features --- Makefile | 21 +++++++++++---- cmd/control/cmd/completion.go | 48 +++++++++++++++++++++++++++++++++++ cmd/control/cmd/disable.go | 24 ++++++++++++++++++ cmd/control/cmd/enable.go | 24 ++++++++++++++++++ cmd/control/cmd/root.go | 39 ++++++++++++++++++++++++++++ cmd/control/main.go | 17 +++++++++++++ cmd/control/status/status.go | 17 +++++++++++++ go.mod | 1 + go.sum | 30 ++++++++++++++++++++++ scripts/bindutil.sh | 37 +++++++++++++++++++++++++++ scripts/reshctl.sh | 33 ++++++++++++++++++++++++ scripts/shellrc.sh | 41 +++++++++++++++++++++++++----- scripts/widgets.sh | 10 ++++++++ 13 files changed, 331 insertions(+), 11 deletions(-) create mode 100644 cmd/control/cmd/completion.go create mode 100644 cmd/control/cmd/disable.go create mode 100644 cmd/control/cmd/enable.go create mode 100644 cmd/control/cmd/root.go create mode 100644 cmd/control/main.go create mode 100644 cmd/control/status/status.go create mode 100644 scripts/bindutil.sh create mode 100644 scripts/reshctl.sh create mode 100644 scripts/widgets.sh diff --git a/Makefile b/Makefile index 69a79be..30402fd 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-collect 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/bindutil.sh scripts/widgets.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 ... @@ -116,10 +125,12 @@ uninstall: # Uninstalling ... -rm -rf ~/.resh/ +bin/resh-control: cmd/control/cmd/*.go + bin/resh-%: cmd/%/main.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 $@ 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/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..c4642e7 --- /dev/null +++ b/cmd/control/cmd/root.go @@ -0,0 +1,39 @@ +package cmd + +import ( + "fmt" + "log" + + "github.com/curusarn/resh/cmd/control/status" + "github.com/spf13/cobra" +) + +var exitCode status.Code = status.DefaultInvalid + +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) + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + return status.Fail + } + if exitCode == status.DefaultInvalid { + log.Println("reshctl FATAL ERROR: (sub)command didn't set exitCode!") + 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..246225a --- /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 ( + // DefaultInvalid exit code + DefaultInvalid Code = -1 + // Success exit code + Success = 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 +) diff --git a/go.mod b/go.mod index b13b13d..64dd27e 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/mattn/go-shellwords v1.0.6 github.com/mb-14/gomarkov v0.0.0-20190125094512-044dd0dcb5e7 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..e6a3477 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,49 @@ 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/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/scripts/bindutil.sh b/scripts/bindutil.sh new file mode 100644 index 0000000..53e192e --- /dev/null +++ b/scripts/bindutil.sh @@ -0,0 +1,37 @@ + + +# shellcheck source=../submodules/bash-zsh-compat-widgets/bindfunc.sh +. ~/.resh/bindfunc.sh +# shellcheck source=widgets.sh +. ~/.resh/widgets.sh + +__resh_bind_arrows() { + echo "bindfunc __resh_widget_arrow_up" + echo "bindfunc __resh_widget_arrow_down" + return 0 +} + +__resh_bind_control_R() { + echo "bindfunc __resh_widget_control_R" + return 0 +} +__resh_unbind_arrows() { + echo "\ bindfunc __resh_widget_arrow_up" + echo "\ bindfunc __resh_widget_arrow_down" + return 0 +} + +__resh_unbind_control_R() { + echo "\ bindfunc __resh_widget_control_R" + return 0 +} + +__resh_bind_all() { + __resh_bind_arrows + __resh_bind_control_R +} + +__resh_unbind_all() { + __resh_unbind_arrows + __resh_unbind_control_R +} diff --git a/scripts/reshctl.sh b/scripts/reshctl.sh new file mode 100644 index 0000000..4676c43 --- /dev/null +++ b/scripts/reshctl.sh @@ -0,0 +1,33 @@ + +# source bindutil - contains functions to bind and unbind RESH widgets +# shellcheck source=bindutil.sh +. ~/.resh/bindutil.sh + +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 + ;; + *) + 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..acdbeeb 100644 --- a/scripts/shellrc.sh +++ b/scripts/shellrc.sh @@ -4,6 +4,9 @@ PATH=$PATH:~/.resh/bin # zmodload zsh/datetime # fi +# shellcheck source=reshctl.sh +. ~/.resh/reshctl.sh + __resh_get_uuid() { cat /proc/sys/kernel/random/uuid 2>/dev/null || resh-uuid } @@ -40,6 +43,27 @@ __resh_run_daemon() { nohup resh-daemon &>/dev/null & 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_MACOS=0 __RESH_LINUX=0 __RESH_UNAME=$(uname) @@ -53,19 +77,22 @@ 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_ID; __RESH_SESSION_ID=$(__resh_get_uuid) export __RESH_SESSION_PID="$$" # TODO add sesson time fi @@ -152,20 +179,22 @@ __resh_precmd() { __RESH_TZ_AFTER=$(date +%z) __RESH_PWD_AFTER="$PWD" if [ -n "${__RESH_COLLECT}" ]; then - if [ "$__RESH_VERSION" != $(resh-collect -version) ]; then + if [ "$__RESH_VERSION" != "$(resh-collect -version)" ]; then + # shellcheck source=shellrc.sh source ~/.resh/shellrc - if [ "$__RESH_VERSION" != $(resh-collect -version) ]; then + 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 + elif [ "$__RESH_REVISION" != "$(resh-collect -revision)" ]; then + # shellcheck source=shellrc.sh source ~/.resh/shellrc - if [ "$__RESH_REVISION" != $(resh-collect -revision) ]; then + 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 + if [ "$__RESH_VERSION" = "$(resh-collect -version)" ] && [ "$__RESH_REVISION" = "$(resh-collect -revision)" ]; then resh-collect -requireVersion "$__RESH_VERSION" \ -requireRevision "$__RESH_REVISION" \ -cmdLine "$__RESH_CMDLINE" \ diff --git a/scripts/widgets.sh b/scripts/widgets.sh new file mode 100644 index 0000000..c6cea7d --- /dev/null +++ b/scripts/widgets.sh @@ -0,0 +1,10 @@ + +__resh_widget_arrow_up() { + return 0 +} +__resh_widget_arrow_down() { + return 0 +} +__resh_widget_control_R() { + return 0 +} \ No newline at end of file From d4959cffbe7ff972f7dd1f4c1d1bbbdb08eea7d7 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Thu, 3 Oct 2019 21:20:21 +0200 Subject: [PATCH 03/14] Minor change --- cmd/control/cmd/root.go | 7 +------ cmd/control/status/status.go | 4 +--- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/cmd/control/cmd/root.go b/cmd/control/cmd/root.go index c4642e7..b83c6c9 100644 --- a/cmd/control/cmd/root.go +++ b/cmd/control/cmd/root.go @@ -2,13 +2,12 @@ package cmd import ( "fmt" - "log" "github.com/curusarn/resh/cmd/control/status" "github.com/spf13/cobra" ) -var exitCode status.Code = status.DefaultInvalid +var exitCode status.Code var rootCmd = &cobra.Command{ Use: "reshctl", @@ -31,9 +30,5 @@ func Execute() status.Code { fmt.Println(err) return status.Fail } - if exitCode == status.DefaultInvalid { - log.Println("reshctl FATAL ERROR: (sub)command didn't set exitCode!") - return status.Fail - } return exitCode } diff --git a/cmd/control/status/status.go b/cmd/control/status/status.go index 246225a..28eb3a7 100644 --- a/cmd/control/status/status.go +++ b/cmd/control/status/status.go @@ -4,10 +4,8 @@ package status type Code int const ( - // DefaultInvalid exit code - DefaultInvalid Code = -1 // Success exit code - Success = 0 + Success Code = 0 // Fail exit code Fail = 1 // EnableAll exit code - tells reshctl() wrapper to enable_all From 7b437b687017eb1493f2e11242c028dc0d3576df Mon Sep 17 00:00:00 2001 From: Simon Let Date: Thu, 3 Oct 2019 22:11:57 +0200 Subject: [PATCH 04/14] Add git *after* members to record --- VERSION | 2 +- cmd/collect/main.go | 19 +++++++++++++++---- cmd/daemon/main.go | 1 + pkg/records/records.go | 11 +++++++---- scripts/shellrc.sh | 13 +++++++++++++ 5 files changed, 37 insertions(+), 9 deletions(-) diff --git a/VERSION b/VERSION index 781dcb0..65087b4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.3 +1.1.4 diff --git a/cmd/collect/main.go b/cmd/collect/main.go index 75cc61d..c38055a 100644 --- a/cmd/collect/main.go +++ b/cmd/collect/main.go @@ -75,9 +75,13 @@ func main() { machtype := flag.String("machtype", "", "$MACHTYPE") gitCdup := flag.String("gitCdup", "", "git rev-parse --show-cdup") gitRemote := flag.String("gitRemote", "", "git remote get-url origin") + gitCdupAfter := flag.String("gitCdupAfter", "", "git rev-parse --show-cdup") + gitRemoteAfter := flag.String("gitRemoteAfter", "", "git remote get-url origin") gitCdupExitCode := flag.Int("gitCdupExitCode", -1, "... $?") gitRemoteExitCode := flag.Int("gitRemoteExitCode", -1, "... $?") + gitCdupExitCodeAfter := flag.Int("gitCdupExitCodeAfter", -1, "... $?") + gitRemoteExitCodeAfter := flag.Int("gitRemoteExitCodeAfter", -1, "... $?") // before after timezoneBefore := flag.String("timezoneBefore", "", "") @@ -161,6 +165,10 @@ func main() { if *gitRemoteExitCode != 0 { *gitRemote = "" } + gitDirAfter, gitRealDirAfter := getGitDirs(*gitCdupAfter, *gitCdupExitCodeAfter, *pwd) + if *gitRemoteExitCodeAfter != 0 { + *gitRemoteAfter = "" + } if *osReleaseID == "" { *osReleaseID = "linux" @@ -219,10 +227,13 @@ func main() { RealtimeSinceSessionStart: realtimeSinceSessionStart, RealtimeSinceBoot: realtimeSinceBoot, - GitDir: gitDir, - GitRealDir: gitRealDir, - GitOriginRemote: *gitRemote, - MachineID: readFileContent(machineIDPath), + GitDir: gitDir, + GitRealDir: gitRealDir, + GitOriginRemote: *gitRemote, + GitDirAfter: gitDirAfter, + GitRealDirAfter: gitRealDirAfter, + GitOriginRemoteAfter: *gitRemoteAfter, + MachineID: readFileContent(machineIDPath), OsReleaseID: *osReleaseID, OsReleaseVersionID: *osReleaseVersionID, diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index 9411350..91d1e28 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -126,6 +126,7 @@ func (h *recordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func runServer(port int, outputPath string) { http.HandleFunc("/status", statusHandler) http.Handle("/record", &recordHandler{OutputPath: outputPath}) + //http.Handle("/session_start", &recordHandler{OutputPath: outputPath}) http.ListenAndServe(":"+strconv.Itoa(port), nil) } diff --git a/pkg/records/records.go b/pkg/records/records.go index f15c717..8a5216b 100644 --- a/pkg/records/records.go +++ b/pkg/records/records.go @@ -56,10 +56,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"` diff --git a/scripts/shellrc.sh b/scripts/shellrc.sh index acdbeeb..c9804fe 100644 --- a/scripts/shellrc.sh +++ b/scripts/shellrc.sh @@ -43,6 +43,10 @@ __resh_run_daemon() { nohup resh-daemon &>/dev/null & disown } +# __resh_session_init() { +# resh-event session_start --sessionId "$__RESH_SESSION_ID" --sessionPid "$__RESH_SESSION_PID" +# } +# __resh_bash_completion_init() { local bash_completion_dir=~/.resh/bash_completion.d # source user completion directory definitions @@ -95,6 +99,7 @@ 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_session_init __RESH_SESSION_ID __RESH_SESSION_PID fi # posix @@ -178,6 +183,10 @@ __resh_precmd() { __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-collect -version)" ]; then # shellcheck source=shellrc.sh @@ -223,6 +232,10 @@ __resh_precmd() { -gitCdupExitCode "$__RESH_GIT_CDUP_EXIT_CODE" \ -gitRemote "$__RESH_GIT_REMOTE" \ -gitRemoteExitCode "$__RESH_GIT_REMOTE_EXIT_CODE" \ + -gitCdupAfter "$__RESH_GIT_CDUP_AFTER" \ + -gitCdupExitCodeAfter "$__RESH_GIT_CDUP_EXIT_CODE_AFTER" \ + -gitRemoteAfter "$__RESH_GIT_REMOTE_AFTER" \ + -gitRemoteExitCodeAfter "$__RESH_GIT_REMOTE_EXIT_CODE_AFTER" \ -realtimeBefore "$__RESH_RT_BEFORE" \ -realtimeAfter "$__RESH_RT_AFTER" \ -realtimeSession "$__RESH_RT_SESSION" \ From b8c00b6c73a23dfa8f8f8b6d7b1b3217a8e2b75f Mon Sep 17 00:00:00 2001 From: Simon Let Date: Fri, 4 Oct 2019 00:30:58 +0200 Subject: [PATCH 05/14] Split collect into collect and postcollect Realpaths for before fields are actually evaluated before. Hard prerequisite for any recall functionality --- Makefile | 2 +- VERSION | 2 +- cmd/collect/main.go | 171 +++++---------------------------------- cmd/daemon/histfile.go | 57 +++++++++++++ cmd/daemon/main.go | 27 +++---- cmd/event/main.go | 7 ++ cmd/postcollect/main.go | 147 +++++++++++++++++++++++++++++++++ cmd/sanitize/main.go | 2 +- pkg/collect/collect.go | 78 ++++++++++++++++++ pkg/histanal/histload.go | 2 +- pkg/records/records.go | 45 ++++++++++- scripts/shellrc.sh | 106 ++++++++++++++---------- 12 files changed, 429 insertions(+), 217 deletions(-) create mode 100644 cmd/daemon/histfile.go create mode 100644 cmd/event/main.go create mode 100644 cmd/postcollect/main.go create mode 100644 pkg/collect/collect.go diff --git a/Makefile b/Makefile index 30402fd..b11529a 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ sanitize: # # -build: submodules bin/resh-collect bin/resh-daemon bin/resh-evaluate bin/resh-sanitize bin/resh-control +build: submodules bin/resh-collect bin/resh-postcollect bin/resh-daemon bin/resh-evaluate bin/resh-sanitize bin/resh-control test_go: # Running tests diff --git a/VERSION b/VERSION index 65087b4..e25d8d9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.4 +1.1.5 diff --git a/cmd/collect/main.go b/cmd/collect/main.go index c38055a..d2045c8 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 @@ -60,7 +56,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") @@ -75,17 +70,12 @@ func main() { machtype := flag.String("machtype", "", "$MACHTYPE") gitCdup := flag.String("gitCdup", "", "git rev-parse --show-cdup") gitRemote := flag.String("gitRemote", "", "git remote get-url origin") - gitCdupAfter := flag.String("gitCdupAfter", "", "git rev-parse --show-cdup") - gitRemoteAfter := flag.String("gitRemoteAfter", "", "git remote get-url origin") gitCdupExitCode := flag.Int("gitCdupExitCode", -1, "... $?") gitRemoteExitCode := flag.Int("gitRemoteExitCode", -1, "... $?") - gitCdupExitCodeAfter := flag.Int("gitCdupExitCodeAfter", -1, "... $?") - gitRemoteExitCodeAfter := flag.Int("gitRemoteExitCodeAfter", -1, "... $?") // before after timezoneBefore := flag.String("timezoneBefore", "", "") - timezoneAfter := flag.String("timezoneAfter", "", "") osReleaseID := flag.String("osReleaseId", "", "/etc/os-release ID") osReleaseVersionID := flag.String("osReleaseVersionId", "", @@ -96,7 +86,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", @@ -125,10 +114,6 @@ func main() { ")") 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) @@ -141,34 +126,22 @@ 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 = "" } - gitDirAfter, gitRealDirAfter := getGitDirs(*gitCdupAfter, *gitCdupExitCodeAfter, *pwd) - if *gitRemoteExitCodeAfter != 0 { - *gitRemoteAfter = "" - } if *osReleaseID == "" { *osReleaseID = "linux" @@ -199,41 +172,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, - GitDirAfter: gitDirAfter, - GitRealDirAfter: gitRealDirAfter, - GitOriginRemoteAfter: *gitRemoteAfter, - MachineID: readFileContent(machineIDPath), + GitDir: gitDir, + GitRealDir: gitRealDir, + GitOriginRemote: *gitRemote, + MachineID: collect.ReadFileContent(machineIDPath), OsReleaseID: *osReleaseID, OsReleaseVersionID: *osReleaseVersionID, @@ -241,109 +205,12 @@ 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 :(") - } + collect.SendRecord(rec, strconv.Itoa(config.Port)) } - -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 - } - 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) -} - -// 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/daemon/histfile.go b/cmd/daemon/histfile.go new file mode 100644 index 0000000..d608ca0 --- /dev/null +++ b/cmd/daemon/histfile.go @@ -0,0 +1,57 @@ +package main + +import ( + "encoding/json" + "log" + "os" + + "github.com/curusarn/resh/pkg/records" +) + +// HistfileWriter - reads records from channel, merges them and wrotes them to file +func HistfileWriter(input chan records.Record, outputPath string) { + sessions := map[string]records.Record{} + + for { + record := <-input + if record.PartOne { + if _, found := sessions[record.SessionID]; found { + log.Println("ERROR: Got another first part of the records before merging the previous one - overwriting!") + } + sessions[record.SessionID] = record + } else { + part1, found := sessions[record.SessionID] + if found == false { + log.Println("ERROR: Got second part of records and nothing to merge it with - ignoring!") + continue + } + delete(sessions, record.SessionID) + go mergeAndWriteRecord(part1, record, outputPath) + } + } +} + +func mergeAndWriteRecord(part1, part2 records.Record, outputPath string) { + err := part1.Merge(part2) + if err != nil { + log.Println("Error while merging", err) + return + } + recJSON, err := json.Marshal(part1) + 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", part1, err) + return + } +} diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index 91d1e28..69c031f 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -84,7 +84,7 @@ func statusHandler(w http.ResponseWriter, r *http.Request) { } type recordHandler struct { - OutputPath string + histfile chan records.Record } func (h *recordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -103,19 +103,12 @@ func (h *recordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 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 + h.histfile <- record + part := "2" + if record.PartOne { + part = "1" } - 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) + log.Println("Received:", record.CmdLine, " - part", part) // fmt.Println("cmd:", r.CmdLine) // fmt.Println("pwd:", r.Pwd) @@ -124,9 +117,13 @@ func (h *recordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func runServer(port int, outputPath string) { + histfile := make(chan records.Record) + go HistfileWriter(histfile, outputPath) + http.HandleFunc("/status", statusHandler) - http.Handle("/record", &recordHandler{OutputPath: outputPath}) - //http.Handle("/session_start", &recordHandler{OutputPath: outputPath}) + http.Handle("/record", &recordHandler{histfile: histfile}) + //http.Handle("/session_init", &sessionInitHandler{OutputPath: outputPath}) + //http.Handle("/recall", &recallHandler{OutputPath: outputPath}) http.ListenAndServe(":"+strconv.Itoa(port), nil) } 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..fc02704 --- /dev/null +++ b/cmd/postcollect/main.go @@ -0,0 +1,147 @@ +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") + + // 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, + + 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)) +} 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/pkg/collect/collect.go b/pkg/collect/collect.go new file mode 100644 index 0000000..e53ef02 --- /dev/null +++ b/pkg/collect/collect.go @@ -0,0 +1,78 @@ +package collect + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "log" + "net/http" + "path/filepath" + "strconv" + "strings" + + "github.com/curusarn/resh/pkg/records" +) + +// SendRecord to daemon +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 :(") + } +} + +// 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/records/records.go b/pkg/records/records.go index 8a5216b..0ded51b 100644 --- a/pkg/records/records.go +++ b/pkg/records/records.go @@ -74,6 +74,18 @@ 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 string `json:"recallHistno,omitempty"` + RecallStrategy string `json:"recallStrategy,omitempty"` + RecallActions []string `json:"recallActions,omitempty"` + // added by sanitizatizer Sanitized bool `json:"sanitized,omitempty"` CmdLength int `json:"cmdLength,omitempty"` @@ -111,8 +123,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 @@ -154,6 +166,35 @@ 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 || r.RealtimeBefore != r2.RealtimeBefore { + return errors.New("Records to merge are not parts of the same records - r1:" + + r.CmdLine + "(" + strconv.FormatFloat(r.RealtimeBefore, 'f', -1, 64) + ") r2:" + + r2.CmdLine + "(" + strconv.FormatFloat(r2.RealtimeBefore, 'f', -1, 64) + ")") + } + 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 == "" { diff --git a/scripts/shellrc.sh b/scripts/shellrc.sh index c9804fe..83db854 100644 --- a/scripts/shellrc.sh +++ b/scripts/shellrc.sh @@ -168,14 +168,59 @@ __resh_preexec() { # __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 - + 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" \ + -cmdLine "$__RESH_CMDLINE" \ + -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" \ + &>~/.resh/client_last_run_out.txt || echo "resh-collect ERROR: $(head -n 1 ~/.resh/client_last_run_out.txt)" + fi } __resh_precmd() { @@ -204,55 +249,28 @@ __resh_precmd() { fi fi if [ "$__RESH_VERSION" = "$(resh-collect -version)" ] && [ "$__RESH_REVISION" = "$(resh-collect -revision)" ]; then - resh-collect -requireVersion "$__RESH_VERSION" \ + resh-postcollect -requireVersion "$__RESH_VERSION" \ -requireRevision "$__RESH_REVISION" \ -cmdLine "$__RESH_CMDLINE" \ + -realtimeBefore "$__RESH_RT_BEFORE" \ -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" \ -gitCdupAfter "$__RESH_GIT_CDUP_AFTER" \ -gitCdupExitCodeAfter "$__RESH_GIT_CDUP_EXIT_CODE_AFTER" \ -gitRemoteAfter "$__RESH_GIT_REMOTE_AFTER" \ -gitRemoteExitCodeAfter "$__RESH_GIT_REMOTE_EXIT_CODE_AFTER" \ - -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" \ + &>~/.resh/client_last_run_out.txt || echo "resh-postcollect ERROR: $(head -n 1 ~/.resh/client_last_run_out.txt)" fi fi unset __RESH_COLLECT } -preexec_functions+=(__resh_preexec) -precmd_functions+=(__resh_precmd) +# do not add more hooks when shellrc is sourced again +if [ -z "${__RESH_PREEXEC_PRECMD_HOOKS_ADDED+x}" ]; then + preexec_functions+=(__resh_preexec) + precmd_functions+=(__resh_precmd) + __RESH_PREEXEC_PRECMD_HOOKS_ADDED=1 +fi From 77d94c9e12348efb0139ec56cc233717713589ac Mon Sep 17 00:00:00 2001 From: Simon Let Date: Fri, 4 Oct 2019 14:31:19 +0200 Subject: [PATCH 06/14] Save std{out,err} of last run, add reshctl debug reshctl debug reads all the last_run files --- Makefile | 2 +- cmd/control/cmd/debug.go | 44 ++++++++++++++++++++++++++++++++++++++++ cmd/control/cmd/root.go | 2 ++ scripts/shellrc.sh | 16 +++++++-------- 4 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 cmd/control/cmd/debug.go diff --git a/Makefile b/Makefile index b11529a..277de70 100644 --- a/Makefile +++ b/Makefile @@ -90,7 +90,7 @@ 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 # Final touch touch ~/.resh_history.json diff --git a/cmd/control/cmd/debug.go b/cmd/control/cmd/debug.go new file mode 100644 index 0000000..c4dd299 --- /dev/null +++ b/cmd/control/cmd/debug.go @@ -0,0 +1,44 @@ +package cmd + +import ( + "fmt" + "io/ioutil" + "os/user" + "path/filepath" + + "github.com/curusarn/resh/cmd/control/status" + "github.com/spf13/cobra" +) + +// completionCmd represents the completion command +var debugCmd = &cobra.Command{ + Use: "debug", + Short: "Shows logs and output from last runs of resh", + Long: "Shows logs and 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", + } + 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/root.go b/cmd/control/cmd/root.go index b83c6c9..b2f57a0 100644 --- a/cmd/control/cmd/root.go +++ b/cmd/control/cmd/root.go @@ -26,6 +26,8 @@ func Execute() status.Code { rootCmd.AddCommand(completionCmd) completionCmd.AddCommand(completionBashCmd) completionCmd.AddCommand(completionZshCmd) + + rootCmd.AddCommand(debugCmd) if err := rootCmd.Execute(); err != nil { fmt.Println(err) return status.Fail diff --git a/scripts/shellrc.sh b/scripts/shellrc.sh index 83db854..739c299 100644 --- a/scripts/shellrc.sh +++ b/scripts/shellrc.sh @@ -40,7 +40,7 @@ __resh_run_daemon() { if [ -n "$ZSH_VERSION" ]; then setopt LOCAL_OPTIONS NO_NOTIFY NO_MONITOR fi - nohup resh-daemon &>/dev/null & disown + nohup resh-daemon &>~/.resh/daemon_last_run_out.txt & disown } # __resh_session_init() { @@ -219,7 +219,7 @@ __resh_preexec() { -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-collect ERROR: $(head -n 1 ~/.resh/client_last_run_out.txt)" + &>~/.resh/collect_last_run_out.txt || echo "resh-collect ERROR: $(head -n 1 ~/.resh/collect_last_run_out.txt)" fi } @@ -233,22 +233,22 @@ __resh_precmd() { __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-collect -version)" ]; then + if [ "$__RESH_VERSION" != "$(resh-postcollect -version)" ]; then # shellcheck source=shellrc.sh source ~/.resh/shellrc - if [ "$__RESH_VERSION" != "$(resh-collect -version)" ]; then + 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-collect -revision)" ]; then + elif [ "$__RESH_REVISION" != "$(resh-postcollect -revision)" ]; then # shellcheck source=shellrc.sh source ~/.resh/shellrc - if [ "$__RESH_REVISION" != "$(resh-collect -revision)" ]; then + 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-collect -version)" ] && [ "$__RESH_REVISION" = "$(resh-collect -revision)" ]; then + if [ "$__RESH_VERSION" = "$(resh-postcollect -version)" ] && [ "$__RESH_REVISION" = "$(resh-postcollect -revision)" ]; then resh-postcollect -requireVersion "$__RESH_VERSION" \ -requireRevision "$__RESH_REVISION" \ -cmdLine "$__RESH_CMDLINE" \ @@ -262,7 +262,7 @@ __resh_precmd() { -gitRemoteExitCodeAfter "$__RESH_GIT_REMOTE_EXIT_CODE_AFTER" \ -realtimeAfter "$__RESH_RT_AFTER" \ -timezoneAfter "$__RESH_TZ_AFTER" \ - &>~/.resh/client_last_run_out.txt || echo "resh-postcollect ERROR: $(head -n 1 ~/.resh/client_last_run_out.txt)" + &>~/.resh/postcollect_last_run_out.txt || echo "resh-postcollect ERROR: $(head -n 1 ~/.resh/postcollect_last_run_out.txt)" fi fi unset __RESH_COLLECT From dc1d5e4848f238be643c0023a0fe4ae4d887612a Mon Sep 17 00:00:00 2001 From: Simon Let Date: Fri, 4 Oct 2019 14:35:47 +0200 Subject: [PATCH 07/14] Add session watcher, improve history file handler Session watcher recieves all incoming records and and periodically checks if the session is still running. If session exits it sends message to other parts of RESH to drop the session. History handler recieves sessions to drop from session watcher. --- cmd/daemon/histfile.go | 57 ---------------------- cmd/daemon/main.go | 58 ++++++++++++++--------- go.mod | 1 + go.sum | 2 + pkg/histfile/histfile.go | 97 ++++++++++++++++++++++++++++++++++++++ pkg/sesswatch/sesswatch.go | 69 +++++++++++++++++++++++++++ 6 files changed, 205 insertions(+), 79 deletions(-) delete mode 100644 cmd/daemon/histfile.go create mode 100644 pkg/histfile/histfile.go create mode 100644 pkg/sesswatch/sesswatch.go diff --git a/cmd/daemon/histfile.go b/cmd/daemon/histfile.go deleted file mode 100644 index d608ca0..0000000 --- a/cmd/daemon/histfile.go +++ /dev/null @@ -1,57 +0,0 @@ -package main - -import ( - "encoding/json" - "log" - "os" - - "github.com/curusarn/resh/pkg/records" -) - -// HistfileWriter - reads records from channel, merges them and wrotes them to file -func HistfileWriter(input chan records.Record, outputPath string) { - sessions := map[string]records.Record{} - - for { - record := <-input - if record.PartOne { - if _, found := sessions[record.SessionID]; found { - log.Println("ERROR: Got another first part of the records before merging the previous one - overwriting!") - } - sessions[record.SessionID] = record - } else { - part1, found := sessions[record.SessionID] - if found == false { - log.Println("ERROR: Got second part of records and nothing to merge it with - ignoring!") - continue - } - delete(sessions, record.SessionID) - go mergeAndWriteRecord(part1, record, outputPath) - } - } -} - -func mergeAndWriteRecord(part1, part2 records.Record, outputPath string) { - err := part1.Merge(part2) - if err != nil { - log.Println("Error while merging", err) - return - } - recJSON, err := json.Marshal(part1) - 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", part1, err) - return - } -} diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index 69c031f..5b9c4ea 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -15,7 +15,9 @@ import ( "github.com/BurntSushi/toml" "github.com/curusarn/resh/pkg/cfg" + "github.com/curusarn/resh/pkg/histfile" "github.com/curusarn/resh/pkg/records" + "github.com/curusarn/resh/pkg/sesswatch" ) // Version from git set during build @@ -84,31 +86,35 @@ func statusHandler(w http.ResponseWriter, r *http.Request) { } type recordHandler struct { - histfile chan records.Record + subscribers []chan records.Record } 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 - } + // run rest of the handler as goroutine to prevent any hangups + go func() { + 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 - } - h.histfile <- record - part := "2" - if record.PartOne { - part = "1" - } - log.Println("Received:", record.CmdLine, " - part", part) + 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("Received:", record.CmdLine, " - part", part) + }() // fmt.Println("cmd:", r.CmdLine) // fmt.Println("pwd:", r.Pwd) @@ -117,11 +123,19 @@ func (h *recordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func runServer(port int, outputPath string) { - histfile := make(chan records.Record) - go HistfileWriter(histfile, outputPath) + var recordSubscribers []chan records.Record + + histfileChan := make(chan records.Record) + recordSubscribers = append(recordSubscribers, histfileChan) + sessionsToDrop := make(chan string) + histfile.Go(histfileChan, outputPath, sessionsToDrop) + + sesswatchChan := make(chan records.Record) + recordSubscribers = append(recordSubscribers, sesswatchChan) + sesswatch.Go(sesswatchChan, []chan string{sessionsToDrop}, 10) http.HandleFunc("/status", statusHandler) - http.Handle("/record", &recordHandler{histfile: histfile}) + http.Handle("/record", &recordHandler{subscribers: recordSubscribers}) //http.Handle("/session_init", &sessionInitHandler{OutputPath: outputPath}) //http.Handle("/recall", &recallHandler{OutputPath: outputPath}) http.ListenAndServe(":"+strconv.Itoa(port), nil) diff --git a/go.mod b/go.mod index 64dd27e..910abe6 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ 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 diff --git a/go.sum b/go.sum index e6a3477..beb087d 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,8 @@ github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vq 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= diff --git a/pkg/histfile/histfile.go b/pkg/histfile/histfile.go new file mode 100644 index 0000000..5418c45 --- /dev/null +++ b/pkg/histfile/histfile.go @@ -0,0 +1,97 @@ +package histfile + +import ( + "encoding/json" + "log" + "os" + "sync" + + "github.com/curusarn/resh/pkg/records" +) + +type histfile struct { + mutex sync.Mutex + sessions map[string]records.Record + outputPath string +} + +// Go creates histfile and runs two gorutines on it +func Go(input chan records.Record, outputPath string, sessionsToDrop chan string) { + hf := histfile{sessions: map[string]records.Record{}, outputPath: outputPath} + go hf.writer(input) + go hf.sessionGC(sessionsToDrop) +} + +// 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.mutex.Lock() + defer h.mutex.Unlock() + if part1, found := h.sessions[session]; found == true { + log.Println("histfile: Dropping session:", session) + delete(h.sessions, session) + go writeRecord(part1, h.outputPath) + } 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.mutex.Lock() + defer h.mutex.Unlock() + + if record.PartOne { + if _, found := h.sessions[record.SessionID]; found { + log.Println("histfile ERROR: Got another first part of the records before merging the previous one - overwriting!") + } + h.sessions[record.SessionID] = record + } else { + part1, found := h.sessions[record.SessionID] + if found == false { + log.Println("histfile ERROR: Got second part of records and nothing to merge it with - ignoring!") + } else { + delete(h.sessions, record.SessionID) + go mergeAndWriteRecord(part1, record, h.outputPath) + } + } + }() + } +} + +func mergeAndWriteRecord(part1, part2 records.Record, outputPath string) { + err := part1.Merge(part2) + if err != nil { + log.Println("Error while merging", err) + return + } + writeRecord(part1, outputPath) +} + +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 + } +} diff --git a/pkg/sesswatch/sesswatch.go b/pkg/sesswatch/sesswatch.go new file mode 100644 index 0000000..54b2c09 --- /dev/null +++ b/pkg/sesswatch/sesswatch.go @@ -0,0 +1,69 @@ +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(input chan records.Record, sessionsToDrop []chan string, sleepSeconds uint) { + sw := sesswatch{sessionsToDrop: sessionsToDrop, sleepSeconds: sleepSeconds, watchedSessions: map[string]bool{}} + go sw.waiter(input) +} + +func (s *sesswatch) waiter(sessionsToWatch chan records.Record) { + for { + func() { + record := <-sessionsToWatch + session := record.SessionID + pid := record.SessionPid + if record.PartOne == false { + log.Println("sesswatch: part2 - ignoring:", session, "~", pid) + return // continue + } + log.Println("sesswatch: got session ~ pid:", session, "~", pid) + s.mutex.Lock() + defer s.mutex.Unlock() + if s.watchedSessions[session] == false { + log.Println("sesswatch: start watching NEW session ~ pid:", session, "~", pid) + s.watchedSessions[session] = true + go s.watcher(session, record.SessionPid) + } + }() + } +} + +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 + } + } +} From ce7768949a447bcb99acfcf9dcf4a54319649945 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Fri, 4 Oct 2019 14:39:48 +0200 Subject: [PATCH 08/14] Make sesswatch period configurable --- cmd/daemon/main.go | 8 ++++---- conf/config.toml | 1 + pkg/cfg/cfg.go | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index 5b9c4ea..7d150af 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -72,7 +72,7 @@ func main() { if err != nil { log.Fatal("Could not create pidfile", err) } - runServer(config.Port, outputPath) + runServer(config, outputPath) err = os.Remove(pidfilePath) if err != nil { log.Println("Could not delete pidfile", err) @@ -122,7 +122,7 @@ func (h *recordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // fmt.Println("exit_code:", r.ExitCode) } -func runServer(port int, outputPath string) { +func runServer(config cfg.Config, outputPath string) { var recordSubscribers []chan records.Record histfileChan := make(chan records.Record) @@ -132,13 +132,13 @@ func runServer(port int, outputPath string) { sesswatchChan := make(chan records.Record) recordSubscribers = append(recordSubscribers, sesswatchChan) - sesswatch.Go(sesswatchChan, []chan string{sessionsToDrop}, 10) + sesswatch.Go(sesswatchChan, []chan string{sessionsToDrop}, config.SesswatchPeriodSeconds) http.HandleFunc("/status", statusHandler) http.Handle("/record", &recordHandler{subscribers: recordSubscribers}) //http.Handle("/session_init", &sessionInitHandler{OutputPath: outputPath}) //http.Handle("/recall", &recallHandler{OutputPath: outputPath}) - http.ListenAndServe(":"+strconv.Itoa(port), nil) + http.ListenAndServe(":"+strconv.Itoa(config.Port), nil) } func killDaemon(pidfile string) error { diff --git a/conf/config.toml b/conf/config.toml index c015120..6642be0 100644 --- a/conf/config.toml +++ b/conf/config.toml @@ -1 +1,2 @@ port = 2627 +sesswatchPeriodSeconds = 120 diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 8373306..0e5fb61 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -2,5 +2,6 @@ package cfg // Config struct type Config struct { - Port int + Port int + SesswatchPeriodSeconds uint } From e12e366dda6be0d84538b3e538d49094e9b3c245 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Sun, 6 Oct 2019 19:05:02 +0200 Subject: [PATCH 09/14] draft of arrow bindings add session history dispatch extend reshctl add recall - both command and handler for the daemon add session_init --- Makefile | 12 +- cmd/collect/main.go | 17 ++- cmd/control/cmd/debug.go | 23 +++- cmd/control/cmd/root.go | 2 + cmd/control/status/status.go | 2 + cmd/daemon/main.go | 82 +------------- cmd/daemon/recall.go | 48 ++++++++ cmd/daemon/record.go | 47 ++++++++ cmd/daemon/run-server.go | 46 ++++++++ cmd/daemon/session-init.go | 38 +++++++ cmd/postcollect/main.go | 2 +- cmd/session-init/main.go | 186 ++++++++++++++++++++++++++++++ pkg/collect/collect.go | 44 +++++++- pkg/records/records.go | 4 +- pkg/sess/sess.go | 7 ++ pkg/sesshist/sesshist.go | 135 ++++++++++++++++++++++ pkg/sesswatch/sesswatch.go | 21 ++-- scripts/bindutil.sh | 37 ------ scripts/hooks.sh | 147 ++++++++++++++++++++++++ scripts/reshctl.sh | 43 ++++++- scripts/shellrc.sh | 212 ++--------------------------------- scripts/util.sh | 136 ++++++++++++++++++++++ scripts/widgets.sh | 26 ++++- 23 files changed, 964 insertions(+), 353 deletions(-) create mode 100644 cmd/daemon/recall.go create mode 100644 cmd/daemon/record.go create mode 100644 cmd/daemon/run-server.go create mode 100644 cmd/daemon/session-init.go create mode 100644 cmd/session-init/main.go create mode 100644 pkg/sess/sess.go create mode 100644 pkg/sesshist/sesshist.go delete mode 100644 scripts/bindutil.sh create mode 100644 scripts/hooks.sh create mode 100644 scripts/util.sh diff --git a/Makefile b/Makefile index 277de70..9b86acd 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ sanitize: # # -build: submodules 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 test_go: # Running tests @@ -69,7 +69,7 @@ install: build submodules/bash-preexec/bash-preexec.sh scripts/shellrc.sh conf/c cp -f conf/config.toml ~/.config/resh.toml cp -f scripts/shellrc.sh ~/.resh/shellrc - cp -f scripts/reshctl.sh scripts/bindutil.sh scripts/widgets.sh ~/.resh/ + 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 @@ -92,6 +92,12 @@ install: build submodules/bash-preexec/bash-preexec.sh scripts/shellrc.sh conf/c # Restarting resh daemon ... -[ ! -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 # @@ -127,7 +133,7 @@ uninstall: bin/resh-control: cmd/control/cmd/*.go -bin/resh-%: cmd/%/main.go pkg/*/*.go VERSION +bin/resh-%: cmd/%/*.go pkg/*/*.go VERSION go build ${GOFLAGS} -o $@ cmd/$*/*.go $(HOME)/.resh $(HOME)/.resh/bin $(HOME)/.config $(HOME)/.resh/bash_completion.d $(HOME)/.resh/zsh_completion.d: diff --git a/cmd/collect/main.go b/cmd/collect/main.go index d2045c8..64ff6c8 100644 --- a/cmd/collect/main.go +++ b/cmd/collect/main.go @@ -35,6 +35,9 @@ func main() { if _, err := toml.DecodeFile(configPath, &config); err != nil { log.Fatal("Error reading config:", err) } + recall := flag.Bool("recall", false, "Recall command on position --histno") + recallHistno := flag.Int("histno", 0, "Recall command on position --histno") + showVersion := flag.Bool("version", false, "Show version and exit") showRevision := flag.Bool("revision", false, "Show git revision and exit") @@ -114,6 +117,10 @@ func main() { ")") os.Exit(3) } + if *recallHistno != 0 && *recall == false { + log.Println("Option '--recall' only works with '--histno' option - exiting!") + os.Exit(4) + } realtimeBefore, err := strconv.ParseFloat(*rtb, 64) if err != nil { log.Fatal("Flag Parsing error (rtb):", err) @@ -159,6 +166,8 @@ func main() { Lines: *lines, // core BaseRecord: records.BaseRecord{ + RecallHistno: *recallHistno, + CmdLine: *cmdLine, ExitCode: *exitCode, Shell: *shell, @@ -178,7 +187,7 @@ func main() { // non-posix RealPwd: realPwd, Pid: *pid, - SessionPid: *sessionPid, + SessionPID: *sessionPid, Host: *host, Hosttype: *hosttype, Ostype: *ostype, @@ -212,5 +221,9 @@ func main() { ReshRevision: Revision, }, } - collect.SendRecord(rec, strconv.Itoa(config.Port)) + if *recall { + fmt.Print(collect.SendRecallRequest(rec, strconv.Itoa(config.Port))) + } else { + collect.SendRecord(rec, strconv.Itoa(config.Port), "/record") + } } diff --git a/cmd/control/cmd/debug.go b/cmd/control/cmd/debug.go index c4dd299..fc10550 100644 --- a/cmd/control/cmd/debug.go +++ b/cmd/control/cmd/debug.go @@ -10,16 +10,33 @@ import ( "github.com/spf13/cobra" ) -// completionCmd represents the completion command var debugCmd = &cobra.Command{ Use: "debug", - Short: "Shows logs and output from last runs of resh", - Long: "Shows logs and output from last runs of 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", + 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 diff --git a/cmd/control/cmd/root.go b/cmd/control/cmd/root.go index b2f57a0..a00d342 100644 --- a/cmd/control/cmd/root.go +++ b/cmd/control/cmd/root.go @@ -28,6 +28,8 @@ func Execute() status.Code { completionCmd.AddCommand(completionZshCmd) rootCmd.AddCommand(debugCmd) + debugCmd.AddCommand(debugReloadCmd) + debugCmd.AddCommand(debugOutputCmd) if err := rootCmd.Execute(); err != nil { fmt.Println(err) return status.Fail diff --git a/cmd/control/status/status.go b/cmd/control/status/status.go index 28eb3a7..8b9eeec 100644 --- a/cmd/control/status/status.go +++ b/cmd/control/status/status.go @@ -12,4 +12,6 @@ const ( 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 7d150af..e5fc2b1 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,9 +15,6 @@ import ( "github.com/BurntSushi/toml" "github.com/curusarn/resh/pkg/cfg" - "github.com/curusarn/resh/pkg/histfile" - "github.com/curusarn/resh/pkg/records" - "github.com/curusarn/resh/pkg/sesswatch" ) // Version from git set during build @@ -85,62 +82,6 @@ func statusHandler(w http.ResponseWriter, r *http.Request) { log.Println("Status OK") } -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("Received:", 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) -} - -func runServer(config cfg.Config, outputPath string) { - var recordSubscribers []chan records.Record - - histfileChan := make(chan records.Record) - recordSubscribers = append(recordSubscribers, histfileChan) - sessionsToDrop := make(chan string) - histfile.Go(histfileChan, outputPath, sessionsToDrop) - - sesswatchChan := make(chan records.Record) - recordSubscribers = append(recordSubscribers, sesswatchChan) - sesswatch.Go(sesswatchChan, []chan string{sessionsToDrop}, config.SesswatchPeriodSeconds) - - http.HandleFunc("/status", statusHandler) - http.Handle("/record", &recordHandler{subscribers: recordSubscribers}) - //http.Handle("/session_init", &sessionInitHandler{OutputPath: outputPath}) - //http.Handle("/recall", &recallHandler{OutputPath: outputPath}) - http.ListenAndServe(":"+strconv.Itoa(config.Port), nil) -} - func killDaemon(pidfile string) error { dat, err := ioutil.ReadFile(pidfile) if err != nil { @@ -170,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..d9ba682 --- /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) + 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..92d725f --- /dev/null +++ b/cmd/daemon/run-server.go @@ -0,0 +1,46 @@ +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, outputPath 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) + sesshistDispatch := sesshist.NewDispatch(sesshistSessionsToInit, sesshistSessionsToDrop, sesshistRecords) + + // histfile + histfileRecords := make(chan records.Record) + recordSubscribers = append(recordSubscribers, histfileRecords) + histfileSessionsToDrop := make(chan string) + sessionDropSubscribers = append(sessionDropSubscribers, histfileSessionsToDrop) + histfile.Go(histfileRecords, outputPath, histfileSessionsToDrop) + + // 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/postcollect/main.go b/cmd/postcollect/main.go index fc02704..edca110 100644 --- a/cmd/postcollect/main.go +++ b/cmd/postcollect/main.go @@ -143,5 +143,5 @@ func main() { ReshRevision: Revision, }, } - collect.SendRecord(rec, strconv.Itoa(config.Port)) + collect.SendRecord(rec, strconv.Itoa(config.Port), "/record") } 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/pkg/collect/collect.go b/pkg/collect/collect.go index e53ef02..7f26b4e 100644 --- a/pkg/collect/collect.go +++ b/pkg/collect/collect.go @@ -13,14 +13,54 @@ import ( "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 string) { +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+"/record", + req, err := http.NewRequest("POST", "http://localhost:"+port+path, bytes.NewBuffer(recJSON)) if err != nil { log.Fatal("send err 2", err) diff --git a/pkg/records/records.go b/pkg/records/records.go index 0ded51b..ce89a8b 100644 --- a/pkg/records/records.go +++ b/pkg/records/records.go @@ -35,7 +35,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"` @@ -82,7 +82,7 @@ type BaseRecord struct { // recall metadata Recalled bool `json:"recalled"` - RecallHistno string `json:"recallHistno,omitempty"` + RecallHistno int `json:"recallHistno,omitempty"` RecallStrategy string `json:"recallStrategy,omitempty"` RecallActions []string `json:"recallActions,omitempty"` 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..7a36492 --- /dev/null +++ b/pkg/sesshist/sesshist.go @@ -0,0 +1,135 @@ +package sesshist + +import ( + "errors" + "log" + "strconv" + "sync" + + "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 +} + +// NewDispatch creates a new sesshist.Dispatch and starts necessary gorutines +func NewDispatch(sessionsToInit chan records.Record, sessionsToDrop chan string, recordsToAdd chan records.Record) *Dispatch { + s := Dispatch{sessions: map[string]*sesshist{}} + 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 { + 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) + } + + s.mutex.Lock() + defer s.mutex.Unlock() + s.sessions[sessionID] = &sesshist{} + 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 { + return errors.New("sesshist ERROR: No session history for SessionID " + sessionID + " (should we create one?)") + } + session.mutex.Lock() + defer session.mutex.Unlock() + session.recent = append(session.recent, record) + log.Println("sesshist: record:", record.CmdLine, "; added to session:", sessionID, "; session len:", len(session.recent)) + return nil +} + +// Recall command from recent session history +func (s *Dispatch) Recall(sessionID string, histno int) (string, error) { + s.mutex.RLock() + session, found := s.sessions[sessionID] + s.mutex.RUnlock() + + if found == false { + return "", errors.New("sesshist ERROR: No session history for SessionID " + sessionID) + } + session.mutex.Lock() + defer session.mutex.Unlock() + return session.getRecordByHistno(histno) +} + +type sesshist struct { + recent []records.Record + mutex sync.Mutex +} + +func (s *sesshist) getRecordByHistno(histno int) (string, error) { + // records get appended 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.recent) - histno + if index < 0 { + return "", errors.New("sesshist ERROR: 'histno > number of commands in the session' (" + strconv.Itoa(len(s.recent)) + ")") + } + return s.recent[index].CmdLine, nil +} diff --git a/pkg/sesswatch/sesswatch.go b/pkg/sesswatch/sesswatch.go index 54b2c09..cb3d41e 100644 --- a/pkg/sesswatch/sesswatch.go +++ b/pkg/sesswatch/sesswatch.go @@ -18,28 +18,23 @@ type sesswatch struct { } // Go runs the session watcher - watches sessions and sends -func Go(input chan records.Record, sessionsToDrop []chan string, sleepSeconds uint) { +func Go(sessionsToWatch chan records.Record, sessionsToDrop []chan string, sleepSeconds uint) { sw := sesswatch{sessionsToDrop: sessionsToDrop, sleepSeconds: sleepSeconds, watchedSessions: map[string]bool{}} - go sw.waiter(input) + go sw.waiter(sessionsToWatch) } func (s *sesswatch) waiter(sessionsToWatch chan records.Record) { for { func() { record := <-sessionsToWatch - session := record.SessionID - pid := record.SessionPid - if record.PartOne == false { - log.Println("sesswatch: part2 - ignoring:", session, "~", pid) - return // continue - } - log.Println("sesswatch: got session ~ pid:", session, "~", pid) + id := record.SessionID + pid := record.SessionPID s.mutex.Lock() defer s.mutex.Unlock() - if s.watchedSessions[session] == false { - log.Println("sesswatch: start watching NEW session ~ pid:", session, "~", pid) - s.watchedSessions[session] = true - go s.watcher(session, record.SessionPid) + if s.watchedSessions[id] == false { + log.Println("sesswatch: start watching NEW session ~ pid:", id, "~", pid) + s.watchedSessions[id] = true + go s.watcher(id, pid) } }() } diff --git a/scripts/bindutil.sh b/scripts/bindutil.sh deleted file mode 100644 index 53e192e..0000000 --- a/scripts/bindutil.sh +++ /dev/null @@ -1,37 +0,0 @@ - - -# shellcheck source=../submodules/bash-zsh-compat-widgets/bindfunc.sh -. ~/.resh/bindfunc.sh -# shellcheck source=widgets.sh -. ~/.resh/widgets.sh - -__resh_bind_arrows() { - echo "bindfunc __resh_widget_arrow_up" - echo "bindfunc __resh_widget_arrow_down" - return 0 -} - -__resh_bind_control_R() { - echo "bindfunc __resh_widget_control_R" - return 0 -} -__resh_unbind_arrows() { - echo "\ bindfunc __resh_widget_arrow_up" - echo "\ bindfunc __resh_widget_arrow_down" - return 0 -} - -__resh_unbind_control_R() { - echo "\ bindfunc __resh_widget_control_R" - return 0 -} - -__resh_bind_all() { - __resh_bind_arrows - __resh_bind_control_R -} - -__resh_unbind_all() { - __resh_unbind_arrows - __resh_unbind_control_R -} diff --git a/scripts/hooks.sh b/scripts/hooks.sh new file mode 100644 index 0000000..322b5fc --- /dev/null +++ b/scripts/hooks.sh @@ -0,0 +1,147 @@ + +__resh_preexec() { + __RESH_HISTNO=0 + # core + __RESH_COLLECT=1 + __RESH_CMDLINE="$1" # not local to preserve it for postcollect (useful as sanity check) + __resh_collect --cmdLine "$__RESH_CMDLINE" \ + &>~/.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" \ + "$@" + 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 + __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" \ + -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 + fi + unset __RESH_COLLECT +} diff --git a/scripts/reshctl.sh b/scripts/reshctl.sh index 4676c43..5702153 100644 --- a/scripts/reshctl.sh +++ b/scripts/reshctl.sh @@ -1,7 +1,39 @@ -# source bindutil - contains functions to bind and unbind RESH widgets -# shellcheck source=bindutil.sh -. ~/.resh/bindutil.sh +# shellcheck source=../submodules/bash-zsh-compat-widgets/bindfunc.sh +. ~/.resh/bindfunc.sh +# shellcheck source=widgets.sh +. ~/.resh/widgets.sh + +__resh_bind_arrows() { + bindfunc '\C-k' __resh_widget_arrow_up_compat + bindfunc '\C-j' __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 @@ -25,6 +57,11 @@ reshctl() { __resh_unbind_all return 0 ;; + 200) + # reload rc files + . ~/.resh/shellrc + return 0 + ;; *) echo "reshctl() FATAL ERROR: unknown status" >&2 return "$status" diff --git a/scripts/shellrc.sh b/scripts/shellrc.sh index 739c299..865e2fd 100644 --- a/scripts/shellrc.sh +++ b/scripts/shellrc.sh @@ -4,70 +4,13 @@ PATH=$PATH:~/.resh/bin # zmodload zsh/datetime # fi +# shellcheck source=hooks.sh +. ~/.resh/hooks.sh +# shellcheck source=util.sh +. ~/.resh/util.sh # shellcheck source=reshctl.sh . ~/.resh/reshctl.sh -__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 &>~/.resh/daemon_last_run_out.txt & disown -} - -# __resh_session_init() { -# resh-event session_start --sessionId "$__RESH_SESSION_ID" --sessionPid "$__RESH_SESSION_PID" -# } -# -__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_MACOS=0 __RESH_LINUX=0 __RESH_UNAME=$(uname) @@ -95,13 +38,6 @@ else echo "resh PANIC unrecognized shell" fi -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_session_init __RESH_SESSION_ID __RESH_SESSION_PID -fi - # posix __RESH_HOME="$HOME" __RESH_LOGIN="$LOGNAME" @@ -133,140 +69,12 @@ __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) - - 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" \ - -cmdLine "$__RESH_CMDLINE" \ - -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" \ - &>~/.resh/collect_last_run_out.txt || echo "resh-collect ERROR: $(head -n 1 ~/.resh/collect_last_run_out.txt)" - fi -} - -__resh_precmd() { - __RESH_EXIT_CODE=$? - __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" \ - -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 - fi - unset __RESH_COLLECT -} +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_session_init +fi # do not add more hooks when shellrc is sourced again if [ -z "${__RESH_PREEXEC_PRECMD_HOOKS_ADDED+x}" ]; then 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 index c6cea7d..77a7306 100644 --- a/scripts/widgets.sh +++ b/scripts/widgets.sh @@ -1,10 +1,28 @@ +# shellcheck source=hooks.sh +. ~/.resh/hooks.sh + __resh_widget_arrow_up() { - return 0 + (( __RESH_HISTNO++ )) + BUFFER="$(__resh_collect --recall --histno "$__RESH_HISTNO" 2> ~/.resh/arrow_up_last_run_out.txt)" } __resh_widget_arrow_down() { - return 0 + (( __RESH_HISTNO-- )) + BUFFER="$(__resh_collect --recall --histno "$__RESH_HISTNO" 2> ~/.resh/arrow_down_last_run_out.txt)" } __resh_widget_control_R() { - return 0 -} \ No newline at end of file + # 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 +} From 2697ee2f4967621386081e76a2a57152182730dc Mon Sep 17 00:00:00 2001 From: Simon Let Date: Sun, 6 Oct 2019 19:21:28 +0200 Subject: [PATCH 10/14] zsh comaptibility patch --- scripts/reshctl.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/reshctl.sh b/scripts/reshctl.sh index 5702153..1f59217 100644 --- a/scripts/reshctl.sh +++ b/scripts/reshctl.sh @@ -39,11 +39,11 @@ reshctl() { # run resh-control aka the real reshctl resh-control "$@" # modify current shell session based on exit status - local status=$? - case "$status" in + local _status=$? + case "$_status" in 0|1) # success | fail - return "$status" + return "$_status" ;; # enable 100) @@ -64,7 +64,7 @@ reshctl() { ;; *) echo "reshctl() FATAL ERROR: unknown status" >&2 - return "$status" + return "$_status" ;; esac } \ No newline at end of file From a3c2190c8a6b15a8934c2f812131241314687add Mon Sep 17 00:00:00 2001 From: Simon Let Date: Sun, 6 Oct 2019 19:23:56 +0200 Subject: [PATCH 11/14] extend zsh syntax check on more files --- scripts/test.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 ..." From ca660a0e9bcc9952491a647f0dbfc78a73dc20c4 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Wed, 9 Oct 2019 00:41:16 +0200 Subject: [PATCH 12/14] Implement widgets for arrow up/down, minor changes --- VERSION | 2 +- cmd/collect/main.go | 33 +++++++++++++++------ cmd/daemon/recall.go | 2 +- pkg/records/records.go | 3 ++ pkg/sesshist/sesshist.go | 63 +++++++++++++++++++++++++++++++++------- scripts/hooks.sh | 14 +++++++-- scripts/shellrc.sh | 1 + scripts/widgets.sh | 60 ++++++++++++++++++++++++++++++++++++-- 8 files changed, 153 insertions(+), 25 deletions(-) diff --git a/VERSION b/VERSION index e25d8d9..0664a8f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.5 +1.1.6 diff --git a/cmd/collect/main.go b/cmd/collect/main.go index 64ff6c8..55235a8 100644 --- a/cmd/collect/main.go +++ b/cmd/collect/main.go @@ -35,21 +35,28 @@ 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") + // posix variables cols := flag.String("cols", "-1", "$COLUMNS") lines := flag.String("lines", "-1", "$LINES") @@ -121,6 +128,11 @@ func main() { log.Println("Option '--recall' only works with '--histno' option - exiting!") os.Exit(4) } + 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) @@ -150,15 +162,15 @@ func main() { *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 @@ -219,6 +231,9 @@ func main() { ReshUUID: collect.ReadFileContent(reshUUIDPath), ReshVersion: Version, ReshRevision: Revision, + + RecallActions: []string{*recallActions}, + RecallPrefix: *recallPrefix, }, } if *recall { diff --git a/cmd/daemon/recall.go b/cmd/daemon/recall.go index d9ba682..7c5efeb 100644 --- a/cmd/daemon/recall.go +++ b/cmd/daemon/recall.go @@ -29,7 +29,7 @@ func (h *recallHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Println("Payload:", jsn) return } - cmd, err := h.sesshistDispatch.Recall(rec.SessionID, rec.RecallHistno) + 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) diff --git a/pkg/records/records.go b/pkg/records/records.go index ce89a8b..d3668a5 100644 --- a/pkg/records/records.go +++ b/pkg/records/records.go @@ -86,6 +86,9 @@ type BaseRecord struct { RecallStrategy string `json:"recallStrategy,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"` diff --git a/pkg/sesshist/sesshist.go b/pkg/sesshist/sesshist.go index 7a36492..09f7e69 100644 --- a/pkg/sesshist/sesshist.go +++ b/pkg/sesshist/sesshist.go @@ -4,6 +4,7 @@ import ( "errors" "log" "strconv" + "strings" "sync" "github.com/curusarn/resh/pkg/records" @@ -94,13 +95,22 @@ func (s *Dispatch) addRecentRecord(sessionID string, record records.Record) erro } session.mutex.Lock() defer session.mutex.Unlock() - session.recent = append(session.recent, record) - log.Println("sesshist: record:", record.CmdLine, "; added to session:", sessionID, "; session len:", len(session.recent)) + session.recentRecords = append(session.recentRecords, record) + // remove previous occurance of record + for i := len(session.recentCmdLines) - 1; i >= 0; i-- { + if session.recentCmdLines[i] == record.CmdLine { + session.recentCmdLines = append(session.recentCmdLines[:i], session.recentCmdLines[i+1:]...) + } + } + // append new record + session.recentCmdLines = append(session.recentCmdLines, record.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) (string, error) { +func (s *Dispatch) Recall(sessionID string, histno int, prefix string) (string, error) { s.mutex.RLock() session, found := s.sessions[sessionID] s.mutex.RUnlock() @@ -108,18 +118,25 @@ func (s *Dispatch) Recall(sessionID string, histno int) (string, error) { if found == false { return "", errors.New("sesshist ERROR: No session history for SessionID " + sessionID) } + if prefix == "" { + session.mutex.Lock() + defer session.mutex.Unlock() + return session.getRecordByHistno(histno) + } session.mutex.Lock() defer session.mutex.Unlock() - return session.getRecordByHistno(histno) + return session.searchRecordByPrefix(prefix, histno) } type sesshist struct { - recent []records.Record - mutex sync.Mutex + recentRecords []records.Record + recentCmdLines []string // deduplicated + // cmdLines map[string]int + mutex sync.Mutex } func (s *sesshist) getRecordByHistno(histno int) (string, error) { - // records get appended to the end of the slice + // 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") @@ -127,9 +144,35 @@ 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.recent) - histno + 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.recent)) + ")") + 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 s.recent[index].CmdLine, nil + return cmdLines[histno-1], nil } diff --git a/scripts/hooks.sh b/scripts/hooks.sh index 322b5fc..be11652 100644 --- a/scripts/hooks.sh +++ b/scripts/hooks.sh @@ -1,11 +1,20 @@ -__resh_preexec() { +__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_preexec() { # core __RESH_COLLECT=1 __RESH_CMDLINE="$1" # not local to preserve it for postcollect (useful as sanity check) - __resh_collect --cmdLine "$__RESH_CMDLINE" \ + __resh_collect --cmdLine "$__RESH_CMDLINE" --recall-actions "$__RESH_HIST_RECALL_ACTIONS" \ &>~/.resh/collect_last_run_out.txt || echo "resh-collect ERROR: $(head -n 1 ~/.resh/collect_last_run_out.txt)" + + __resh_reset_variables } # used for collect and collect --recall @@ -90,6 +99,7 @@ __resh_collect() { -osReleaseIdLike "$__RESH_OS_RELEASE_ID_LIKE" \ -osReleaseName "$__RESH_OS_RELEASE_NAME" \ -osReleasePrettyName "$__RESH_OS_RELEASE_PRETTY_NAME" \ + -histno "$__RESH_HISTNO" \ "$@" fi } diff --git a/scripts/shellrc.sh b/scripts/shellrc.sh index 865e2fd..d5357fd 100644 --- a/scripts/shellrc.sh +++ b/scripts/shellrc.sh @@ -73,6 +73,7 @@ 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 diff --git a/scripts/widgets.sh b/scripts/widgets.sh index 77a7306..9960da7 100644 --- a/scripts/widgets.sh +++ b/scripts/widgets.sh @@ -2,15 +2,71 @@ # shellcheck source=hooks.sh . ~/.resh/hooks.sh +__resh_helper_arrow_pre() { + # 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="" + # append curent recall action + __RESH_HIST_RECALL_ACTIONS="$__RESH_HIST_RECALL_ACTIONS;arrow_up:$__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 + # increment histno (( __RESH_HISTNO++ )) - BUFFER="$(__resh_collect --recall --histno "$__RESH_HISTNO" 2> ~/.resh/arrow_up_last_run_out.txt)" + # 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 --histno "$__RESH_HISTNO" --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-- )) + fi + # run post helper + __resh_helper_arrow_post } __resh_widget_arrow_down() { + # run helper function + __resh_helper_arrow_pre + # increment histno (( __RESH_HISTNO-- )) - BUFFER="$(__resh_collect --recall --histno "$__RESH_HISTNO" 2> ~/.resh/arrow_down_last_run_out.txt)" + # 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 --histno "$__RESH_HISTNO" --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_LBUFFER=${BUFFER:0:CURSOR} + __RESH_HIST_RECALL_ACTIONS="$__RESH_HIST_RECALL_ACTIONS;control_R:$__RESH_LBUFFER" # resh-collect --hstr hstr } From 1ef128895b86d722003e5ee6bbe6360c4e71048f Mon Sep 17 00:00:00 2001 From: Simon Let Date: Fri, 11 Oct 2019 01:40:42 +0200 Subject: [PATCH 13/14] work around bash-preexec issues, load history on init bash-preexec doesn't get along with `bind -x` 'widgets' history gets loaded from file on daemon start recent history is used to populate session history on session start add some metadata about recalls --- VERSION | 2 +- cmd/collect/main.go | 10 +++--- cmd/daemon/main.go | 4 +-- cmd/daemon/run-server.go | 8 +++-- conf/config.toml | 1 + pkg/cfg/cfg.go | 5 +-- pkg/histfile/histfile.go | 77 +++++++++++++++++++++++++++++----------- pkg/records/records.go | 65 ++++++++++++++++++++++++++++----- pkg/sesshist/sesshist.go | 51 ++++++++++++++++++-------- scripts/hooks.sh | 8 +++-- scripts/reshctl.sh | 4 +-- scripts/widgets.sh | 29 +++++++++------ 12 files changed, 193 insertions(+), 71 deletions(-) diff --git a/VERSION b/VERSION index 0664a8f..6085e94 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.6 +1.2.1 diff --git a/cmd/collect/main.go b/cmd/collect/main.go index 55235a8..5699ac6 100644 --- a/cmd/collect/main.go +++ b/cmd/collect/main.go @@ -56,6 +56,7 @@ func main() { // 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") @@ -124,10 +125,6 @@ func main() { ")") os.Exit(3) } - if *recallHistno != 0 && *recall == false { - log.Println("Option '--recall' only works with '--histno' option - exiting!") - os.Exit(4) - } if *recallPrefix != "" && *recall == false { log.Println("Option '--prefix-search' only works with '--recall' option - exiting!") os.Exit(4) @@ -232,8 +229,9 @@ func main() { ReshVersion: Version, ReshRevision: Revision, - RecallActions: []string{*recallActions}, - RecallPrefix: *recallPrefix, + RecallActionsRaw: *recallActions, + RecallPrefix: *recallPrefix, + RecallStrategy: *recallStrategy, }, } if *recall { diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index e5fc2b1..a0ff6df 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -31,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) @@ -69,7 +69,7 @@ func main() { if err != nil { log.Fatal("Could not create pidfile", err) } - runServer(config, outputPath) + runServer(config, historyPath) err = os.Remove(pidfilePath) if err != nil { log.Println("Could not delete pidfile", err) diff --git a/cmd/daemon/run-server.go b/cmd/daemon/run-server.go index 92d725f..6eb44b7 100644 --- a/cmd/daemon/run-server.go +++ b/cmd/daemon/run-server.go @@ -11,7 +11,7 @@ import ( "github.com/curusarn/resh/pkg/sesswatch" ) -func runServer(config cfg.Config, outputPath string) { +func runServer(config cfg.Config, historyPath string) { var recordSubscribers []chan records.Record var sessionInitSubscribers []chan records.Record var sessionDropSubscribers []chan string @@ -23,14 +23,16 @@ func runServer(config cfg.Config, outputPath string) { sessionDropSubscribers = append(sessionDropSubscribers, sesshistSessionsToDrop) sesshistRecords := make(chan records.Record) recordSubscribers = append(recordSubscribers, sesshistRecords) - sesshistDispatch := sesshist.NewDispatch(sesshistSessionsToInit, sesshistSessionsToDrop, sesshistRecords) // histfile histfileRecords := make(chan records.Record) recordSubscribers = append(recordSubscribers, histfileRecords) histfileSessionsToDrop := make(chan string) sessionDropSubscribers = append(sessionDropSubscribers, histfileSessionsToDrop) - histfile.Go(histfileRecords, outputPath, 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) diff --git a/conf/config.toml b/conf/config.toml index 6642be0..1970f34 100644 --- a/conf/config.toml +++ b/conf/config.toml @@ -1,2 +1,3 @@ port = 2627 sesswatchPeriodSeconds = 120 +sesshistInitHistorySize = 1000 diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index 0e5fb61..16726bb 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -2,6 +2,7 @@ package cfg // Config struct type Config struct { - Port int - SesswatchPeriodSeconds uint + Port int + SesswatchPeriodSeconds uint + SesshistInitHistorySize int } diff --git a/pkg/histfile/histfile.go b/pkg/histfile/histfile.go index 5418c45..879efe1 100644 --- a/pkg/histfile/histfile.go +++ b/pkg/histfile/histfile.go @@ -9,31 +9,49 @@ import ( "github.com/curusarn/resh/pkg/records" ) -type histfile struct { - mutex sync.Mutex - sessions map[string]records.Record - outputPath string +// 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 } -// Go creates histfile and runs two gorutines on it -func Go(input chan records.Record, outputPath string, sessionsToDrop chan string) { - hf := histfile{sessions: map[string]records.Record{}, outputPath: outputPath} +// 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) { +func (h *Histfile) sessionGC(sessionsToDrop chan string) { for { func() { session := <-sessionsToDrop log.Println("histfile: got session to drop", session) - h.mutex.Lock() - defer h.mutex.Unlock() + 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.outputPath) + go writeRecord(part1, h.historyPath) } else { log.Println("histfile: No hanging parts for session:", session) } @@ -42,38 +60,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) { for { func() { record := <-input - h.mutex.Lock() - defer h.mutex.Unlock() + h.sessionsMutex.Lock() + defer h.sessionsMutex.Unlock() if record.PartOne { if _, found := h.sessions[record.SessionID]; found { - log.Println("histfile ERROR: Got another first part of the records before merging the previous one - overwriting!") + 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[record.SessionID] = record } else { - part1, found := h.sessions[record.SessionID] - if found == false { + if part1, found := h.sessions[record.SessionID]; found == false { log.Println("histfile ERROR: Got second part of records and nothing to merge it with - ignoring!") } else { delete(h.sessions, record.SessionID) - go mergeAndWriteRecord(part1, record, h.outputPath) + go h.mergeAndWriteRecord(part1, record) } } }() } } -func mergeAndWriteRecord(part1, part2 records.Record, outputPath string) { +func (h *Histfile) mergeAndWriteRecord(part1, part2 records.Record) { err := part1.Merge(part2) if err != nil { log.Println("Error while merging", err) return } - writeRecord(part1, outputPath) + + 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) { @@ -95,3 +127,8 @@ func writeRecord(rec records.Record, outputPath string) { 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 d3668a5..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" @@ -81,10 +83,11 @@ type BaseRecord struct { SessionExit bool `json:"sessionExit,omitempty"` // recall metadata - Recalled bool `json:"recalled"` - RecallHistno int `json:"recallHistno,omitempty"` - RecallStrategy string `json:"recallStrategy,omitempty"` - RecallActions []string `json:"recallActions,omitempty"` + 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"` @@ -177,11 +180,10 @@ func (r *Record) Merge(r2 Record) error { 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 || r.RealtimeBefore != r2.RealtimeBefore { - return errors.New("Records to merge are not parts of the same records - r1:" + - r.CmdLine + "(" + strconv.FormatFloat(r.RealtimeBefore, 'f', -1, 64) + ") r2:" + - r2.CmdLine + "(" + strconv.FormatFloat(r2.RealtimeBefore, 'f', -1, 64) + ")") + 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 @@ -436,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/sesshist/sesshist.go b/pkg/sesshist/sesshist.go index 09f7e69..50b79ec 100644 --- a/pkg/sesshist/sesshist.go +++ b/pkg/sesshist/sesshist.go @@ -7,6 +7,7 @@ import ( "strings" "sync" + "github.com/curusarn/resh/pkg/histfile" "github.com/curusarn/resh/pkg/records" ) @@ -14,11 +15,20 @@ import ( 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) *Dispatch { - s := Dispatch{sessions: map[string]*sesshist{}} +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) @@ -54,6 +64,7 @@ func (s *Dispatch) recordAdder(recordsToAdd chan records.Record) { // 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() @@ -62,9 +73,17 @@ func (s *Dispatch) initSession(sessionID string) error { 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() - s.sessions[sessionID] = &sesshist{} + // 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 } @@ -91,19 +110,22 @@ func (s *Dispatch) addRecentRecord(sessionID string, record records.Record) erro s.mutex.RUnlock() if found == false { - return errors.New("sesshist ERROR: No session history for SessionID " + sessionID + " (should we create one?)") + 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 - for i := len(session.recentCmdLines) - 1; i >= 0; i-- { - if session.recentCmdLines[i] == record.CmdLine { - session.recentCmdLines = append(session.recentCmdLines[:i], session.recentCmdLines[i+1:]...) - } + 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, record.CmdLine) + 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 @@ -116,7 +138,8 @@ func (s *Dispatch) Recall(sessionID string, histno int, prefix string) (string, s.mutex.RUnlock() if found == false { - return "", errors.New("sesshist ERROR: No session history for SessionID " + sessionID) + // go s.initSession(sessionID) + return "", errors.New("sesshist ERROR: No session history for SessionID " + sessionID + " - should we create one?") } if prefix == "" { session.mutex.Lock() @@ -129,10 +152,10 @@ func (s *Dispatch) Recall(sessionID string, histno int, prefix string) (string, } type sesshist struct { - recentRecords []records.Record - recentCmdLines []string // deduplicated - // cmdLines map[string]int - mutex sync.Mutex + recentRecords []records.Record + recentCmdLines []string // deduplicated + cmdLinesLastIndex map[string]int + mutex sync.Mutex } func (s *sesshist) getRecordByHistno(histno int) (string, error) { diff --git a/scripts/hooks.sh b/scripts/hooks.sh index be11652..1595a64 100644 --- a/scripts/hooks.sh +++ b/scripts/hooks.sh @@ -5,16 +5,17 @@ __resh_reset_variables() { __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" \ + __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)" - - __resh_reset_variables } # used for collect and collect --recall @@ -152,6 +153,7 @@ __resh_precmd() { -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 index 1f59217..2656055 100644 --- a/scripts/reshctl.sh +++ b/scripts/reshctl.sh @@ -5,8 +5,8 @@ . ~/.resh/widgets.sh __resh_bind_arrows() { - bindfunc '\C-k' __resh_widget_arrow_up_compat - bindfunc '\C-j' __resh_widget_arrow_down_compat + bindfunc '\e[A' __resh_widget_arrow_up_compat + bindfunc '\e[B' __resh_widget_arrow_down_compat return 0 } diff --git a/scripts/widgets.sh b/scripts/widgets.sh index 9960da7..d1d48e5 100644 --- a/scripts/widgets.sh +++ b/scripts/widgets.sh @@ -3,16 +3,21 @@ . ~/.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} + __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="" - # append curent recall action - __RESH_HIST_RECALL_ACTIONS="$__RESH_HIST_RECALL_ACTIONS;arrow_up:$__RESH_PREFIX" # histno == 0 => save current line [ $__RESH_HISTNO -eq 0 ] && __RESH_HISTNO_ZERO_LINE=$BUFFER } @@ -28,18 +33,20 @@ __resh_helper_arrow_post() { __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=$((__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 --histno "$__RESH_HISTNO" --prefix-search "$__RESH_PREFIX" 2> ~/.resh/arrow_up_last_run_out.txt)" + 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-- )) + [ ${#NEW_BUFFER} -gt 0 ] && BUFFER=$NEW_BUFFER || __RESH_HISTNO=$((__RESH_HISTNO-1)) fi # run post helper __resh_helper_arrow_post @@ -47,8 +54,10 @@ __resh_widget_arrow_up() { __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=$((__RESH_HISTNO-1)) # prevent HISTNO from getting negative (for now) [ $__RESH_HISTNO -lt 0 ] && __RESH_HISTNO=0 # back at histno == 0 => restore original line @@ -57,7 +66,7 @@ __resh_widget_arrow_down() { else # run recall local NEW_BUFFER - NEW_BUFFER="$(__resh_collect --recall --histno "$__RESH_HISTNO" --prefix-search "$__RESH_PREFIX" 2> ~/.resh/arrow_down_last_run_out.txt)" + 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++ )) @@ -65,8 +74,8 @@ __resh_widget_arrow_down() { __resh_helper_arrow_post } __resh_widget_control_R() { - local __RESH_LBUFFER=${BUFFER:0:CURSOR} - __RESH_HIST_RECALL_ACTIONS="$__RESH_HIST_RECALL_ACTIONS;control_R:$__RESH_LBUFFER" + local __RESH_PREFIX=${BUFFER:0:CURSOR} + __RESH_HIST_RECALL_ACTIONS="$__RESH_HIST_RECALL_ACTIONS;control_R:$__RESH_PREFIX" # resh-collect --hstr hstr } From cf3b54a870e0b2b92d4a06265a6ccd63595300e6 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Fri, 11 Oct 2019 02:17:05 +0200 Subject: [PATCH 14/14] handle nested shells --- cmd/postcollect/main.go | 2 ++ pkg/histfile/histfile.go | 13 ++++++++----- scripts/hooks.sh | 2 ++ scripts/shellrc.sh | 11 ++++++++--- scripts/widgets.sh | 12 ++++++------ 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/cmd/postcollect/main.go b/cmd/postcollect/main.go index edca110..6029bff 100644 --- a/cmd/postcollect/main.go +++ b/cmd/postcollect/main.go @@ -44,6 +44,7 @@ func main() { 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") @@ -116,6 +117,7 @@ func main() { CmdLine: *cmdLine, ExitCode: *exitCode, SessionID: *sessionID, + Shlvl: *shlvl, PwdAfter: *pwdAfter, diff --git a/pkg/histfile/histfile.go b/pkg/histfile/histfile.go index 879efe1..5964603 100644 --- a/pkg/histfile/histfile.go +++ b/pkg/histfile/histfile.go @@ -4,6 +4,7 @@ import ( "encoding/json" "log" "os" + "strconv" "sync" "github.com/curusarn/resh/pkg/records" @@ -67,17 +68,19 @@ func (h *Histfile) writer(input chan records.Record) { 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[record.SessionID]; found { + 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[record.SessionID] = record + h.sessions[mergeID] = record } else { - if part1, found := h.sessions[record.SessionID]; found == false { - log.Println("histfile ERROR: Got second part of records and nothing to merge it with - ignoring!") + 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, record.SessionID) + delete(h.sessions, mergeID) go h.mergeAndWriteRecord(part1, record) } } diff --git a/scripts/hooks.sh b/scripts/hooks.sh index 1595a64..4963f95 100644 --- a/scripts/hooks.sh +++ b/scripts/hooks.sh @@ -114,6 +114,7 @@ __resh_precmd() { 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" @@ -144,6 +145,7 @@ __resh_precmd() { -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" \ diff --git a/scripts/shellrc.sh b/scripts/shellrc.sh index d5357fd..c4d3afc 100644 --- a/scripts/shellrc.sh +++ b/scripts/shellrc.sh @@ -69,6 +69,8 @@ __RESH_REVISION=$(resh-collect -revision) __resh_run_daemon +# 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="$$" @@ -77,9 +79,12 @@ if [ -z "${__RESH_SESSION_ID+x}" ]; then __resh_session_init fi -# do not add more hooks when shellrc is sourced again -if [ -z "${__RESH_PREEXEC_PRECMD_HOOKS_ADDED+x}" ]; then +# 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_PREEXEC_PRECMD_HOOKS_ADDED=1 + + __resh_reset_variables + + __RESH_INIT_DONE=1 fi diff --git a/scripts/widgets.sh b/scripts/widgets.sh index d1d48e5..82761bd 100644 --- a/scripts/widgets.sh +++ b/scripts/widgets.sh @@ -19,7 +19,7 @@ __resh_helper_arrow_pre() { # "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_HISTNO" -eq 0 ] && __RESH_HISTNO_ZERO_LINE=$BUFFER } __resh_helper_arrow_post() { # cursor at the beginning of the line => activate "NO_PREFIX_MODE" @@ -38,7 +38,7 @@ __resh_widget_arrow_up() { # increment histno __RESH_HISTNO=$((__RESH_HISTNO+1)) # back at histno == 0 => restore original line - if [ $__RESH_HISTNO -eq 0 ]; then + if [ "$__RESH_HISTNO" -eq 0 ]; then BUFFER=$__RESH_HISTNO_ZERO_LINE else # run recall @@ -46,7 +46,7 @@ __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)) + [ "${#NEW_BUFFER}" -gt 0 ] && BUFFER=$NEW_BUFFER || __RESH_HISTNO=$((__RESH_HISTNO-1)) fi # run post helper __resh_helper_arrow_post @@ -59,9 +59,9 @@ __resh_widget_arrow_down() { # increment histno __RESH_HISTNO=$((__RESH_HISTNO-1)) # prevent HISTNO from getting negative (for now) - [ $__RESH_HISTNO -lt 0 ] && __RESH_HISTNO=0 + [ "$__RESH_HISTNO" -lt 0 ] && __RESH_HISTNO=0 # back at histno == 0 => restore original line - if [ $__RESH_HISTNO -eq 0 ]; then + if [ "$__RESH_HISTNO" -eq 0 ]; then BUFFER=$__RESH_HISTNO_ZERO_LINE else # run recall @@ -69,7 +69,7 @@ __resh_widget_arrow_down() { 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++ )) + [ "${#NEW_BUFFER}" -gt 0 ] && BUFFER=$NEW_BUFFER || (( __RESH_HISTNO++ )) fi __resh_helper_arrow_post }