diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml index 07deb74f..ceccc785 100644 --- a/.github/workflows/go.yaml +++ b/.github/workflows/go.yaml @@ -7,13 +7,18 @@ jobs: runs-on: ubuntu-latest steps: - - name: Set up Go - uses: actions/setup-go@v2 + - name: Checkout + uses: actions/checkout@v3 with: - go-version: 1.16 + fetch-depth: 0 + + - name: Get Go version + run: echo "GO_VERSION=$(grep '^go ' go.mod | cut -d ' ' -f 2)" >> $GITHUB_ENV && cat $GITHUB_ENV - - name: Check out code into the Go module directory - uses: actions/checkout@v2 + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: ${{ env.GO_VERSION }} - name: Test run: go test -v ./... diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e2ba986c..32194f36 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -8,19 +8,25 @@ on: jobs: goreleaser: + name: Goreleaser runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 + with: + fetch-depth: 0 - - name: Checkout submodules - run: git submodule update --init --recursive + - name: Get Go version + run: echo "GO_VERSION=$(grep '^go ' go.mod | cut -d ' ' -f 2)" >> $GITHUB_ENV && cat $GITHUB_ENV - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v3 with: - go-version: 1.16 + go-version: ${{ env.GO_VERSION }} + + - name: Checkout submodules + run: git submodule update --init --recursive - name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 @@ -28,4 +34,4 @@ jobs: version: latest args: release env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/sh.yaml b/.github/workflows/sh.yaml index fdd34f56..0dcfb475 100644 --- a/.github/workflows/sh.yaml +++ b/.github/workflows/sh.yaml @@ -7,11 +7,13 @@ jobs: runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up run: sudo apt-get install -y shellcheck zsh - - name: Check out code into the Go module directory - uses: actions/checkout@v2 - - name: Test run: scripts/test.sh diff --git a/.goreleaser.yml b/.goreleaser.yml index 11c048dd..b364a830 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -76,55 +76,46 @@ builds: - arm - arm64 - - id: "evaluate" - main: ./cmd/evaluate - binary: bin/resh-evaluate - goarch: - - 386 - - amd64 - - arm - - arm64 - - - id: "event" - main: ./cmd/event - binary: bin/resh-event + id: "postcollect" + main: ./cmd/postcollect + binary: bin/resh-postcollect goarch: - 386 - amd64 - arm - arm64 - - id: "inspect" - main: ./cmd/inspect - binary: bin/resh-inspect + id: "session-init" + main: ./cmd/session-init + binary: bin/resh-session-init goarch: - 386 - amd64 - arm - arm64 - - id: "postcollect" - main: ./cmd/postcollect - binary: bin/resh-postcollect - goarch: + id: "install-utils" + main: ./cmd/install-utils + binary: bin/resh-install-utils + goarch: - 386 - amd64 - arm - arm64 - - id: "sanitize" - main: ./cmd/sanitize - binary: bin/resh-sanitize - goarch: + id: "generate-uuid" + main: ./cmd/generate-uuid + binary: bin/resh-generate-uuid + goarch: - 386 - amd64 - arm - arm64 - - id: "session-init" - main: ./cmd/session-init - binary: bin/resh-session-init - goarch: + id: "get-epochtime" + main: ./cmd/get-epochtime + binary: bin/resh-get-epochtime + goarch: - 386 - amd64 - arm diff --git a/Makefile b/Makefile index 02bb7e04..a67c36d3 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,25 @@ SHELL=/bin/bash LATEST_TAG=$(shell git describe --tags) -REVISION=$(shell [ -z "$(git status --untracked-files=no --porcelain)" ] && git rev-parse --short=12 HEAD || echo "no_revision") -VERSION="${LATEST_TAG}-DEV" -GOFLAGS=-ldflags "-X main.version=${VERSION} -X main.commit=${REVISION}" +VERSION:="${LATEST_TAG}-$(shell date +%s)" +COMMIT:=$(shell [ -z "$(git status --untracked-files=no --porcelain)" ] && git rev-parse --short=12 HEAD || echo "no_commit") +GOFLAGS=-ldflags "-X main.version=${VERSION} -X main.commit=${COMMIT} -X main.development=true" +build: submodules bin/resh-session-init bin/resh-collect bin/resh-postcollect\ + bin/resh-daemon bin/resh-control bin/resh-config bin/resh-cli\ + bin/resh-install-utils bin/resh-generate-uuid bin/resh-get-epochtime -build: submodules bin/resh-session-init bin/resh-collect bin/resh-postcollect bin/resh-daemon\ - bin/resh-evaluate bin/resh-sanitize bin/resh-control bin/resh-config bin/resh-inspect bin/resh-cli - +# We disable jobserver for the actual installation because we want it to run serially +# Make waits to the daemon process we launch during install and hangs install: build scripts/install.sh +# Rebuild binaries and install +# Very useful to ensure that all binaries get new VERSION variable which is used for shell config reloading +clean_install: + make clean + make build + make install + test: go test -v ./... go vet ./... @@ -21,19 +30,19 @@ rebuild: make build clean: - rm -f bin/resh-* + rm -f -- bin/* uninstall: # Uninstalling ... - -rm -rf ~/.resh/ + -rm -rf -- ~/.resh/ -bin/resh-%: cmd/%/*.go pkg/*/*.go cmd/control/cmd/*.go cmd/control/status/status.go +go_files = $(shell find -name '*.go') +bin/resh-%: $(go_files) grep $@ .goreleaser.yml -q # all build targets need to be included in .goreleaser.yml go build ${GOFLAGS} -o $@ cmd/$*/*.go .PHONY: submodules build install rebuild uninstall clean test - 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 diff --git a/README.md b/README.md index b2e12a73..ee7cc973 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,16 @@ -![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/curusarn/resh?sort=semver) -![Go test](https://github.com/curusarn/resh/actions/workflows/go.yaml/badge.svg) -![Shell test](https://github.com/curusarn/resh/actions/workflows/sh.yaml/badge.svg) +[![Latest version](https://img.shields.io/github/v/tag/curusarn/resh?sort=semver)](https://github.com/curusarn/resh/releases) +[![Go Report Card](https://goreportcard.com/badge/github.com/curusarn/resh)](https://goreportcard.com/report/github.com/curusarn/resh) +[![Go test](https://github.com/curusarn/resh/actions/workflows/go.yaml/badge.svg)](https://github.com/curusarn/resh/actions/workflows/go.yaml) +[![Shell test](https://github.com/curusarn/resh/actions/workflows/sh.yaml/badge.svg)](https://github.com/curusarn/resh/actions/workflows/sh.yaml) -# Rich Enhanced Shell History +# RESH + +Context-based replacement for `zsh` and `bash` shell history. + +**Full-text search your shell history.** +Relevant results are displayed first based on current directory, git repo, and exit status. -Context-based replacement/enhancement for zsh and bash shell history @@ -17,114 +22,36 @@ Context-based replacement/enhancement for zsh and bash shell history -**Search your history by commands and get relevant results based on current directory, git repo, exit status, and host.** - -## Installation - -### Prerequisites - -Standard stuff: `bash(4.3+)`, `curl`, `tar`, ... - -Bash completions will only work if you have `bash-completion` installed +## Install -MacOS: `coreutils` (`brew install coreutils`) - -### Simplest installation - -Run this command. +Install RESH with one command: ```sh -curl -fsSL https://raw.githubusercontent.com/curusarn/resh/master/scripts/rawinstall.sh | bash +curl -fsSL https://raw.githubusercontent.com/curusarn/resh/master/scripts/rawinstall.sh | sh ``` -### Simple installation - -Run - -```shell -git clone https://github.com/curusarn/resh.git -cd resh && scripts/rawinstall.sh -``` - -### Update - -Check for updates and update - -```sh -reshctl update -``` - -## Roadmap - -[Overview of the features of the project](./roadmap.md) - -## RESH SEARCH application - -This is the most important part of this project. - -RESH SEARCH app searches your history by commands. It uses host, directories, git remote, and exit status to show you relevant results first. +ℹ️ You will need to have `curl` and `tar` installed. -All this context is not in the regular shell history. RESH records shell history with context to use it when searching. +More options on [Installation page ⇗](./installation.md) -At first, the search application will look something like this. Some history with context and most of it without. As you can see, you can still search the history just fine. +## Search your history -![resh search app](img/screen-resh-cli-v2-7-init.png) +Press Ctrl + R to search: -Eventually most of your history will have context and RESH SEARCH app will get more useful. - -![resh search app](img/screen-resh-cli-v2-7.png) - -Without a query, RESH SEARCH app shows you the latest history based on the current context (host, directory, git). - -![resh search app](img/screen-resh-cli-v2-7-no-query.png) - -RESH SEARCH app replaces the standard reverse search - launch it using Ctrl+R. - -Enable/disable the Ctrl+R keybinding: - -```sh -reshctl enable ctrl_r_binding -reshctl disable ctrl_r_binding -``` +RESH search app screenshot ### In-app key bindings -- Type to search/filter -- Up/Down or Ctrl+P/Ctrl+N to select results -- Right to paste selected command onto the command line so you can edit it before execution -- Enter to execute -- Ctrl+C/Ctrl+D to quit -- Ctrl+G to abort and paste the current query onto the command line -- Ctrl+R to switch between RAW and NORMAL mode - -### View the recorded history - -Resh history is saved to `~/.resh_history.json` - -Each line is a JSON that represents one executed command line. - -This is how I view it `tail -f ~/.resh_history.json | jq` or `jq < ~/.resh_history.json`. - -You can install `jq` using your favourite package manager or you can use other JSON parser to view the history. - -![screenshot](img/screen.png) - -*Recorded metadata will be reduced to only include useful information in the future.* - -## Known issues - -### Q: I use bash on macOS and resh doesn't work - -**A:** You have to add `[ -f ~/.bashrc ] && . ~/.bashrc` to your `~/.bash_profile`. - -**Long Answer:** Under macOS bash shell only loads `~/.bash_profile` because every shell runs as login shell. I will definitely work around this in the future but since this doesn't affect many people I decided to not solve this issue at the moment. - -## Issues and ideas - -Please do create issues if you encounter any problems or if you have a suggestions: https://github.com/curusarn/resh/issues +- Type to search +- Up / Down or Ctrl + P / Ctrl + N to select results +- Enter to execute selected command +- Right to paste selected command onto the command line so you can edit it before execution +- Ctrl + C or Ctrl + D to quit +- Ctrl + G to abort and paste the current query onto the command line +- Ctrl + R to search without context (toggle) -## Uninstallation +## Issues & ideas -You can uninstall this project at any time by running `rm -rf ~/.resh/`. +Find help on [Troubleshooting page ⇗](./troubleshooting.md) -You won't lose any recorded history by removing `~/.resh` directory because history is saved in `~/.resh_history.json`. +Problem persists? [Create an issue ⇗](https://github.com/curusarn/resh/issues) diff --git a/cmd/cli/main.go b/cmd/cli/main.go index a58ff107..222f738d 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -4,10 +4,8 @@ import ( "bytes" "encoding/json" "errors" - "flag" "fmt" - "io/ioutil" - "log" + "io" "net/http" "os" "sort" @@ -15,84 +13,81 @@ import ( "sync" "time" - "github.com/BurntSushi/toml" "github.com/awesome-gocui/gocui" - "github.com/curusarn/resh/pkg/cfg" - "github.com/curusarn/resh/pkg/msg" - "github.com/curusarn/resh/pkg/records" - "github.com/curusarn/resh/pkg/searchapp" + "github.com/curusarn/resh/internal/cfg" + "github.com/curusarn/resh/internal/datadir" + "github.com/curusarn/resh/internal/device" + "github.com/curusarn/resh/internal/logger" + "github.com/curusarn/resh/internal/msg" + "github.com/curusarn/resh/internal/opt" + "github.com/curusarn/resh/internal/output" + "github.com/curusarn/resh/internal/recordint" + "github.com/curusarn/resh/internal/searchapp" + "github.com/spf13/pflag" + "go.uber.org/zap" - "os/user" - "path/filepath" "strconv" ) -// version from git set during build +// info passed during build var version string - -// commit from git set during build var commit string +var development string // special constant recognized by RESH wrappers const exitCodeExecute = 111 -var debug bool - func main() { - output, exitCode := runReshCli() + config, errCfg := cfg.New() + logger, err := logger.New("search-app", config.LogLevel, development) + if err != nil { + fmt.Printf("Error while creating logger: %v", err) + } + defer logger.Sync() // flushes buffer, if any + if errCfg != nil { + logger.Error("Error while getting configuration", zap.Error(errCfg)) + } + out := output.New(logger, "resh-search-app ERROR") + + output, exitCode := runReshCli(out, config) fmt.Print(output) os.Exit(exitCode) } -func runReshCli() (string, int) { - usr, _ := user.Current() - dir := usr.HomeDir - configPath := filepath.Join(dir, "/.config/resh.toml") - logPath := filepath.Join(dir, ".resh/cli.log") - - f, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) - if err != nil { - log.Fatal("Error opening file:", err) - } - defer f.Close() +func runReshCli(out *output.Output, config cfg.Config) (string, int) { + args := opt.HandleVersionOpts(out, os.Args, version, commit) - log.SetOutput(f) + const missing = "" + flags := pflag.NewFlagSet("", pflag.ExitOnError) + sessionID := flags.String("session-id", missing, "Resh generated session ID") + pwd := flags.String("pwd", missing, "$PWD - present working directory") + gitOriginRemote := flags.String("git-remote", missing, "> git remote get-url origin") + query := flags.String("query", "", "Search query") + flags.Parse(args) - var config cfg.Config - if _, err := toml.DecodeFile(configPath, &config); err != nil { - log.Fatal("Error reading config:", err) + // TODO: These errors should tell the user that they should not be running the command directly + errMsg := "Failed to get required command-line arguments" + if *sessionID == missing { + out.FatalE(errMsg, errors.New("missing required option --session-id")) } - if config.Debug { - debug = true - log.SetFlags(log.LstdFlags | log.Lmicroseconds) - log.Println("DEBUG is ON") + if *pwd == missing { + out.FatalE(errMsg, errors.New("missing required option --pwd")) } - - sessionID := flag.String("sessionID", "", "resh generated session id") - host := flag.String("host", "", "host") - pwd := flag.String("pwd", "", "present working directory") - gitOriginRemote := flag.String("gitOriginRemote", "DEFAULT", "git origin remote") - query := flag.String("query", "", "search query") - testHistory := flag.String("test-history", "", "load history from a file instead from the daemon (for testing purposes only!)") - testHistoryLines := flag.Int("test-lines", 0, "the number of lines to load from a file passed with --test-history (for testing purposes only!)") - flag.Parse() - - if *sessionID == "" { - log.Println("Error: you need to specify sessionId") - } - if *host == "" { - log.Println("Error: you need to specify HOST") + if *gitOriginRemote == missing { + out.FatalE(errMsg, errors.New("missing required option --git-origin-remote")) } - if *pwd == "" { - log.Println("Error: you need to specify PWD") + dataDir, err := datadir.GetPath() + if err != nil { + out.FatalE("Could not get user data directory", err) } - if *gitOriginRemote == "DEFAULT" { - log.Println("Error: you need to specify gitOriginRemote") + deviceName, err := device.GetName(dataDir) + if err != nil { + out.FatalE("Could not get device name", err) } g, err := gocui.NewGui(gocui.OutputNormal, false) if err != nil { - log.Panicln(err) + out.FatalE("Failed to launch TUI", err) } defer g.Close() @@ -102,80 +97,79 @@ func runReshCli() (string, int) { g.Highlight = true var resp msg.CliResponse - if *testHistory == "" { - mess := msg.CliMsg{ - SessionID: *sessionID, - PWD: *pwd, - } - resp = SendCliMsg(mess, strconv.Itoa(config.Port)) - } else { - resp = searchapp.LoadHistoryFromFile(*testHistory, *testHistoryLines) + mess := msg.CliMsg{ + SessionID: *sessionID, + PWD: *pwd, } + resp = SendCliMsg(out, mess, strconv.Itoa(config.Port)) st := state{ // lock sync.Mutex - cliRecords: resp.CliRecords, + cliRecords: resp.Records, initialQuery: *query, } + // TODO: Use device ID layout := manager{ + out: out, + config: config, sessionID: *sessionID, - host: *host, + host: deviceName, pwd: *pwd, - gitOriginRemote: records.NormalizeGitRemote(*gitOriginRemote), - config: config, + gitOriginRemote: *gitOriginRemote, s: &st, } g.SetManager(layout) + errMsg = "Failed to set keybindings" if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, layout.Next); err != nil { - log.Panicln(err) + out.FatalE(errMsg, err) } if err := g.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, layout.Next); err != nil { - log.Panicln(err) + out.FatalE(errMsg, err) } if err := g.SetKeybinding("", gocui.KeyCtrlN, gocui.ModNone, layout.Next); err != nil { - log.Panicln(err) + out.FatalE(errMsg, err) } if err := g.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, layout.Prev); err != nil { - log.Panicln(err) + out.FatalE(errMsg, err) } if err := g.SetKeybinding("", gocui.KeyCtrlP, gocui.ModNone, layout.Prev); err != nil { - log.Panicln(err) + out.FatalE(errMsg, err) } if err := g.SetKeybinding("", gocui.KeyArrowRight, gocui.ModNone, layout.SelectPaste); err != nil { - log.Panicln(err) + out.FatalE(errMsg, err) } if err := g.SetKeybinding("", gocui.KeyEnter, gocui.ModNone, layout.SelectExecute); err != nil { - log.Panicln(err) + out.FatalE(errMsg, err) } if err := g.SetKeybinding("", gocui.KeyCtrlG, gocui.ModNone, layout.AbortPaste); err != nil { - log.Panicln(err) + out.FatalE(errMsg, err) } if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { - log.Panicln(err) + out.FatalE(errMsg, err) } if err := g.SetKeybinding("", gocui.KeyCtrlD, gocui.ModNone, quit); err != nil { - log.Panicln(err) + out.FatalE(errMsg, err) } if err := g.SetKeybinding("", gocui.KeyCtrlR, gocui.ModNone, layout.SwitchModes); err != nil { - log.Panicln(err) + out.FatalE(errMsg, err) } layout.UpdateData(*query) layout.UpdateRawData(*query) err = g.MainLoop() if err != nil && !errors.Is(err, gocui.ErrQuit) { - log.Panicln(err) + out.FatalE("Main application loop finished with error", err) } return layout.s.output, layout.s.exitCode } type state struct { lock sync.Mutex - cliRecords []records.CliRecord + cliRecords []recordint.SearchApp data []searchapp.Item rawData []searchapp.RawItem highlightedItem int @@ -190,11 +184,13 @@ type state struct { } type manager struct { + out *output.Output + config cfg.Config + sessionID string host string pwd string gitOriginRemote string - config cfg.Config s *state } @@ -204,13 +200,13 @@ func (m manager) SelectExecute(g *gocui.Gui, v *gocui.View) error { defer m.s.lock.Unlock() if m.s.rawMode { if m.s.highlightedItem < len(m.s.rawData) { - m.s.output = m.s.rawData[m.s.highlightedItem].CmdLine + m.s.output = m.s.rawData[m.s.highlightedItem].CmdLineOut m.s.exitCode = exitCodeExecute return gocui.ErrQuit } } else { if m.s.highlightedItem < len(m.s.data) { - m.s.output = m.s.data[m.s.highlightedItem].CmdLine + m.s.output = m.s.data[m.s.highlightedItem].CmdLineOut m.s.exitCode = exitCodeExecute return gocui.ErrQuit } @@ -223,13 +219,13 @@ func (m manager) SelectPaste(g *gocui.Gui, v *gocui.View) error { defer m.s.lock.Unlock() if m.s.rawMode { if m.s.highlightedItem < len(m.s.rawData) { - m.s.output = m.s.rawData[m.s.highlightedItem].CmdLine + m.s.output = m.s.rawData[m.s.highlightedItem].CmdLineOut m.s.exitCode = 0 // success return gocui.ErrQuit } } else { if m.s.highlightedItem < len(m.s.data) { - m.s.output = m.s.data[m.s.highlightedItem].CmdLine + m.s.output = m.s.data[m.s.highlightedItem].CmdLineOut m.s.exitCode = 0 // success return gocui.ErrQuit } @@ -248,18 +244,13 @@ func (m manager) AbortPaste(g *gocui.Gui, v *gocui.View) error { return nil } -type dedupRecord struct { - dataIndex int - score float32 -} - func (m manager) UpdateData(input string) { - if debug { - log.Println("EDIT start") - log.Println("len(fullRecords) =", len(m.s.cliRecords)) - log.Println("len(data) =", len(m.s.data)) - } - query := searchapp.NewQueryFromString(input, m.host, m.pwd, m.gitOriginRemote, m.config.Debug) + sugar := m.out.Logger.Sugar() + sugar.Debugw("Starting data update ...", + "recordCount", len(m.s.cliRecords), + "itemCount", len(m.s.data), + ) + query := searchapp.NewQueryFromString(sugar, input, m.host, m.pwd, m.gitOriginRemote, m.config.Debug) var data []searchapp.Item itemSet := make(map[string]int) m.s.lock.Lock() @@ -268,7 +259,7 @@ func (m manager) UpdateData(input string) { itm, err := searchapp.NewItemFromRecordForQuery(rec, query, m.config.Debug) if err != nil { // records didn't match the query - // log.Println(" * continue (no match)", rec.Pwd) + // sugar.Println(" * continue (no match)", rec.Pwd) continue } if idx, ok := itemSet[itm.Key]; ok { @@ -285,9 +276,9 @@ func (m manager) UpdateData(input string) { itemSet[itm.Key] = len(data) data = append(data, itm) } - if debug { - log.Println("len(tmpdata) =", len(data)) - } + sugar.Debugw("Got new items from records for query, sorting items ...", + "itemCount", len(data), + ) sort.SliceStable(data, func(p, q int) bool { return data[p].Score > data[q].Score }) @@ -299,19 +290,18 @@ func (m manager) UpdateData(input string) { m.s.data = append(m.s.data, itm) } m.s.highlightedItem = 0 - if debug { - log.Println("len(fullRecords) =", len(m.s.cliRecords)) - log.Println("len(data) =", len(m.s.data)) - log.Println("EDIT end") - } + sugar.Debugw("Done with data update", + "recordCount", len(m.s.cliRecords), + "itemCount", len(m.s.data), + ) } func (m manager) UpdateRawData(input string) { - if m.config.Debug { - log.Println("EDIT start") - log.Println("len(fullRecords) =", len(m.s.cliRecords)) - log.Println("len(data) =", len(m.s.data)) - } + sugar := m.out.Logger.Sugar() + sugar.Debugw("Starting RAW data update ...", + "recordCount", len(m.s.cliRecords), + "itemCount", len(m.s.data), + ) query := searchapp.GetRawTermsFromString(input, m.config.Debug) var data []searchapp.RawItem itemSet := make(map[string]bool) @@ -321,20 +311,20 @@ func (m manager) UpdateRawData(input string) { itm, err := searchapp.NewRawItemFromRecordForQuery(rec, query, m.config.Debug) if err != nil { // records didn't match the query - // log.Println(" * continue (no match)", rec.Pwd) + // sugar.Println(" * continue (no match)", rec.Pwd) continue } if itemSet[itm.Key] { - // log.Println(" * continue (already present)", itm.key(), itm.pwd) + // sugar.Println(" * continue (already present)", itm.key(), itm.pwd) continue } itemSet[itm.Key] = true data = append(data, itm) - // log.Println("DATA =", itm.display) - } - if debug { - log.Println("len(tmpdata) =", len(data)) + // sugar.Println("DATA =", itm.display) } + sugar.Debugw("Got new RAW items from records for query, sorting items ...", + "itemCount", len(data), + ) sort.SliceStable(data, func(p, q int) bool { return data[p].Score > data[q].Score }) @@ -346,11 +336,10 @@ func (m manager) UpdateRawData(input string) { m.s.rawData = append(m.s.rawData, itm) } m.s.highlightedItem = 0 - if debug { - log.Println("len(fullRecords) =", len(m.s.cliRecords)) - log.Println("len(data) =", len(m.s.data)) - log.Println("EDIT end") - } + sugar.Debugw("Done with RAW data update", + "recordCount", len(m.s.cliRecords), + "itemCount", len(m.s.data), + ) } func (m manager) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) { gocui.DefaultEditor.Edit(v, key, ch, mod) @@ -398,7 +387,7 @@ func (m manager) Layout(g *gocui.Gui) error { v, err := g.SetView("input", 0, 0, maxX-1, 2, b) if err != nil && !errors.Is(err, gocui.ErrUnknownView) { - log.Panicln(err.Error()) + m.out.FatalE("Failed to set view 'input'", err) } v.Editable = true @@ -421,7 +410,7 @@ func (m manager) Layout(g *gocui.Gui) error { v, err = g.SetView("body", 0, 2, maxX-1, maxY, b) if err != nil && !errors.Is(err, gocui.ErrUnknownView) { - log.Panicln(err.Error()) + m.out.FatalE("Failed to set view 'body'", err) } v.Frame = false v.Autoscroll = false @@ -438,13 +427,14 @@ func quit(g *gocui.Gui, v *gocui.View) error { return gocui.ErrQuit } -const smallTerminalTresholdWidth = 110 +const smallTerminalThresholdWidth = 110 func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error { + sugar := m.out.Logger.Sugar() maxX, maxY := g.Size() compactRenderingMode := false - if maxX < smallTerminalTresholdWidth { + if maxX < smallTerminalThresholdWidth { compactRenderingMode = true } @@ -459,7 +449,7 @@ func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error { if i == maxY { break } - ic := itm.DrawItemColumns(compactRenderingMode, debug) + ic := itm.DrawItemColumns(compactRenderingMode, m.config.Debug) data = append(data, ic) if i > maxPossibleMainViewHeight { // do not stretch columns because of results that will end up outside of the page @@ -505,7 +495,7 @@ func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error { // header // header := getHeader() // error is expected for header - dispStr, _, _ := header.ProduceLine(longestDateLen, longestLocationLen, longestFlagsLen, true, true, debug) + dispStr, _, _ := header.ProduceLine(longestDateLen, longestLocationLen, longestFlagsLen, true, true, m.config.Debug) dispStr = searchapp.DoHighlightHeader(dispStr, maxX*2) v.WriteString(dispStr + "\n") @@ -513,33 +503,24 @@ func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error { for index < len(data) { itm := data[index] if index >= mainViewHeight { - if debug { - log.Printf("Finished drawing page. mainViewHeight: %v, predictedMax: %v\n", - mainViewHeight, maxPossibleMainViewHeight) - } + sugar.Debugw("Reached bottom of the page while producing lines", + "mainViewHeight", mainViewHeight, + "predictedMaxViewHeight", maxPossibleMainViewHeight, + ) // page is full break } - displayStr, _, err := itm.ProduceLine(longestDateLen, longestLocationLen, longestFlagsLen, false, true, debug) + displayStr, _, err := itm.ProduceLine(longestDateLen, longestLocationLen, longestFlagsLen, false, true, m.config.Debug) if err != nil { - log.Printf("produceLine error: %v\n", err) + sugar.Error("Error while drawing item", zap.Error(err)) } if m.s.highlightedItem == index { // maxX * 2 because there are escape sequences that make it hard to tell the real string length displayStr = searchapp.DoHighlightString(displayStr, maxX*3) - if debug { - log.Println("### HightlightedItem string :", displayStr) - } - } else if debug { - log.Println(displayStr) } if strings.Contains(displayStr, "\n") { - log.Println("display string contained \\n") displayStr = strings.ReplaceAll(displayStr, "\n", "#") - if debug { - log.Println("display string contained \\n") - } } v.WriteString(displayStr + "\n") index++ @@ -553,59 +534,46 @@ func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error { v.WriteString(line) } v.WriteString(helpLine) - if debug { - log.Println("len(data) =", len(m.s.data)) - log.Println("highlightedItem =", m.s.highlightedItem) - } + sugar.Debugw("Done drawing page", + "itemCount", len(m.s.data), + "highlightedItemIndex", m.s.highlightedItem, + ) return nil } func (m manager) rawMode(g *gocui.Gui, v *gocui.View) error { + sugar := m.out.Logger.Sugar() maxX, maxY := g.Size() topBoxSize := 3 m.s.displayedItemsCount = maxY - topBoxSize for i, itm := range m.s.rawData { if i == maxY { - if debug { - log.Println(maxY) - } break } displayStr := itm.CmdLineWithColor if m.s.highlightedItem == i { - // use actual min requried length instead of 420 constant + // Use actual min required length instead of 420 constant displayStr = searchapp.DoHighlightString(displayStr, maxX*2) - if debug { - log.Println("### HightlightedItem string :", displayStr) - } - } else if debug { - log.Println(displayStr) } if strings.Contains(displayStr, "\n") { - log.Println("display string contained \\n") displayStr = strings.ReplaceAll(displayStr, "\n", "#") - if debug { - log.Println("display string contained \\n") - } } v.WriteString(displayStr + "\n") - // if m.s.highlightedItem == i { - // v.SetHighlight(m.s.highlightedItem, true) - // } - } - if debug { - log.Println("len(data) =", len(m.s.data)) - log.Println("highlightedItem =", m.s.highlightedItem) } + sugar.Debugw("Done drawing page in RAW mode", + "itemCount", len(m.s.data), + "highlightedItemIndex", m.s.highlightedItem, + ) return nil } // SendCliMsg to daemon -func SendCliMsg(m msg.CliMsg, port string) msg.CliResponse { +func SendCliMsg(out *output.Output, m msg.CliMsg, port string) msg.CliResponse { + sugar := out.Logger.Sugar() recJSON, err := json.Marshal(m) if err != nil { - log.Fatalf("Failed to marshal message: %v\n", err) + out.FatalE("Failed to marshal message", err) } req, err := http.NewRequest( @@ -613,7 +581,7 @@ func SendCliMsg(m msg.CliMsg, port string) msg.CliResponse { "http://localhost:"+port+"/dump", bytes.NewBuffer(recJSON)) if err != nil { - log.Fatalf("Failed to build request: %v\n", err) + out.FatalE("Failed to build request", err) } req.Header.Set("Content-Type", "application/json") @@ -622,22 +590,22 @@ func SendCliMsg(m msg.CliMsg, port string) msg.CliResponse { } resp, err := client.Do(req) if err != nil { - log.Fatal("resh-daemon is not running - try restarting this terminal") + out.FatalDaemonNotRunning(err) } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { - log.Fatalf("Read response error: %v\n", err) + out.FatalE("Failed read response", err) } - // log.Println(string(body)) + // sugar.Println(string(body)) response := msg.CliResponse{} err = json.Unmarshal(body, &response) if err != nil { - log.Fatalf("Unmarshal resp error: %v\n", err) - } - if debug { - log.Printf("Recieved %d records from daemon\n", len(response.CliRecords)) + out.FatalE("Failed decode response", err) } + sugar.Debugw("Received records from daemon", + "recordCount", len(response.Records), + ) return response } diff --git a/cmd/collect/main.go b/cmd/collect/main.go index d2d2815f..cd8bb2f3 100644 --- a/cmd/collect/main.go +++ b/cmd/collect/main.go @@ -1,255 +1,91 @@ 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" + "github.com/curusarn/resh/internal/cfg" + "github.com/curusarn/resh/internal/collect" + "github.com/curusarn/resh/internal/logger" + "github.com/curusarn/resh/internal/opt" + "github.com/curusarn/resh/internal/output" + "github.com/curusarn/resh/internal/recordint" + "github.com/curusarn/resh/record" + "github.com/spf13/pflag" + "go.uber.org/zap" - // "os/exec" - "os/user" "path/filepath" "strconv" ) -// version tag from git set during build +// info passed during build var version string - -// Commit hash from git set during build var commit string +var development 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) - } - // 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") - recordID := flag.String("recordId", "", "resh generated record id") - - // recall metadata - recallActions := flag.String("recall-actions", "", "recall actions that took place before executing the command") - recallStrategy := flag.String("recall-strategy", "", "recall strategy used during recall actions") - recallLastCmdLine := flag.String("recall-last-cmdline", "", "last recalled cmdline") - - // 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") - // path := flag.String("path", "", "$PATH") - pwd := flag.String("pwd", "", "$PWD - present working directory") - 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") - gitCdup := flag.String("gitCdup", "", "git rev-parse --show-cdup") - gitRemote := flag.String("gitRemote", "", "git remote get-url origin") - - gitCdupExitCode := flag.Int("gitCdupExitCode", -1, "... $?") - gitRemoteExitCode := flag.Int("gitRemoteExitCode", -1, "... $?") - - // 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(commit) - 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 != commit { - fmt.Println("Please restart/reload this terminal session " + - "(resh revision: " + commit + - "; resh revision of this terminal session: " + *requireRevision + - ")") - os.Exit(3) - } - if *recallPrefix != "" && *recall == false { - log.Println("Option '--prefix-search' only works with '--recall' option - exiting!") - os.Exit(4) - } - - realtimeBefore, err := strconv.ParseFloat(*rtb, 64) + config, errCfg := cfg.New() + logger, err := logger.New("collect", config.LogLevel, development) if err != nil { - log.Fatal("Flag Parsing error (rtb):", err) + fmt.Printf("Error while creating logger: %v", err) } - realtimeSessionStart, err := strconv.ParseFloat(*rtsess, 64) - if err != nil { - log.Fatal("Flag Parsing error (rt sess):", err) + defer logger.Sync() // flushes buffer, if any + if errCfg != nil { + logger.Error("Error while getting configuration", zap.Error(errCfg)) } - realtimeSessSinceBoot, err := strconv.ParseFloat(*rtsessboot, 64) + out := output.New(logger, "resh-collect ERROR") + + args := opt.HandleVersionOpts(out, os.Args, version, commit) + + flags := pflag.NewFlagSet("", pflag.ExitOnError) + cmdLine := flags.String("cmd-line", "", "Command line") + gitRemote := flags.String("git-remote", "", "> git remote get-url origin") + home := flags.String("home", "", "$HOME") + pwd := flags.String("pwd", "", "$PWD - present working directory") + recordID := flags.String("record-id", "", "Resh generated record ID") + sessionID := flags.String("session-id", "", "Resh generated session ID") + sessionPID := flags.Int("session-pid", -1, "$$ - Shell session PID") + shell := flags.String("shell", "", "Current shell") + shlvl := flags.Int("shlvl", -1, "$SHLVL") + timeStr := flags.String("time", "-1", "$EPOCHREALTIME") + flags.Parse(args) + + time, err := strconv.ParseFloat(*timeStr, 64) if err != nil { - log.Fatal("Flag Parsing error (rt sess boot):", err) + out.FatalE("Error while parsing flag --time", err) } - realtimeSinceSessionStart := realtimeBefore - realtimeSessionStart - realtimeSinceBoot := realtimeSessSinceBoot + realtimeSinceSessionStart - - timezoneBeforeOffset := collect.GetTimezoneOffsetInSeconds(*timezoneBefore) - realtimeBeforeLocal := realtimeBefore + timezoneBeforeOffset realPwd, err := filepath.EvalSymlinks(*pwd) if err != nil { - log.Println("err while handling pwd realpath:", err) + out.ErrorE("Error while evaluating symlinks in PWD", err) realPwd = "" } - gitDir, gitRealDir := collect.GetGitDirs(*gitCdup, *gitCdupExitCode, *pwd) - if *gitRemoteExitCode != 0 { - *gitRemote = "" - } + rec := recordint.Collect{ + SessionID: *sessionID, + Shlvl: *shlvl, + SessionPID: *sessionPID, + + Shell: *shell, + + Rec: record.V1{ + SessionID: *sessionID, + RecordID: *recordID, + + CmdLine: *cmdLine, - // if *osReleaseID == "" { - // *osReleaseID = "linux" - // } - // if *osReleaseName == "" { - // *osReleaseName = "Linux" - // } - // if *osReleasePrettyName == "" { - // *osReleasePrettyName = "Linux" - // } - - if *recall { - rec := records.SlimRecord{ - SessionID: *sessionID, - RecallHistno: *recallHistno, - RecallPrefix: *recallPrefix, - } - str, found := collect.SendRecallRequest(rec, strconv.Itoa(config.Port)) - if found == false { - os.Exit(1) - } - fmt.Println(str) - } else { - rec := records.Record{ // posix - Cols: *cols, - Lines: *lines, - // core - BaseRecord: records.BaseRecord{ - RecallHistno: *recallHistno, - - CmdLine: *cmdLine, - ExitCode: *exitCode, - Shell: *shell, - Uname: *uname, - SessionID: *sessionID, - RecordID: *recordID, - - // posix - Home: *home, - Lang: *lang, - LcAll: *lcAll, - Login: *login, - // Path: *path, - Pwd: *pwd, - ShellEnv: *shellEnv, - Term: *term, - - // non-posix - RealPwd: realPwd, - Pid: *pid, - SessionPID: *sessionPid, - Host: *host, - Hosttype: *hosttype, - Ostype: *ostype, - Machtype: *machtype, - Shlvl: *shlvl, - - // before after - TimezoneBefore: *timezoneBefore, - - RealtimeBefore: realtimeBefore, - RealtimeBeforeLocal: realtimeBeforeLocal, - - RealtimeSinceSessionStart: realtimeSinceSessionStart, - RealtimeSinceBoot: realtimeSinceBoot, - - GitDir: gitDir, - GitRealDir: gitRealDir, - GitOriginRemote: *gitRemote, - MachineID: collect.ReadFileContent(machineIDPath), - - OsReleaseID: *osReleaseID, - OsReleaseVersionID: *osReleaseVersionID, - OsReleaseIDLike: *osReleaseIDLike, - OsReleaseName: *osReleaseName, - OsReleasePrettyName: *osReleasePrettyName, - - PartOne: true, - - ReshUUID: collect.ReadFileContent(reshUUIDPath), - ReshVersion: version, - ReshRevision: commit, - - RecallActionsRaw: *recallActions, - RecallPrefix: *recallPrefix, - RecallStrategy: *recallStrategy, - RecallLastCmdLine: *recallLastCmdLine, - }, - } - collect.SendRecord(rec, strconv.Itoa(config.Port), "/record") + Home: *home, + Pwd: *pwd, + RealPwd: realPwd, + + GitOriginRemote: *gitRemote, + + Time: fmt.Sprintf("%.4f", time), + + PartOne: true, + PartsNotMerged: true, + }, } + collect.SendRecord(out, rec, strconv.Itoa(config.Port), "/record") } diff --git a/cmd/config/main.go b/cmd/config/main.go index e67ee7b9..e397901a 100644 --- a/cmd/config/main.go +++ b/cmd/config/main.go @@ -4,24 +4,24 @@ import ( "flag" "fmt" "os" - "os/user" - "path/filepath" "strings" - "github.com/BurntSushi/toml" - "github.com/curusarn/resh/pkg/cfg" + "github.com/curusarn/resh/internal/cfg" + "github.com/curusarn/resh/internal/logger" + "go.uber.org/zap" ) +// info passed during build +var version string +var commit string +var development string + func main() { - usr, _ := user.Current() - dir := usr.HomeDir - configPath := filepath.Join(dir, ".config/resh.toml") - - var config cfg.Config - _, err := toml.DecodeFile(configPath, &config) - if err != nil { - fmt.Println("Error reading config", err) - os.Exit(1) + config, errCfg := cfg.New() + logger, _ := logger.New("config", config.LogLevel, development) + defer logger.Sync() // flushes buffer, if any + if errCfg != nil { + logger.Error("Error while getting configuration", zap.Error(errCfg)) } configKey := flag.String("key", "", "Key of the requested config entry") @@ -39,9 +39,9 @@ func main() { case "port": fmt.Println(config.Port) case "sesswatchperiodseconds": - fmt.Println(config.SesswatchPeriodSeconds) + fmt.Println(config.SessionWatchPeriodSeconds) case "sesshistinithistorysize": - fmt.Println(config.SesshistInitHistorySize) + fmt.Println(config.ReshHistoryMinSize) default: fmt.Println("Error: illegal --key!") os.Exit(1) diff --git a/cmd/control/cmd/completion.go b/cmd/control/cmd/completion.go deleted file mode 100644 index e80fa56d..00000000 --- a/cmd/control/cmd/completion.go +++ /dev/null @@ -1,48 +0,0 @@ -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: "generate bash/zsh completion scripts", - Long: `To load completion run - -. <(reshctl completion bash) - -OR - -. <(reshctl completion zsh) && compdef _reshctl reshctl -`, -} - -var completionBashCmd = &cobra.Command{ - Use: "bash", - Short: "generate 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: "generate zsh completion scripts", - Long: `To load completion run - -. <(reshctl completion zsh) && compdef _reshctl reshctl -`, - Run: func(cmd *cobra.Command, args []string) { - rootCmd.GenZshCompletion(os.Stdout) - exitCode = status.Success - }, -} diff --git a/cmd/control/cmd/debug.go b/cmd/control/cmd/debug.go deleted file mode 100644 index f1401c7a..00000000 --- a/cmd/control/cmd/debug.go +++ /dev/null @@ -1,66 +0,0 @@ -package cmd - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - - "github.com/curusarn/resh/cmd/control/status" - "github.com/spf13/cobra" -) - -var debugCmd = &cobra.Command{ - Use: "debug", - Short: "debug utils for resh", - Long: "Reloads resh rc files. Shows logs and output from last runs of resh", -} - -var debugReloadCmd = &cobra.Command{ - Use: "reload", - Short: "reload resh rc files", - Long: "Reload resh rc files", - Run: func(cmd *cobra.Command, args []string) { - exitCode = status.ReloadRcFiles - }, -} - -var debugInspectCmd = &cobra.Command{ - Use: "inspect", - Short: "inspect session history", - Run: func(cmd *cobra.Command, args []string) { - exitCode = status.InspectSessionHistory - }, -} - -var debugOutputCmd = &cobra.Command{ - Use: "output", - Short: "shows output from last runs of resh", - 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", - "cli_last_run_out.txt", - } - dir := os.Getenv("__RESH_XDG_CACHE_HOME") - for _, fpath := range files { - fpath := filepath.Join(dir, 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/doctor.go b/cmd/control/cmd/doctor.go new file mode 100644 index 00000000..f7400c99 --- /dev/null +++ b/cmd/control/cmd/doctor.go @@ -0,0 +1,147 @@ +package cmd + +import ( + "fmt" + "os" + "os/exec" + "time" + + "github.com/curusarn/resh/internal/cfg" + "github.com/curusarn/resh/internal/check" + "github.com/curusarn/resh/internal/msg" + "github.com/curusarn/resh/internal/status" + "github.com/spf13/cobra" + "go.uber.org/zap" +) + +func doctorCmdFunc(config cfg.Config) func(*cobra.Command, []string) { + return func(cmd *cobra.Command, args []string) { + allOK := true + if !checkDaemon(config) { + allOK = false + printDivider() + } + if !checkShellSession() { + allOK = false + printDivider() + } + if !checkShells() { + allOK = false + printDivider() + } + + if allOK { + out.Info("Everything looks good.") + } + } +} + +func printDivider() { + fmt.Printf("\n") +} + +var msgFailedDaemonStart = `Failed to start RESH daemon. + -> Start RESH daemon manually - run: resh-daemon-start + -> Or restart this terminal window to bring RESH daemon back up + -> You can check logs: ~/.local/share/resh/log.json (or ~/$XDG_DATA_HOME/resh/log.json) + -> You can create an issue at: https://github.com/curusarn/resh/issues +` + +func checkDaemon(config cfg.Config) bool { + ok := true + resp, err := status.GetDaemonStatus(config.Port) + if err != nil { + out.InfoE("RESH Daemon is not running", err) + out.Info("Attempting to start RESH daemon ...") + resp, err = startDaemon(config.Port, 5, 200*time.Millisecond) + if err != nil { + out.InfoE(msgFailedDaemonStart, err) + return false + } + ok = false + out.Info("Successfully started daemon.") + } + if version != resp.Version { + out.InfoDaemonVersionMismatch(version, resp.Version) + return false + } + return ok +} + +func startDaemon(port int, maxRetries int, backoff time.Duration) (*msg.StatusResponse, error) { + err := exec.Command("resh-daemon-start").Run() + if err != nil { + return nil, err + } + var resp *msg.StatusResponse + retry := 0 + for { + time.Sleep(backoff) + resp, err = status.GetDaemonStatus(port) + if err == nil { + break + } + if retry == maxRetries { + return nil, err + } + out.Logger.Error("Failed to get daemon status - retrying", zap.Error(err), zap.Int("retry", retry)) + retry++ + continue + } + return resp, nil +} + +var msgShellFilesNotLoaded = `RESH shell files were not properly loaded in this terminal + -> Try restarting this terminal to see if the issue persists + -> Check your shell rc files (e.g. .zshrc, .bashrc, ...) + -> You can create an issue at: https://github.com/curusarn/resh/issues +` + +func checkShellSession() bool { + versionEnv, found := os.LookupEnv("__RESH_VERSION") + if !found { + out.Info(msgShellFilesNotLoaded) + return false + } + if version != versionEnv { + out.InfoTerminalVersionMismatch(version, versionEnv) + return false + } + return true +} + +func checkShells() bool { + allOK := true + + msg, err := check.LoginShell() + if err != nil { + out.InfoE("Failed to get login shell", err) + allOK = false + } + if msg != "" { + out.Info(msg) + allOK = false + } + + msg, err = check.ZshVersion() + if err != nil { + out.InfoE("Failed to check zsh version", err) + allOK = false + } + if msg != "" { + out.Info(msg) + allOK = false + } + + msg, err = check.BashVersion() + if err != nil { + out.InfoE("Failed to check bash version", err) + allOK = false + } + if msg != "" { + out.Info(msg) + allOK = false + } + + return allOK +} diff --git a/cmd/control/cmd/enable.go b/cmd/control/cmd/enable.go deleted file mode 100644 index 360be5bd..00000000 --- a/cmd/control/cmd/enable.go +++ /dev/null @@ -1,80 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "os/user" - "path/filepath" - - "github.com/BurntSushi/toml" - "github.com/curusarn/resh/cmd/control/status" - "github.com/curusarn/resh/pkg/cfg" - "github.com/spf13/cobra" -) - -// Enable commands - -var enableCmd = &cobra.Command{ - Use: "enable", - Short: "enable RESH features (bindings)", -} - -var enableControlRBindingCmd = &cobra.Command{ - Use: "ctrl_r_binding", - Short: "enable RESH-CLI binding for Ctrl+R", - Run: func(cmd *cobra.Command, args []string) { - exitCode = enableDisableControlRBindingGlobally(true) - if exitCode == status.Success { - exitCode = status.EnableControlRBinding - } - }, -} - -// Disable commands - -var disableCmd = &cobra.Command{ - Use: "disable", - Short: "disable RESH features (bindings)", -} - -var disableControlRBindingCmd = &cobra.Command{ - Use: "ctrl_r_binding", - Short: "disable RESH-CLI binding for Ctrl+R", - Run: func(cmd *cobra.Command, args []string) { - exitCode = enableDisableControlRBindingGlobally(false) - if exitCode == status.Success { - exitCode = status.DisableControlRBinding - } - }, -} - -func enableDisableControlRBindingGlobally(value bool) status.Code { - usr, _ := user.Current() - dir := usr.HomeDir - configPath := filepath.Join(dir, ".config/resh.toml") - var config cfg.Config - if _, err := toml.DecodeFile(configPath, &config); err != nil { - fmt.Println("Error reading config", err) - return status.Fail - } - if config.BindControlR != value { - config.BindControlR = value - - f, err := os.Create(configPath) - if err != nil { - fmt.Println("Error: Failed to create/open file:", configPath, "; error:", err) - return status.Fail - } - defer f.Close() - if err := toml.NewEncoder(f).Encode(config); err != nil { - fmt.Println("Error: Failed to encode and write the config values to hdd. error:", err) - return status.Fail - } - } - if value { - fmt.Println("RESH SEARCH app Ctrl+R binding: ENABLED") - } else { - fmt.Println("RESH SEARCH app Ctrl+R binding: DISABLED") - } - return status.Success -} diff --git a/cmd/control/cmd/root.go b/cmd/control/cmd/root.go index 35c770d2..cf567c81 100644 --- a/cmd/control/cmd/root.go +++ b/cmd/control/cmd/root.go @@ -2,70 +2,58 @@ package cmd import ( "fmt" - "log" - "os/user" - "path/filepath" - "github.com/BurntSushi/toml" - "github.com/curusarn/resh/cmd/control/status" - "github.com/curusarn/resh/pkg/cfg" + "github.com/curusarn/resh/internal/cfg" + "github.com/curusarn/resh/internal/logger" + "github.com/curusarn/resh/internal/output" "github.com/spf13/cobra" ) -// globals -var exitCode status.Code var version string var commit string -var debug = false -var config cfg.Config + +// globals +var out *output.Output var rootCmd = &cobra.Command{ Use: "reshctl", - Short: "Reshctl (RESH control) - check status, update, enable/disable features, sanitize history and more.", + Short: "Reshctl (RESH control) - check status, update", } // Execute reshctl -func Execute(ver, com string) status.Code { +func Execute(ver, com, development string) { version = ver commit = com - usr, _ := user.Current() - dir := usr.HomeDir - configPath := filepath.Join(dir, ".config/resh.toml") - if _, err := toml.DecodeFile(configPath, &config); err != nil { - log.Println("Error reading config", err) - return status.Fail + config, errCfg := cfg.New() + logger, err := logger.New("reshctl", config.LogLevel, development) + if err != nil { + fmt.Printf("Error while creating logger: %v", err) } - if config.Debug { - debug = true - // log.SetFlags(log.LstdFlags | log.Lmicroseconds) + defer logger.Sync() // flushes buffer, if any + out = output.New(logger, "ERROR") + if errCfg != nil { + out.ErrorE("Error while getting configuration", errCfg) } - rootCmd.AddCommand(enableCmd) - enableCmd.AddCommand(enableControlRBindingCmd) - - rootCmd.AddCommand(disableCmd) - disableCmd.AddCommand(disableControlRBindingCmd) - - rootCmd.AddCommand(completionCmd) - completionCmd.AddCommand(completionBashCmd) - completionCmd.AddCommand(completionZshCmd) - - rootCmd.AddCommand(debugCmd) - debugCmd.AddCommand(debugReloadCmd) - debugCmd.AddCommand(debugInspectCmd) - debugCmd.AddCommand(debugOutputCmd) + var versionCmd = cobra.Command{ + Use: "version", + Short: "show RESH version", + Run: versionCmdFunc(config), + } + rootCmd.AddCommand(&versionCmd) - rootCmd.AddCommand(statusCmd) + doctorCmd := cobra.Command{ + Use: "doctor", + Short: "check common problems", + Run: doctorCmdFunc(config), + } + rootCmd.AddCommand(&doctorCmd) updateCmd.Flags().BoolVar(&betaFlag, "beta", false, "Update to latest version even if it's beta.") rootCmd.AddCommand(updateCmd) - rootCmd.AddCommand(sanitizeCmd) - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - return status.Fail + out.FatalE("Command ended with error", err) } - return exitCode } diff --git a/cmd/control/cmd/sanitize.go b/cmd/control/cmd/sanitize.go deleted file mode 100644 index 08f29da3..00000000 --- a/cmd/control/cmd/sanitize.go +++ /dev/null @@ -1,54 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "os/exec" - "os/user" - - "github.com/curusarn/resh/cmd/control/status" - "github.com/spf13/cobra" -) - -var sanitizeCmd = &cobra.Command{ - Use: "sanitize", - Short: "produce a sanitized version of your RESH history", - Run: func(cmd *cobra.Command, args []string) { - exitCode = status.Success - usr, _ := user.Current() - dir := usr.HomeDir - - fmt.Println() - fmt.Println(" HOW IT WORKS") - fmt.Println(" In sanitized history, all sensitive information is replaced with its SHA256 hashes.") - fmt.Println() - fmt.Println("Creating sanitized history files ...") - fmt.Println(" * ~/resh_history_sanitized.json (full lengh hashes)") - execCmd := exec.Command("resh-sanitize", "-trim-hashes", "0", "--output", dir+"/resh_history_sanitized.json") - execCmd.Stdout = os.Stdout - execCmd.Stderr = os.Stderr - err := execCmd.Run() - if err != nil { - exitCode = status.Fail - } - - fmt.Println(" * ~/resh_history_sanitized_trim12.json (12 char hashes)") - execCmd = exec.Command("resh-sanitize", "-trim-hashes", "12", "--output", dir+"/resh_history_sanitized_trim12.json") - execCmd.Stdout = os.Stdout - execCmd.Stderr = os.Stderr - err = execCmd.Run() - if err != nil { - exitCode = status.Fail - } - fmt.Println() - fmt.Println("Please direct all questions and/or issues to: https://github.com/curusarn/resh/issues") - fmt.Println() - fmt.Println("Please look at the resulting sanitized history using commands below.") - fmt.Println(" * Pretty print JSON") - fmt.Println(" cat ~/resh_history_sanitized_trim12.json | jq") - fmt.Println() - fmt.Println(" * Only show commands, don't show metadata") - fmt.Println(" cat ~/resh_history_sanitized_trim12.json | jq '.[\"cmdLine\"]'") - fmt.Println() - }, -} diff --git a/cmd/control/cmd/status.go b/cmd/control/cmd/status.go deleted file mode 100644 index f12b7fa3..00000000 --- a/cmd/control/cmd/status.go +++ /dev/null @@ -1,72 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" - "os" - "strconv" - - "github.com/curusarn/resh/cmd/control/status" - "github.com/curusarn/resh/pkg/msg" - "github.com/spf13/cobra" -) - -var statusCmd = &cobra.Command{ - Use: "status", - Short: "show RESH status", - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("resh " + version) - fmt.Println() - fmt.Println("Resh versions ...") - fmt.Println(" * installed: " + version + " (" + commit + ")") - versionEnv, found := os.LookupEnv("__RESH_VERSION") - if found == false { - versionEnv = "UNKNOWN!" - } - commitEnv, found := os.LookupEnv("__RESH_REVISION") - if found == false { - commitEnv = "unknown" - } - fmt.Println(" * this shell session: " + versionEnv + " (" + commitEnv + ")") - - resp, err := getDaemonStatus(config.Port) - if err != nil { - fmt.Println(" * RESH-DAEMON IS NOT RUNNING") - fmt.Println(" * Please REPORT this here: https://github.com/curusarn/resh/issues") - fmt.Println(" * Please RESTART this terminal window") - exitCode = status.Fail - return - } - fmt.Println(" * daemon: " + resp.Version + " (" + resp.Commit + ")") - - if version != resp.Version || version != versionEnv { - fmt.Println(" * THERE IS A MISMATCH BETWEEN VERSIONS!") - fmt.Println(" * Please REPORT this here: https://github.com/curusarn/resh/issues") - fmt.Println(" * Please RESTART this terminal window") - } - - exitCode = status.ReshStatus - }, -} - -func getDaemonStatus(port int) (msg.StatusResponse, error) { - mess := msg.StatusResponse{} - url := "http://localhost:" + strconv.Itoa(port) + "/status" - resp, err := http.Get(url) - if err != nil { - return mess, err - } - defer resp.Body.Close() - jsn, err := ioutil.ReadAll(resp.Body) - if err != nil { - log.Fatal("Error while reading 'daemon /status' response:", err) - } - err = json.Unmarshal(jsn, &mess) - if err != nil { - log.Fatal("Error while decoding 'daemon /status' response:", err) - } - return mess, nil -} diff --git a/cmd/control/cmd/update.go b/cmd/control/cmd/update.go index dcdb21dd..c74c555b 100644 --- a/cmd/control/cmd/update.go +++ b/cmd/control/cmd/update.go @@ -3,10 +3,8 @@ package cmd import ( "os" "os/exec" - "os/user" "path/filepath" - "github.com/curusarn/resh/cmd/control/status" "github.com/spf13/cobra" ) @@ -15,9 +13,11 @@ var updateCmd = &cobra.Command{ Use: "update", Short: "check for updates and update RESH", Run: func(cmd *cobra.Command, args []string) { - usr, _ := user.Current() - dir := usr.HomeDir - rawinstallPath := filepath.Join(dir, ".resh/rawinstall.sh") + homeDir, err := os.UserHomeDir() + if err != nil { + out.FatalE("Could not get user home dir", err) + } + rawinstallPath := filepath.Join(homeDir, ".resh/rawinstall.sh") execArgs := []string{rawinstallPath} if betaFlag { execArgs = append(execArgs, "--beta") @@ -25,9 +25,9 @@ var updateCmd = &cobra.Command{ execCmd := exec.Command("bash", execArgs...) execCmd.Stdout = os.Stdout execCmd.Stderr = os.Stderr - err := execCmd.Run() - if err == nil { - exitCode = status.Success + err = execCmd.Run() + if err != nil { + out.FatalE("Update ended with error", err) } }, } diff --git a/cmd/control/cmd/version.go b/cmd/control/cmd/version.go new file mode 100644 index 00000000..11f884f7 --- /dev/null +++ b/cmd/control/cmd/version.go @@ -0,0 +1,45 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/curusarn/resh/internal/cfg" + "github.com/curusarn/resh/internal/status" + "github.com/spf13/cobra" +) + +func versionCmdFunc(config cfg.Config) func(*cobra.Command, []string) { + return func(cmd *cobra.Command, args []string) { + + fmt.Printf("Installed: %s\n", version) + + versionEnv := getEnvVarWithDefault("__RESH_VERSION", "") + fmt.Printf("This terminal session: %s\n", version) + + resp, err := status.GetDaemonStatus(config.Port) + if err != nil { + fmt.Printf("Running checks: %s\n", version) + out.ErrorDaemonNotRunning(err) + return + } + fmt.Printf("Currently running daemon: %s\n", resp.Version) + + if version != resp.Version { + out.ErrorDaemonVersionMismatch(version, resp.Version) + return + } + if version != versionEnv { + out.ErrorTerminalVersionMismatch(version, versionEnv) + return + } + } +} + +func getEnvVarWithDefault(varName, defaultValue string) string { + val, found := os.LookupEnv(varName) + if !found { + return defaultValue + } + return val +} diff --git a/cmd/control/main.go b/cmd/control/main.go index ecae4e0a..b431b670 100644 --- a/cmd/control/main.go +++ b/cmd/control/main.go @@ -1,17 +1,13 @@ package main import ( - "os" - "github.com/curusarn/resh/cmd/control/cmd" ) -// version from git set during build var version string - -// commit from git set during build var commit string +var development string func main() { - os.Exit(int(cmd.Execute(version, commit))) + cmd.Execute(version, commit, development) } diff --git a/cmd/control/status/status.go b/cmd/control/status/status.go deleted file mode 100644 index 0d797d79..00000000 --- a/cmd/control/status/status.go +++ /dev/null @@ -1,25 +0,0 @@ -package status - -// Code - exit code of the resh-control command -type Code int - -const ( - // Success exit code - Success Code = 0 - // Fail exit code - Fail = 1 - // EnableResh exit code - tells reshctl() wrapper to enable resh - // EnableResh = 30 - - // EnableControlRBinding exit code - tells reshctl() wrapper to enable control R binding - EnableControlRBinding = 32 - - // DisableControlRBinding exit code - tells reshctl() wrapper to disable control R binding - DisableControlRBinding = 42 - // ReloadRcFiles exit code - tells reshctl() wrapper to reload shellrc resh file - ReloadRcFiles = 50 - // InspectSessionHistory exit code - tells reshctl() wrapper to take current sessionID and send /inspect request to daemon - InspectSessionHistory = 51 - // ReshStatus exit code - tells reshctl() wrapper to show RESH status (aka systemctl status) - ReshStatus = 52 -) diff --git a/cmd/daemon/dump.go b/cmd/daemon/dump.go index 375da76a..d3a2e1fa 100644 --- a/cmd/daemon/dump.go +++ b/cmd/daemon/dump.go @@ -2,53 +2,50 @@ package main import ( "encoding/json" - "io/ioutil" - "log" + "io" "net/http" - "github.com/curusarn/resh/pkg/histfile" - "github.com/curusarn/resh/pkg/msg" + "github.com/curusarn/resh/internal/histfile" + "github.com/curusarn/resh/internal/msg" + "go.uber.org/zap" ) type dumpHandler struct { + sugar *zap.SugaredLogger histfileBox *histfile.Histfile } func (h *dumpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if Debug { - log.Println("/dump START") - log.Println("/dump reading body ...") - } - jsn, err := ioutil.ReadAll(r.Body) + sugar := h.sugar.With(zap.String("endpoint", "/dump")) + sugar.Debugw("Handling request, reading body ...") + jsn, err := io.ReadAll(r.Body) if err != nil { - log.Println("Error reading the body", err) + sugar.Errorw("Error reading body", "error", err) return } + sugar.Debugw("Unmarshaling record ...") mess := msg.CliMsg{} - if Debug { - log.Println("/dump unmarshaling record ...") - } err = json.Unmarshal(jsn, &mess) if err != nil { - log.Println("Decoding error:", err) - log.Println("Payload:", jsn) + sugar.Errorw("Error during unmarshaling", + "error", err, + "payload", jsn, + ) return } - if Debug { - log.Println("/dump dumping ...") - } + sugar.Debugw("Getting records to send ...") fullRecords := h.histfileBox.DumpCliRecords() if err != nil { - log.Println("Dump error:", err) + sugar.Errorw("Error when getting records", "error", err) } - resp := msg.CliResponse{CliRecords: fullRecords.List} + resp := msg.CliResponse{Records: fullRecords.List} jsn, err = json.Marshal(&resp) if err != nil { - log.Println("Encoding error:", err) + sugar.Errorw("Error when marshaling", "error", err) return } w.Write(jsn) - log.Println("/dump END") + sugar.Infow("Request handled") } diff --git a/cmd/daemon/kill.go b/cmd/daemon/kill.go deleted file mode 100644 index 7c403a9a..00000000 --- a/cmd/daemon/kill.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "io/ioutil" - "log" - "os/exec" - "strconv" - "strings" -) - -func killDaemon(pidfile string) error { - dat, err := ioutil.ReadFile(pidfile) - if err != nil { - log.Println("Reading pid file failed", err) - } - log.Print(string(dat)) - pid, err := strconv.Atoi(strings.TrimSuffix(string(dat), "\n")) - if err != nil { - log.Fatal("Pidfile contents are malformed", err) - } - cmd := exec.Command("kill", "-s", "sigint", strconv.Itoa(pid)) - err = cmd.Run() - if err != nil { - log.Printf("Command finished with error: %v", err) - return err - } - return nil -} diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index fc7b5bde..01e9105c 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -1,87 +1,165 @@ package main import ( - //"flag" - - "io/ioutil" - "log" + "fmt" "os" - "os/user" + "os/exec" "path/filepath" "strconv" + "strings" - "github.com/BurntSushi/toml" - "github.com/curusarn/resh/pkg/cfg" + "github.com/curusarn/resh/internal/cfg" + "github.com/curusarn/resh/internal/datadir" + "github.com/curusarn/resh/internal/device" + "github.com/curusarn/resh/internal/logger" + "github.com/curusarn/resh/internal/status" + "go.uber.org/zap" ) -// version from git set during build +// info passed during build var version string - -// commit from git set during build var commit string +var development string + +const helpMsg = `ERROR: resh-daemon doesn't accept any arguments + +WARNING: + You shouldn't typically need to start RESH daemon yourself. + Unless its already running, RESH daemon is started when a new terminal is opened. + RESH daemon will not start if it's already running even when you run it manually. + +USAGE: + $ resh-daemon + Runs the daemon as foreground process. You can kill it with CTRL+C. + + $ resh-daemon-start + Runs the daemon as background process detached from terminal. + +LOGS & DEBUGGING: + Logs are located in: + ${XDG_DATA_HOME}/resh/log.json (if XDG_DATA_HOME is set) + ~/.local/share/resh/log.json (otherwise - more common) + + A good way to see the logs as they are being produced is: + $ tail -f ~/.local/share/resh/log.json -// Debug switch -var Debug = false +MORE INFO: + https://github.com/curusarn/resh/ +` func main() { - log.Println("Daemon starting... \n" + - "version: " + version + - " commit: " + commit) - usr, _ := user.Current() - dir := usr.HomeDir - pidfilePath := filepath.Join(dir, ".resh/resh.pid") - configPath := filepath.Join(dir, ".config/resh.toml") - reshHistoryPath := filepath.Join(dir, ".resh_history.json") - bashHistoryPath := filepath.Join(dir, ".bash_history") - zshHistoryPath := filepath.Join(dir, ".zsh_history") - logPath := filepath.Join(dir, ".resh/daemon.log") - - f, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) + if len(os.Args) > 1 { + fmt.Fprint(os.Stderr, helpMsg) + os.Exit(1) + } + config, errCfg := cfg.New() + logger, err := logger.New("daemon", config.LogLevel, development) if err != nil { - log.Fatalf("Error opening file: %v\n", err) + fmt.Printf("Error while creating logger: %v", err) } - defer f.Close() - - log.SetOutput(f) - log.SetPrefix(strconv.Itoa(os.Getpid()) + " | ") - - var config cfg.Config - if _, err := toml.DecodeFile(configPath, &config); err != nil { - log.Printf("Error reading config: %v\n", err) - return + defer logger.Sync() // flushes buffer, if any + if errCfg != nil { + logger.Error("Error while getting configuration", zap.Error(errCfg)) + } + sugar := logger.Sugar() + d := daemon{sugar: sugar} + sugar.Infow("Daemon starting ...", + "version", version, + "commit", commit, + ) + dataDir, err := datadir.MakePath() + if err != nil { + sugar.Fatalw("Could not get user data directory", zap.Error(err)) + } + homeDir, err := os.UserHomeDir() + if err != nil { + sugar.Fatalw("Could not get user home directory", zap.Error(err)) } - if config.Debug { - Debug = true - log.SetFlags(log.LstdFlags | log.Lmicroseconds) + // TODO: These paths should be probably defined in a package + pidFile := filepath.Join(dataDir, "daemon.pid") + reshHistoryPath := filepath.Join(dataDir, datadir.HistoryFileName) + bashHistoryPath := filepath.Join(homeDir, ".bash_history") + zshHistoryPath := filepath.Join(homeDir, ".zsh_history") + deviceID, err := device.GetID(dataDir) + if err != nil { + sugar.Fatalw("Could not get resh device ID", zap.Error(err)) + } + deviceName, err := device.GetName(dataDir) + if err != nil { + sugar.Fatalw("Could not get resh device name", zap.Error(err)) } - res, err := isDaemonRunning(config.Port) + sugar = sugar.With(zap.Int("daemonPID", os.Getpid())) + + res, err := status.IsDaemonRunning(config.Port) if err != nil { - log.Printf("Error while checking if the daemon is runnnig"+ - " - it's probably not running: %v\n", err) + sugar.Errorw("Error while checking daemon status - it's probably not running", + "error", err) } if res { - log.Println("Daemon is already running - exiting!") + sugar.Errorw("Daemon is already running - exiting!") return } - _, err = os.Stat(pidfilePath) + _, err = os.Stat(pidFile) if err == nil { - log.Println("Pidfile exists") + sugar.Warnw("PID file exists", + "PIDFile", pidFile) // kill daemon - err = killDaemon(pidfilePath) + err = d.killDaemon(pidFile) if err != nil { - log.Printf("Error while killing daemon: %v\n", err) + sugar.Errorw("Could not kill daemon", + "error", err, + ) } } - err = ioutil.WriteFile(pidfilePath, []byte(strconv.Itoa(os.Getpid())), 0644) + err = os.WriteFile(pidFile, []byte(strconv.Itoa(os.Getpid())), 0644) + if err != nil { + sugar.Fatalw("Could not create PID file", + "error", err, + "PIDFile", pidFile, + ) + } + server := Server{ + sugar: sugar, + config: config, + reshHistoryPath: reshHistoryPath, + bashHistoryPath: bashHistoryPath, + zshHistoryPath: zshHistoryPath, + + deviceID: deviceID, + deviceName: deviceName, + } + server.Run() + sugar.Infow("Removing PID file ...", + "PIDFile", pidFile, + ) + err = os.Remove(pidFile) + if err != nil { + sugar.Errorw("Could not delete PID file", "error", err) + } + sugar.Info("Shutting down ...") +} + +type daemon struct { + sugar *zap.SugaredLogger +} + +func (d *daemon) killDaemon(pidFile string) error { + dat, err := os.ReadFile(pidFile) + if err != nil { + d.sugar.Errorw("Reading PID file failed", + "PIDFile", pidFile, + "error", err) + } + d.sugar.Infow("Successfully read PID file", "contents", string(dat)) + pid, err := strconv.Atoi(strings.TrimSuffix(string(dat), "\n")) if err != nil { - log.Fatalf("Could not create pidfile: %v\n", err) + return fmt.Errorf("could not parse PID file contents: %w", err) } - runServer(config, reshHistoryPath, bashHistoryPath, zshHistoryPath) - log.Println("main: Removing pidfile ...") - err = os.Remove(pidfilePath) + d.sugar.Infow("Successfully parsed PID", "PID", pid) + err = exec.Command("kill", "-SIGTERM", fmt.Sprintf("%d", pid)).Run() if err != nil { - log.Printf("Could not delete pidfile: %v\n", err) + return fmt.Errorf("kill command finished with error: %w", err) } - log.Println("main: Shutdown - bye") + return nil } diff --git a/cmd/daemon/recall.go b/cmd/daemon/recall.go deleted file mode 100644 index 930537ca..00000000 --- a/cmd/daemon/recall.go +++ /dev/null @@ -1,109 +0,0 @@ -package main - -import ( - "encoding/json" - "io/ioutil" - "log" - "net/http" - - "github.com/curusarn/resh/pkg/collect" - "github.com/curusarn/resh/pkg/msg" - "github.com/curusarn/resh/pkg/records" - "github.com/curusarn/resh/pkg/sesshist" -) - -type recallHandler struct { - sesshistDispatch *sesshist.Dispatch -} - -func (h *recallHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if Debug { - log.Println("/recall START") - log.Println("/recall reading body ...") - } - jsn, err := ioutil.ReadAll(r.Body) - if err != nil { - log.Println("Error reading the body", err) - return - } - - rec := records.SlimRecord{} - if Debug { - log.Println("/recall unmarshaling record ...") - } - err = json.Unmarshal(jsn, &rec) - if err != nil { - log.Println("Decoding error:", err) - log.Println("Payload:", jsn) - return - } - if Debug { - log.Println("/recall recalling ...") - } - found := true - 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) - found = false - cmd = "" - } - resp := collect.SingleResponse{CmdLine: cmd, Found: found} - if Debug { - log.Println("/recall marshaling response ...") - } - jsn, err = json.Marshal(&resp) - if err != nil { - log.Println("Encoding error:", err) - log.Println("Response:", resp) - return - } - if Debug { - log.Println(string(jsn)) - log.Println("/recall writing response ...") - } - w.Write(jsn) - log.Println("/recall END - sess id:", rec.SessionID, " - histno:", rec.RecallHistno, " -> ", cmd, " (found:", found, ")") -} - -type inspectHandler struct { - sesshistDispatch *sesshist.Dispatch -} - -func (h *inspectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - log.Println("/inspect START") - log.Println("/inspect reading body ...") - jsn, err := ioutil.ReadAll(r.Body) - if err != nil { - log.Println("Error reading the body", err) - return - } - - mess := msg.InspectMsg{} - log.Println("/inspect unmarshaling record ...") - err = json.Unmarshal(jsn, &mess) - if err != nil { - log.Println("Decoding error:", err) - log.Println("Payload:", jsn) - return - } - log.Println("/inspect recalling ...") - cmds, err := h.sesshistDispatch.Inspect(mess.SessionID, int(mess.Count)) - if err != nil { - log.Println("/inspect - sess id:", mess.SessionID, " - count:", mess.Count, " -> ERROR") - log.Println("Inspect error:", err) - return - } - resp := msg.MultiResponse{CmdLines: cmds} - log.Println("/inspect marshaling response ...") - jsn, err = json.Marshal(&resp) - if err != nil { - log.Println("Encoding error:", err) - log.Println("Response:", resp) - return - } - // log.Println(string(jsn)) - log.Println("/inspect writing response ...") - w.Write(jsn) - log.Println("/inspect END - sess id:", mess.SessionID, " - count:", mess.Count) -} diff --git a/cmd/daemon/record.go b/cmd/daemon/record.go index ee403f8f..93a0dd36 100644 --- a/cmd/daemon/record.go +++ b/cmd/daemon/record.go @@ -2,46 +2,64 @@ package main import ( "encoding/json" - "io/ioutil" - "log" + "io" "net/http" - "github.com/curusarn/resh/pkg/records" + "github.com/curusarn/resh/internal/recordint" + "go.uber.org/zap" ) +func NewRecordHandler(sugar *zap.SugaredLogger, subscribers []chan recordint.Collect) recordHandler { + return recordHandler{ + sugar: sugar.With(zap.String("endpoint", "/record")), + subscribers: subscribers, + } +} + type recordHandler struct { - subscribers []chan records.Record + sugar *zap.SugaredLogger + subscribers []chan recordint.Collect + + deviceID string + deviceName string } func (h *recordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + sugar := h.sugar.With(zap.String("endpoint", "/record")) + sugar.Debugw("Handling request, sending response, reading body ...") w.Write([]byte("OK\n")) - jsn, err := ioutil.ReadAll(r.Body) + jsn, err := io.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) + sugar.Errorw("Error reading body", "error", err) return } - record := records.Record{} - err = json.Unmarshal(jsn, &record) + sugar.Debugw("Unmarshaling record ...") + rec := recordint.Collect{} + err = json.Unmarshal(jsn, &rec) if err != nil { - log.Println("Decoding error: ", err) - log.Println("Payload: ", jsn) + sugar.Errorw("Error during unmarshaling", + "error", err, + "payload", jsn, + ) return } - for _, sub := range h.subscribers { - sub <- record - } part := "2" - if record.PartOne { + if rec.Rec.PartOne { part = "1" } - log.Println("/record - ", record.CmdLine, " - part", part) + sugar := sugar.With( + "cmdLine", rec.Rec.CmdLine, + "part", part, + ) + rec.Rec.DeviceID = h.deviceID + rec.Rec.Device = h.deviceName + sugar.Debugw("Got record, sending to subscribers ...") + for _, sub := range h.subscribers { + sub <- rec + } + sugar.Debugw("Record sent to subscribers") }() - - // 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 index fc708259..e7631c41 100644 --- a/cmd/daemon/run-server.go +++ b/cmd/daemon/run-server.go @@ -4,33 +4,40 @@ import ( "net/http" "os" "strconv" + "time" - "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" - "github.com/curusarn/resh/pkg/signalhandler" + "github.com/curusarn/resh/internal/cfg" + "github.com/curusarn/resh/internal/histfile" + "github.com/curusarn/resh/internal/recordint" + "github.com/curusarn/resh/internal/sesswatch" + "github.com/curusarn/resh/internal/signalhandler" + "go.uber.org/zap" ) -func runServer(config cfg.Config, reshHistoryPath, bashHistoryPath, zshHistoryPath string) { - var recordSubscribers []chan records.Record - var sessionInitSubscribers []chan records.Record +// TODO: turn server and handlers into package + +type Server struct { + sugar *zap.SugaredLogger + config cfg.Config + + reshHistoryPath string + bashHistoryPath string + zshHistoryPath string + + deviceID string + deviceName string +} + +func (s *Server) Run() { + var recordSubscribers []chan recordint.Collect + var sessionInitSubscribers []chan recordint.SessionInit var sessionDropSubscribers []chan string var signalSubscribers []chan os.Signal shutdown := make(chan string) - // sessshist - sesshistSessionsToInit := make(chan records.Record) - sessionInitSubscribers = append(sessionInitSubscribers, sesshistSessionsToInit) - sesshistSessionsToDrop := make(chan string) - sessionDropSubscribers = append(sessionDropSubscribers, sesshistSessionsToDrop) - sesshistRecords := make(chan records.Record) - recordSubscribers = append(recordSubscribers, sesshistRecords) - // histfile - histfileRecords := make(chan records.Record) + histfileRecords := make(chan recordint.Collect) recordSubscribers = append(recordSubscribers, histfileRecords) histfileSessionsToDrop := make(chan string) sessionDropSubscribers = append(sessionDropSubscribers, histfileSessionsToDrop) @@ -38,38 +45,46 @@ func runServer(config cfg.Config, reshHistoryPath, bashHistoryPath, zshHistoryPa signalSubscribers = append(signalSubscribers, histfileSignals) maxHistSize := 10000 // lines minHistSizeKB := 2000 // roughly lines - histfileBox := histfile.New(histfileRecords, histfileSessionsToDrop, - reshHistoryPath, bashHistoryPath, zshHistoryPath, + histfileBox := histfile.New(s.sugar, histfileRecords, histfileSessionsToDrop, + s.reshHistoryPath, s.bashHistoryPath, s.zshHistoryPath, maxHistSize, minHistSizeKB, histfileSignals, shutdown) - // sesshist New - sesshistDispatch := sesshist.NewDispatch(sesshistSessionsToInit, sesshistSessionsToDrop, - sesshistRecords, histfileBox, - config.SesshistInitHistorySize) - // sesswatch - sesswatchRecords := make(chan records.Record) + sesswatchRecords := make(chan recordint.Collect) recordSubscribers = append(recordSubscribers, sesswatchRecords) - sesswatchSessionsToWatch := make(chan records.Record) - sessionInitSubscribers = append(sessionInitSubscribers, sesswatchRecords, sesswatchSessionsToWatch) - sesswatch.Go(sesswatchSessionsToWatch, sesswatchRecords, sessionDropSubscribers, config.SesswatchPeriodSeconds) + sesswatchSessionsToWatch := make(chan recordint.SessionInit) + sessionInitSubscribers = append(sessionInitSubscribers, sesswatchSessionsToWatch) + sesswatch.Go( + s.sugar, + sesswatchSessionsToWatch, + sesswatchRecords, + sessionDropSubscribers, + s.config.SessionWatchPeriodSeconds, + ) // handlers mux := http.NewServeMux() - mux.HandleFunc("/status", statusHandler) - mux.Handle("/record", &recordHandler{subscribers: recordSubscribers}) - mux.Handle("/session_init", &sessionInitHandler{subscribers: sessionInitSubscribers}) - mux.Handle("/recall", &recallHandler{sesshistDispatch: sesshistDispatch}) - mux.Handle("/inspect", &inspectHandler{sesshistDispatch: sesshistDispatch}) - mux.Handle("/dump", &dumpHandler{histfileBox: histfileBox}) + mux.Handle("/status", &statusHandler{sugar: s.sugar}) + mux.Handle("/record", &recordHandler{ + sugar: s.sugar, + subscribers: recordSubscribers, + deviceID: s.deviceID, + deviceName: s.deviceName, + }) + mux.Handle("/session_init", &sessionInitHandler{sugar: s.sugar, subscribers: sessionInitSubscribers}) + mux.Handle("/dump", &dumpHandler{sugar: s.sugar, histfileBox: histfileBox}) server := &http.Server{ - Addr: "localhost:" + strconv.Itoa(config.Port), - Handler: mux, + Addr: "localhost:" + strconv.Itoa(s.config.Port), + Handler: mux, + ReadTimeout: 1 * time.Second, + WriteTimeout: 1 * time.Second, + ReadHeaderTimeout: 1 * time.Second, + IdleTimeout: 30 * time.Second, } go server.ListenAndServe() // signalhandler - takes over the main goroutine so when signal handler exists the whole program exits - signalhandler.Run(signalSubscribers, shutdown, server) + signalhandler.Run(s.sugar, signalSubscribers, shutdown, server) } diff --git a/cmd/daemon/session-init.go b/cmd/daemon/session-init.go index 27a1b275..86fdb921 100644 --- a/cmd/daemon/session-init.go +++ b/cmd/daemon/session-init.go @@ -2,37 +2,49 @@ package main import ( "encoding/json" - "io/ioutil" - "log" + "io" "net/http" - "github.com/curusarn/resh/pkg/records" + "github.com/curusarn/resh/internal/recordint" + "go.uber.org/zap" ) type sessionInitHandler struct { - subscribers []chan records.Record + sugar *zap.SugaredLogger + subscribers []chan recordint.SessionInit } func (h *sessionInitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + sugar := h.sugar.With(zap.String("endpoint", "/session_init")) + sugar.Debugw("Handling request, sending response, reading body ...") w.Write([]byte("OK\n")) - jsn, err := ioutil.ReadAll(r.Body) + // TODO: should we somehow check for errors here? + jsn, err := io.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) + sugar.Errorw("Error reading body", "error", err) return } - record := records.Record{} - err = json.Unmarshal(jsn, &record) + sugar.Debugw("Unmarshaling record ...") + rec := recordint.SessionInit{} + err = json.Unmarshal(jsn, &rec) if err != nil { - log.Println("Decoding error: ", err) - log.Println("Payload: ", jsn) + sugar.Errorw("Error during unmarshaling", + "error", err, + "payload", jsn, + ) return } + sugar := sugar.With( + "sessionID", rec.SessionID, + "sessionPID", rec.SessionPID, + ) + sugar.Infow("Got session, sending to subscribers ...") for _, sub := range h.subscribers { - sub <- record + sub <- rec } - log.Println("/session_init - id:", record.SessionID, " - pid:", record.SessionPID) + sugar.Debugw("Session sent to subscribers") }() } diff --git a/cmd/daemon/status.go b/cmd/daemon/status.go index 599b8dd2..3c6b416a 100644 --- a/cmd/daemon/status.go +++ b/cmd/daemon/status.go @@ -2,16 +2,19 @@ package main import ( "encoding/json" - "log" "net/http" - "strconv" - "github.com/curusarn/resh/pkg/httpclient" - "github.com/curusarn/resh/pkg/msg" + "github.com/curusarn/resh/internal/msg" + "go.uber.org/zap" ) -func statusHandler(w http.ResponseWriter, r *http.Request) { - log.Println("/status START") +type statusHandler struct { + sugar *zap.SugaredLogger +} + +func (h *statusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + sugar := h.sugar.With(zap.String("endpoint", "/status")) + sugar.Debugw("Handling request ...") resp := msg.StatusResponse{ Status: true, Version: version, @@ -19,23 +22,12 @@ func statusHandler(w http.ResponseWriter, r *http.Request) { } jsn, err := json.Marshal(&resp) if err != nil { - log.Println("Encoding error:", err) - log.Println("Response:", resp) + sugar.Errorw("Error when marshaling", + "error", err, + "response", resp, + ) return } w.Write(jsn) - log.Println("/status END") -} - -func isDaemonRunning(port int) (bool, error) { - url := "http://localhost:" + strconv.Itoa(port) + "/status" - client := httpclient.New() - resp, err := client.Get(url) - if err != nil { - log.Printf("Error while checking daemon status - "+ - "it's probably not running: %v\n", err) - return false, err - } - defer resp.Body.Close() - return true, nil + sugar.Infow("Request handled") } diff --git a/cmd/evaluate/main.go b/cmd/evaluate/main.go deleted file mode 100644 index 5b4bda86..00000000 --- a/cmd/evaluate/main.go +++ /dev/null @@ -1,152 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "log" - "os" - "os/user" - "path/filepath" - - "github.com/curusarn/resh/pkg/histanal" - "github.com/curusarn/resh/pkg/strat" -) - -// version from git set during build -var version string - -// commit from git set during build -var commit string - -func main() { - const maxCandidates = 50 - - usr, _ := user.Current() - dir := usr.HomeDir - historyPath := filepath.Join(dir, ".resh_history.json") - historyPathBatchMode := filepath.Join(dir, "resh_history.json") - sanitizedHistoryPath := filepath.Join(dir, "resh_history_sanitized.json") - // tmpPath := "/tmp/resh-evaluate-tmp.json" - - showVersion := flag.Bool("version", false, "Show version and exit") - showRevision := flag.Bool("revision", false, "Show git revision and exit") - input := flag.String("input", "", - "Input file (default: "+historyPath+" OR "+sanitizedHistoryPath+ - " depending on --sanitized-input option)") - // outputDir := flag.String("output", "/tmp/resh-evaluate", "Output directory") - sanitizedInput := flag.Bool("sanitized-input", false, - "Handle input as sanitized (also changes default value for input argument)") - plottingScript := flag.String("plotting-script", "resh-evaluate-plot.py", "Script to use for plotting") - inputDataRoot := flag.String("input-data-root", "", - "Input data root, enables batch mode, looks for files matching --input option") - slow := flag.Bool("slow", false, - "Enables strategies that takes a long time (e.g. markov chain strategies).") - skipFailedCmds := flag.Bool("skip-failed-cmds", false, - "Skips records with non-zero exit status.") - debugRecords := flag.Float64("debug", 0, "Debug records - percentage of records that should be debugged.") - - flag.Parse() - - // handle show{Version,Revision} options - if *showVersion == true { - fmt.Println(version) - os.Exit(0) - } - if *showRevision == true { - fmt.Println(commit) - os.Exit(0) - } - - // handle batch mode - batchMode := false - if *inputDataRoot != "" { - batchMode = true - } - // set default input - if *input == "" { - if *sanitizedInput { - *input = sanitizedHistoryPath - } else if batchMode { - *input = historyPathBatchMode - } else { - *input = historyPath - } - } - - var evaluator histanal.HistEval - if batchMode { - evaluator = histanal.NewHistEvalBatchMode(*input, *inputDataRoot, maxCandidates, *skipFailedCmds, *debugRecords, *sanitizedInput) - } else { - evaluator = histanal.NewHistEval(*input, maxCandidates, *skipFailedCmds, *debugRecords, *sanitizedInput) - } - - var simpleStrategies []strat.ISimpleStrategy - var strategies []strat.IStrategy - - // dummy := strategyDummy{} - // simpleStrategies = append(simpleStrategies, &dummy) - - simpleStrategies = append(simpleStrategies, &strat.Recent{}) - - // frequent := strategyFrequent{} - // frequent.init() - // simpleStrategies = append(simpleStrategies, &frequent) - - // random := strategyRandom{candidatesSize: maxCandidates} - // random.init() - // simpleStrategies = append(simpleStrategies, &random) - - directory := strat.DirectorySensitive{} - directory.Init() - simpleStrategies = append(simpleStrategies, &directory) - - // dynamicDistG := strat.DynamicRecordDistance{ - // MaxDepth: 3000, - // DistParams: records.DistParams{Pwd: 10, RealPwd: 10, SessionID: 1, Time: 1, Git: 10}, - // Label: "10*pwd,10*realpwd,session,time,10*git", - // } - // dynamicDistG.Init() - // strategies = append(strategies, &dynamicDistG) - - // NOTE: this is the decent one !!! - // distanceStaticBest := strat.RecordDistance{ - // MaxDepth: 3000, - // DistParams: records.DistParams{Pwd: 10, RealPwd: 10, SessionID: 1, Time: 1}, - // Label: "10*pwd,10*realpwd,session,time", - // } - // strategies = append(strategies, &distanceStaticBest) - - recentBash := strat.RecentBash{} - recentBash.Init() - strategies = append(strategies, &recentBash) - - if *slow { - - markovCmd := strat.MarkovChainCmd{Order: 1} - markovCmd.Init() - - markovCmd2 := strat.MarkovChainCmd{Order: 2} - markovCmd2.Init() - - markov := strat.MarkovChain{Order: 1} - markov.Init() - - markov2 := strat.MarkovChain{Order: 2} - markov2.Init() - - simpleStrategies = append(simpleStrategies, &markovCmd2, &markovCmd, &markov2, &markov) - } - - for _, strategy := range simpleStrategies { - strategies = append(strategies, strat.NewSimpleStrategyWrapper(strategy)) - } - - for _, strat := range strategies { - err := evaluator.Evaluate(strat) - if err != nil { - log.Println("Evaluator evaluate() error:", err) - } - } - - evaluator.CalculateStatsAndPlot(*plottingScript) -} diff --git a/cmd/event/main.go b/cmd/event/main.go deleted file mode 100644 index fe3cb721..00000000 --- a/cmd/event/main.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -import "fmt" - -func main() { - fmt.Println("Hell world") -} diff --git a/cmd/generate-uuid/main.go b/cmd/generate-uuid/main.go new file mode 100644 index 00000000..8c238a90 --- /dev/null +++ b/cmd/generate-uuid/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + "os" + + "github.com/google/uuid" +) + +// Small utility to generate UUID's using google/uuid golang package +// Doesn't check arguments +// Exits with status 1 on error +func main() { + rnd, err := uuid.NewRandom() + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: could not get new random source: %v", err) + os.Exit(1) + } + id := rnd.String() + if id == "" { + fmt.Fprintf(os.Stderr, "ERROR: got invalid UUID from package") + os.Exit(1) + } + // No newline + fmt.Print(id) +} diff --git a/cmd/get-epochtime/main.go b/cmd/get-epochtime/main.go new file mode 100644 index 00000000..2000414c --- /dev/null +++ b/cmd/get-epochtime/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "fmt" + + "github.com/curusarn/resh/internal/epochtime" +) + +// Small utility to get epochtime in millisecond precision +// Doesn't check arguments +// Exits with status 1 on error +func main() { + fmt.Printf("%s", epochtime.Now()) +} diff --git a/cmd/inspect/main.go b/cmd/inspect/main.go deleted file mode 100644 index 7b76a40c..00000000 --- a/cmd/inspect/main.go +++ /dev/null @@ -1,87 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "flag" - "fmt" - "io/ioutil" - "log" - "net/http" - "time" - - "github.com/BurntSushi/toml" - "github.com/curusarn/resh/pkg/cfg" - "github.com/curusarn/resh/pkg/msg" - - "os/user" - "path/filepath" - "strconv" -) - -// version from git set during build -var version string - -// commit from git set during build -var commit string - -func main() { - usr, _ := user.Current() - dir := usr.HomeDir - configPath := filepath.Join(dir, "/.config/resh.toml") - - var config cfg.Config - if _, err := toml.DecodeFile(configPath, &config); err != nil { - log.Fatal("Error reading config:", err) - } - - sessionID := flag.String("sessionID", "", "resh generated session id") - count := flag.Uint("count", 10, "Number of cmdLines to return") - flag.Parse() - - if *sessionID == "" { - fmt.Println("Error: you need to specify sessionId") - } - - m := msg.InspectMsg{SessionID: *sessionID, Count: *count} - resp := SendInspectMsg(m, strconv.Itoa(config.Port)) - for _, cmdLine := range resp.CmdLines { - fmt.Println("`" + cmdLine + "'") - } -} - -// SendInspectMsg to daemon -func SendInspectMsg(m msg.InspectMsg, port string) msg.MultiResponse { - recJSON, err := json.Marshal(m) - if err != nil { - log.Fatal("send err 1", err) - } - - req, err := http.NewRequest("POST", "http://localhost:"+port+"/inspect", - bytes.NewBuffer(recJSON)) - if err != nil { - log.Fatal("send err 2", err) - } - req.Header.Set("Content-Type", "application/json") - - client := http.Client{ - Timeout: 3 * time.Second, - } - resp, err := client.Do(req) - if err != nil { - log.Fatal("resh-daemon is not running - try restarting this terminal") - } - - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - log.Fatal("read response error") - } - // log.Println(string(body)) - response := msg.MultiResponse{} - err = json.Unmarshal(body, &response) - if err != nil { - log.Fatal("unmarshal resp error: ", err) - } - return response -} diff --git a/cmd/install-utils/device.go b/cmd/install-utils/device.go new file mode 100644 index 00000000..903e69f8 --- /dev/null +++ b/cmd/install-utils/device.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "os" + + "github.com/curusarn/resh/internal/datadir" + "github.com/curusarn/resh/internal/device" + "github.com/curusarn/resh/internal/output" +) + +func setupDevice(out *output.Output) { + dataDir, err := datadir.MakePath() + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: Failed to get/setup data directory: %v\n", err) + os.Exit(1) + } + err = device.SetupName(out, dataDir) + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: Failed to check/setup device name: %v\n", err) + os.Exit(1) + } + err = device.SetupID(dataDir) + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: Failed to check/setup device ID: %v\n", err) + os.Exit(1) + } +} diff --git a/cmd/install-utils/main.go b/cmd/install-utils/main.go new file mode 100644 index 00000000..4b72676f --- /dev/null +++ b/cmd/install-utils/main.go @@ -0,0 +1,67 @@ +package main + +import ( + "fmt" + "os" + + "github.com/curusarn/resh/internal/cfg" + "github.com/curusarn/resh/internal/logger" + "github.com/curusarn/resh/internal/output" + "go.uber.org/zap" +) + +// info passed during build +var version string +var commit string +var development string + +func main() { + config, errCfg := cfg.New() + logger, err := logger.New("install-utils", config.LogLevel, development) + if err != nil { + fmt.Printf("Error while creating logger: %v", err) + } + defer logger.Sync() // flushes buffer, if any + if errCfg != nil { + logger.Error("Error while getting configuration", zap.Error(errCfg)) + } + sugar := logger.Sugar() + sugar.Infow("Install-utils invoked ...", + "version", version, + "commit", commit, + ) + out := output.New(logger, "install-utils ERROR") + + if len(os.Args) < 2 { + out.Error("ERROR: Not enough arguments\n") + printUsage(os.Stderr) + os.Exit(1) + } + command := os.Args[1] + switch command { + case "setup-device": + setupDevice(out) + case "migrate-all": + migrateAll(out) + case "help": + printUsage(os.Stdout) + default: + out.Error(fmt.Sprintf("ERROR: Unknown command: %s\n", command)) + printUsage(os.Stderr) + os.Exit(1) + } +} + +func printUsage(f *os.File) { + usage := ` +USAGE: ./install-utils COMMAND +Utils used during RESH installation. + +COMMANDS: + setup-device setup device name and device ID + migrate-all update config and history to latest format + help show this help + +` + fmt.Fprint(f, usage) +} diff --git a/cmd/install-utils/migrate.go b/cmd/install-utils/migrate.go new file mode 100644 index 00000000..07a06664 --- /dev/null +++ b/cmd/install-utils/migrate.go @@ -0,0 +1,195 @@ +package main + +import ( + "fmt" + "os" + "path" + + "github.com/curusarn/resh/internal/cfg" + "github.com/curusarn/resh/internal/datadir" + "github.com/curusarn/resh/internal/futil" + "github.com/curusarn/resh/internal/output" + "github.com/curusarn/resh/internal/recio" +) + +func printRecoveryInfo(rf *futil.RestorableFile) { + fmt.Printf(" -> Backup is '%s'\n"+ + " -> Original file location is '%s'\n"+ + " -> Please copy the backup over the file - run: cp -f '%s' '%s'\n\n", + rf.PathBackup, rf.Path, + rf.PathBackup, rf.Path, + ) +} + +func migrateAll(out *output.Output) { + cfgBackup, err := migrateConfig(out) + if err != nil { + // out.InfoE("Failed to update config file format", err) + out.FatalE("Failed to update config file format", err) + } + err = migrateHistory(out) + if err != nil { + errHist := err + out.InfoE("Failed to update RESH history", errHist) + out.Info("Restoring config from backup ...") + err = cfgBackup.Restore() + if err != nil { + out.InfoE("FAILED TO RESTORE CONFIG FROM BACKUP!", err) + printRecoveryInfo(cfgBackup) + } else { + out.Info("Config file was restored successfully") + } + out.FatalE("Failed to update history", errHist) + } +} + +func migrateConfig(out *output.Output) (*futil.RestorableFile, error) { + cfgPath, err := cfg.GetPath() + if err != nil { + return nil, fmt.Errorf("could not get config file path: %w", err) + } + + // Touch config to get rid of edge-cases + created, err := futil.TouchFile(cfgPath) + if err != nil { + return nil, fmt.Errorf("failed to touch config file: %w", err) + } + + // Backup + backup, err := futil.BackupFile(cfgPath) + if err != nil { + return nil, fmt.Errorf("could not backup config file: %w", err) + } + + // Migrate + changes, err := cfg.Migrate() + if err != nil { + // Restore + errMigrate := err + errMigrateWrap := fmt.Errorf("failed to update config file: %w", errMigrate) + out.InfoE("Failed to update config file format", errMigrate) + out.Info("Restoring config from backup ...") + err = backup.Restore() + if err != nil { + out.InfoE("FAILED TO RESTORE CONFIG FROM BACKUP!", err) + printRecoveryInfo(backup) + } else { + out.Info("Config file was restored successfully") + } + // We are returning the root cause - there might be a better solution how to report the errors + return nil, errMigrateWrap + } + if created { + out.Info(fmt.Sprintf("RESH config created in '%s'", cfgPath)) + } else if changes { + out.Info("RESH config file format has changed since last update - your config was updated to reflect the changes.") + } + return backup, nil +} + +func migrateHistory(out *output.Output) error { + err := migrateHistoryLocation(out) + if err != nil { + return fmt.Errorf("failed to move history to new location %w", err) + } + return migrateHistoryFormat(out) +} + +// Find first existing history and use it +// Don't bother with merging of history in multiple locations - it could get messy and it shouldn't be necessary +func migrateHistoryLocation(out *output.Output) error { + dataDir, err := datadir.MakePath() + if err != nil { + return fmt.Errorf("failed to get data directory: %w", err) + } + historyPath := path.Join(dataDir, datadir.HistoryFileName) + + exists, err := futil.FileExists(historyPath) + if err != nil { + return fmt.Errorf("failed to check history file: %w", err) + } + if exists { + // TODO: get rid of this output (later) + out.Info(fmt.Sprintf("Found history file in '%s' - nothing to move", historyPath)) + return nil + } + + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get user home directory: %w", err) + } + + legacyHistoryPaths := []string{ + path.Join(homeDir, ".resh_history.json"), + path.Join(homeDir, ".resh/history.json"), + } + for _, path := range legacyHistoryPaths { + exists, err = futil.FileExists(path) + if err != nil { + return fmt.Errorf("failed to check existence of legacy history file: %w", err) + } + if exists { + // TODO: maybe get rid of this output later + out.Info(fmt.Sprintf("Copying history file to new location: '%s' -> '%s' ...", path, historyPath)) + err = futil.CopyFile(path, historyPath) + if err != nil { + return fmt.Errorf("failed to copy history file: %w", err) + } + out.Info("History file copied successfully") + return nil + } + } + // out.Info("WARNING: No RESH history file found (this is normal during new installation)") + return nil +} + +func migrateHistoryFormat(out *output.Output) error { + dataDir, err := datadir.MakePath() + if err != nil { + return fmt.Errorf("could not get user data directory: %w", err) + } + historyPath := path.Join(dataDir, datadir.HistoryFileName) + + exists, err := futil.FileExists(historyPath) + if err != nil { + return fmt.Errorf("failed to check existence of history file: %w", err) + } + if !exists { + out.Error("There is no RESH history file - this is normal if you are installing RESH for the first time on this device") + _, err = futil.TouchFile(historyPath) + if err != nil { + return fmt.Errorf("failed to touch history file: %w", err) + } + return nil + } + + backup, err := futil.BackupFile(historyPath) + if err != nil { + return fmt.Errorf("could not back up history file: %w", err) + } + + rio := recio.New(out.Logger.Sugar()) + + recs, err := rio.ReadAndFixFile(historyPath, 3) + if err != nil { + return fmt.Errorf("could not load history file: %w", err) + } + err = rio.OverwriteFile(historyPath, recs) + if err != nil { + // Restore + errMigrate := err + errMigrateWrap := fmt.Errorf("failed to update format of history file: %w", errMigrate) + out.InfoE("Failed to update RESH history file format", errMigrate) + out.Info("Restoring RESH history from backup ...") + err = backup.Restore() + if err != nil { + out.InfoE("FAILED TO RESTORE RESH HISTORY FROM BACKUP!", err) + printRecoveryInfo(backup) + } else { + out.Info("RESH history file was restored successfully") + } + // We are returning the root cause - there might be a better solution how to report the errors + return errMigrateWrap + } + return nil +} diff --git a/cmd/postcollect/main.go b/cmd/postcollect/main.go index b5cb6b04..e67e93ba 100644 --- a/cmd/postcollect/main.go +++ b/cmd/postcollect/main.go @@ -1,154 +1,74 @@ 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" + "github.com/curusarn/resh/internal/cfg" + "github.com/curusarn/resh/internal/collect" + "github.com/curusarn/resh/internal/logger" + "github.com/curusarn/resh/internal/opt" + "github.com/curusarn/resh/internal/output" + "github.com/curusarn/resh/internal/recordint" + "github.com/curusarn/resh/record" + "github.com/spf13/pflag" + "go.uber.org/zap" - // "os/exec" - "os/user" - "path/filepath" "strconv" ) -// version from git set during build +// info passed during build var version string - -// commit from git set during build var commit string +var development 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) + config, errCfg := cfg.New() + logger, err := logger.New("postcollect", config.LogLevel, development) + if err != nil { + fmt.Printf("Error while creating logger: %v", 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") - recordID := flag.String("recordId", "", "resh generated record id") - - shlvl := flag.Int("shlvl", -1, "$SHLVL") - shell := flag.String("shell", "", "actual shell") - - // 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, "... $?") + defer logger.Sync() // flushes buffer, if any + if errCfg != nil { + logger.Error("Error while getting configuration", zap.Error(errCfg)) + } + out := output.New(logger, "resh-postcollect ERROR") - // before after - timezoneAfter := flag.String("timezoneAfter", "", "") + args := opt.HandleVersionOpts(out, os.Args, version, commit) - rtb := flag.String("realtimeBefore", "-1", "before $EPOCHREALTIME") - rta := flag.String("realtimeAfter", "-1", "after $EPOCHREALTIME") - flag.Parse() + flags := pflag.NewFlagSet("", pflag.ExitOnError) + exitCode := flags.Int("exit-code", -1, "Exit code") + sessionID := flags.String("session-id", "", "Resh generated session ID") + recordID := flags.String("record-id", "", "Resh generated record ID") + shlvl := flags.Int("shlvl", -1, "$SHLVL") + rtb := flags.String("time-before", "-1", "Before $EPOCHREALTIME") + rta := flags.String("time-after", "-1", "After $EPOCHREALTIME") + flags.Parse(args) - if *showVersion == true { - fmt.Println(version) - os.Exit(0) - } - if *showRevision == true { - fmt.Println(commit) - 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 != commit { - fmt.Println("Please restart/reload this terminal session " + - "(resh revision: " + commit + - "; resh revision of this terminal session: " + *requireRevision + - ")") - os.Exit(3) - } - realtimeAfter, err := strconv.ParseFloat(*rta, 64) + timeAfter, err := strconv.ParseFloat(*rta, 64) if err != nil { - log.Fatal("Flag Parsing error (rta):", err) + out.FatalE("Error while parsing flag --time-after", err) } - realtimeBefore, err := strconv.ParseFloat(*rtb, 64) + timeBefore, err := strconv.ParseFloat(*rtb, 64) if err != nil { - log.Fatal("Flag Parsing error (rtb):", err) + out.FatalE("Error while parsing flag --time-before", err) } - realtimeDuration := realtimeAfter - realtimeBefore - - timezoneAfterOffset := collect.GetTimezoneOffsetInSeconds(*timezoneAfter) - realtimeAfterLocal := realtimeAfter + timezoneAfterOffset + duration := timeAfter - timeBefore - realPwdAfter, err := filepath.EvalSymlinks(*pwdAfter) - if err != nil { - log.Println("err while handling pwdAfter realpath:", err) - realPwdAfter = "" - } + // FIXME: use recordint.Postcollect + rec := recordint.Collect{ + SessionID: *sessionID, + Shlvl: *shlvl, - gitDirAfter, gitRealDirAfter := collect.GetGitDirs(*gitCdupAfter, *gitCdupExitCodeAfter, *pwdAfter) - if *gitRemoteExitCodeAfter != 0 { - *gitRemoteAfter = "" - } - - rec := records.Record{ - // core - BaseRecord: records.BaseRecord{ - CmdLine: *cmdLine, - ExitCode: *exitCode, - SessionID: *sessionID, + Rec: record.V1{ RecordID: *recordID, - Shlvl: *shlvl, - Shell: *shell, - - 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), + SessionID: *sessionID, - PartOne: false, + ExitCode: *exitCode, + Duration: fmt.Sprintf("%.4f", duration), - ReshUUID: collect.ReadFileContent(reshUUIDPath), - ReshVersion: version, - ReshRevision: commit, + PartsNotMerged: true, }, } - collect.SendRecord(rec, strconv.Itoa(config.Port), "/record") + collect.SendRecord(out, rec, strconv.Itoa(config.Port), "/record") } diff --git a/cmd/sanitize/main.go b/cmd/sanitize/main.go deleted file mode 100644 index c4fd60a7..00000000 --- a/cmd/sanitize/main.go +++ /dev/null @@ -1,523 +0,0 @@ -package main - -import ( - "bufio" - "crypto/sha256" - "encoding/binary" - "encoding/hex" - "encoding/json" - "errors" - "flag" - "fmt" - "log" - "math" - "net/url" - "os" - "os/user" - "path" - "path/filepath" - "strconv" - "strings" - "unicode" - - "github.com/coreos/go-semver/semver" - "github.com/curusarn/resh/pkg/records" - giturls "github.com/whilp/git-urls" -) - -// version from git set during build -var version string - -// commit from git set during build -var commit string - -func main() { - usr, _ := user.Current() - dir := usr.HomeDir - historyPath := filepath.Join(dir, ".resh_history.json") - // outputPath := filepath.Join(dir, "resh_history_sanitized.json") - sanitizerDataPath := filepath.Join(dir, ".resh", "sanitizer_data") - - showVersion := flag.Bool("version", false, "Show version and exit") - showRevision := flag.Bool("revision", false, "Show git revision and exit") - trimHashes := flag.Int("trim-hashes", 12, "Trim hashes to N characters, '0' turns off trimming") - inputPath := flag.String("input", historyPath, "Input file") - outputPath := flag.String("output", "", "Output file (default: use stdout)") - - flag.Parse() - - if *showVersion == true { - fmt.Println(version) - os.Exit(0) - } - if *showRevision == true { - fmt.Println(commit) - os.Exit(0) - } - sanitizer := sanitizer{hashLength: *trimHashes} - err := sanitizer.init(sanitizerDataPath) - if err != nil { - log.Fatal("Sanitizer init() error:", err) - } - - inputFile, err := os.Open(*inputPath) - if err != nil { - log.Fatal("Open() resh history file error:", err) - } - defer inputFile.Close() - - var writer *bufio.Writer - if *outputPath == "" { - writer = bufio.NewWriter(os.Stdout) - } else { - outputFile, err := os.Create(*outputPath) - if err != nil { - log.Fatal("Create() output file error:", err) - } - defer outputFile.Close() - writer = bufio.NewWriter(outputFile) - } - defer writer.Flush() - - scanner := bufio.NewScanner(inputFile) - for scanner.Scan() { - record := records.Record{} - fallbackRecord := records.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 = records.Convert(&fallbackRecord) - } - err = sanitizer.sanitizeRecord(&record) - if err != nil { - log.Println("Line:", line) - log.Fatal("Sanitization error:", err) - } - outLine, err := json.Marshal(&record) - if err != nil { - log.Println("Line:", line) - log.Fatal("Encoding error:", err) - } - // fmt.Println(string(outLine)) - n, err := writer.WriteString(string(outLine) + "\n") - if err != nil { - log.Fatal(err) - } - if n == 0 { - log.Fatal("Nothing was written", n) - } - } -} - -type sanitizer struct { - hashLength int - whitelist map[string]bool -} - -func (s *sanitizer) init(dataPath string) error { - globalData := path.Join(dataPath, "whitelist.txt") - s.whitelist = loadData(globalData) - return nil -} - -func loadData(fname string) map[string]bool { - file, err := os.Open(fname) - if err != nil { - log.Fatal("Open() file error:", err) - } - defer file.Close() - - scanner := bufio.NewScanner(file) - data := make(map[string]bool) - for scanner.Scan() { - line := scanner.Text() - data[line] = true - } - return data -} - -func (s *sanitizer) sanitizeRecord(record *records.Record) error { - // hash directories of the paths - record.Pwd = s.sanitizePath(record.Pwd) - record.RealPwd = s.sanitizePath(record.RealPwd) - record.PwdAfter = s.sanitizePath(record.PwdAfter) - record.RealPwdAfter = s.sanitizePath(record.RealPwdAfter) - record.GitDir = s.sanitizePath(record.GitDir) - record.GitDirAfter = s.sanitizePath(record.GitDirAfter) - record.GitRealDir = s.sanitizePath(record.GitRealDir) - record.GitRealDirAfter = s.sanitizePath(record.GitRealDirAfter) - record.Home = s.sanitizePath(record.Home) - record.ShellEnv = s.sanitizePath(record.ShellEnv) - - // hash the most sensitive info, do not tokenize - record.Host = s.hashToken(record.Host) - record.Login = s.hashToken(record.Login) - record.MachineID = s.hashToken(record.MachineID) - - var err error - // this changes git url a bit but I'm still happy with the result - // e.g. "git@github.com:curusarn/resh" becomes "ssh://git@github.com/3385162f14d7/5a7b2909005c" - // notice the "ssh://" prefix - record.GitOriginRemote, err = s.sanitizeGitURL(record.GitOriginRemote) - if err != nil { - log.Println("Error while snitizing GitOriginRemote url", record.GitOriginRemote, ":", err) - return err - } - record.GitOriginRemoteAfter, err = s.sanitizeGitURL(record.GitOriginRemoteAfter) - if err != nil { - log.Println("Error while snitizing GitOriginRemoteAfter url", record.GitOriginRemoteAfter, ":", err) - return err - } - - // sanitization destroys original CmdLine length -> save it - record.CmdLength = len(record.CmdLine) - - record.CmdLine, err = s.sanitizeCmdLine(record.CmdLine) - if err != nil { - log.Fatal("Cmd:", record.CmdLine, "; sanitization error:", err) - } - record.RecallLastCmdLine, err = s.sanitizeCmdLine(record.RecallLastCmdLine) - if err != nil { - log.Fatal("RecallLastCmdLine:", record.RecallLastCmdLine, "; sanitization error:", err) - } - - if len(record.RecallActionsRaw) > 0 { - record.RecallActionsRaw, err = s.sanitizeRecallActions(record.RecallActionsRaw, record.ReshVersion) - if err != nil { - log.Println("RecallActionsRaw:", record.RecallActionsRaw, "; sanitization error:", err) - } - } - // add a flag to signify that the record has been sanitized - record.Sanitized = true - return nil -} - -func fixSeparator(str string) string { - if len(str) > 0 && str[0] == ';' { - return "|||" + str[1:] - } - return str -} - -func minIndex(str string, substrs []string) (idx, substrIdx int) { - minMatch := math.MaxInt32 - for i, sep := range substrs { - match := strings.Index(str, sep) - if match != -1 && match < minMatch { - minMatch = match - substrIdx = i - } - } - idx = minMatch - return -} - -// sanitizes the recall actions by replacing the recall prefix with it's length -func (s *sanitizer) sanitizeRecallActions(str string, reshVersion string) (string, error) { - if len(str) == 0 { - return "", nil - } - var separators []string - seps := []string{"|||"} - refVersion, err := semver.NewVersion("2.5.14") - if err != nil { - return str, fmt.Errorf("sanitizeRecallActions: semver error: %s", err.Error()) - } - if len(reshVersion) == 0 { - return str, errors.New("sanitizeRecallActions: record.ReshVersion is an empty string") - } - if reshVersion == "dev" { - reshVersion = "0.0.0" - } - if reshVersion[0] == 'v' { - reshVersion = reshVersion[1:] - } - recordVersion, err := semver.NewVersion(reshVersion) - if err != nil { - return str, fmt.Errorf("sanitizeRecallActions: semver error: %s; version string: %s", err.Error(), reshVersion) - } - if recordVersion.LessThan(*refVersion) { - seps = append(seps, ";") - } - - actions := []string{"arrow_up", "arrow_down", "control_R"} - for _, sep := range seps { - for _, action := range actions { - separators = append(separators, sep+action+":") - } - } - /* - - find any of {|||,;}{arrow_up,arrow_down,control_R}: in the recallActions (on the lowest index) - - use found substring to parse out the next prefix - - sanitize prefix - - add fixed substring and sanitized prefix to output - */ - doBreak := false - sanStr := "" - idx := 0 - var currSeparator string - tokenLen, sepIdx := minIndex(str, separators) - if tokenLen != 0 { - return str, errors.New("sanitizeReacallActions: unexpected string before first action/separator") - } - currSeparator = separators[sepIdx] - idx += len(currSeparator) - for !doBreak { - tokenLen, sepIdx := minIndex(str[idx:], separators) - if tokenLen > len(str[idx:]) { - tokenLen = len(str[idx:]) - doBreak = true - } - // token := str[idx : idx+tokenLen] - sanStr += fixSeparator(currSeparator) + strconv.Itoa(tokenLen) - currSeparator = separators[sepIdx] - idx += tokenLen + len(currSeparator) - } - return sanStr, nil -} - -func (s *sanitizer) sanitizeCmdLine(cmdLine string) (string, error) { - const optionEndingChars = "\"$'\\#[]!><|;{}()*,?~&=`:@^/+%." // all bash control characters, '=', ... - const optionAllowedChars = "-_" // characters commonly found inside of options - sanCmdLine := "" - buff := "" - - // simple options shouldn't be sanitized - // 1) whitespace 2) "-" or "--" 3) letters, digits, "-", "_" 4) ending whitespace or any of "=;)" - var optionDetected bool - - prevR3 := ' ' - prevR2 := ' ' - prevR := ' ' - for _, r := range cmdLine { - switch optionDetected { - case true: - if unicode.IsSpace(r) || strings.ContainsRune(optionEndingChars, r) { - // whitespace or option ends the option - // => add option unsanitized - optionDetected = false - if len(buff) > 0 { - sanCmdLine += buff - buff = "" - } - sanCmdLine += string(r) - } else if unicode.IsLetter(r) == false && unicode.IsDigit(r) == false && - strings.ContainsRune(optionAllowedChars, r) == false { - // r is not any of allowed chars for an option: letter, digit, "-" or "_" - // => sanitize - if len(buff) > 0 { - sanToken, err := s.sanitizeCmdToken(buff) - if err != nil { - log.Println("WARN: got error while sanitizing cmdLine:", cmdLine) - // return cmdLine, err - } - sanCmdLine += sanToken - buff = "" - } - sanCmdLine += string(r) - } else { - buff += string(r) - } - case false: - // split command on all non-letter and non-digit characters - if unicode.IsLetter(r) == false && unicode.IsDigit(r) == false { - // split token - if len(buff) > 0 { - sanToken, err := s.sanitizeCmdToken(buff) - if err != nil { - log.Println("WARN: got error while sanitizing cmdLine:", cmdLine) - // return cmdLine, err - } - sanCmdLine += sanToken - buff = "" - } - sanCmdLine += string(r) - } else { - if (unicode.IsSpace(prevR2) && prevR == '-') || - (unicode.IsSpace(prevR3) && prevR2 == '-' && prevR == '-') { - optionDetected = true - } - buff += string(r) - } - } - prevR3 = prevR2 - prevR2 = prevR - prevR = r - } - if len(buff) <= 0 { - // nothing in the buffer => work is done - return sanCmdLine, nil - } - if optionDetected { - // option detected => dont sanitize - sanCmdLine += buff - return sanCmdLine, nil - } - // sanitize - sanToken, err := s.sanitizeCmdToken(buff) - if err != nil { - log.Println("WARN: got error while sanitizing cmdLine:", cmdLine) - // return cmdLine, err - } - sanCmdLine += sanToken - return sanCmdLine, nil -} - -func (s *sanitizer) sanitizeGitURL(rawURL string) (string, error) { - if len(rawURL) <= 0 { - return rawURL, nil - } - parsedURL, err := giturls.Parse(rawURL) - if err != nil { - return rawURL, err - } - return s.sanitizeParsedURL(parsedURL) -} - -func (s *sanitizer) sanitizeURL(rawURL string) (string, error) { - if len(rawURL) <= 0 { - return rawURL, nil - } - parsedURL, err := url.Parse(rawURL) - if err != nil { - return rawURL, err - } - return s.sanitizeParsedURL(parsedURL) -} - -func (s *sanitizer) sanitizeParsedURL(parsedURL *url.URL) (string, error) { - parsedURL.Opaque = s.sanitizeToken(parsedURL.Opaque) - - userinfo := parsedURL.User.Username() // only get username => password won't even make it to the sanitized data - if len(userinfo) > 0 { - parsedURL.User = url.User(s.sanitizeToken(userinfo)) - } else { - // we need to do this because `gitUrls.Parse()` sets `User` to `url.User("")` instead of `nil` - parsedURL.User = nil - } - var err error - parsedURL.Host, err = s.sanitizeTwoPartToken(parsedURL.Host, ":") - if err != nil { - return parsedURL.String(), err - } - parsedURL.Path = s.sanitizePath(parsedURL.Path) - // ForceQuery bool - parsedURL.RawQuery = s.sanitizeToken(parsedURL.RawQuery) - parsedURL.Fragment = s.sanitizeToken(parsedURL.Fragment) - - return parsedURL.String(), nil -} - -func (s *sanitizer) sanitizePath(path string) string { - var sanPath string - for _, token := range strings.Split(path, "/") { - if s.whitelist[token] != true { - token = s.hashToken(token) - } - sanPath += token + "/" - } - if len(sanPath) > 0 { - sanPath = sanPath[:len(sanPath)-1] - } - return sanPath -} - -func (s *sanitizer) sanitizeTwoPartToken(token string, delimeter string) (string, error) { - tokenParts := strings.Split(token, delimeter) - if len(tokenParts) <= 1 { - return s.sanitizeToken(token), nil - } - if len(tokenParts) == 2 { - return s.sanitizeToken(tokenParts[0]) + delimeter + s.sanitizeToken(tokenParts[1]), nil - } - return token, errors.New("Token has more than two parts") -} - -func (s *sanitizer) sanitizeCmdToken(token string) (string, error) { - // there shouldn't be tokens with letters or digits mixed together with symbols - if len(token) <= 1 { - // NOTE: do not sanitize single letter tokens - return token, nil - } - if s.isInWhitelist(token) == true { - return token, nil - } - - isLettersOrDigits := true - // isDigits := true - isOtherCharacters := true - for _, r := range token { - if unicode.IsDigit(r) == false && unicode.IsLetter(r) == false { - isLettersOrDigits = false - // isDigits = false - } - // if unicode.IsDigit(r) == false { - // isDigits = false - // } - if unicode.IsDigit(r) || unicode.IsLetter(r) { - isOtherCharacters = false - } - } - // NOTE: I decided that I don't want a special sanitization for numbers - // if isDigits { - // return s.hashNumericToken(token), nil - // } - if isLettersOrDigits { - return s.hashToken(token), nil - } - if isOtherCharacters { - return token, nil - } - log.Println("WARN: cmd token is made of mix of letters or digits and other characters; token:", token) - // return token, errors.New("cmd token is made of mix of letters or digits and other characters") - return s.hashToken(token), errors.New("cmd token is made of mix of letters or digits and other characters") -} - -func (s *sanitizer) sanitizeToken(token string) string { - if len(token) <= 1 { - // NOTE: do not sanitize single letter tokens - return token - } - if s.isInWhitelist(token) { - return token - } - return s.hashToken(token) -} - -func (s *sanitizer) hashToken(token string) string { - if len(token) <= 0 { - return token - } - // hash with sha256 - sum := sha256.Sum256([]byte(token)) - return s.trimHash(hex.EncodeToString(sum[:])) -} - -func (s *sanitizer) hashNumericToken(token string) string { - if len(token) <= 0 { - return token - } - sum := sha256.Sum256([]byte(token)) - sumInt := int(binary.LittleEndian.Uint64(sum[:])) - if sumInt < 0 { - return strconv.Itoa(sumInt * -1) - } - return s.trimHash(strconv.Itoa(sumInt)) -} - -func (s *sanitizer) trimHash(hash string) string { - length := s.hashLength - if length <= 0 || len(hash) < length { - length = len(hash) - } - return hash[:length] -} - -func (s *sanitizer) isInWhitelist(token string) bool { - return s.whitelist[strings.ToLower(token)] == true -} diff --git a/cmd/session-init/main.go b/cmd/session-init/main.go index 75f5298c..2798fc71 100644 --- a/cmd/session-init/main.go +++ b/cmd/session-init/main.go @@ -1,186 +1,48 @@ 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" + "github.com/curusarn/resh/internal/cfg" + "github.com/curusarn/resh/internal/collect" + "github.com/curusarn/resh/internal/logger" + "github.com/curusarn/resh/internal/opt" + "github.com/curusarn/resh/internal/output" + "github.com/curusarn/resh/internal/recordint" + "github.com/spf13/pflag" + "go.uber.org/zap" - "os/user" - "path/filepath" "strconv" ) -// version from git set during build +// info passed during build var version string - -// commit from git set during build var commit string +var development 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(commit) - 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 != commit { - fmt.Println("Please restart/reload this terminal session " + - "(resh revision: " + commit + - "; 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) + config, errCfg := cfg.New() + logger, err := logger.New("session-init", config.LogLevel, development) 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" + fmt.Printf("Error while creating logger: %v", err) } - if *osReleaseName == "" { - *osReleaseName = "Linux" + defer logger.Sync() // flushes buffer, if any + if errCfg != nil { + logger.Error("Error while getting configuration", zap.Error(errCfg)) } - 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, + out := output.New(logger, "resh-collect ERROR") - MachineID: collect.ReadFileContent(machineIDPath), + args := opt.HandleVersionOpts(out, os.Args, version, commit) - OsReleaseID: *osReleaseID, - OsReleaseVersionID: *osReleaseVersionID, - OsReleaseIDLike: *osReleaseIDLike, - OsReleaseName: *osReleaseName, - OsReleasePrettyName: *osReleasePrettyName, + flags := pflag.NewFlagSet("", pflag.ExitOnError) + sessionID := flags.String("session-id", "", "Resh generated session ID") + sessionPID := flags.Int("session-pid", -1, "$$ - Shell session PID") + flags.Parse(args) - ReshUUID: collect.ReadFileContent(reshUUIDPath), - ReshVersion: version, - ReshRevision: commit, - }, + rec := recordint.SessionInit{ + SessionID: *sessionID, + SessionPID: *sessionPID, } - collect.SendRecord(rec, strconv.Itoa(config.Port), "/session_init") + collect.SendSessionInit(out, rec, strconv.Itoa(config.Port)) } diff --git a/conf/config.toml b/conf/config.toml deleted file mode 100644 index 9db8b943..00000000 --- a/conf/config.toml +++ /dev/null @@ -1,5 +0,0 @@ -port = 2627 -sesswatchPeriodSeconds = 120 -sesshistInitHistorySize = 1000 -debug = false -bindControlR = true diff --git a/data/sanitizer/copyright_information.md b/data/sanitizer/copyright_information.md deleted file mode 100644 index abdbf33e..00000000 --- a/data/sanitizer/copyright_information.md +++ /dev/null @@ -1,7 +0,0 @@ -# copyright information - -Whitelist contains content from variety of sources. - -Part of the whitelist (`./whitelist.txt`) is made of copyrighted content from [FileInfo.com](https://fileinfo.com/filetypes/common). - -This content was used with permission from FileInfo.com. diff --git a/data/sanitizer/whitelist.txt b/data/sanitizer/whitelist.txt deleted file mode 100644 index 180e9c32..00000000 --- a/data/sanitizer/whitelist.txt +++ /dev/null @@ -1,1195 +0,0 @@ - -! -- -. -.. -: -[ -[[ -]] -{ -} -3dm -3ds -3g2 -3gp -7z -accdb -add -addgnupghome -addgroup -addpart -addr2line -add-shell -adduser -agetty -ai -aif -alias -alternatives -apk -app -applydeltarpm -applygnupgdefaults -apt -apt-cache -apt-cdrom -apt-config -apt-get -apt-key -apt-mark -ar -arch -arpd -arping -as -asf -asm -asp -aspx -au -autoload -avi -awk -b -b2sum -badblocks -bak -base32 -base64 -basename -basenc -bash -bashbug -bashbug-64 -bat -bg -bin -bind -bindkey -bisect -blend -blkdeactivate -blkdiscard -blkid -blkzone -blockdev -bmp -boot -bootctl -br -branch -break -bridge -brotli -build-locale-archive -builtin -bunzip2 -busctl -bye -bz2 -bzcat -bzcmp -bzdiff -bzegrep -bzexe -bzfgrep -bzgrep -bzip2 -bzip2recover -bzless -bzmore -c -cab -cal -ca-legacy -caller -capsh -captoinfo -case -cat -catchsegv -cbr -cc -cd -cer -certutil -cfdisk -cfg -c++filt -cfm -cgi -chacl -chage -chardetect -chattr -chcon -chcpu -chdir -checkout -chfn -chgpasswd -chgrp -chkconfig -chmem -chmod -choom -chown -chpasswd -chroot -chrt -chsh -cksum -class -clear -clear_console -clock -clockdiff -clone -cmp -cmsutil -co -code -col -colcrt -colrm -column -com -combinedeltarpm -comm -command -commit -compadd -comparguments -compcall -compctl -compdescribe -compfiles -compgen -compgroups -complete -compopt -compquote -compset -comptags -comptry -compvalues -conf -continue -convert -coproc -coredumpctl -cp -cpgr -cpio -cpl -cpp -cppw -cracklib-check -cracklib-format -cracklib-packer -cracklib-unpacker -crdownload -create-cracklib-dict -crlutil -crx -cs -csplit -csr -css -csv -ctrlaltdel -ctstat -cue -cur -curl -cut -cvtsudoers -cz -dash -dat -date -db -db_archive -db_checkpoint -db_deadlock -db_dump -db_dump185 -dbf -db_hotbackup -db_load -db_log_verify -db_printlog -db_recover -db_replicate -db_stat -db_tuner -db_upgrade -dbus-binding-tool -dbus-broker -dbus-broker-launch -dbus-cleanup-sockets -dbus-daemon -dbus-monitor -dbus-run-session -dbus-send -dbus-test-tool -dbus-update-activation-environment -dbus-uuidgen -db_verify -dcr -dd -dds -de -deb -debconf -debconf-apt-progress -debconf-communicate -debconf-copydb -debconf-escape -debconf-set-selections -debconf-show -deb-systemd-helper -deb-systemd-invoke -debugfs -debuginfo-install -declare -delgroup -delpart -deluser -dem -depmod -deskthemepack -desktop -dev -devlink -df -dgawk -diff -diff3 -dir -dircolors -dirmngr -dirmngr-client -dirname -dirs -disable -disown -dll -dmesg -dmfilemapd -dmg -dmp -dmsetup -dmstats -dnf -dnf-3 -dnsdomainname -do -doc -docker -Dockerfile -docx -domainname -done -dpkg -dpkg-deb -dpkg-divert -dpkg-maintscript-helper -dpkg-preconfigure -dpkg-query -dpkg-reconfigure -dpkg-split -dpkg-statoverride -dpkg-trigger -dracut -drv -dtd -du -dumpe2fs -dwg -dwp -dxf -e2freefrag -e2fsck -e2image -e2label -e2mmpstatus -e2undo -e4crypt -e4defrag -easy_install-3.7 -echo -echotc -echoti -egrep -eject -elfedit -elif -else -emacs -emulate -enable -end -env -eps -esac -etc -eval -evmctl -ex -exe -exec -exit -expand -expiry -export -expr -factor -faillock -faillog -fallocate -false -fc -fdformat -fdisk -fetch -ffmpeg -fg -fgrep -fi -filefrag -fincore -find -findfs -findmnt -find-repos-of-install -fips-finish-install -fips-mode-setup -fish -fla -float -flock -flv -fmt -fnt -fold -fon -for -foreach -free -fsck -fsck.cramfs -fsck.ext2 -fsck.ext3 -fsck.ext4 -fsck.minix -fsfreeze -fstab-decode -fstrim -function -functions -g13 -g13-syshelp -gadget -gam -gapplication -gawk -gdbus -ged -gencat -genl -getcap -getconf -getent -getfacl -getln -getopt -getopts -getpcaps -getty -gif -gio -gio-launch-desktop -gio-querymodules-64 -git -github.com -glib-compile-schemas -glibc_post_upgrade.x86_64 -go -gpasswd -gpg -gpg2 -gpg-agent -gpgconf -gpg-connect-agent -gpg-error -gpgme-json -gpgparsemail -gpgsplit -gpgv -gpgv2 -gpg-wks-server -gpg-zip -gprof -gpx -grep -groupadd -groupdel -groupmems -groupmod -groups -grpck -grpconv -grpunconv -gsettings -gtar -gunzip -gz -gzexe -gzip -h -halt -hardlink -hash -head -heic -help -hexdump -history -home -hostid -hostname -hostnamectl -hqx -htm -html -http -https -hwclock -i386 -icns -ico -iconv -iconvconfig -iconvconfig.x86_64 -ics -id -idn -if -ifenslave -iff -igawk -in -indd -info -infocmp -infokey -infotocap -ini -init -initctl -insmod -install -install-info -installkernel -integer -invoke-rc.d -ionice -ip -ipcmk -ipcrm -ipcs -ir -ischroot -iso -isosize -it -jar -java -jobs -join -journalctl -jpg -jq -js -json -jsp -kernel-install -key -keychain -kill -killall5 -kml -kmod -kmz -kpartx -ksp -kss -kwd -last -lastb -lastlog -lchage -lchfn -lchsh -ld -ldattach -ld.bfd -ldconfig -ldconfig.real -ldd -ld.gold -let -lgroupadd -lgroupdel -lgroupmod -lib -lib64 -lid -limit -link -linux32 -linux64 -ln -lnewusers -lnk -lnstat -local -locale -locale-check -localectl -localedef -localhost -log -logger -login -loginctl -logname -logout -logsave -look -losetup -lost+found -lpasswd -ls -lsattr -lsblk -lscpu -lsinitrd -lsipc -lslocks -lslogins -lsmem -lsmod -lsns -lua -luac -luseradd -luserdel -lusermod -lz4 -lz4c -lz4cat -m -m3u -m4a -m4p -m4v -machinectl -make -makedb -makedeltarpm -make-dummy-cert -Makefile -man -mapfile -master -mawk -max -mcookie -md5 -md5sum -md5sums -md5sum.textutils -mdb -mdf -media -merge -mesg -mid -mim -mkdict -mkdir -mke2fs -mkfifo -mkfs -mkfs.bfs -mkfs.cramfs -mkfs.ext2 -mkfs.ext3 -mkfs.ext4 -mkfs.minix -mkhomedir_helper -mkinitrd -mklost+found -mknod -mkpasswd -mkswap -mktemp -mnt -mo -modinfo -modprobe -modulemd-validator-v1 -modutil -more -mount -mountpoint -mov -mp3 -mp4 -mpa -mpg -msg -msi -mv -namei -nawk -needs-restarting -nes -net -networkctl -newgidmap -newgrp -newuidmap -newusers -nice -nisdomainname -nl -nm -no -nocorrect -noglob -nohup -nologin -nproc -nsenter -nstat -numfmt -o -obj -objcopy -objdump -od -odt -ogg -oldfind -openssl -opt -org -origin -otf -p11-kit -package-cleanup -packer -pager -pages -pam-auth-update -pam_console_apply -pam_extrausers_chkpwd -pam_extrausers_update -pam_getenv -pam_tally -pam_tally2 -pam_timestamp_check -part -partx -passwd -paste -patch -pathchk -pct -pdb -pdf -perl -perl5.26.1 -perl5.28.1 -pgawk -pgrep -php -phps -phtml -pidof -pinentry -pinentry-curses -ping -ping4 -ping6 -pinky -pip-3 -pip3 -pip-3.7 -pip3.7 -pivot_root -pk12util -pkg -pkg-config -pkill -pkl -pl -pldd -pls -plugin -pmap -png -policy-rc.d -popd -portablectl -pov -poweroff -pps -ppt -pptx -pr -prf -print -printenv -printf -private -prlimit -proc -properties -ps -psd -pspimage -ptx -pull -push -pushd -pushln -pwck -pwconv -pwd -pwdx -pwhistory_helper -pwmake -pwscore -pwunconv -py -pyc -pydoc -pydoc3 -pydoc3.7 -pyo -python -python2 -python2.7 -python3 -python3.7 -python3.7m -pyvenv -pyvenv-3.7 -r -ranlib -rar -raw -rbash -rc -rdf -rdisc -rdma -read -readarray -readelf -readlink -readonly -readprofile -realpath -rebase -reboot -rehash -remove-shell -rename -rename.ul -renew-dummy-cert -renice -repeat -repoclosure -repodiff -repo-graph -repomanage -repoquery -repo-rss -reposync -repotrack -reset -resh -resize2fs -resizepart -resolvconf -resolvectl -return -rev -rfkill -rgrep -rm -rmdir -rmmod -rmt -rmt-tar -rom -root -routef -routel -rpcgen -rpm -rpm2archive -rpm2cpio -rpmdb -rpmdumpheader -rpmkeys -rpmquery -rpmverify -rss -rtacct -rtcwake -rtf -rtmon -rtstat -ru -run -runcon -run-help -runlevel -run-parts -runuser -rvi -rview -s -sasldblistusers2 -saslpasswd2 -sav -savelog -sbin -sched -script -scriptreplay -sdf -sdiff -sed -sefcontext_compile -select -select-editor -sensible-browser -sensible-editor -sensible-pager -seq -service -set -setarch -setcap -setfacl -setopt -setpriv -setsid -setterm -setup-nsssysinit -setup-nsssysinit.sh -sfdisk -sg -sh -sha1sum -sha224sum -sha256sum -sha384sum -sha512sum -shadowconfig -share -sh.distrib -shift -shopt -show -show-changed-rco -show-installed -shred -shuf -shutdown -signtool -signver -sitx -size -skill -slabtop -sleep -sln -snice -so -sort -sotruss -source -split -sprof -sql -sqlite3 -srt -srv -ss -ssh -ssltap -start-stop-daemon -stat -status -stdbuf -strings -strip -stty -su -sudo -sudoedit -sudoreplay -sulogin -sum -suspend -svg -swaplabel -swapoff -swapon -swf -swift -switch_root -sync -sys -sysctl -systemctl -systemd-analyze -systemd-ask-password -systemd-cat -systemd-cgls -systemd-cgtop -systemd-coredumpctl -systemd-delta -systemd-detect-virt -systemd-escape -systemd-firstboot -systemd-hwdb -systemd-id128 -systemd-inhibit -systemd-loginctl -systemd-machine-id-setup -systemd-mount -systemd-notify -systemd-nspawn -systemd-path -systemd-resolve -systemd-run -systemd-socket-activate -systemd-stdio-bridge -systemd-sysusers -systemd-tmpfiles -systemd-tty-ask-password-agent -systemd-umount -tabs -tac -tag -tail -tailf -tar -tarcat -taskset -tax2016 -tax2018 -tc -tee -telinit -tempfile -test -testgdbm -tex -tga -tgz -then -thm -tic -tif -tiff -tig -time -timedatectl -timeout -times -tipc -tload -tmp -toast -toe -top -torrent -touch -tput -tr -tracepath -tracepath6 -trap -true -truncate -trust -tset -tsort -ttf -tty -ttyctl -tune2fs -txt -type -typeset -tzconfig -tzselect -udevadm -uk -ul -ulimit -umask -umount -unalias -uname -uname26 -unbound-anchor -uncompress -unexpand -unfunction -unhash -uniq -unix_chkpwd -unix_update -unlimit -unlink -unlz4 -unminimize -unset -unsetopt -unshare -until -unxz -update-alternatives -update-ca-trust -update-crypto-policies -update-mime-database -update-passwd -update-rc.d -uptime -urlgrabber -useradd -userdel -usermod -users -usr -utmpdump -uue -uuidgen -uuidparse -Vagrantfile -var -vared -vb -vcd -vcf -vcxproj -vdir -verifytree -vi -view -vigr -vim -vipw -visudo -vlc -vmstat -vob -w -wait -wall -watch -watchgnupg -wav -wc -wdctl -weak-modules -whence -where -whereis -which -which-command -while -who -whoami -wipefs -wma -wmv -wpd -w.procps -wps -write -wsf -x86_64 -xargs -xbel -xcodeproj -xhtml -xlr -xls -xlsx -xml -xmlcatalog -xmllint -xmlwf -xpm -xsd -xsl -xz -xzcat -xzcmp -xzdec -xzdiff -xzegrep -xzfgrep -xzgrep -xzless -xzmore -yaourt -yes -ypdomainname -yum -yum-builddep -yum-complete-transaction -yum-config-manager -yumdb -yum-debug-dump -yum-debug-restore -yumdownloader -yum-groups-manager -yuv -Z -zcat -zcmp -zcompile -zdiff -zdump -zegrep -zfgrep -zforce -zformat -zgrep -zic -zip -zipx -zle -zless -zmodload -zmore -znew -zparseopts -zramctl -zregexparse -zsh -zstyle diff --git a/go.mod b/go.mod index 0ca87cda..9ba16a5d 100644 --- a/go.mod +++ b/go.mod @@ -1,23 +1,30 @@ module github.com/curusarn/resh -go 1.16 +go 1.19 require ( - github.com/BurntSushi/toml v0.4.1 - github.com/awesome-gocui/gocui v1.0.0 - github.com/coreos/go-semver v0.3.0 - github.com/gdamore/tcell/v2 v2.4.0 // indirect - github.com/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629 - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect - github.com/mattn/go-shellwords v1.0.12 - github.com/mb-14/gomarkov v0.0.0-20210216094942-a5b484cc0243 + github.com/BurntSushi/toml v1.2.1 + github.com/awesome-gocui/gocui v1.1.0 + github.com/google/uuid v1.3.0 + github.com/mattn/go-isatty v0.0.17 github.com/mitchellh/go-ps v1.0.0 - github.com/schollz/progressbar v1.0.0 - github.com/spf13/cobra v1.2.1 + github.com/spf13/cobra v1.6.1 + github.com/spf13/pflag v1.0.5 github.com/whilp/git-urls v1.0.0 - golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 - golang.org/x/sys v0.0.0-20210903071746-97244b99971b // indirect - golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect - golang.org/x/text v0.3.7 // indirect + go.uber.org/zap v1.24.0 + golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 +) + +require ( + github.com/gdamore/encoding v1.0.0 // indirect + github.com/gdamore/tcell/v2 v2.6.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/term v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect ) diff --git a/go.sum b/go.sum index 60670d55..6f8fb611 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -47,6 +49,10 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/awesome-gocui/gocui v1.0.0 h1:1bf0DAr2JqWNxGFS8Kex4fM/khICjEnCi+a1+NfWy+w= github.com/awesome-gocui/gocui v1.0.0/go.mod h1:UvP3dP6+UsTGl9IuqP36wzz6Lemo90wn5p3tJvZ2OqY= +github.com/awesome-gocui/gocui v1.1.0 h1:db2j7yFEoHZjpQFeE2xqiatS8bm1lO3THeLwE6MzOII= +github.com/awesome-gocui/gocui v1.1.0/go.mod h1:M2BXkrp7PR97CKnPRT7Rk0+rtswChPtksw/vRAESGpg= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -57,11 +63,12 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -77,6 +84,8 @@ github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo github.com/gdamore/tcell/v2 v2.0.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA= github.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM= github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= +github.com/gdamore/tcell/v2 v2.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCySg= +github.com/gdamore/tcell/v2 v2.6.0/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -141,7 +150,10 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -170,8 +182,9 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629 h1:1dSBUfGlorLAua2CRx0zFN7kQsTpE2DQSmr7rrTNgY8= -github.com/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629/go.mod h1:mb5nS4uRANwOJSZj8rlCWAfAcGi72GGMIXx+xGOjA7M= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -187,16 +200,17 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= -github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= -github.com/mb-14/gomarkov v0.0.0-20210216094942-a5b484cc0243 h1:F0IAcxxFNzC8/HOxI5Q2hpsWAoGdy+lGMjoVyrcMeSw= -github.com/mb-14/gomarkov v0.0.0-20210216094942-a5b484cc0243/go.mod h1:5F3Y03oxWIyMq3Wa4AxU544RYnXNZwHBfqpDpdLibBY= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -211,23 +225,26 @@ github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/montanaflynn/stats v0.6.3/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -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/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -236,6 +253,8 @@ github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -246,7 +265,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/whilp/git-urls v1.0.0 h1:95f6UMWN5FKW71ECsXRUd3FVYiXdrE7aX4NZKcPmIjU= github.com/whilp/git-urls v1.0.0/go.mod h1:J16SAmobsqc3Qcy98brfl5f5+e0clUvg1krgwk/qCfE= @@ -255,6 +276,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= @@ -265,9 +287,21 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -275,6 +309,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -286,6 +321,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI= +golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -311,6 +348,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -347,6 +385,7 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -370,6 +409,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -415,10 +455,18 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210903071746-97244b99971b h1:3Dq0eVHn0uaQJmPO+/aYPI/fRMqdrVDbu7MQcku54gg= golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -428,6 +476,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -482,6 +532,8 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -598,7 +650,10 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/installation.md b/installation.md new file mode 100644 index 00000000..2ddf8165 --- /dev/null +++ b/installation.md @@ -0,0 +1,77 @@ +# Installation + +## One command installation + +Feel free to check the `rawinstall.sh` script before running it. + +```sh +curl -fsSL https://raw.githubusercontent.com/curusarn/resh/master/scripts/rawinstall.sh | sh +``` + +ℹ️ You will need to have `curl` and `tar` installed. + +## Clone & install + +```sh +git clone https://github.com/curusarn/resh.git +cd resh +scripts/rawinstall.sh +``` + +## Build from source + +:warning: Building from source is intended for development and troubleshooting. + +```sh +git clone https://github.com/curusarn/resh.git +cd resh +make install +``` + +## Update + +Once installed RESH can be updated using: +```sh +reshctl update +``` + +## Disabling RESH + +If you have a persistent issue with RESH you can temporarily disable it and then enable it later. + +ℹ️ You won't lose your history nor configuration. + +Go to `~/.zshrc` and `~/.bashrc` and comment out following lines: +```sh +[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc +[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh # bashrc only +``` +The second line is bash-specific so you won't find it in `~/.zshrc` + +You can re-enable RESH by uncommenting the lines above or by re-installing it. + +## Uninstallation + +You can uninstall RESH by running: `rm -rf ~/.resh/`. + +⚠️ Restart all open terminals after uninstall! + +### Installed files + +Binaries and shell files are in `~/.resh/`. + +Recorded history, device files, and logs are in one of: +- `~/.local/share/resh/` +- `$XDG_DATA_HOME/resh/` (if set) + +RESH config file is read from one of: +- `~/.config/resh.toml` +- `$XDG_CONFIG_HOME/resh.toml` (if set) + +RESH also adds a following lines to `~/.zshrc` and `~/.bashrc` to load itself on terminal startup: +```sh +[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc +[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh # bashrc only +``` + +:information_source: RESH follows [XDG directory specification ⇗](https://maex.me/2019/12/the-power-of-the-xdg-base-directory-specification/) diff --git a/internal/cfg/cfg.go b/internal/cfg/cfg.go new file mode 100644 index 00000000..534443d4 --- /dev/null +++ b/internal/cfg/cfg.go @@ -0,0 +1,191 @@ +package cfg + +import ( + "fmt" + "os" + "path" + + "github.com/BurntSushi/toml" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// configFile used to parse the config file +type configFile struct { + // ConfigVersion - never remove this + ConfigVersion *string + + // added in legacy + Port *int + SesswatchPeriodSeconds *uint + SesshistInitHistorySize *int + BindControlR *bool + Debug *bool + + // added in v1 + LogLevel *string + + // added in legacy + // deprecated in v1 + BindArrowKeysBash *bool + BindArrowKeysZsh *bool +} + +// Config returned by this package to be used in the rest of the project +type Config struct { + // Port used by daemon and rest of the components to communicate + // Make sure to restart the daemon when you change it + Port int + + // BindControlR causes CTRL+R to launch the search app + BindControlR bool + // LogLevel used to filter logs + LogLevel zapcore.Level + + // Debug mode for search app + Debug bool + // SessionWatchPeriodSeconds is how often should daemon check if terminal + // sessions are still alive + // There is not much need to adjust the value because both memory overhead of watched sessions + // and the CPU overhead of checking them are quite low + SessionWatchPeriodSeconds uint + // ReshHistoryMinSize is how large resh history needs to be for + // daemon to ignore standard shell history files + // Ignoring standard shell history gives us more consistent experience + // but you can increase this to something large to see standard shell history in RESH search + ReshHistoryMinSize int +} + +// defaults for config +var defaults = Config{ + Port: 2627, + LogLevel: zap.InfoLevel, + BindControlR: true, + + Debug: false, + SessionWatchPeriodSeconds: 600, + ReshHistoryMinSize: 1000, +} + +const headerComment = `## +###################### +## RESH config (v1) ## +###################### +## Here you can find info about RESH configuration options. +## You can uncomment the options and customize them. + +## Required. +## The config format can change in future versions. +## ConfigVersion helps us seamlessly upgrade to the new formats. +# ConfigVersion = "v1" + +## Port used by RESH daemon and rest of the components to communicate. +## Make sure to restart the daemon (pkill resh-daemon) when you change it. +# Port = 2627 + +## Controls how much and how detailed logs all RESH components produce. +## Use "debug" for full logs when you encounter an issue +## Options: "debug", "info", "warn", "error", "fatal" +# LogLevel = "info" + +## When BindControlR is "true" RESH search app is bound to CTRL+R on terminal startup +# BindControlR = true + +## When Debug is "true" the RESH search app runs in debug mode. +## This is useful for development. +# Debug = false + +## Daemon keeps track of running terminal sessions. +## SessionWatchPeriodSeconds controls how often daemon checks if the sessions are still alive. +## You shouldn't need to adjust this. +# SessionWatchPeriodSeconds = 600 + +## When RESH is first installed there is no RESH history so there is nothing to search. +## As a temporary workaround, RESH daemon parses bash/zsh shell history and searches it. +## Once RESH history is big enough RESH stops using bash/zsh history. +## ReshHistoryMinSize controls how big RESH history needs to be before this happens. +## You can increase this this to e.g. 10000 to get RESH to use bash/zsh history longer. +# ReshHistoryMinSize = 1000 + +` + +func getConfigPath() (string, error) { + fname := "resh.toml" + xdgDir, found := os.LookupEnv("XDG_CONFIG_HOME") + if found { + return path.Join(xdgDir, fname), nil + } + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("could not get user home dir: %w", err) + } + return path.Join(homeDir, ".config", fname), nil +} + +func readConfig(path string) (*configFile, error) { + var config configFile + if _, err := toml.DecodeFile(path, &config); err != nil { + return &config, fmt.Errorf("could not decode config: %w", err) + } + return &config, nil +} + +func getConfig() (*configFile, error) { + path, err := getConfigPath() + if err != nil { + return nil, fmt.Errorf("could not get config file path: %w", err) + } + return readConfig(path) +} + +// returned config is always usable, returned errors are informative +func processAndFillDefaults(configF *configFile) (Config, error) { + config := defaults + + if configF.Port != nil { + config.Port = *configF.Port + } + if configF.SesswatchPeriodSeconds != nil { + config.SessionWatchPeriodSeconds = *configF.SesswatchPeriodSeconds + } + if configF.SesshistInitHistorySize != nil { + config.ReshHistoryMinSize = *configF.SesshistInitHistorySize + } + + var err error + if configF.LogLevel != nil { + logLevel, err := zapcore.ParseLevel(*configF.LogLevel) + if err != nil { + err = fmt.Errorf("could not parse log level: %w", err) + } else { + config.LogLevel = logLevel + } + } + + if configF.BindControlR != nil { + config.BindControlR = *configF.BindControlR + } + + return config, err +} + +// New returns a config file +// returned config is always usable, returned errors are informative +func New() (Config, error) { + configF, err := getConfig() + if err != nil { + return defaults, fmt.Errorf("using default config because of error while getting/reading config: %w", err) + } + + config, err := processAndFillDefaults(configF) + if err != nil { + return config, fmt.Errorf("errors while processing config: %w", err) + } + return config, nil +} + +// GetPath returns path to config +// Shouldn't be necessary for basic use +func GetPath() (string, error) { + return getConfigPath() +} diff --git a/internal/cfg/migrate.go b/internal/cfg/migrate.go new file mode 100644 index 00000000..bc92af39 --- /dev/null +++ b/internal/cfg/migrate.go @@ -0,0 +1,100 @@ +package cfg + +import ( + "fmt" + "os" + + "github.com/BurntSushi/toml" +) + +// Migrate old config versions to current config version +// returns true if any changes were made to the config +func Migrate() (bool, error) { + fpath, err := getConfigPath() + if err != nil { + return false, fmt.Errorf("could not get config file path: %w", err) + } + configF, err := readConfig(fpath) + if err != nil { + return false, fmt.Errorf("could not read config: %w", err) + } + const current = "v1" + if configF.ConfigVersion != nil && *configF.ConfigVersion == current { + return false, nil + } + + if configF.ConfigVersion == nil { + configF, err = legacyToV1(configF) + if err != nil { + return true, fmt.Errorf("error converting config from version 'legacy' to 'v1': %w", err) + } + } + + if *configF.ConfigVersion != current { + return false, fmt.Errorf("unrecognized config version: '%s'", *configF.ConfigVersion) + } + err = writeConfig(configF, fpath) + if err != nil { + return true, fmt.Errorf("could not write migrated config: %w", err) + } + return true, nil +} + +// writeConfig should only be used when migrating config to new version +// writing the config file discards all comments in the config file (limitation of TOML library) +// to make up for lost comments we add header comment to the start of the file +func writeConfig(config *configFile, path string) error { + file, err := os.OpenFile(path, os.O_RDWR|os.O_TRUNC, 0666) + if err != nil { + return fmt.Errorf("could not open config for writing: %w", err) + } + defer file.Close() + _, err = file.WriteString(headerComment) + if err != nil { + return fmt.Errorf("could not write config header: %w", err) + } + err = toml.NewEncoder(file).Encode(config) + if err != nil { + return fmt.Errorf("could not encode config: %w", err) + } + return nil +} + +func legacyToV1(config *configFile) (*configFile, error) { + if config.ConfigVersion != nil { + return nil, fmt.Errorf("config version is not 'legacy': '%s'", *config.ConfigVersion) + } + version := "v1" + newConf := configFile{ + ConfigVersion: &version, + } + // Remove defaults + if config.Port != nil && *config.Port != 2627 { + newConf.Port = config.Port + } + if config.SesswatchPeriodSeconds != nil && *config.SesswatchPeriodSeconds != 120 { + newConf.SesswatchPeriodSeconds = config.SesswatchPeriodSeconds + } + if config.SesshistInitHistorySize != nil && *config.SesshistInitHistorySize != 1000 { + newConf.SesshistInitHistorySize = config.SesshistInitHistorySize + } + if config.BindControlR != nil && *config.BindControlR != true { + newConf.BindControlR = config.BindControlR + } + if config.Debug != nil && *config.Debug != false { + newConf.Debug = config.Debug + } + return &newConf, nil +} + +// func v1ToV2(config *configFile) (*configFile, error) { +// if *config.ConfigVersion != "v1" { +// return nil, fmt.Errorf("config version is not 'legacy': '%s'", *config.ConfigVersion) +// } +// version := "v2" +// newConf := configFile{ +// ConfigVersion: &version, +// // Here goes all config fields - no need to prune defaults like we do for legacy +// } +// return &newConf, nil +// } diff --git a/internal/check/check.go b/internal/check/check.go new file mode 100644 index 00000000..dab32634 --- /dev/null +++ b/internal/check/check.go @@ -0,0 +1,92 @@ +package check + +import ( + "fmt" + "os" + "os/exec" + "strconv" + "strings" +) + +func LoginShell() (string, error) { + shellPath, found := os.LookupEnv("SHELL") + if !found { + return "", fmt.Errorf("env variable $SHELL is not set") + } + parts := strings.Split(shellPath, "/") + shell := parts[len(parts)-1] + if shell != "bash" && shell != "zsh" { + return fmt.Sprintf("Current shell (%s) is unsupported\n", shell), nil + } + return "", nil +} + +func msgShellVersion(shell, expectedVer, actualVer string) string { + return fmt.Sprintf( + "Minimal supported %s version is %s. You have %s.\n"+ + " -> Update to %s %s+ if you want to use RESH with it", + shell, expectedVer, actualVer, + shell, expectedVer, + ) +} + +func BashVersion() (string, error) { + out, err := exec.Command("bash", "-c", "echo $BASH_VERSION").Output() + if err != nil { + return "", fmt.Errorf("command failed: %w", err) + } + verStr := strings.TrimSuffix(string(out), "\n") + ver, err := parseVersion(verStr) + if err != nil { + return "", fmt.Errorf("failed to parse version: %w", err) + } + + if ver.Major < 4 || (ver.Major == 4 && ver.Minor < 3) { + return msgShellVersion("bash", "4.3", verStr), nil + } + return "", nil +} + +func ZshVersion() (string, error) { + out, err := exec.Command("zsh", "-c", "echo $ZSH_VERSION").Output() + if err != nil { + return "", fmt.Errorf("command failed: %w", err) + } + verStr := strings.TrimSuffix(string(out), "\n") + ver, err := parseVersion(string(out)) + if err != nil { + return "", fmt.Errorf("failed to parse version: %w", err) + } + + if ver.Major < 5 { + return msgShellVersion("zsh", "5.0", verStr), nil + } + return "", nil +} + +type version struct { + Major int + Minor int + Rest string +} + +func parseVersion(str string) (version, error) { + parts := strings.SplitN(str, ".", 3) + if len(parts) < 3 { + return version{}, fmt.Errorf("not enough parts") + } + major, err := strconv.Atoi(parts[0]) + if err != nil { + return version{}, fmt.Errorf("failed to parse major version: %w", err) + } + minor, err := strconv.Atoi(parts[1]) + if err != nil { + return version{}, fmt.Errorf("failed to parse minor version: %w", err) + } + ver := version{ + Major: major, + Minor: minor, + Rest: parts[2], + } + return ver, nil +} diff --git a/internal/collect/collect.go b/internal/collect/collect.go new file mode 100644 index 00000000..1d215cf6 --- /dev/null +++ b/internal/collect/collect.go @@ -0,0 +1,116 @@ +package collect + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/curusarn/resh/internal/output" + "github.com/curusarn/resh/internal/recordint" + "go.uber.org/zap" +) + +// SendRecord to daemon +func SendRecord(out *output.Output, r recordint.Collect, port, path string) { + out.Logger.Debug("Sending record ...", + zap.String("cmdLine", r.Rec.CmdLine), + zap.String("sessionID", r.SessionID), + ) + recJSON, err := json.Marshal(r) + if err != nil { + out.FatalE("Error while encoding record", err) + } + + req, err := http.NewRequest("POST", "http://localhost:"+port+path, + bytes.NewBuffer(recJSON)) + if err != nil { + out.FatalE("Error while sending record", err) + } + req.Header.Set("Content-Type", "application/json") + + client := http.Client{ + Timeout: 1 * time.Second, + } + _, err = client.Do(req) + if err != nil { + out.FatalDaemonNotRunning(err) + } +} + +// SendSessionInit to daemon +func SendSessionInit(out *output.Output, r recordint.SessionInit, port string) { + out.Logger.Debug("Sending session init ...", + zap.String("sessionID", r.SessionID), + zap.Int("sessionPID", r.SessionPID), + ) + recJSON, err := json.Marshal(r) + if err != nil { + out.FatalE("Error while encoding record", err) + } + + req, err := http.NewRequest("POST", "http://localhost:"+port+"/session_init", + bytes.NewBuffer(recJSON)) + if err != nil { + out.FatalE("Error while sending record", err) + } + req.Header.Set("Content-Type", "application/json") + + client := http.Client{ + Timeout: 1 * time.Second, + } + _, err = client.Do(req) + if err != nil { + out.FatalDaemonNotRunning(err) + } +} + +// ReadFileContent and return it as a string +func ReadFileContent(logger *zap.Logger, path string) string { + dat, err := ioutil.ReadFile(path) + if err != nil { + logger.Error("Error reading file", + zap.String("filePath", path), + zap.Error(err), + ) + return "" + } + return strings.TrimSuffix(string(dat), "\n") +} + +// GetGitDirs based on result of git "cdup" command +func GetGitDirs(logger *zap.Logger, 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 { + logger.Error("Error while handling git dir paths", zap.Error(err)) + return "", "" + } + return absPath, realPath +} + +// GetTimezoneOffsetInSeconds based on zone returned by date command +func GetTimezoneOffsetInSeconds(logger *zap.Logger, zone string) float64 { + // date +%z -> "+0200" + hoursStr := zone[:3] + minsStr := zone[3:] + hours, err := strconv.Atoi(hoursStr) + if err != nil { + logger.Error("Error while parsing hours in timezone offset", zap.Error(err)) + return -1 + } + mins, err := strconv.Atoi(minsStr) + if err != nil { + logger.Error("Errot while parsing minutes in timezone offset:", zap.Error(err)) + return -1 + } + secs := ((hours * 60) + mins) * 60 + return float64(secs) +} diff --git a/internal/datadir/datadir.go b/internal/datadir/datadir.go new file mode 100644 index 00000000..fea36f86 --- /dev/null +++ b/internal/datadir/datadir.go @@ -0,0 +1,36 @@ +package datadir + +import ( + "fmt" + "os" + "path" +) + +// Maybe there is a better place for this constant +const HistoryFileName = "history.reshjson" + +func GetPath() (string, error) { + reshDir := "resh" + xdgDir, found := os.LookupEnv("XDG_DATA_HOME") + if found { + return path.Join(xdgDir, reshDir), nil + } + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("error while getting home dir: %w", err) + } + return path.Join(homeDir, ".local/share/", reshDir), nil +} + +func MakePath() (string, error) { + path, err := GetPath() + if err != nil { + return "", err + } + err = os.MkdirAll(path, 0755) + // skip "exists" error + if err != nil && !os.IsExist(err) { + return "", fmt.Errorf("error while creating directories: %w", err) + } + return path, nil +} diff --git a/internal/device/device.go b/internal/device/device.go new file mode 100644 index 00000000..1e9fdab9 --- /dev/null +++ b/internal/device/device.go @@ -0,0 +1,145 @@ +// device implements helpers that get/set device config files +package device + +import ( + "bufio" + "fmt" + "os" + "path" + "strings" + + "github.com/curusarn/resh/internal/futil" + "github.com/curusarn/resh/internal/output" + "github.com/google/uuid" + isatty "github.com/mattn/go-isatty" +) + +const fnameID = "device-id" +const fnameName = "device-name" + +const fpathIDLegacy = ".resh/resh-uuid" + +const filePerm = 0644 + +// Getters + +func GetID(dataDir string) (string, error) { + return readValue(dataDir, fnameID) +} + +func GetName(dataDir string) (string, error) { + return readValue(dataDir, fnameName) +} + +// Install helpers + +func SetupID(dataDir string) error { + return setIDIfUnset(dataDir) +} + +func SetupName(out *output.Output, dataDir string) error { + return promptAndWriteNameIfUnset(out, dataDir) +} + +func readValue(dataDir, fname string) (string, error) { + fpath := path.Join(dataDir, fname) + dat, err := os.ReadFile(fpath) + if err != nil { + return "", fmt.Errorf("could not read file with %s: %w", fname, err) + } + val := strings.TrimRight(string(dat), "\n") + return val, nil +} + +func setIDIfUnset(dataDir string) error { + fpath := path.Join(dataDir, fnameID) + exists, err := futil.FileExists(fpath) + if err != nil { + return err + } + if exists { + return nil + } + + // Try copy device ID from legacy location + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("could not get user home: %w", err) + } + fpathLegacy := path.Join(homeDir, fpathIDLegacy) + exists, err = futil.FileExists(fpath) + if err != nil { + return err + } + if exists { + futil.CopyFile(fpathLegacy, fpath) + if err != nil { + return fmt.Errorf("could not copy device ID from legacy location: %w", err) + } + return nil + } + + // Generate new device ID + rnd, err := uuid.NewRandom() + if err != nil { + return fmt.Errorf("could not get new random source: %w", err) + } + id := rnd.String() + if id == "" { + return fmt.Errorf("got invalid UUID from package") + } + err = os.WriteFile(fpath, []byte(id), filePerm) + if err != nil { + return fmt.Errorf("could not write generated ID to file: %w", err) + } + return nil +} + +func promptAndWriteNameIfUnset(out *output.Output, dataDir string) error { + fpath := path.Join(dataDir, fnameName) + exists, err := futil.FileExists(fpath) + if err != nil { + return err + } + if exists { + return nil + } + + name, err := promptForName(out, fpath) + if err != nil { + return fmt.Errorf("error while prompting for input: %w", err) + } + err = os.WriteFile(fpath, []byte(name), filePerm) + if err != nil { + return fmt.Errorf("could not write name to file: %w", err) + } + return nil +} + +func promptForName(out *output.Output, fpath string) (string, error) { + // This function should be only ran from install-utils with attached terminal + if !isatty.IsTerminal(os.Stdout.Fd()) { + return "", fmt.Errorf("output is not a terminal - write name of this device to '%s' to bypass this error", fpath) + } + host, err := os.Hostname() + if err != nil { + return "", fmt.Errorf("could not get hostname (prompt default): %w", err) + } + hostStub := strings.Split(host, ".")[0] + fmt.Printf("\nPlease choose a short name for this device (default: '%s'): ", hostStub) + var input string + scanner := bufio.NewScanner(os.Stdin) + if scanner.Scan() { + input = scanner.Text() + } + if err = scanner.Err(); err != nil { + return "", fmt.Errorf("scanner error: %w", err) + } + if input == "" { + out.Info("Got no input - using default ...") + input = hostStub + } + out.Info(fmt.Sprintf("Device name set to '%s'", input)) + fmt.Printf("You can change the device name at any time by editing '%s' file\n", fpath) + return input, nil +} diff --git a/internal/epochtime/epochtime.go b/internal/epochtime/epochtime.go new file mode 100644 index 00000000..82d4dbc0 --- /dev/null +++ b/internal/epochtime/epochtime.go @@ -0,0 +1,14 @@ +package epochtime + +import ( + "fmt" + "time" +) + +func TimeToString(t time.Time) string { + return fmt.Sprintf("%.2f", float64(t.UnixMilli())/1000) +} + +func Now() string { + return TimeToString(time.Now()) +} diff --git a/internal/epochtime/epochtime_test.go b/internal/epochtime/epochtime_test.go new file mode 100644 index 00000000..596ab132 --- /dev/null +++ b/internal/epochtime/epochtime_test.go @@ -0,0 +1,18 @@ +package epochtime + +import ( + "strconv" + "testing" + "time" +) + +func TestConversion(t *testing.T) { + epochTime := "1672702332.64" + seconds, err := strconv.ParseFloat(epochTime, 64) + if err != nil { + t.Fatal("Test setup failed: Failed to convert constant") + } + if TimeToString(time.UnixMilli(int64(seconds*1000))) != epochTime { + t.Fatal("EpochTime changed during conversion") + } +} diff --git a/internal/futil/futil.go b/internal/futil/futil.go new file mode 100644 index 00000000..4cdbc249 --- /dev/null +++ b/internal/futil/futil.go @@ -0,0 +1,113 @@ +// futil implements common file-related utilities +package futil + +import ( + "fmt" + "io" + "os" + "time" +) + +func CopyFile(source, dest string) error { + from, err := os.Open(source) + if err != nil { + return err + } + defer from.Close() + + // This is equivalent to: os.OpenFile(dest, os.O_RDWR|os.O_CREATE, 0666) + to, err := os.Create(dest) + if err != nil { + return err + } + + _, err = io.Copy(to, from) + if err != nil { + return err + } + return to.Close() +} + +func FileExists(fpath string) (bool, error) { + _, err := os.Stat(fpath) + if err == nil { + // File exists + return true, nil + } + if os.IsNotExist(err) { + // File doesn't exist + return false, nil + } + // Any other error + return false, fmt.Errorf("could not stat file: %w", err) +} + +// TouchFile touches file +// Returns true if file was created false otherwise +func TouchFile(fpath string) (bool, error) { + exists, err := FileExists(fpath) + if err != nil { + return false, err + } + + file, err := os.OpenFile(fpath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666) + if err != nil { + return false, fmt.Errorf("could not open/create file: %w", err) + } + err = file.Close() + if err != nil { + return false, fmt.Errorf("could not close file: %w", err) + } + return !exists, nil +} + +func getBackupPath(fpath string) string { + ext := fmt.Sprintf(".backup-%d", time.Now().Unix()) + return fpath + ext +} + +// BackupFile backups file using unique suffix +// Returns path to backup +func BackupFile(fpath string) (*RestorableFile, error) { + fpathBackup := getBackupPath(fpath) + exists, err := FileExists(fpathBackup) + if err != nil { + return nil, err + } + if exists { + return nil, fmt.Errorf("backup already exists in the determined path") + } + err = CopyFile(fpath, fpathBackup) + if err != nil { + return nil, fmt.Errorf("failed to copy file: %w ", err) + } + rf := RestorableFile{ + Path: fpath, + PathBackup: fpathBackup, + } + return &rf, nil +} + +type RestorableFile struct { + Path string + PathBackup string +} + +func (r RestorableFile) Restore() error { + return restoreFileFromBackup(r.Path, r.PathBackup) +} + +func restoreFileFromBackup(fpath, fpathBak string) error { + exists, err := FileExists(fpathBak) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("backup not found in given path: no such file or directory: %s", fpathBak) + } + err = CopyFile(fpathBak, fpath) + if err != nil { + return fmt.Errorf("failed to copy file: %w ", err) + } + return nil +} diff --git a/pkg/histcli/histcli.go b/internal/histcli/histcli.go similarity index 50% rename from pkg/histcli/histcli.go rename to internal/histcli/histcli.go index f9b66114..0b75decf 100644 --- a/pkg/histcli/histcli.go +++ b/internal/histcli/histcli.go @@ -1,31 +1,34 @@ package histcli import ( - "github.com/curusarn/resh/pkg/records" + "github.com/curusarn/resh/internal/recordint" + "github.com/curusarn/resh/record" + "go.uber.org/zap" ) // Histcli is a dump of history preprocessed for resh cli purposes type Histcli struct { // list of records - List []records.CliRecord + List []recordint.SearchApp + + sugar *zap.SugaredLogger } // New Histcli -func New() Histcli { +func New(sugar *zap.SugaredLogger) Histcli { return Histcli{} } // AddRecord to the histcli -func (h *Histcli) AddRecord(record records.Record) { - enriched := records.Enriched(record) - cli := records.NewCliRecord(enriched) +func (h *Histcli) AddRecord(rec *record.V1) { + cli := recordint.NewSearchApp(h.sugar, rec) h.List = append(h.List, cli) } // AddCmdLine to the histcli func (h *Histcli) AddCmdLine(cmdline string) { - cli := records.NewCliRecordFromCmdLine(cmdline) + cli := recordint.NewSearchAppFromCmdLine(cmdline) h.List = append(h.List, cli) } diff --git a/internal/histfile/histfile.go b/internal/histfile/histfile.go new file mode 100644 index 00000000..cdf04f93 --- /dev/null +++ b/internal/histfile/histfile.go @@ -0,0 +1,283 @@ +package histfile + +import ( + "math" + "os" + "strconv" + "sync" + + "github.com/curusarn/resh/internal/histcli" + "github.com/curusarn/resh/internal/histlist" + "github.com/curusarn/resh/internal/recio" + "github.com/curusarn/resh/internal/recordint" + "github.com/curusarn/resh/internal/records" + "github.com/curusarn/resh/internal/recutil" + "github.com/curusarn/resh/record" + "go.uber.org/zap" +) + +// TODO: get rid of histfile - use histio instead +// Histfile writes records to histfile +type Histfile struct { + sugar *zap.SugaredLogger + + sessionsMutex sync.Mutex + sessions map[string]recordint.Collect + historyPath string + + // NOTE: we have separate histories which only differ if there was not enough resh_history + // resh_history itself is common for both bash and zsh + bashCmdLines histlist.Histlist + zshCmdLines histlist.Histlist + + cliRecords histcli.Histcli + + rio *recio.RecIO +} + +// New creates new histfile and runs its goroutines +func New(sugar *zap.SugaredLogger, input chan recordint.Collect, sessionsToDrop chan string, + reshHistoryPath string, bashHistoryPath string, zshHistoryPath string, + maxInitHistSize int, minInitHistSizeKB int, + signals chan os.Signal, shutdownDone chan string) *Histfile { + + rio := recio.New(sugar.With("module", "histfile")) + hf := Histfile{ + sugar: sugar.With("module", "histfile"), + sessions: map[string]recordint.Collect{}, + historyPath: reshHistoryPath, + bashCmdLines: histlist.New(sugar), + zshCmdLines: histlist.New(sugar), + cliRecords: histcli.New(sugar), + rio: &rio, + } + go hf.loadHistory(bashHistoryPath, zshHistoryPath, maxInitHistSize, minInitHistSizeKB) + go hf.writer(input, signals, shutdownDone) + go hf.sessionGC(sessionsToDrop) + return &hf +} + +// load records from resh history, reverse, enrich and save +func (h *Histfile) loadCliRecords(recs []record.V1) { + for _, cmdline := range h.bashCmdLines.List { + h.cliRecords.AddCmdLine(cmdline) + } + for _, cmdline := range h.zshCmdLines.List { + h.cliRecords.AddCmdLine(cmdline) + } + for i := len(recs) - 1; i >= 0; i-- { + rec := recs[i] + h.cliRecords.AddRecord(&rec) + } + h.sugar.Infow("Resh history loaded", + "historyRecordsCount", len(h.cliRecords.List), + ) +} + +// loadsHistory from resh_history and if there is not enough of it also load native shell histories +func (h *Histfile) loadHistory(bashHistoryPath, zshHistoryPath string, maxInitHistSize, minInitHistSizeKB int) { + h.sugar.Infow("Checking if resh_history is large enough ...") + fi, err := os.Stat(h.historyPath) + var size int + if err != nil { + h.sugar.Errorw("Failed to stat resh_history file", "error", err) + } else { + size = int(fi.Size()) + } + useNativeHistories := false + if size/1024 < minInitHistSizeKB { + useNativeHistories = true + h.sugar.Warnw("Resh_history is too small - loading native bash and zsh history ...") + h.bashCmdLines = records.LoadCmdLinesFromBashFile(h.sugar, bashHistoryPath) + h.sugar.Infow("Bash history loaded", "cmdLineCount", len(h.bashCmdLines.List)) + h.zshCmdLines = records.LoadCmdLinesFromZshFile(h.sugar, zshHistoryPath) + h.sugar.Infow("Zsh history loaded", "cmdLineCount", len(h.zshCmdLines.List)) + // no maxInitHistSize when using native histories + maxInitHistSize = math.MaxInt32 + } + h.sugar.Debugw("Loading resh history from file ...", + "historyFile", h.historyPath, + ) + history, err := h.rio.ReadAndFixFile(h.historyPath, 3) + if err != nil { + h.sugar.Fatalf("Failed to read history file: %v", err) + } + h.sugar.Infow("Resh history loaded from file", + "historyFile", h.historyPath, + "recordCount", len(history), + ) + go h.loadCliRecords(history) + // NOTE: keeping this weird interface for now because we might use it in the future + // when we only load bash or zsh history + reshCmdLines := loadCmdLines(h.sugar, history) + h.sugar.Infow("Resh history loaded and processed", + "recordCount", len(reshCmdLines.List), + ) + if !useNativeHistories { + h.bashCmdLines = reshCmdLines + h.zshCmdLines = histlist.Copy(reshCmdLines) + return + } + h.bashCmdLines.AddHistlist(reshCmdLines) + h.sugar.Infow("Processed bash history and resh history together", "cmdLinecount", len(h.bashCmdLines.List)) + h.zshCmdLines.AddHistlist(reshCmdLines) + h.sugar.Infow("Processed zsh history and resh history together", "cmdLineCount", len(h.zshCmdLines.List)) +} + +// sessionGC reads sessionIDs from channel and deletes them from histfile struct +func (h *Histfile) sessionGC(sessionsToDrop chan string) { + for { + func() { + session := <-sessionsToDrop + sugar := h.sugar.With("sessionID", session) + sugar.Debugw("Got session to drop") + h.sessionsMutex.Lock() + defer h.sessionsMutex.Unlock() + if part1, found := h.sessions[session]; found == true { + sugar.Infow("Dropping session") + delete(h.sessions, session) + go h.rio.AppendToFile(h.historyPath, []record.V1{part1.Rec}) + } else { + sugar.Infow("No hanging parts for session - nothing to drop") + } + }() + } +} + +// writer reads records from channel, merges them and writes them to file +func (h *Histfile) writer(collect chan recordint.Collect, signals chan os.Signal, shutdownDone chan string) { + for { + func() { + select { + case rec := <-collect: + part := "2" + if rec.Rec.PartOne { + part = "1" + } + sugar := h.sugar.With( + "recordCmdLine", rec.Rec.CmdLine, + "recordPart", part, + "recordShell", rec.Shell, + ) + sugar.Debugw("Got record") + h.sessionsMutex.Lock() + defer h.sessionsMutex.Unlock() + + // allows nested sessions to merge records properly + mergeID := rec.SessionID + "_" + strconv.Itoa(rec.Shlvl) + sugar = sugar.With("mergeID", mergeID) + if rec.Rec.PartOne { + if _, found := h.sessions[mergeID]; found { + msg := "Got another first part of the records before merging the previous one - overwriting!" + if rec.Shell == "zsh" { + sugar.Warnw(msg) + } else { + sugar.Infow(msg + " Unfortunately this is normal in bash, it can't be prevented.") + } + } + h.sessions[mergeID] = rec + } else { + if part1, found := h.sessions[mergeID]; found == false { + sugar.Warnw("Got second part of record and nothing to merge it with - ignoring!") + } else { + delete(h.sessions, mergeID) + go h.mergeAndWriteRecord(sugar, part1, rec) + } + } + case sig := <-signals: + sugar := h.sugar.With( + "signal", sig.String(), + ) + sugar.Infow("Got signal") + h.sessionsMutex.Lock() + defer h.sessionsMutex.Unlock() + sugar.Debugw("Unlocked mutex") + + for sessID, rec := range h.sessions { + sugar.Warnw("Writing incomplete record for session", + "sessionID", sessID, + ) + h.writeRecord(sugar, rec.Rec) + } + sugar.Debugw("Shutdown successful") + shutdownDone <- "histfile" + return + } + }() + } +} + +func (h *Histfile) writeRecord(sugar *zap.SugaredLogger, rec record.V1) { + h.rio.AppendToFile(h.historyPath, []record.V1{rec}) +} + +func (h *Histfile) mergeAndWriteRecord(sugar *zap.SugaredLogger, part1 recordint.Collect, part2 recordint.Collect) { + rec, err := recutil.Merge(&part1, &part2) + if err != nil { + sugar.Errorw("Error while merging records", "error", err) + return + } + + recV1 := record.V1(rec) + func() { + cmdLine := rec.CmdLine + h.bashCmdLines.AddCmdLine(cmdLine) + h.zshCmdLines.AddCmdLine(cmdLine) + h.cliRecords.AddRecord(&recV1) + }() + + h.rio.AppendToFile(h.historyPath, []record.V1{recV1}) +} + +// TODO: use errors in RecIO +// func writeRecord(sugar *zap.SugaredLogger, rec record.V1, outputPath string) { +// recJSON, err := json.Marshal(rec) +// if err != nil { +// sugar.Errorw("Marshalling error", "error", err) +// return +// } +// f, err := os.OpenFile(outputPath, +// os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) +// if err != nil { +// sugar.Errorw("Could not open file", "error", err) +// return +// } +// defer f.Close() +// _, err = f.Write(append(recJSON, []byte("\n")...)) +// if err != nil { +// sugar.Errorw("Error while writing record", +// "recordRaw", rec, +// "error", err, +// ) +// return +// } +// } + +// DumpCliRecords returns enriched records +func (h *Histfile) DumpCliRecords() histcli.Histcli { + // don't forget locks in the future + return h.cliRecords +} + +func loadCmdLines(sugar *zap.SugaredLogger, recs []record.V1) histlist.Histlist { + hl := histlist.New(sugar) + // go from bottom and deduplicate + 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 + // } + } + // add everything to histlist + for _, cmdLine := range cmdLines { + hl.AddCmdLine(cmdLine) + } + return hl +} diff --git a/internal/histio/file.go b/internal/histio/file.go new file mode 100644 index 00000000..40f2544b --- /dev/null +++ b/internal/histio/file.go @@ -0,0 +1,56 @@ +package histio + +import ( + "fmt" + "os" + "sync" + + "github.com/curusarn/resh/internal/recio" + "github.com/curusarn/resh/record" + "go.uber.org/zap" +) + +type histfile struct { + sugar *zap.SugaredLogger + // deviceID string + path string + + mu sync.RWMutex + data []record.V1 + fileinfo os.FileInfo +} + +func newHistfile(sugar *zap.SugaredLogger, path string) *histfile { + return &histfile{ + sugar: sugar.With( + // FIXME: drop V1 once original histfile is gone + "component", "histfileV1", + "path", path, + ), + // deviceID: deviceID, + path: path, + } +} + +func (h *histfile) updateFromFile() error { + rio := recio.New(h.sugar) + // TODO: decide and handle errors + newData, _, err := rio.ReadFile(h.path) + if err != nil { + return fmt.Errorf("could not read history file: %w", err) + } + h.mu.Lock() + defer h.mu.Unlock() + h.data = newData + h.updateFileInfo() + return nil +} + +func (h *histfile) updateFileInfo() error { + info, err := os.Stat(h.path) + if err != nil { + return fmt.Errorf("history file not found: %w", err) + } + h.fileinfo = info + return nil +} diff --git a/internal/histio/histio.go b/internal/histio/histio.go new file mode 100644 index 00000000..001623ad --- /dev/null +++ b/internal/histio/histio.go @@ -0,0 +1,43 @@ +package histio + +import ( + "path" + + "github.com/curusarn/resh/record" + "go.uber.org/zap" +) + +type Histio struct { + sugar *zap.SugaredLogger + histDir string + + thisDeviceID string + thisHistory *histfile + // TODO: remote histories + // moreHistories map[string]*histfile + + recordsToAppend chan record.V1 + // recordsToFlag chan recordint.Flag +} + +func New(sugar *zap.SugaredLogger, dataDir, deviceID string) *Histio { + sugarHistio := sugar.With(zap.String("component", "histio")) + histDir := path.Join(dataDir, "history") + currPath := path.Join(histDir, deviceID) + // TODO: file extension for the history, yes or no? (.reshjson vs. ) + + // TODO: discover other history files, exclude current + + return &Histio{ + sugar: sugarHistio, + histDir: histDir, + + thisDeviceID: deviceID, + thisHistory: newHistfile(sugar, currPath), + // moreHistories: ... + } +} + +func (h *Histio) Append(r *record.V1) { + +} diff --git a/pkg/histlist/histlist.go b/internal/histlist/histlist.go similarity index 62% rename from pkg/histlist/histlist.go rename to internal/histlist/histlist.go index a3f4334b..7c80c7f4 100644 --- a/pkg/histlist/histlist.go +++ b/internal/histlist/histlist.go @@ -1,9 +1,11 @@ package histlist -import "log" +import "go.uber.org/zap" // Histlist is a deduplicated list of cmdLines type Histlist struct { + // TODO: I'm not excited about logger being passed here + sugar *zap.SugaredLogger // list of commands lines (deduplicated) List []string // lookup: cmdLine -> last index @@ -11,13 +13,16 @@ type Histlist struct { } // New Histlist -func New() Histlist { - return Histlist{LastIndex: make(map[string]int)} +func New(sugar *zap.SugaredLogger) Histlist { + return Histlist{ + sugar: sugar.With("component", "histlist"), + LastIndex: make(map[string]int), + } } // Copy Histlist func Copy(hl Histlist) Histlist { - newHl := New() + newHl := New(hl.sugar) // copy list newHl.List = make([]string, len(hl.List)) copy(newHl.List, hl.List) @@ -36,7 +41,10 @@ func (h *Histlist) AddCmdLine(cmdLine string) { if found { // remove duplicate if cmdLine != h.List[idx] { - log.Println("histlist ERROR: Adding cmdLine:", cmdLine, " != LastIndex[cmdLine]:", h.List[idx]) + h.sugar.DPanicw("Index key is different than actual cmd line in the list", + "indexKeyCmdLine", cmdLine, + "actualCmdLine", h.List[idx], + ) } h.List = append(h.List[:idx], h.List[idx+1:]...) // idx++ @@ -44,7 +52,10 @@ func (h *Histlist) AddCmdLine(cmdLine string) { cmdLn := h.List[idx] h.LastIndex[cmdLn]-- if idx != h.LastIndex[cmdLn] { - log.Println("histlist ERROR: Shifting LastIndex idx:", idx, " != LastIndex[cmdLn]:", h.LastIndex[cmdLn]) + h.sugar.DPanicw("Index position is different than actual position of the cmd line", + "actualPosition", idx, + "indexedPosition", h.LastIndex[cmdLn], + ) } idx++ } @@ -53,7 +64,10 @@ func (h *Histlist) AddCmdLine(cmdLine string) { h.LastIndex[cmdLine] = len(h.List) // append new cmdline h.List = append(h.List, cmdLine) - // log.Println("histlist: Added cmdLine:", cmdLine, "; history length:", lenBefore, "->", len(h.List)) + h.sugar.Debugw("Added cmdLine", + "cmdLine", cmdLine, + "historyLength", len(h.List), + ) } // AddHistlist contents of another histlist to this histlist diff --git a/pkg/httpclient/httpclient.go b/internal/httpclient/httpclient.go similarity index 100% rename from pkg/httpclient/httpclient.go rename to internal/httpclient/httpclient.go diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 00000000..ef25d72e --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,27 @@ +package logger + +import ( + "fmt" + "path/filepath" + + "github.com/curusarn/resh/internal/datadir" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func New(executable string, level zapcore.Level, development string) (*zap.Logger, error) { + dataDir, err := datadir.MakePath() + if err != nil { + return nil, fmt.Errorf("error while getting RESH data dir: %w", err) + } + logPath := filepath.Join(dataDir, "log.json") + loggerConfig := zap.NewProductionConfig() + loggerConfig.OutputPaths = []string{logPath} + loggerConfig.Level.SetLevel(level) + loggerConfig.Development = development == "true" // DPanic panics in development + logger, err := loggerConfig.Build() + if err != nil { + return logger, fmt.Errorf("error while creating logger: %w", err) + } + return logger.With(zap.String("executable", executable)), err +} diff --git a/internal/msg/msg.go b/internal/msg/msg.go new file mode 100644 index 00000000..7d3b103b --- /dev/null +++ b/internal/msg/msg.go @@ -0,0 +1,21 @@ +package msg + +import "github.com/curusarn/resh/internal/recordint" + +// CliMsg struct +type CliMsg struct { + SessionID string + PWD string +} + +// CliResponse struct +type CliResponse struct { + Records []recordint.SearchApp +} + +// StatusResponse struct +type StatusResponse struct { + Status bool `json:"status"` + Version string `json:"version"` + Commit string `json:"commit"` +} diff --git a/internal/normalize/normailze.go b/internal/normalize/normailze.go new file mode 100644 index 00000000..6bd72039 --- /dev/null +++ b/internal/normalize/normailze.go @@ -0,0 +1,31 @@ +package normalize + +import ( + "net/url" + "strings" + + giturls "github.com/whilp/git-urls" + "go.uber.org/zap" +) + +// GitRemote helper +// Returns normalized git remote - valid even on error +func GitRemote(sugar *zap.SugaredLogger, gitRemote string) string { + if len(gitRemote) == 0 { + return "" + } + gitRemote = strings.TrimSuffix(gitRemote, ".git") + parsedURL, err := giturls.Parse(gitRemote) + if err != nil { + sugar.Errorw("Failed to parse git remote", zap.Error(err), + "gitRemote", gitRemote, + ) + return gitRemote + } + if parsedURL.User == nil || parsedURL.User.Username() == "" { + parsedURL.User = url.User("git") + } + // TODO: figure out what scheme we want + parsedURL.Scheme = "git+ssh" + return parsedURL.String() +} diff --git a/internal/normalize/normalize_test.go b/internal/normalize/normalize_test.go new file mode 100644 index 00000000..a85b5383 --- /dev/null +++ b/internal/normalize/normalize_test.go @@ -0,0 +1,51 @@ +package normalize_test + +import ( + "testing" + + "github.com/curusarn/resh/internal/normalize" + "go.uber.org/zap" +) + +// TestLeftCutPadString +func TestGitRemote(t *testing.T) { + sugar := zap.NewNop().Sugar() + + data := [][]string{ + { + "git@github.com:curusarn/resh.git", // git + "git@github.com:curusarn/resh", // git no ".git" + "http://github.com/curusarn/resh.git", // http + "https://github.com/curusarn/resh.git", // https + "ssh://git@github.com/curusarn/resh.git", // ssh + "git+ssh://git@github.com/curusarn/resh.git", // git+ssh + }, + { + "git@host.example.com:org/user/repo.git", // git + "git@host.example.com:org/user/repo", // git no ".git" + "http://host.example.com/org/user/repo.git", // http + "https://host.example.com/org/user/repo.git", // https + "ssh://git@host.example.com/org/user/repo.git", // ssh + "git+ssh://git@host.example.com/org/user/repo.git", // git+ssh + }, + } + + for _, arr := range data { + n := len(arr) + for i := 0; i < n-1; i++ { + for j := i + 1; j < n; j++ { + one := normalize.GitRemote(sugar, arr[i]) + two := normalize.GitRemote(sugar, arr[j]) + if one != two { + t.Fatalf("Normalized git remotes should match for '%s' and '%s'\n -> got '%s' != '%s'", + arr[i], arr[j], one, two) + } + } + } + } + + empty := normalize.GitRemote(sugar, "") + if len(empty) != 0 { + t.Fatalf("Normalized git remotes for '' should be ''\n -> got '%s'", empty) + } +} diff --git a/internal/opt/opt.go b/internal/opt/opt.go new file mode 100644 index 00000000..efbada2c --- /dev/null +++ b/internal/opt/opt.go @@ -0,0 +1,36 @@ +package opt + +import ( + "fmt" + "os" + + "github.com/curusarn/resh/internal/output" +) + +// HandleVersionOpts reads the first option and handles it +// This is a helper for resh-{collect,postcollect,session-init} commands +func HandleVersionOpts(out *output.Output, args []string, version, commit string) []string { + if len(os.Args) == 0 { + return os.Args[1:] + } + // We use go-like options because of backwards compatibility. + // Not ideal but we should support them because they have worked once + // and adding "more correct" variants would mean supporting more variants. + switch os.Args[1] { + case "-version": + fmt.Print(version) + os.Exit(0) + case "-revision": + fmt.Print(commit) + os.Exit(0) + case "-requireVersion": + if len(os.Args) < 3 { + out.FatalTerminalVersionMismatch(version, "") + } + if os.Args[2] != version { + out.FatalTerminalVersionMismatch(version, os.Args[2]) + } + return os.Args[3:] + } + return os.Args[1:] +} diff --git a/internal/output/output.go b/internal/output/output.go new file mode 100644 index 00000000..4f7840de --- /dev/null +++ b/internal/output/output.go @@ -0,0 +1,143 @@ +package output + +import ( + "fmt" + "os" + + "go.uber.org/zap" +) + +// Output wrapper for writing to logger and stdout/stderr at the same time +// useful for errors that should be presented to the user +type Output struct { + Logger *zap.Logger + ErrPrefix string +} + +func New(logger *zap.Logger, prefix string) *Output { + return &Output{ + Logger: logger, + ErrPrefix: prefix, + } +} + +// Info outputs string to stdout and to log (as info) +// This is how we write output to users from interactive commands +// This way we have full record in logs +func (f *Output) Info(msg string) { + fmt.Printf("%s\n", msg) + f.Logger.Info(msg) +} + +// InfoE outputs string to stdout and to log (as error) +// Passed error is only written to log +// This is how we output errors to users from interactive commands +// This way we have errors in logs +func (f *Output) InfoE(msg string, err error) { + fmt.Printf("%s\n", msg) + f.Logger.Error(msg, zap.Error(err)) +} + +// Error outputs string to stderr and to log (as error) +// This is how we output errors from non-interactive commands +func (f *Output) Error(msg string) { + fmt.Fprintf(os.Stderr, "%s: %s\n", f.ErrPrefix, msg) + f.Logger.Error(msg) +} + +// ErrorE outputs string and error to stderr and to log (as error) +// This is how we output errors from non-interactive commands +func (f *Output) ErrorE(msg string, err error) { + fmt.Fprintf(os.Stderr, "%s: %s: %v\n", f.ErrPrefix, msg, err) + f.Logger.Error(msg, zap.Error(err)) +} + +// FatalE outputs string and error to stderr and to log (as fatal) +// This is how we raise fatal errors from non-interactive commands +func (f *Output) FatalE(msg string, err error) { + fmt.Fprintf(os.Stderr, "%s: %s: %v\n", f.ErrPrefix, msg, err) + f.Logger.Fatal(msg, zap.Error(err)) +} + +var msgDaemonNotRunning = `RESH daemon didn't respond - it's probably not running. + -> Start RESH daemon manually - run: resh-daemon-start + -> Or restart this terminal window to bring RESH daemon back up + -> You can check logs: ~/.local/share/resh/log.json (or ~/$XDG_DATA_HOME/resh/log.json) + -> You can create an issue at: https://github.com/curusarn/resh/issues + +` +var msgTerminalVersionMismatch = `This terminal session was started with different RESH version than is installed now. +It looks like you updated RESH and didn't restart this terminal. + -> Restart this terminal window to fix that + +` + +var msgDaemonVersionMismatch = `RESH daemon is running in different version than is installed now. +It looks like something went wrong during RESH update. + -> Kill resh-daemon and then launch a new terminal window to fix that: killall resh-daemon + -> You can create an issue at: https://github.com/curusarn/resh/issues + +` + +func (f *Output) InfoDaemonNotRunning(err error) { + fmt.Printf("%s", msgDaemonNotRunning) + f.Logger.Error("Daemon is not running", zap.Error(err)) +} + +func (f *Output) ErrorDaemonNotRunning(err error) { + fmt.Fprintf(os.Stderr, "%s: %s", f.ErrPrefix, msgDaemonNotRunning) + f.Logger.Error("Daemon is not running", zap.Error(err)) +} + +func (f *Output) FatalDaemonNotRunning(err error) { + fmt.Fprintf(os.Stderr, "%s: %s", f.ErrPrefix, msgDaemonNotRunning) + f.Logger.Fatal("Daemon is not running", zap.Error(err)) +} + +func (f *Output) InfoTerminalVersionMismatch(installedVer, terminalVer string) { + fmt.Printf("%s(installed version: %s, this terminal version: %s)\n\n", + msgTerminalVersionMismatch, installedVer, terminalVer) + f.Logger.Fatal("Version mismatch", + zap.String("installed", installedVer), + zap.String("terminal", terminalVer)) +} + +func (f *Output) ErrorTerminalVersionMismatch(installedVer, terminalVer string) { + fmt.Fprintf(os.Stderr, "%s: %s(installed version: %s, this terminal version: %s)\n\n", + f.ErrPrefix, msgTerminalVersionMismatch, installedVer, terminalVer) + f.Logger.Fatal("Version mismatch", + zap.String("installed", installedVer), + zap.String("terminal", terminalVer)) +} + +func (f *Output) FatalTerminalVersionMismatch(installedVer, terminalVer string) { + fmt.Fprintf(os.Stderr, "%s: %s(installed version: %s, this terminal version: %s)\n\n", + f.ErrPrefix, msgTerminalVersionMismatch, installedVer, terminalVer) + f.Logger.Fatal("Version mismatch", + zap.String("installed", installedVer), + zap.String("terminal", terminalVer)) +} + +func (f *Output) InfoDaemonVersionMismatch(installedVer, daemonVer string) { + fmt.Printf("%s(installed version: %s, running daemon version: %s)\n\n", + msgDaemonVersionMismatch, installedVer, daemonVer) + f.Logger.Error("Version mismatch", + zap.String("installed", installedVer), + zap.String("daemon", daemonVer)) +} + +func (f *Output) ErrorDaemonVersionMismatch(installedVer, daemonVer string) { + fmt.Fprintf(os.Stderr, "%s: %s(installed version: %s, running daemon version: %s)\n\n", + f.ErrPrefix, msgDaemonVersionMismatch, installedVer, daemonVer) + f.Logger.Error("Version mismatch", + zap.String("installed", installedVer), + zap.String("daemon", daemonVer)) +} + +func (f *Output) FatalDaemonVersionMismatch(installedVer, daemonVer string) { + fmt.Fprintf(os.Stderr, "%s: %s(installed version: %s, running daemon version: %s)\n\n", + f.ErrPrefix, msgDaemonVersionMismatch, installedVer, daemonVer) + f.Logger.Fatal("Version mismatch", + zap.String("installed", installedVer), + zap.String("daemon", daemonVer)) +} diff --git a/internal/recconv/recconv.go b/internal/recconv/recconv.go new file mode 100644 index 00000000..7690ec43 --- /dev/null +++ b/internal/recconv/recconv.go @@ -0,0 +1,37 @@ +package recconv + +import ( + "fmt" + + "github.com/curusarn/resh/record" +) + +func LegacyToV1(r *record.Legacy) *record.V1 { + return &record.V1{ + // FIXME: fill in all the fields + + // Flags: 0, + + CmdLine: r.CmdLine, + ExitCode: r.ExitCode, + + DeviceID: r.ReshUUID, + SessionID: r.SessionID, + RecordID: r.RecordID, + + Home: r.Home, + Pwd: r.Pwd, + RealPwd: r.RealPwd, + + // Logname: r.Login, + Device: r.Host, + + GitOriginRemote: r.GitOriginRemote, + + Time: fmt.Sprintf("%.4f", r.RealtimeBefore), + Duration: fmt.Sprintf("%.4f", r.RealtimeDuration), + + PartOne: r.PartOne, + PartsNotMerged: !r.PartsMerged, + } +} diff --git a/internal/recio/read.go b/internal/recio/read.go new file mode 100644 index 00000000..e88d86c8 --- /dev/null +++ b/internal/recio/read.go @@ -0,0 +1,144 @@ +package recio + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "os" + "strings" + + "github.com/curusarn/resh/internal/futil" + "github.com/curusarn/resh/internal/recconv" + "github.com/curusarn/resh/record" + "go.uber.org/zap" +) + +func (r *RecIO) ReadAndFixFile(fpath string, maxErrors int) ([]record.V1, error) { + recs, decodeErrs, err := r.ReadFile(fpath) + if err != nil { + return nil, err + } + numErrs := len(decodeErrs) + if numErrs > maxErrors { + r.sugar.Errorw("Encountered too many decoding errors", + "errorsCount", numErrs, + "individualErrors", "", + ) + return nil, fmt.Errorf("encountered too many decoding errors, last error: %w", decodeErrs[len(decodeErrs)-1]) + } + if numErrs == 0 { + return recs, nil + } + + r.sugar.Warnw("Some history records could not be decoded - fixing RESH history file by dropping them", + "corruptedRecords", numErrs, + "lastError", decodeErrs[len(decodeErrs)-1], + "individualErrors", "", + ) + + fpathBak := fpath + ".bak" + r.sugar.Infow("Backing up current corrupted history file", + "historyFileBackup", fpathBak, + ) + err = futil.CopyFile(fpath, fpathBak) + if err != nil { + r.sugar.Errorw("Failed to create a backup history file - aborting fixing history file", + "historyFileBackup", fpathBak, + zap.Error(err), + ) + return recs, nil + } + r.sugar.Info("Writing resh history file without errors ...") + err = r.OverwriteFile(fpath, recs) + if err != nil { + r.sugar.Errorw("Failed write fixed history file - restoring history file from backup", + "historyFile", fpath, + zap.Error(err), + ) + + err = futil.CopyFile(fpathBak, fpath) + if err != nil { + r.sugar.Errorw("Failed restore history file from backup", + "historyFile", fpath, + "HistoryFileBackup", fpathBak, + zap.Error(err), + ) + } + } + return recs, nil +} + +func (r *RecIO) ReadFile(fpath string) ([]record.V1, []error, error) { + var recs []record.V1 + file, err := os.Open(fpath) + if err != nil { + return nil, nil, fmt.Errorf("failed to open history file: %w", err) + } + defer file.Close() + + reader := bufio.NewReader(file) + var decodeErrs []error + for { + var line string + line, err = reader.ReadString('\n') + if err != nil { + break + } + rec, err := r.decodeLine(line) + if err != nil { + r.sugar.Errorw("Error while decoding line", zap.Error(err), + "filePath", fpath, + "line", line, + ) + decodeErrs = append(decodeErrs, err) + continue + } + recs = append(recs, *rec) + } + if err != io.EOF { + r.sugar.Error("Error while reading file", zap.Error(err)) + return recs, decodeErrs, err + } + r.sugar.Infow("Loaded resh history records", + "recordCount", len(recs), + ) + return recs, decodeErrs, nil +} + +func (r *RecIO) decodeLine(line string) (*record.V1, error) { + idx := strings.Index(line, "{") + if idx == -1 { + return nil, fmt.Errorf("no opening brace found") + } + schema := line[:idx] + jsn := line[idx:] + switch schema { + case "v1": + var rec record.V1 + err := decodeAnyRecord(jsn, &rec) + if err != nil { + return nil, err + } + return &rec, nil + case "": + var rec record.Legacy + err := decodeAnyRecord(jsn, &rec) + if err != nil { + return nil, err + } + return recconv.LegacyToV1(&rec), nil + default: + return nil, fmt.Errorf("unknown record schema/type '%s'", schema) + } +} + +// TODO: find out if we are loosing performance because of the use of interface{} + +func decodeAnyRecord(jsn string, rec interface{}) error { + err := json.Unmarshal([]byte(jsn), &rec) + if err != nil { + return fmt.Errorf("failed to decode json: %w", err) + } + return nil +} diff --git a/internal/recio/recio.go b/internal/recio/recio.go new file mode 100644 index 00000000..5ea986ba --- /dev/null +++ b/internal/recio/recio.go @@ -0,0 +1,13 @@ +package recio + +import ( + "go.uber.org/zap" +) + +type RecIO struct { + sugar *zap.SugaredLogger +} + +func New(sugar *zap.SugaredLogger) RecIO { + return RecIO{sugar: sugar} +} diff --git a/internal/recio/write.go b/internal/recio/write.go new file mode 100644 index 00000000..a772d165 --- /dev/null +++ b/internal/recio/write.go @@ -0,0 +1,64 @@ +package recio + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/curusarn/resh/record" +) + +func (r *RecIO) OverwriteFile(fpath string, recs []record.V1) error { + file, err := os.Create(fpath) + if err != nil { + return fmt.Errorf("could not create/truncate file: %w", err) + } + err = writeRecords(file, recs) + if err != nil { + return fmt.Errorf("error while writing records: %w", err) + } + err = file.Close() + if err != nil { + return fmt.Errorf("could not close file: %w", err) + } + return nil +} + +func (r *RecIO) AppendToFile(fpath string, recs []record.V1) error { + file, err := os.OpenFile(fpath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("could not open/create file: %w", err) + } + err = writeRecords(file, recs) + if err != nil { + return fmt.Errorf("error while writing records: %w", err) + } + err = file.Close() + if err != nil { + return fmt.Errorf("could not close file: %w", err) + } + return nil +} + +func writeRecords(file *os.File, recs []record.V1) error { + for _, rec := range recs { + jsn, err := encodeV1Record(rec) + if err != nil { + return fmt.Errorf("could not encode record: %w", err) + } + _, err = file.Write(jsn) + if err != nil { + return fmt.Errorf("could not write json: %w", err) + } + } + return nil +} + +func encodeV1Record(rec record.V1) ([]byte, error) { + version := []byte("v1") + jsn, err := json.Marshal(rec) + if err != nil { + return nil, fmt.Errorf("failed to encode json: %w", err) + } + return append(append(version, jsn...), []byte("\n")...), nil +} diff --git a/internal/recordint/collect.go b/internal/recordint/collect.go new file mode 100644 index 00000000..1a7d6a74 --- /dev/null +++ b/internal/recordint/collect.go @@ -0,0 +1,34 @@ +package recordint + +import "github.com/curusarn/resh/record" + +type Collect struct { + // record merging + SessionID string + Shlvl int + // session watching + SessionPID int + Shell string + + Rec record.V1 +} + +type Postcollect struct { + // record merging + SessionID string + Shlvl int + // session watching + SessionPID int + + RecordID string + ExitCode int + Duration float64 +} + +type SessionInit struct { + // record merging + SessionID string + Shlvl int + // session watching + SessionPID int +} diff --git a/internal/recordint/recordint.go b/internal/recordint/recordint.go new file mode 100644 index 00000000..73457cd7 --- /dev/null +++ b/internal/recordint/recordint.go @@ -0,0 +1,2 @@ +// Package recordint provides internal record types that are passed between resh components +package recordint diff --git a/internal/recordint/searchapp.go b/internal/recordint/searchapp.go new file mode 100644 index 00000000..5ccc537f --- /dev/null +++ b/internal/recordint/searchapp.go @@ -0,0 +1,56 @@ +package recordint + +import ( + "strconv" + + "github.com/curusarn/resh/internal/normalize" + "github.com/curusarn/resh/record" + "go.uber.org/zap" +) + +// SearchApp record used for sending records to RESH-CLI +type SearchApp struct { + IsRaw bool + SessionID string + DeviceID string + + CmdLine string + Host string + Pwd string + Home string // helps us to collapse /home/user to tilde + GitOriginRemote string + ExitCode int + + Time float64 + + // file index + Idx int +} + +func NewSearchAppFromCmdLine(cmdLine string) SearchApp { + return SearchApp{ + IsRaw: true, + CmdLine: cmdLine, + } +} + +// The error handling here could be better +func NewSearchApp(sugar *zap.SugaredLogger, r *record.V1) SearchApp { + time, err := strconv.ParseFloat(r.Time, 64) + if err != nil { + sugar.Errorw("Error while parsing time as float", zap.Error(err), + "time", time) + } + return SearchApp{ + IsRaw: false, + SessionID: r.SessionID, + CmdLine: r.CmdLine, + Host: r.Device, + Pwd: r.Pwd, + Home: r.Home, + // TODO: is this the right place to normalize the git remote? + GitOriginRemote: normalize.GitRemote(sugar, r.GitOriginRemote), + ExitCode: r.ExitCode, + Time: time, + } +} diff --git a/internal/records/records.go b/internal/records/records.go new file mode 100644 index 00000000..699a50a5 --- /dev/null +++ b/internal/records/records.go @@ -0,0 +1,84 @@ +package records + +// DEPRECATION NOTICE: This package should be removed in favor of: +// - record: public record definitions +// - recordint: internal record definitions +// - recutil: record-related utils + +import ( + "bufio" + "os" + "strings" + + "github.com/curusarn/resh/internal/histlist" + "go.uber.org/zap" +) + +// LoadCmdLinesFromZshFile loads cmdlines from zsh history file +func LoadCmdLinesFromZshFile(sugar *zap.SugaredLogger, fname string) histlist.Histlist { + hl := histlist.New(sugar) + file, err := os.Open(fname) + if err != nil { + sugar.Error("Failed to open zsh history file - skipping reading zsh history", zap.Error(err)) + return hl + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + // trim newline + line = strings.TrimRight(line, "\n") + var cmd string + // zsh format EXTENDED_HISTORY + // : 1576270617:0;make install + // zsh format no EXTENDED_HISTORY + // make install + if len(line) == 0 { + // skip empty + continue + } + if strings.Contains(line, ":") && strings.Contains(line, ";") && + len(strings.Split(line, ":")) >= 3 && len(strings.Split(line, ";")) >= 2 { + // contains at least 2x ':' and 1x ';' => assume EXTENDED_HISTORY + cmd = strings.Split(line, ";")[1] + } else { + cmd = line + } + hl.AddCmdLine(cmd) + } + return hl +} + +// LoadCmdLinesFromBashFile loads cmdlines from bash history file +func LoadCmdLinesFromBashFile(sugar *zap.SugaredLogger, fname string) histlist.Histlist { + hl := histlist.New(sugar) + file, err := os.Open(fname) + if err != nil { + sugar.Error("Failed to open bash history file - skipping reading bash history", zap.Error(err)) + return hl + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + // trim newline + line = strings.TrimRight(line, "\n") + // trim spaces from left + line = strings.TrimLeft(line, " ") + // bash format (two lines) + // #1576199174 + // make install + if strings.HasPrefix(line, "#") { + // is either timestamp or comment => skip + continue + } + if len(line) == 0 { + // skip empty + continue + } + hl.AddCmdLine(line) + } + return hl +} diff --git a/internal/recutil/recutil.go b/internal/recutil/recutil.go new file mode 100644 index 00000000..1862741a --- /dev/null +++ b/internal/recutil/recutil.go @@ -0,0 +1,51 @@ +package recutil + +import ( + "errors" + + "github.com/curusarn/resh/internal/recordint" + "github.com/curusarn/resh/record" +) + +// TODO: reintroduce validation +// Validate returns error if the record is invalid +// func Validate(r *record.V1) error { +// if r.CmdLine == "" { +// return errors.New("There is no CmdLine") +// } +// if r.Time == 0 { +// return errors.New("There is no Time") +// } +// if r.RealPwd == "" { +// return errors.New("There is no Real Pwd") +// } +// if r.Pwd == "" { +// return errors.New("There is no Pwd") +// } +// return nil +// } + +// TODO: maybe more to a more appropriate place +// TODO: cleanup the interface - stop modifying the part1 and returning a new record at the same time +// Merge two records (part1 - collect + part2 - postcollect) +func Merge(r1 *recordint.Collect, r2 *recordint.Collect) (record.V1, error) { + if r1.SessionID != r2.SessionID { + return record.V1{}, errors.New("Records to merge are not from the same session - r1:" + r1.SessionID + " r2:" + r2.SessionID) + } + if r1.Rec.RecordID != r2.Rec.RecordID { + return record.V1{}, errors.New("Records to merge do not have the same ID - r1:" + r1.Rec.RecordID + " r2:" + r2.Rec.RecordID) + } + + r := recordint.Collect{ + SessionID: r1.SessionID, + Shlvl: r1.Shlvl, + SessionPID: r1.SessionPID, + + Rec: r1.Rec, + } + r.Rec.ExitCode = r2.Rec.ExitCode + r.Rec.Duration = r2.Rec.Duration + r.Rec.PartOne = false + r.Rec.PartsNotMerged = false + return r.Rec, nil +} diff --git a/pkg/searchapp/highlight.go b/internal/searchapp/highlight.go similarity index 100% rename from pkg/searchapp/highlight.go rename to internal/searchapp/highlight.go diff --git a/pkg/searchapp/item.go b/internal/searchapp/item.go similarity index 86% rename from pkg/searchapp/item.go rename to internal/searchapp/item.go index 6908c258..96603485 100644 --- a/pkg/searchapp/item.go +++ b/internal/searchapp/item.go @@ -2,13 +2,13 @@ package searchapp import ( "fmt" - "log" "math" "strconv" "strings" "time" + "unicode" - "github.com/curusarn/resh/pkg/records" + "github.com/curusarn/resh/internal/recordint" "golang.org/x/exp/utf8string" ) @@ -19,7 +19,7 @@ const dots = "…" type Item struct { isRaw bool - realtimeBefore float64 + time float64 // [host:]pwd differentHost bool @@ -32,8 +32,11 @@ type Item struct { sameGitRepo bool exitCode int + // Shown in TUI CmdLineWithColor string CmdLine string + // Unchanged cmdline to paste to command line + CmdLineOut string Score float64 @@ -106,8 +109,8 @@ func (i Item) DrawStatusLine(compactRendering bool, printedLineLength, realLineL if i.isRaw { return splitStatusLineToLines(i.CmdLine, printedLineLength, realLineLength) } - secs := int64(i.realtimeBefore) - nsecs := int64((i.realtimeBefore - float64(secs)) * 1e9) + secs := int64(i.time) + nsecs := int64((i.time - float64(secs)) * 1e9) tm := time.Unix(secs, nsecs) const timeFormat = "2006-01-02 15:04:05" timeString := tm.Format(timeFormat) @@ -143,8 +146,8 @@ func (i Item) DrawItemColumns(compactRendering bool, debug bool) ItemColumns { // DISPLAY // DISPLAY > date - secs := int64(i.realtimeBefore) - nsecs := int64((i.realtimeBefore - float64(secs)) * 1e9) + secs := int64(i.time) + nsecs := int64((i.time - float64(secs)) * 1e9) tm := time.Unix(secs, nsecs) var date string @@ -228,9 +231,6 @@ func produceLocation(length int, host string, pwdTilde string, differentHost boo shrinkFactor := float64(length) / float64(totalLen) shrinkedHostLen := int(math.Ceil(float64(hostLen) * shrinkFactor)) - if debug { - log.Printf("shrinkFactor: %f\n", shrinkFactor) - } halfLocationLen := length/2 - colonLen newHostLen = minInt(hostLen, shrinkedHostLen, halfLocationLen) @@ -238,7 +238,14 @@ func produceLocation(length int, host string, pwdTilde string, differentHost boo // pwd length is the rest of the length newPwdLen := length - colonLen - newHostLen - hostWithColor := rightCutPadString(host, newHostLen) + // adjust pwd length + if newPwdLen > pwdLen { + diff := newPwdLen - pwdLen + newHostLen += diff + newPwdLen -= diff + } + + hostWithColor := rightCutLeftPadString(host, newHostLen) if differentHost { hostWithColor = highlightHost(hostWithColor) } @@ -279,6 +286,20 @@ func (ic ItemColumns) ProduceLine(dateLength int, locationLength int, flagsLengt return line, length, err } +func rightCutLeftPadString(str string, newLen int) string { + if newLen <= 0 { + return "" + } + utf8Str := utf8string.NewString(str) + strLen := utf8Str.RuneCount() + if newLen > strLen { + return strings.Repeat(" ", newLen-strLen) + str + } else if newLen < strLen { + return utf8Str.Slice(0, newLen-1) + dots + } + return str +} + func leftCutPadString(str string, newLen int) string { if newLen <= 0 { return "" @@ -308,24 +329,22 @@ func rightCutPadString(str string, newLen int) string { } // proper match for path is when whole directory is matched -// proper match for command is when term matches word delimeted by whitespace +// proper match for command is when term matches word delimited by whitespace func properMatch(str, term, padChar string) bool { - if strings.Contains(padChar+str+padChar, padChar+term+padChar) { - return true - } - return false + return strings.Contains(padChar+str+padChar, padChar+term+padChar) } // NewItemFromRecordForQuery creates new item from record based on given query -// returns error if the query doesn't match the record -func NewItemFromRecordForQuery(record records.CliRecord, query Query, debug bool) (Item, error) { +// +// returns error if the query doesn't match the record +func NewItemFromRecordForQuery(record recordint.SearchApp, query Query, debug bool) (Item, error) { // Use numbers that won't add up to same score for any number of query words - // query score weigth 1.51 + // query score weight 1.51 const hitScore = 1.517 // 1 * 1.51 const properMatchScore = 0.501 // 0.33 * 1.51 const hitScoreConsecutive = 0.00302 // 0.002 * 1.51 - // context score weigth 1 + // context score weight 1 // Host penalty var actualPwdScore = 0.9 var sameGitRepoScore = 0.8 @@ -360,22 +379,17 @@ func NewItemFromRecordForQuery(record records.CliRecord, query Query, debug bool // DISPLAY > cmdline // cmd := "<" + strings.ReplaceAll(record.CmdLine, "\n", ";") + ">" - cmdLine := strings.ReplaceAll(record.CmdLine, "\n", ";") - cmdLineWithColor := strings.ReplaceAll(cmd, "\n", ";") + cmdLine := strings.ReplaceAll(record.CmdLine, "\n", "\\n ") + cmdLineWithColor := strings.ReplaceAll(cmd, "\n", "\\n ") // KEY for deduplication + key := strings.TrimRightFunc(record.CmdLine, unicode.IsSpace) - key := record.CmdLine - // NOTE: since we import standard history we need a compatible key without metadata - /* - unlikelySeparator := "|||||" - key := record.CmdLine + unlikelySeparator + record.Pwd + unlikelySeparator + - record.GitOriginRemote + unlikelySeparator + record.Host - */ if record.IsRaw { return Item{ isRaw: true, + CmdLineOut: record.CmdLine, CmdLine: cmdLine, CmdLineWithColor: cmdLineWithColor, Score: score, @@ -387,7 +401,7 @@ func NewItemFromRecordForQuery(record records.CliRecord, query Query, debug bool // -> N matches against the command // -> 1 extra match for the actual directory match sameGitRepo := false - if query.gitOriginRemote != "" && query.gitOriginRemote == record.GitOriginRemote { + if len(query.gitOriginRemote) != 0 && query.gitOriginRemote == record.GitOriginRemote { sameGitRepo = true } @@ -415,10 +429,10 @@ func NewItemFromRecordForQuery(record records.CliRecord, query Query, debug bool // if score <= 0 && !anyHit { // return Item{}, errors.New("no match for given record and query") // } - score += record.RealtimeBefore * timeScoreCoef + score += record.Time * timeScoreCoef it := Item{ - realtimeBefore: record.RealtimeBefore, + time: record.Time, differentHost: differentHost, host: record.Host, @@ -428,6 +442,7 @@ func NewItemFromRecordForQuery(record records.CliRecord, query Query, debug bool sameGitRepo: sameGitRepo, exitCode: record.ExitCode, + CmdLineOut: record.CmdLine, CmdLine: cmdLine, CmdLineWithColor: cmdLineWithColor, Score: score, @@ -465,6 +480,7 @@ func GetHeader(compactRendering bool) ItemColumns { type RawItem struct { CmdLineWithColor string CmdLine string + CmdLineOut string Score float64 @@ -473,8 +489,9 @@ type RawItem struct { } // NewRawItemFromRecordForQuery creates new item from record based on given query -// returns error if the query doesn't match the record -func NewRawItemFromRecordForQuery(record records.CliRecord, terms []string, debug bool) (RawItem, error) { +// +// returns error if the query doesn't match the record +func NewRawItemFromRecordForQuery(record recordint.SearchApp, terms []string, debug bool) (RawItem, error) { const hitScore = 1.0 const hitScoreConsecutive = 0.01 const properMatchScore = 0.3 @@ -493,7 +510,7 @@ func NewRawItemFromRecordForQuery(record records.CliRecord, terms []string, debu cmd = strings.ReplaceAll(cmd, term, highlightMatch(term)) } } - score += record.RealtimeBefore * timeScoreCoef + score += record.Time * timeScoreCoef // KEY for deduplication key := record.CmdLine diff --git a/pkg/searchapp/item_test.go b/internal/searchapp/item_test.go similarity index 65% rename from pkg/searchapp/item_test.go rename to internal/searchapp/item_test.go index 903157ab..38caad0a 100644 --- a/pkg/searchapp/item_test.go +++ b/internal/searchapp/item_test.go @@ -87,3 +87,45 @@ func TestRightCutPadString(t *testing.T) { t.Fatal("Incorrect right pad from ♥♥♥♥ to '♥♥♥♥ '") } } + +// TestRightCutLeftPadString +func TestRightCutLeftPadString(t *testing.T) { + if rightCutLeftPadString("abc", -1) != "" { + t.Fatal("Incorrect right cut from abc to '' (negative)") + } + if rightCutLeftPadString("abc", 0) != "" { + t.Fatal("Incorrect right cut from abc to ''") + } + if rightCutLeftPadString("abc", 1) != "…" { + t.Fatal("Incorrect right cut from abc to …") + } + if rightCutLeftPadString("abc", 2) != "a…" { + t.Fatal("Incorrect right cut from abc to a…") + } + if rightCutLeftPadString("abc", 3) != "abc" { + t.Fatal("Incorrect right cut from abc to abc") + } + if rightCutLeftPadString("abc", 5) != " abc" { + t.Fatal("Incorrect right pad from abc to ' abc'") + } + + // unicode + if rightCutLeftPadString("♥♥♥♥", -1) != "" { + t.Fatal("Incorrect right cut from ♥♥♥♥ to '' (negative)") + } + if rightCutLeftPadString("♥♥♥♥", 0) != "" { + t.Fatal("Incorrect right cut from ♥♥♥♥ to ''") + } + if rightCutLeftPadString("♥♥♥♥", 1) != "…" { + t.Fatal("Incorrect right cut from ♥♥♥♥ to …") + } + if rightCutLeftPadString("♥♥♥♥", 2) != "♥…" { + t.Fatal("Incorrect right cut from ♥♥♥♥ to ♥…") + } + if rightCutLeftPadString("♥♥♥♥", 4) != "♥♥♥♥" { + t.Fatal("Incorrect right cut from ♥♥♥♥ to ♥♥♥♥") + } + if rightCutLeftPadString("♥♥♥♥", 6) != " ♥♥♥♥" { + t.Fatal("Incorrect right pad from ♥♥♥♥ to ' ♥♥♥♥'") + } +} diff --git a/pkg/searchapp/query.go b/internal/searchapp/query.go similarity index 72% rename from pkg/searchapp/query.go rename to internal/searchapp/query.go index 2b8227d9..507af745 100644 --- a/pkg/searchapp/query.go +++ b/internal/searchapp/query.go @@ -1,9 +1,11 @@ package searchapp import ( - "log" "sort" "strings" + + "github.com/curusarn/resh/internal/normalize" + "go.uber.org/zap" ) // Query holds information that is used for result scoring @@ -36,49 +38,33 @@ func filterTerms(terms []string) []string { } // NewQueryFromString . -func NewQueryFromString(queryInput string, host string, pwd string, gitOriginRemote string, debug bool) Query { - if debug { - log.Println("QUERY input = <" + queryInput + ">") - } +func NewQueryFromString(sugar *zap.SugaredLogger, queryInput string, host string, pwd string, gitOriginRemote string, debug bool) Query { terms := strings.Fields(queryInput) var logStr string for _, term := range terms { logStr += " <" + term + ">" } - if debug { - log.Println("QUERY raw terms =" + logStr) - } terms = filterTerms(terms) logStr = "" for _, term := range terms { logStr += " <" + term + ">" } - if debug { - log.Println("QUERY filtered terms =" + logStr) - log.Println("QUERY pwd =" + pwd) - } sort.SliceStable(terms, func(i, j int) bool { return len(terms[i]) < len(terms[j]) }) return Query{ terms: terms, host: host, pwd: pwd, - gitOriginRemote: gitOriginRemote, + gitOriginRemote: normalize.GitRemote(sugar, gitOriginRemote), } } // GetRawTermsFromString . func GetRawTermsFromString(queryInput string, debug bool) []string { - if debug { - log.Println("QUERY input = <" + queryInput + ">") - } terms := strings.Fields(queryInput) var logStr string for _, term := range terms { logStr += " <" + term + ">" } - if debug { - log.Println("QUERY raw terms =" + logStr) - } terms = filterTerms(terms) logStr = "" for _, term := range terms { diff --git a/pkg/searchapp/time.go b/internal/searchapp/time.go similarity index 100% rename from pkg/searchapp/time.go rename to internal/searchapp/time.go diff --git a/pkg/sess/sess.go b/internal/sess/sess.go similarity index 100% rename from pkg/sess/sess.go rename to internal/sess/sess.go diff --git a/internal/sesswatch/sesswatch.go b/internal/sesswatch/sesswatch.go new file mode 100644 index 00000000..b20bb619 --- /dev/null +++ b/internal/sesswatch/sesswatch.go @@ -0,0 +1,96 @@ +package sesswatch + +import ( + "sync" + "time" + + "github.com/curusarn/resh/internal/recordint" + "github.com/mitchellh/go-ps" + "go.uber.org/zap" +) + +type sesswatch struct { + sugar *zap.SugaredLogger + + sessionsToDrop []chan string + sleepSeconds uint + + watchedSessions map[string]bool + mutex sync.Mutex +} + +// Go runs the session watcher - watches sessions and sends +func Go(sugar *zap.SugaredLogger, + sessionsToWatch chan recordint.SessionInit, sessionsToWatchRecords chan recordint.Collect, + sessionsToDrop []chan string, sleepSeconds uint) { + + sw := sesswatch{ + sugar: sugar.With("module", "sesswatch"), + sessionsToDrop: sessionsToDrop, + sleepSeconds: sleepSeconds, + watchedSessions: map[string]bool{}, + } + go sw.waiter(sessionsToWatch, sessionsToWatchRecords) +} + +func (s *sesswatch) waiter(sessionsToWatch chan recordint.SessionInit, sessionsToWatchRecords chan recordint.Collect) { + for { + func() { + select { + case rec := <-sessionsToWatch: + // normal way to start watching a session + id := rec.SessionID + pid := rec.SessionPID + sugar := s.sugar.With( + "sessionID", rec.SessionID, + "sessionPID", rec.SessionPID, + ) + s.mutex.Lock() + defer s.mutex.Unlock() + if s.watchedSessions[id] == false { + sugar.Infow("Starting watching new session") + s.watchedSessions[id] = true + go s.watcher(sugar, id, pid) + } + case rec := <-sessionsToWatchRecords: + // additional safety - watch sessions that were never properly initialized + id := rec.SessionID + pid := rec.SessionPID + sugar := s.sugar.With( + "sessionID", rec.SessionID, + "sessionPID", rec.SessionPID, + ) + s.mutex.Lock() + defer s.mutex.Unlock() + if s.watchedSessions[id] == false { + sugar.Warnw("Starting watching new session based on '/record'") + s.watchedSessions[id] = true + go s.watcher(sugar, id, pid) + } + } + }() + } +} + +func (s *sesswatch) watcher(sugar *zap.SugaredLogger, sessionID string, sessionPID int) { + for { + time.Sleep(time.Duration(s.sleepSeconds) * time.Second) + proc, err := ps.FindProcess(sessionPID) + if err != nil { + sugar.Errorw("Error while finding process", "error", err) + } else if proc == nil { + sugar.Infow("Dropping session") + func() { + s.mutex.Lock() + defer s.mutex.Unlock() + s.watchedSessions[sessionID] = false + }() + for _, ch := range s.sessionsToDrop { + sugar.Debugw("Sending 'drop session' message ...") + ch <- sessionID + sugar.Debugw("Sending 'drop session' message DONE") + } + break + } + } +} diff --git a/internal/signalhandler/signalhander.go b/internal/signalhandler/signalhander.go new file mode 100644 index 00000000..49c4498d --- /dev/null +++ b/internal/signalhandler/signalhander.go @@ -0,0 +1,74 @@ +package signalhandler + +import ( + "context" + "net/http" + "os" + "os/signal" + "strconv" + "syscall" + "time" + + "go.uber.org/zap" +) + +func sendSignals(sugar *zap.SugaredLogger, sig os.Signal, subscribers []chan os.Signal, done chan string) { + for _, sub := range subscribers { + sub <- sig + } + sugar.Warnw("Sent shutdown signals to components") + chanCount := len(subscribers) + start := time.Now() + delay := time.Millisecond * 100 + timeout := time.Millisecond * 2000 + + for { + select { + case _ = <-done: + chanCount-- + if chanCount == 0 { + sugar.Warnw("All components shut down successfully") + return + } + default: + time.Sleep(delay) + } + if time.Since(start) > timeout { + sugar.Errorw("Timeouted while waiting for proper shutdown", + "componentsStillUp", strconv.Itoa(chanCount), + "timeout", timeout.String(), + ) + return + } + } +} + +// Run catches and handles signals +func Run(sugar *zap.SugaredLogger, subscribers []chan os.Signal, done chan string, server *http.Server) { + sugar = sugar.With("module", "signalhandler") + signals := make(chan os.Signal, 1) + + signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP) + + var sig os.Signal + for { + sig := <-signals + sugarSig := sugar.With("signal", sig.String()) + sugarSig.Infow("Got signal") + if sig == syscall.SIGTERM { + // Shutdown daemon on SIGTERM + break + } + sugarSig.Warnw("Ignoring signal. Send SIGTERM to trigger shutdown.") + } + + sugar.Infow("Sending shutdown signals to components ...") + sendSignals(sugar, sig, subscribers, done) + + sugar.Infow("Shutting down the server ...") + if err := server.Shutdown(context.Background()); err != nil { + sugar.Errorw("Error while shuting down HTTP server", + "error", err, + ) + } +} diff --git a/internal/status/status.go b/internal/status/status.go new file mode 100644 index 00000000..52324d49 --- /dev/null +++ b/internal/status/status.go @@ -0,0 +1,49 @@ +package status + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + + "github.com/curusarn/resh/internal/httpclient" + "github.com/curusarn/resh/internal/msg" +) + +func get(port int) (*http.Response, error) { + url := "http://localhost:" + strconv.Itoa(port) + "/status" + client := httpclient.New() + resp, err := client.Get(url) + if err != nil { + return nil, fmt.Errorf("error while GET'ing daemon /status: %w", err) + } + return resp, nil +} + +func IsDaemonRunning(port int) (bool, error) { + resp, err := get(port) + if err != nil { + return false, err + } + defer resp.Body.Close() + return true, nil +} + +func GetDaemonStatus(port int) (*msg.StatusResponse, error) { + resp, err := get(port) + if err != nil { + return nil, err + } + defer resp.Body.Close() + jsn, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error while reading 'daemon /status' response: %w", err) + } + var msgResp msg.StatusResponse + err = json.Unmarshal(jsn, &msgResp) + if err != nil { + return nil, fmt.Errorf("error while decoding 'daemon /status' response: %w", err) + } + return &msgResp, nil +} diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go deleted file mode 100644 index 06cc44d6..00000000 --- a/pkg/cfg/cfg.go +++ /dev/null @@ -1,12 +0,0 @@ -package cfg - -// Config struct -type Config struct { - Port int - SesswatchPeriodSeconds uint - SesshistInitHistorySize int - Debug bool - BindArrowKeysBash bool - BindArrowKeysZsh bool - BindControlR bool -} diff --git a/pkg/collect/collect.go b/pkg/collect/collect.go deleted file mode 100644 index 5fd849b1..00000000 --- a/pkg/collect/collect.go +++ /dev/null @@ -1,120 +0,0 @@ -package collect - -import ( - "bytes" - "encoding/json" - "io/ioutil" - "log" - "net/http" - "path/filepath" - "strconv" - "strings" - - "github.com/curusarn/resh/pkg/httpclient" - "github.com/curusarn/resh/pkg/records" -) - -// SingleResponse json struct -type SingleResponse struct { - Found bool `json:"found"` - CmdLine string `json:"cmdline"` -} - -// SendRecallRequest to daemon -func SendRecallRequest(r records.SlimRecord, port string) (string, bool) { - 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 := httpclient.New() - resp, err := client.Do(req) - if err != nil { - log.Fatal("resh-daemon is not running - try restarting this terminal") - } - - 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, response.Found -} - -// SendRecord to daemon -func SendRecord(r records.Record, port, path string) { - recJSON, err := json.Marshal(r) - if err != nil { - log.Fatal("send err 1", err) - } - - req, err := http.NewRequest("POST", "http://localhost:"+port+path, - bytes.NewBuffer(recJSON)) - if err != nil { - log.Fatal("send err 2", err) - } - req.Header.Set("Content-Type", "application/json") - - client := httpclient.New() - _, err = client.Do(req) - if err != nil { - log.Fatal("resh-daemon is not running - try restarting this terminal") - } -} - -// 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/histeval.go b/pkg/histanal/histeval.go deleted file mode 100644 index 4d19779f..00000000 --- a/pkg/histanal/histeval.go +++ /dev/null @@ -1,246 +0,0 @@ -package histanal - -import ( - "bytes" - "encoding/json" - "fmt" - "log" - "math/rand" - "os" - "os/exec" - - "github.com/curusarn/resh/pkg/records" - "github.com/curusarn/resh/pkg/strat" - "github.com/jpillora/longestcommon" - - "github.com/schollz/progressbar" -) - -type matchJSON struct { - Match bool - Distance int - CharsRecalled int -} - -type multiMatchItemJSON struct { - Distance int - CharsRecalled int -} - -type multiMatchJSON struct { - Match bool - Entries []multiMatchItemJSON -} - -type strategyJSON struct { - Title string - Description string - Matches []matchJSON - PrefixMatches []multiMatchJSON -} - -// HistEval evaluates history -type HistEval struct { - HistLoad - BatchMode bool - maxCandidates int - Strategies []strategyJSON -} - -// NewHistEval constructs new HistEval -func NewHistEval(inputPath string, - maxCandidates int, skipFailedCmds bool, - debugRecords float64, sanitizedInput bool) HistEval { - - e := HistEval{ - HistLoad: HistLoad{ - skipFailedCmds: skipFailedCmds, - debugRecords: debugRecords, - sanitizedInput: sanitizedInput, - }, - maxCandidates: maxCandidates, - BatchMode: false, - } - records := e.loadHistoryRecords(inputPath) - device := deviceRecords{Records: records} - user := userRecords{} - user.Devices = append(user.Devices, device) - e.UsersRecords = append(e.UsersRecords, user) - e.preprocessRecords() - return e -} - -// NewHistEvalBatchMode constructs new HistEval in batch mode -func NewHistEvalBatchMode(input string, inputDataRoot string, - maxCandidates int, skipFailedCmds bool, - debugRecords float64, sanitizedInput bool) HistEval { - - e := HistEval{ - HistLoad: HistLoad{ - skipFailedCmds: skipFailedCmds, - debugRecords: debugRecords, - sanitizedInput: sanitizedInput, - }, - maxCandidates: maxCandidates, - BatchMode: false, - } - e.UsersRecords = e.loadHistoryRecordsBatchMode(input, inputDataRoot) - e.preprocessRecords() - return e -} - -func (e *HistEval) preprocessDeviceRecords(device deviceRecords) deviceRecords { - sessionIDs := map[string]uint64{} - var nextID uint64 - nextID = 1 // start with 1 because 0 won't get saved to json - for k, record := range device.Records { - id, found := sessionIDs[record.SessionID] - if found == false { - id = nextID - sessionIDs[record.SessionID] = id - nextID++ - } - device.Records[k].SeqSessionID = id - // assert - if record.Sanitized != e.sanitizedInput { - if e.sanitizedInput { - log.Fatal("ASSERT failed: '--sanitized-input' is present but data is not sanitized") - } - log.Fatal("ASSERT failed: data is sanitized but '--sanitized-input' is not present") - } - device.Records[k].SeqSessionID = id - if e.debugRecords > 0 && rand.Float64() < e.debugRecords { - device.Records[k].DebugThisRecord = true - } - } - // sort.SliceStable(device.Records, func(x, y int) bool { - // if device.Records[x].SeqSessionID == device.Records[y].SeqSessionID { - // return device.Records[x].RealtimeAfterLocal < device.Records[y].RealtimeAfterLocal - // } - // return device.Records[x].SeqSessionID < device.Records[y].SeqSessionID - // }) - - // iterate from back and mark last record of each session - sessionIDSet := map[string]bool{} - for i := len(device.Records) - 1; i >= 0; i-- { - var record *records.EnrichedRecord - record = &device.Records[i] - if sessionIDSet[record.SessionID] { - continue - } - sessionIDSet[record.SessionID] = true - record.LastRecordOfSession = true - } - return device -} - -// enrich records and add sequential session ID -func (e *HistEval) preprocessRecords() { - for i := range e.UsersRecords { - for j := range e.UsersRecords[i].Devices { - e.UsersRecords[i].Devices[j] = e.preprocessDeviceRecords(e.UsersRecords[i].Devices[j]) - } - } -} - -// Evaluate a given strategy -func (e *HistEval) Evaluate(strategy strat.IStrategy) error { - title, description := strategy.GetTitleAndDescription() - log.Println("Evaluating strategy:", title, "-", description) - strategyData := strategyJSON{Title: title, Description: description} - for i := range e.UsersRecords { - for j := range e.UsersRecords[i].Devices { - bar := progressbar.New(len(e.UsersRecords[i].Devices[j].Records)) - var prevRecord records.EnrichedRecord - for _, record := range e.UsersRecords[i].Devices[j].Records { - if e.skipFailedCmds && record.ExitCode != 0 { - continue - } - candidates := strategy.GetCandidates(records.Stripped(record)) - if record.DebugThisRecord { - log.Println() - log.Println("===================================================") - log.Println("STRATEGY:", title, "-", description) - log.Println("===================================================") - log.Println("Previous record:") - if prevRecord.RealtimeBefore == 0 { - log.Println("== NIL") - } else { - rec, _ := prevRecord.ToString() - log.Println(rec) - } - log.Println("---------------------------------------------------") - log.Println("Recommendations for:") - rec, _ := record.ToString() - log.Println(rec) - log.Println("---------------------------------------------------") - for i, candidate := range candidates { - if i > 10 { - break - } - log.Println(string(candidate)) - } - log.Println("===================================================") - } - - matchFound := false - longestPrefixMatchLength := 0 - multiMatch := multiMatchJSON{} - for i, candidate := range candidates { - // make an option (--calculate-total) to turn this on/off ? - // if i >= e.maxCandidates { - // break - // } - commonPrefixLength := len(longestcommon.Prefix([]string{candidate, record.CmdLine})) - if commonPrefixLength > longestPrefixMatchLength { - longestPrefixMatchLength = commonPrefixLength - prefixMatch := multiMatchItemJSON{Distance: i + 1, CharsRecalled: commonPrefixLength} - multiMatch.Match = true - multiMatch.Entries = append(multiMatch.Entries, prefixMatch) - } - if candidate == record.CmdLine { - match := matchJSON{Match: true, Distance: i + 1, CharsRecalled: record.CmdLength} - matchFound = true - strategyData.Matches = append(strategyData.Matches, match) - strategyData.PrefixMatches = append(strategyData.PrefixMatches, multiMatch) - break - } - } - if matchFound == false { - strategyData.Matches = append(strategyData.Matches, matchJSON{}) - strategyData.PrefixMatches = append(strategyData.PrefixMatches, multiMatch) - } - err := strategy.AddHistoryRecord(&record) - if err != nil { - log.Println("Error while evauating", err) - return err - } - bar.Add(1) - prevRecord = record - } - strategy.ResetHistory() - fmt.Println() - } - } - e.Strategies = append(e.Strategies, strategyData) - return nil -} - -// CalculateStatsAndPlot results -func (e *HistEval) CalculateStatsAndPlot(scriptName string) { - evalJSON, err := json.Marshal(e) - if err != nil { - log.Fatal("json marshal error", err) - } - buffer := bytes.Buffer{} - buffer.Write(evalJSON) - // run python script to stat and plot/ - cmd := exec.Command(scriptName) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = &buffer - err = cmd.Run() - if err != nil { - log.Printf("Command finished with error: %v", err) - } -} diff --git a/pkg/histanal/histload.go b/pkg/histanal/histload.go deleted file mode 100644 index ec81cc27..00000000 --- a/pkg/histanal/histload.go +++ /dev/null @@ -1,180 +0,0 @@ -package histanal - -import ( - "bufio" - "encoding/json" - "fmt" - "io/ioutil" - "log" - "math/rand" - "os" - "path/filepath" - - "github.com/curusarn/resh/pkg/records" -) - -type deviceRecords struct { - Name string - Records []records.EnrichedRecord -} - -type userRecords struct { - Name string - Devices []deviceRecords -} - -// HistLoad loads history -type HistLoad struct { - UsersRecords []userRecords - skipFailedCmds bool - sanitizedInput bool - debugRecords float64 -} - -func (e *HistLoad) preprocessDeviceRecords(device deviceRecords) deviceRecords { - sessionIDs := map[string]uint64{} - var nextID uint64 - nextID = 1 // start with 1 because 0 won't get saved to json - for k, record := range device.Records { - id, found := sessionIDs[record.SessionID] - if found == false { - id = nextID - sessionIDs[record.SessionID] = id - nextID++ - } - device.Records[k].SeqSessionID = id - // assert - if record.Sanitized != e.sanitizedInput { - if e.sanitizedInput { - log.Fatal("ASSERT failed: '--sanitized-input' is present but data is not sanitized") - } - log.Fatal("ASSERT failed: data is sanitized but '--sanitized-input' is not present") - } - device.Records[k].SeqSessionID = id - if e.debugRecords > 0 && rand.Float64() < e.debugRecords { - device.Records[k].DebugThisRecord = true - } - } - // sort.SliceStable(device.Records, func(x, y int) bool { - // if device.Records[x].SeqSessionID == device.Records[y].SeqSessionID { - // return device.Records[x].RealtimeAfterLocal < device.Records[y].RealtimeAfterLocal - // } - // return device.Records[x].SeqSessionID < device.Records[y].SeqSessionID - // }) - - // iterate from back and mark last record of each session - sessionIDSet := map[string]bool{} - for i := len(device.Records) - 1; i >= 0; i-- { - var record *records.EnrichedRecord - record = &device.Records[i] - if sessionIDSet[record.SessionID] { - continue - } - sessionIDSet[record.SessionID] = true - record.LastRecordOfSession = true - } - return device -} - -// enrich records and add sequential session ID -func (e *HistLoad) preprocessRecords() { - for i := range e.UsersRecords { - for j := range e.UsersRecords[i].Devices { - e.UsersRecords[i].Devices[j] = e.preprocessDeviceRecords(e.UsersRecords[i].Devices[j]) - } - } -} - -func (e *HistLoad) loadHistoryRecordsBatchMode(fname string, dataRootPath string) []userRecords { - var records []userRecords - info, err := os.Stat(dataRootPath) - if err != nil { - log.Fatal("Error: Directory", dataRootPath, "does not exist - exiting! (", err, ")") - } - if info.IsDir() == false { - log.Fatal("Error:", dataRootPath, "is not a directory - exiting!") - } - users, err := ioutil.ReadDir(dataRootPath) - if err != nil { - log.Fatal("Could not read directory:", dataRootPath) - } - fmt.Println("Listing users in <", dataRootPath, ">...") - for _, user := range users { - userRecords := userRecords{Name: user.Name()} - userFullPath := filepath.Join(dataRootPath, user.Name()) - if user.IsDir() == false { - log.Println("Warn: Unexpected file (not a directory) <", userFullPath, "> - skipping.") - continue - } - fmt.Println() - fmt.Printf("*- %s\n", user.Name()) - devices, err := ioutil.ReadDir(userFullPath) - if err != nil { - log.Fatal("Could not read directory:", userFullPath) - } - for _, device := range devices { - deviceRecords := deviceRecords{Name: device.Name()} - deviceFullPath := filepath.Join(userFullPath, device.Name()) - if device.IsDir() == false { - log.Println("Warn: Unexpected file (not a directory) <", deviceFullPath, "> - skipping.") - continue - } - fmt.Printf(" \\- %s\n", device.Name()) - files, err := ioutil.ReadDir(deviceFullPath) - if err != nil { - log.Fatal("Could not read directory:", deviceFullPath) - } - for _, file := range files { - fileFullPath := filepath.Join(deviceFullPath, file.Name()) - if file.Name() == fname { - fmt.Printf(" \\- %s - loading ...", file.Name()) - // load the data - deviceRecords.Records = e.loadHistoryRecords(fileFullPath) - fmt.Println(" OK ✓") - } else { - fmt.Printf(" \\- %s - skipped\n", file.Name()) - } - } - userRecords.Devices = append(userRecords.Devices, deviceRecords) - } - records = append(records, userRecords) - } - return records -} - -func (e *HistLoad) loadHistoryRecords(fname string) []records.EnrichedRecord { - file, err := os.Open(fname) - if err != nil { - log.Fatal("Open() resh history file error:", err) - } - defer file.Close() - - var recs []records.EnrichedRecord - scanner := bufio.NewScanner(file) - for scanner.Scan() { - record := records.Record{} - fallbackRecord := records.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 = records.Convert(&fallbackRecord) - } - if e.sanitizedInput == false { - if record.CmdLength != 0 { - log.Fatal("Assert failed - 'cmdLength' is set in raw data. Maybe you want to use '--sanitized-input' option?") - } - record.CmdLength = len(record.CmdLine) - } else if record.CmdLength == 0 { - log.Fatal("Assert failed - 'cmdLength' is unset in the data. This should not happen.") - } - if !e.skipFailedCmds || record.ExitCode == 0 { - recs = append(recs, records.Enriched(record)) - } - } - return recs -} diff --git a/pkg/histfile/histfile.go b/pkg/histfile/histfile.go deleted file mode 100644 index 73e5309b..00000000 --- a/pkg/histfile/histfile.go +++ /dev/null @@ -1,262 +0,0 @@ -package histfile - -import ( - "encoding/json" - "log" - "math" - "os" - "strconv" - "sync" - - "github.com/curusarn/resh/pkg/histcli" - "github.com/curusarn/resh/pkg/histlist" - "github.com/curusarn/resh/pkg/records" -) - -// Histfile writes records to histfile -type Histfile struct { - sessionsMutex sync.Mutex - sessions map[string]records.Record - historyPath string - - recentMutex sync.Mutex - recentRecords []records.Record - - // NOTE: we have separate histories which only differ if there was not enough resh_history - // resh_history itself is common for both bash and zsh - bashCmdLines histlist.Histlist - zshCmdLines histlist.Histlist - - cliRecords histcli.Histcli -} - -// New creates new histfile and runs its gorutines -func New(input chan records.Record, sessionsToDrop chan string, - reshHistoryPath string, bashHistoryPath string, zshHistoryPath string, - maxInitHistSize int, minInitHistSizeKB int, - signals chan os.Signal, shutdownDone chan string) *Histfile { - - hf := Histfile{ - sessions: map[string]records.Record{}, - historyPath: reshHistoryPath, - bashCmdLines: histlist.New(), - zshCmdLines: histlist.New(), - cliRecords: histcli.New(), - } - go hf.loadHistory(bashHistoryPath, zshHistoryPath, maxInitHistSize, minInitHistSizeKB) - go hf.writer(input, signals, shutdownDone) - go hf.sessionGC(sessionsToDrop) - return &hf -} - -// load records from resh history, reverse, enrich and save -func (h *Histfile) loadCliRecords(recs []records.Record) { - for _, cmdline := range h.bashCmdLines.List { - h.cliRecords.AddCmdLine(cmdline) - } - for _, cmdline := range h.zshCmdLines.List { - h.cliRecords.AddCmdLine(cmdline) - } - for i := len(recs) - 1; i >= 0; i-- { - rec := recs[i] - h.cliRecords.AddRecord(rec) - } - log.Println("histfile: resh history loaded - history records count:", len(h.cliRecords.List)) -} - -// loadsHistory from resh_history and if there is not enough of it also load native shell histories -func (h *Histfile) loadHistory(bashHistoryPath, zshHistoryPath string, maxInitHistSize, minInitHistSizeKB int) { - h.recentMutex.Lock() - defer h.recentMutex.Unlock() - log.Println("histfile: Checking if resh_history is large enough ...") - fi, err := os.Stat(h.historyPath) - var size int - if err != nil { - log.Println("histfile ERROR: failed to stat resh_history file:", err) - } else { - size = int(fi.Size()) - } - useNativeHistories := false - if size/1024 < minInitHistSizeKB { - useNativeHistories = true - log.Println("histfile WARN: resh_history is too small - loading native bash and zsh history ...") - h.bashCmdLines = records.LoadCmdLinesFromBashFile(bashHistoryPath) - log.Println("histfile: bash history loaded - cmdLine count:", len(h.bashCmdLines.List)) - h.zshCmdLines = records.LoadCmdLinesFromZshFile(zshHistoryPath) - log.Println("histfile: zsh history loaded - cmdLine count:", len(h.zshCmdLines.List)) - // no maxInitHistSize when using native histories - maxInitHistSize = math.MaxInt32 - } - log.Println("histfile: Loading resh history from file ...") - history := records.LoadFromFile(h.historyPath, math.MaxInt32) - log.Println("histfile: resh history loaded from file - count:", len(history)) - go h.loadCliRecords(history) - // NOTE: keeping this weird interface for now because we might use it in the future - // when we only load bash or zsh history - reshCmdLines := loadCmdLines(history) - log.Println("histfile: resh history loaded - cmdLine count:", len(reshCmdLines.List)) - if useNativeHistories == false { - h.bashCmdLines = reshCmdLines - h.zshCmdLines = histlist.Copy(reshCmdLines) - return - } - h.bashCmdLines.AddHistlist(reshCmdLines) - log.Println("histfile: bash history + resh history - cmdLine count:", len(h.bashCmdLines.List)) - h.zshCmdLines.AddHistlist(reshCmdLines) - log.Println("histfile: zsh history + resh history - cmdLine count:", len(h.zshCmdLines.List)) -} - -// sessionGC reads sessionIDs from channel and deletes them from histfile struct -func (h *Histfile) sessionGC(sessionsToDrop chan string) { - for { - func() { - session := <-sessionsToDrop - log.Println("histfile: got session to drop", session) - h.sessionsMutex.Lock() - defer h.sessionsMutex.Unlock() - if part1, found := h.sessions[session]; found == true { - log.Println("histfile: Dropping session:", session) - delete(h.sessions, session) - go writeRecord(part1, h.historyPath) - } else { - log.Println("histfile: No hanging parts for session:", session) - } - }() - } -} - -// writer reads records from channel, merges them and writes them to file -func (h *Histfile) writer(input chan records.Record, signals chan os.Signal, shutdownDone chan string) { - for { - func() { - select { - case record := <-input: - h.sessionsMutex.Lock() - defer h.sessionsMutex.Unlock() - - // allows nested sessions to merge records properly - mergeID := record.SessionID + "_" + strconv.Itoa(record.Shlvl) - if record.PartOne { - if _, found := h.sessions[mergeID]; found { - log.Println("histfile WARN: Got another first part of the records before merging the previous one - overwriting! " + - "(this happens in bash because bash-preexec runs when it's not supposed to)") - } - h.sessions[mergeID] = record - } else { - if part1, found := h.sessions[mergeID]; found == false { - log.Println("histfile ERROR: Got second part of records and nothing to merge it with - ignoring! (mergeID:", mergeID, ")") - } else { - delete(h.sessions, mergeID) - go h.mergeAndWriteRecord(part1, record) - } - } - case sig := <-signals: - log.Println("histfile: Got signal " + sig.String()) - h.sessionsMutex.Lock() - defer h.sessionsMutex.Unlock() - log.Println("histfile DEBUG: Unlocked mutex") - - for sessID, record := range h.sessions { - log.Printf("histfile WARN: Writing incomplete record for session: %v\n", sessID) - h.writeRecord(record) - } - log.Println("histfile DEBUG: Shutdown success") - shutdownDone <- "histfile" - return - } - }() - } -} - -func (h *Histfile) writeRecord(part1 records.Record) { - writeRecord(part1, h.historyPath) -} - -func (h *Histfile) mergeAndWriteRecord(part1, part2 records.Record) { - err := part1.Merge(part2) - if err != nil { - log.Println("Error while merging", err) - return - } - - func() { - h.recentMutex.Lock() - defer h.recentMutex.Unlock() - h.recentRecords = append(h.recentRecords, part1) - cmdLine := part1.CmdLine - h.bashCmdLines.AddCmdLine(cmdLine) - h.zshCmdLines.AddCmdLine(cmdLine) - h.cliRecords.AddRecord(part1) - }() - - writeRecord(part1, h.historyPath) -} - -func writeRecord(rec records.Record, outputPath string) { - recJSON, err := json.Marshal(rec) - if err != nil { - log.Println("Marshalling error", err) - return - } - f, err := os.OpenFile(outputPath, - os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - log.Println("Could not open file", err) - return - } - defer f.Close() - _, err = f.Write(append(recJSON, []byte("\n")...)) - if err != nil { - log.Printf("Error while writing: %v, %s\n", rec, err) - return - } -} - -// GetRecentCmdLines returns recent cmdLines -func (h *Histfile) GetRecentCmdLines(shell string, limit int) histlist.Histlist { - // NOTE: limit does nothing atm - h.recentMutex.Lock() - defer h.recentMutex.Unlock() - log.Println("histfile: History requested ...") - var hl histlist.Histlist - if shell == "bash" { - hl = histlist.Copy(h.bashCmdLines) - log.Println("histfile: history copied (bash) - cmdLine count:", len(hl.List)) - return hl - } - if shell != "zsh" { - log.Println("histfile ERROR: Unknown shell: ", shell) - } - hl = histlist.Copy(h.zshCmdLines) - log.Println("histfile: history copied (zsh) - cmdLine count:", len(hl.List)) - return hl -} - -// DumpCliRecords returns enriched records -func (h *Histfile) DumpCliRecords() histcli.Histcli { - // don't forget locks in the future - return h.cliRecords -} - -func loadCmdLines(recs []records.Record) histlist.Histlist { - hl := histlist.New() - // go from bottom and deduplicate - 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 - // } - } - // add everything to histlist - for _, cmdLine := range cmdLines { - hl.AddCmdLine(cmdLine) - } - return hl -} diff --git a/pkg/msg/msg.go b/pkg/msg/msg.go deleted file mode 100644 index 3c859874..00000000 --- a/pkg/msg/msg.go +++ /dev/null @@ -1,32 +0,0 @@ -package msg - -import "github.com/curusarn/resh/pkg/records" - -// CliMsg struct -type CliMsg struct { - SessionID string `json:"sessionID"` - PWD string `json:"pwd"` -} - -// CliResponse struct -type CliResponse struct { - CliRecords []records.CliRecord `json:"cliRecords"` -} - -// InspectMsg struct -type InspectMsg struct { - SessionID string `json:"sessionId"` - Count uint `json:"count"` -} - -// MultiResponse struct -type MultiResponse struct { - CmdLines []string `json:"cmdlines"` -} - -// StatusResponse struct -type StatusResponse struct { - Status bool `json:"status"` - Version string `json:"version"` - Commit string `json:"commit"` -} diff --git a/pkg/records/records.go b/pkg/records/records.go deleted file mode 100644 index 6271ba52..00000000 --- a/pkg/records/records.go +++ /dev/null @@ -1,689 +0,0 @@ -package records - -import ( - "bufio" - "encoding/json" - "errors" - "io" - "log" - "math" - "os" - "strconv" - "strings" - - "github.com/curusarn/resh/pkg/histlist" - "github.com/mattn/go-shellwords" -) - -// BaseRecord - common base for Record and FallbackRecord -type BaseRecord struct { - // core - CmdLine string `json:"cmdLine"` - ExitCode int `json:"exitCode"` - Shell string `json:"shell"` - Uname string `json:"uname"` - SessionID string `json:"sessionId"` - RecordID string `json:"recordId"` - - // posix - Home string `json:"home"` - Lang string `json:"lang"` - LcAll string `json:"lcAll"` - Login string `json:"login"` - //Path string `json:"path"` - Pwd string `json:"pwd"` - PwdAfter string `json:"pwdAfter"` - ShellEnv string `json:"shellEnv"` - Term string `json:"term"` - - // non-posix"` - RealPwd string `json:"realPwd"` - RealPwdAfter string `json:"realPwdAfter"` - Pid int `json:"pid"` - SessionPID int `json:"sessionPid"` - Host string `json:"host"` - Hosttype string `json:"hosttype"` - Ostype string `json:"ostype"` - Machtype string `json:"machtype"` - Shlvl int `json:"shlvl"` - - // before after - TimezoneBefore string `json:"timezoneBefore"` - TimezoneAfter string `json:"timezoneAfter"` - - RealtimeBefore float64 `json:"realtimeBefore"` - RealtimeAfter float64 `json:"realtimeAfter"` - RealtimeBeforeLocal float64 `json:"realtimeBeforeLocal"` - RealtimeAfterLocal float64 `json:"realtimeAfterLocal"` - - RealtimeDuration float64 `json:"realtimeDuration"` - RealtimeSinceSessionStart float64 `json:"realtimeSinceSessionStart"` - RealtimeSinceBoot float64 `json:"realtimeSinceBoot"` - //Logs []string `json: "logs"` - - 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"` - OsReleaseIDLike string `json:"osReleaseIdLike"` - OsReleaseName string `json:"osReleaseName"` - OsReleasePrettyName string `json:"osReleasePrettyName"` - - ReshUUID string `json:"reshUuid"` - ReshVersion string `json:"reshVersion"` - ReshRevision string `json:"reshRevision"` - - // records come in two parts (collect and postcollect) - PartOne bool `json:"partOne,omitempty"` // false => part two - PartsMerged bool `json:"partsMerged"` - // special flag -> not an actual record but an session end - SessionExit bool `json:"sessionExit,omitempty"` - - // recall metadata - Recalled bool `json:"recalled"` - RecallHistno int `json:"recallHistno,omitempty"` - RecallStrategy string `json:"recallStrategy,omitempty"` - RecallActionsRaw string `json:"recallActionsRaw,omitempty"` - RecallActions []string `json:"recallActions,omitempty"` - RecallLastCmdLine string `json:"recallLastCmdLine"` - - // recall command - RecallPrefix string `json:"recallPrefix,omitempty"` - - // added by sanitizatizer - Sanitized bool `json:"sanitized,omitempty"` - CmdLength int `json:"cmdLength,omitempty"` -} - -// Record representing single executed command with its metadata -type Record struct { - BaseRecord - - Cols string `json:"cols"` - Lines string `json:"lines"` -} - -// EnrichedRecord - record enriched with additional data -type EnrichedRecord struct { - Record - - // enriching fields - added "later" - Command string `json:"command"` - FirstWord string `json:"firstWord"` - Invalid bool `json:"invalid"` - SeqSessionID uint64 `json:"seqSessionId"` - LastRecordOfSession bool `json:"lastRecordOfSession"` - DebugThisRecord bool `json:"debugThisRecord"` - Errors []string `json:"errors"` - // SeqSessionID uint64 `json:"seqSessionId,omitempty"` -} - -// FallbackRecord when record is too old and can't be parsed into regular Record -type FallbackRecord struct { - BaseRecord - // older version of the record where cols and lines are int - - Cols int `json:"cols"` // notice the int type - Lines int `json:"lines"` // notice the int type -} - -// SlimRecord used for recalling because unmarshalling record w/ 50+ fields is too slow -type SlimRecord struct { - SessionID string `json:"sessionId"` - RecallHistno int `json:"recallHistno,omitempty"` - RecallPrefix string `json:"recallPrefix,omitempty"` - - // extra recall - we might use these in the future - // Pwd string `json:"pwd"` - // RealPwd string `json:"realPwd"` - // GitDir string `json:"gitDir"` - // GitRealDir string `json:"gitRealDir"` - // GitOriginRemote string `json:"gitOriginRemote"` - -} - -// CliRecord used for sending records to RESH-CLI -type CliRecord struct { - IsRaw bool `json:"isRaw"` - SessionID string `json:"sessionId"` - - CmdLine string `json:"cmdLine"` - Host string `json:"host"` - Pwd string `json:"pwd"` - Home string `json:"home"` // helps us to collapse /home/user to tilde - GitOriginRemote string `json:"gitOriginRemote"` - ExitCode int `json:"exitCode"` - - RealtimeBefore float64 `json:"realtimeBefore"` - // RealtimeAfter float64 `json:"realtimeAfter"` - // RealtimeDuration float64 `json:"realtimeDuration"` -} - -// NewCliRecordFromCmdLine from EnrichedRecord -func NewCliRecordFromCmdLine(cmdLine string) CliRecord { - return CliRecord{ - IsRaw: true, - CmdLine: cmdLine, - } -} - -// NewCliRecord from EnrichedRecord -func NewCliRecord(r EnrichedRecord) CliRecord { - return CliRecord{ - IsRaw: false, - SessionID: r.SessionID, - CmdLine: r.CmdLine, - Host: r.Host, - Pwd: r.Pwd, - Home: r.Home, - GitOriginRemote: r.GitOriginRemote, - ExitCode: r.ExitCode, - RealtimeBefore: r.RealtimeBefore, - } -} - -// 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 - Cols: strconv.Itoa(r.Cols), - Lines: strconv.Itoa(r.Lines), - } -} - -// ToString - returns record the json -func (r EnrichedRecord) ToString() (string, error) { - jsonRec, err := json.Marshal(r) - if err != nil { - return "marshalling error", err - } - return string(jsonRec), nil -} - -// Enriched - returnd enriched record -func Enriched(r Record) EnrichedRecord { - record := EnrichedRecord{Record: r} - // normlize git remote - record.GitOriginRemote = NormalizeGitRemote(record.GitOriginRemote) - record.GitOriginRemoteAfter = NormalizeGitRemote(record.GitOriginRemoteAfter) - // Get command/first word from commandline - var err error - err = r.Validate() - if err != nil { - record.Errors = append(record.Errors, "Validate error:"+err.Error()) - // rec, _ := record.ToString() - // log.Println("Invalid command:", rec) - record.Invalid = true - } - record.Command, record.FirstWord, err = GetCommandAndFirstWord(r.CmdLine) - if err != nil { - record.Errors = append(record.Errors, "GetCommandAndFirstWord error:"+err.Error()) - // rec, _ := record.ToString() - // log.Println("Invalid command:", rec) - record.Invalid = true // should this be really invalid ? - } - return record -} - -// Merge two records (part1 - collect + part2 - postcollect) -func (r *Record) Merge(r2 Record) error { - if r.PartOne == false || r2.PartOne { - return errors.New("Expected part1 and part2 of the same record - usage: part1.Merge(part2)") - } - if r.SessionID != r2.SessionID { - return errors.New("Records to merge are not from the same sesion - r1:" + r.SessionID + " r2:" + r2.SessionID) - } - if r.CmdLine != r2.CmdLine { - return errors.New("Records to merge are not parts of the same records - r1:" + r.CmdLine + " r2:" + r2.CmdLine) - } - if r.RecordID != r2.RecordID { - return errors.New("Records to merge do not have the same ID - r1:" + r.RecordID + " r2:" + r2.RecordID) - } - // r.RealtimeBefore != r2.RealtimeBefore - can't be used because of bash-preexec runs when it's not supposed to - r.ExitCode = r2.ExitCode - r.PwdAfter = r2.PwdAfter - r.RealPwdAfter = r2.RealPwdAfter - r.GitDirAfter = r2.GitDirAfter - r.GitRealDirAfter = r2.GitRealDirAfter - r.RealtimeAfter = r2.RealtimeAfter - r.GitOriginRemoteAfter = r2.GitOriginRemoteAfter - r.TimezoneAfter = r2.TimezoneAfter - r.RealtimeAfterLocal = r2.RealtimeAfterLocal - r.RealtimeDuration = r2.RealtimeDuration - - r.PartsMerged = true - r.PartOne = false - return nil -} - -// Validate - returns error if the record is invalid -func (r *Record) Validate() error { - if r.CmdLine == "" { - return errors.New("There is no CmdLine") - } - if r.RealtimeBefore == 0 || r.RealtimeAfter == 0 { - return errors.New("There is no Time") - } - if r.RealtimeBeforeLocal == 0 || r.RealtimeAfterLocal == 0 { - return errors.New("There is no Local Time") - } - if r.RealPwd == "" || r.RealPwdAfter == "" { - return errors.New("There is no Real Pwd") - } - if r.Pwd == "" || r.PwdAfter == "" { - return errors.New("There is no Pwd") - } - - // TimezoneBefore - // TimezoneAfter - - // RealtimeDuration - // RealtimeSinceSessionStart - TODO: add later - // RealtimeSinceBoot - TODO: add later - - // device extras - // Host - // Hosttype - // Ostype - // Machtype - // OsReleaseID - // OsReleaseVersionID - // OsReleaseIDLike - // OsReleaseName - // OsReleasePrettyName - - // session extras - // Term - // Shlvl - - // static info - // Lang - // LcAll - - // meta - // ReshUUID - // ReshVersion - // ReshRevision - - // added by sanitizatizer - // Sanitized - // CmdLength - return nil -} - -// SetCmdLine sets cmdLine and related members -func (r *EnrichedRecord) SetCmdLine(cmdLine string) { - r.CmdLine = cmdLine - r.CmdLength = len(cmdLine) - r.ExitCode = 0 - var err error - r.Command, r.FirstWord, err = GetCommandAndFirstWord(cmdLine) - if err != nil { - r.Errors = append(r.Errors, "GetCommandAndFirstWord error:"+err.Error()) - // log.Println("Invalid command:", r.CmdLine) - r.Invalid = true - } -} - -// Stripped returns record stripped of all info that is not available during prediction -func Stripped(r EnrichedRecord) EnrichedRecord { - // clear the cmd itself - r.SetCmdLine("") - // replace after info with before info - r.PwdAfter = r.Pwd - r.RealPwdAfter = r.RealPwd - r.TimezoneAfter = r.TimezoneBefore - r.RealtimeAfter = r.RealtimeBefore - r.RealtimeAfterLocal = r.RealtimeBeforeLocal - // clear some more stuff - r.RealtimeDuration = 0 - r.LastRecordOfSession = false - return r -} - -// GetCommandAndFirstWord func -func GetCommandAndFirstWord(cmdLine string) (string, string, error) { - args, err := shellwords.Parse(cmdLine) - if err != nil { - // log.Println("shellwords Error:", err, " (cmdLine: <", cmdLine, "> )") - return "", "", err - } - if len(args) == 0 { - return "", "", nil - } - i := 0 - for true { - // commands in shell sometimes look like this `variable=something command argument otherArgument --option` - // to get the command we skip over tokens that contain '=' - if strings.ContainsRune(args[i], '=') && len(args) > i+1 { - i++ - continue - } - return args[i], args[0], nil - } - log.Fatal("GetCommandAndFirstWord error: this should not happen!") - return "ERROR", "ERROR", errors.New("this should not happen - contact developer ;)") -} - -// NormalizeGitRemote func -func NormalizeGitRemote(gitRemote string) string { - if strings.HasSuffix(gitRemote, ".git") { - return gitRemote[:len(gitRemote)-4] - } - return gitRemote -} - -// DistParams is used to supply params to Enrichedrecords.DistanceTo() -type DistParams struct { - ExitCode float64 - MachineID float64 - SessionID float64 - Login float64 - Shell float64 - Pwd float64 - RealPwd float64 - Git float64 - Time float64 -} - -// DistanceTo another record -func (r *EnrichedRecord) DistanceTo(r2 EnrichedRecord, p DistParams) float64 { - var dist float64 - dist = 0 - - // lev distance or something? TODO later - // CmdLine - - // exit code - if r.ExitCode != r2.ExitCode { - if r.ExitCode == 0 || r2.ExitCode == 0 { - // one success + one error -> 1 - dist += 1 * p.ExitCode - } else { - // two different errors - dist += 0.5 * p.ExitCode - } - } - - // machine/device - if r.MachineID != r2.MachineID { - dist += 1 * p.MachineID - } - // Uname - - // session - if r.SessionID != r2.SessionID { - dist += 1 * p.SessionID - } - // Pid - add because of nested shells? - // SessionPid - - // user - if r.Login != r2.Login { - dist += 1 * p.Login - } - // Home - - // shell - if r.Shell != r2.Shell { - dist += 1 * p.Shell - } - // ShellEnv - - // pwd - if r.Pwd != r2.Pwd { - // TODO: compare using hierarchy - // TODO: make more important - dist += 1 * p.Pwd - } - if r.RealPwd != r2.RealPwd { - // TODO: -||- - dist += 1 * p.RealPwd - } - // PwdAfter - // RealPwdAfter - - // git - if r.GitDir != r2.GitDir { - dist += 1 * p.Git - } - if r.GitRealDir != r2.GitRealDir { - dist += 1 * p.Git - } - if r.GitOriginRemote != r2.GitOriginRemote { - dist += 1 * p.Git - } - - // time - // this can actually get negative for differences of less than one second which is fine - // distance grows by 1 with every order - distTime := math.Log10(math.Abs(r.RealtimeBefore-r2.RealtimeBefore)) * p.Time - if math.IsNaN(distTime) == false && math.IsInf(distTime, 0) == false { - dist += distTime - } - // RealtimeBeforeLocal - // RealtimeAfter - // RealtimeAfterLocal - - // TimezoneBefore - // TimezoneAfter - - // RealtimeDuration - // RealtimeSinceSessionStart - TODO: add later - // RealtimeSinceBoot - TODO: add later - - // device extras - // Host - // Hosttype - // Ostype - // Machtype - // OsReleaseID - // OsReleaseVersionID - // OsReleaseIDLike - // OsReleaseName - // OsReleasePrettyName - - // session extras - // Term - // Shlvl - - // static info - // Lang - // LcAll - - // meta - // ReshUUID - // ReshVersion - // ReshRevision - - // added by sanitizatizer - // Sanitized - // CmdLength - - return dist -} - -// LoadFromFile loads records from 'fname' file -func LoadFromFile(fname string, limit int) []Record { - const allowedErrors = 1 - var encounteredErrors int - // NOTE: limit does nothing atm - var recs []Record - file, err := os.Open(fname) - if err != nil { - log.Println("Open() resh history file error:", err) - log.Println("WARN: Skipping reading resh history!") - return recs - } - defer file.Close() - - reader := bufio.NewReader(file) - var i int - var firstErrLine int - for { - line, err := reader.ReadString('\n') - if err != nil { - break - } - i++ - record := Record{} - fallbackRecord := FallbackRecord{} - err = json.Unmarshal([]byte(line), &record) - if err != nil { - err = json.Unmarshal([]byte(line), &fallbackRecord) - if err != nil { - if encounteredErrors == 0 { - firstErrLine = i - } - encounteredErrors++ - log.Println("Line:", line) - log.Println("Decoding error:", err) - if encounteredErrors > allowedErrors { - log.Fatalf("Fatal: Encountered more than %d decoding errors (%d)", allowedErrors, encounteredErrors) - } - } - record = Convert(&fallbackRecord) - } - recs = append(recs, record) - } - // log.Println("records: done loading file:", err) - if err != io.EOF { - log.Println("records: error while loading file:", err) - } - // log.Println("records: Loaded lines - count:", i) - if encounteredErrors > 0 { - // fix errors in the history file - log.Printf("There were %d decoding errors, the first error happend on line %d/%d", encounteredErrors, firstErrLine, i) - log.Println("Backing up current history file ...") - err := copyFile(fname, fname+".bak") - if err != nil { - log.Fatalln("Failed to backup history file with decode errors") - } - log.Println("Writing out a history file without errors ...") - err = writeHistory(fname, recs) - if err != nil { - log.Fatalln("Fatal: Failed write out new history") - } - } - log.Println("records: Loaded records - count:", len(recs)) - return recs -} - -func copyFile(source, dest string) error { - from, err := os.Open(source) - if err != nil { - // log.Println("Open() resh history file error:", err) - return err - } - defer from.Close() - - // to, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE, 0666) - to, err := os.Create(dest) - if err != nil { - // log.Println("Create() resh history backup error:", err) - return err - } - defer to.Close() - - _, err = io.Copy(to, from) - if err != nil { - // log.Println("Copy() resh history to backup error:", err) - return err - } - return nil -} - -func writeHistory(fname string, history []Record) error { - file, err := os.Create(fname) - if err != nil { - // log.Println("Create() resh history error:", err) - return err - } - defer file.Close() - for _, rec := range history { - jsn, err := json.Marshal(rec) - if err != nil { - log.Fatalln("Encode error!") - } - file.Write(append(jsn, []byte("\n")...)) - } - return nil -} - -// LoadCmdLinesFromZshFile loads cmdlines from zsh history file -func LoadCmdLinesFromZshFile(fname string) histlist.Histlist { - hl := histlist.New() - file, err := os.Open(fname) - if err != nil { - log.Println("Open() zsh history file error:", err) - log.Println("WARN: Skipping reading zsh history!") - return hl - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - // trim newline - line = strings.TrimRight(line, "\n") - var cmd string - // zsh format EXTENDED_HISTORY - // : 1576270617:0;make install - // zsh format no EXTENDED_HISTORY - // make install - if len(line) == 0 { - // skip empty - continue - } - if strings.Contains(line, ":") && strings.Contains(line, ";") && - len(strings.Split(line, ":")) >= 3 && len(strings.Split(line, ";")) >= 2 { - // contains at least 2x ':' and 1x ';' => assume EXTENDED_HISTORY - cmd = strings.Split(line, ";")[1] - } else { - cmd = line - } - hl.AddCmdLine(cmd) - } - return hl -} - -// LoadCmdLinesFromBashFile loads cmdlines from bash history file -func LoadCmdLinesFromBashFile(fname string) histlist.Histlist { - hl := histlist.New() - file, err := os.Open(fname) - if err != nil { - log.Println("Open() bash history file error:", err) - log.Println("WARN: Skipping reading bash history!") - return hl - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - // trim newline - line = strings.TrimRight(line, "\n") - // trim spaces from left - line = strings.TrimLeft(line, " ") - // bash format (two lines) - // #1576199174 - // make install - if strings.HasPrefix(line, "#") { - // is either timestamp or comment => skip - continue - } - if len(line) == 0 { - // skip empty - continue - } - hl.AddCmdLine(line) - } - return hl -} diff --git a/pkg/records/records_test.go b/pkg/records/records_test.go deleted file mode 100644 index 5ef3c55a..00000000 --- a/pkg/records/records_test.go +++ /dev/null @@ -1,152 +0,0 @@ -package records - -import ( - "bufio" - "encoding/json" - "log" - "os" - "testing" -) - -func GetTestRecords() []Record { - file, err := os.Open("testdata/resh_history.json") - 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{} - line := scanner.Text() - err = json.Unmarshal([]byte(line), &record) - if err != nil { - log.Println("Line:", line) - log.Fatal("Decoding error:", err) - } - recs = append(recs, record) - } - return recs -} - -func GetTestEnrichedRecords() []EnrichedRecord { - var recs []EnrichedRecord - for _, rec := range GetTestRecords() { - recs = append(recs, Enriched(rec)) - } - return recs -} - -func TestToString(t *testing.T) { - for _, rec := range GetTestEnrichedRecords() { - _, err := rec.ToString() - if err != nil { - t.Error("ToString() failed") - } - } -} - -func TestEnriched(t *testing.T) { - record := Record{BaseRecord: BaseRecord{CmdLine: "cmd arg1 arg2"}} - enriched := Enriched(record) - if enriched.FirstWord != "cmd" || enriched.Command != "cmd" { - t.Error("Enriched() returned reocord w/ wrong Command OR FirstWord") - } -} - -func TestValidate(t *testing.T) { - record := EnrichedRecord{} - if record.Validate() == nil { - t.Error("Validate() didn't return an error for invalid record") - } - record.CmdLine = "cmd arg" - record.FirstWord = "cmd" - record.Command = "cmd" - time := 1234.5678 - record.RealtimeBefore = time - record.RealtimeAfter = time - record.RealtimeBeforeLocal = time - record.RealtimeAfterLocal = time - pwd := "/pwd" - record.Pwd = pwd - record.PwdAfter = pwd - record.RealPwd = pwd - record.RealPwdAfter = pwd - if record.Validate() != nil { - t.Error("Validate() returned an error for a valid record") - } -} - -func TestSetCmdLine(t *testing.T) { - record := EnrichedRecord{} - cmdline := "cmd arg1 arg2" - record.SetCmdLine(cmdline) - if record.CmdLine != cmdline || record.Command != "cmd" || record.FirstWord != "cmd" { - t.Error() - } -} - -func TestStripped(t *testing.T) { - for _, rec := range GetTestEnrichedRecords() { - stripped := Stripped(rec) - - // there should be no cmdline - if stripped.CmdLine != "" || - stripped.FirstWord != "" || - stripped.Command != "" { - t.Error("Stripped() returned record w/ info about CmdLine, Command OR FirstWord") - } - // *after* fields should be overwritten by *before* fields - if stripped.PwdAfter != stripped.Pwd || - stripped.RealPwdAfter != stripped.RealPwd || - stripped.TimezoneAfter != stripped.TimezoneBefore || - stripped.RealtimeAfter != stripped.RealtimeBefore || - stripped.RealtimeAfterLocal != stripped.RealtimeBeforeLocal { - t.Error("Stripped() returned record w/ different *after* and *before* values - *after* fields should be overwritten by *before* fields") - } - // there should be no information about duration and session end - if stripped.RealtimeDuration != 0 || - stripped.LastRecordOfSession != false { - t.Error("Stripped() returned record with too much information") - } - } -} - -func TestGetCommandAndFirstWord(t *testing.T) { - cmd, stWord, err := GetCommandAndFirstWord("cmd arg1 arg2") - if err != nil || cmd != "cmd" || stWord != "cmd" { - t.Error("GetCommandAndFirstWord() returned wrong Command OR FirstWord") - } -} - -func TestDistanceTo(t *testing.T) { - paramsFull := DistParams{ - ExitCode: 1, - MachineID: 1, - SessionID: 1, - Login: 1, - Shell: 1, - Pwd: 1, - RealPwd: 1, - Git: 1, - Time: 1, - } - paramsZero := DistParams{} - var prevRec EnrichedRecord - for _, rec := range GetTestEnrichedRecords() { - dist := rec.DistanceTo(rec, paramsFull) - if dist != 0 { - t.Error("DistanceTo() itself should be always 0") - } - dist = rec.DistanceTo(prevRec, paramsFull) - if dist == 0 { - t.Error("DistanceTo() between two test records shouldn't be 0") - } - dist = rec.DistanceTo(prevRec, paramsZero) - if dist != 0 { - t.Error("DistanceTo() should be 0 when DistParams is all zeros") - } - prevRec = rec - } -} diff --git a/pkg/records/testdata/resh_history.json b/pkg/records/testdata/resh_history.json deleted file mode 100644 index 40f43ab6..00000000 --- a/pkg/records/testdata/resh_history.json +++ /dev/null @@ -1,27 +0,0 @@ -{"cmdLine":"ls","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"d5c0fe70-c80b-4715-87cb-f8d8d5b4c673","cols":"80","lines":"24","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon","pwdAfter":"/home/simon","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon","realPwdAfter":"/home/simon","pid":14560,"sessionPid":14560,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1566762905.173595,"realtimeAfter":1566762905.1894295,"realtimeBeforeLocal":1566770105.173595,"realtimeAfterLocal":1566770105.1894295,"realtimeDuration":0.015834569931030273,"realtimeSinceSessionStart":1.7122540473937988,"realtimeSinceBoot":20766.542254047396,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"} -{"cmdLine":"find . -name applications","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"c5251955-3a64-4353-952e-08d62a898694","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon","pwdAfter":"/home/simon","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon","realPwdAfter":"/home/simon","pid":3109,"sessionPid":3109,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567420001.2531302,"realtimeAfter":1567420002.4311218,"realtimeBeforeLocal":1567427201.2531302,"realtimeAfterLocal":1567427202.4311218,"realtimeDuration":1.1779916286468506,"realtimeSinceSessionStart":957.4848053455353,"realtimeSinceBoot":2336.594805345535,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"} -{"cmdLine":"desktop-file-validate curusarn.sync-clipboards.desktop ","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"c5251955-3a64-4353-952e-08d62a898694","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/.local/share/applications","pwdAfter":"/home/simon/.local/share/applications","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/.local/share/applications","realPwdAfter":"/home/simon/.local/share/applications","pid":3109,"sessionPid":3109,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567421748.2965438,"realtimeAfter":1567421748.3068867,"realtimeBeforeLocal":1567428948.2965438,"realtimeAfterLocal":1567428948.3068867,"realtimeDuration":0.010342836380004883,"realtimeSinceSessionStart":2704.528218984604,"realtimeSinceBoot":4083.6382189846036,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"} -{"cmdLine":"cat /tmp/extensions | grep '.'","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"f044cdbf-fd51-4c37-8528-dcd98fc7b6d9","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon","pwdAfter":"/home/simon","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon","realPwdAfter":"/home/simon","pid":6887,"sessionPid":6887,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567461416.6871984,"realtimeAfter":1567461416.7336714,"realtimeBeforeLocal":1567468616.6871984,"realtimeAfterLocal":1567468616.7336714,"realtimeDuration":0.046473026275634766,"realtimeSinceSessionStart":21.45597553253174,"realtimeSinceBoot":43752.03597553253,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"} -{"cmdLine":"cd git/resh/","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"f044cdbf-fd51-4c37-8528-dcd98fc7b6d9","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon","realPwdAfter":"/home/simon/git/resh","pid":6887,"sessionPid":6887,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567461667.8806899,"realtimeAfter":1567461667.8949044,"realtimeBeforeLocal":1567468867.8806899,"realtimeAfterLocal":1567468867.8949044,"realtimeDuration":0.014214515686035156,"realtimeSinceSessionStart":272.64946699142456,"realtimeSinceBoot":44003.229466991426,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"} -{"cmdLine":"git s","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"f044cdbf-fd51-4c37-8528-dcd98fc7b6d9","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":6887,"sessionPid":6887,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567461707.6467602,"realtimeAfter":1567461707.7177293,"realtimeBeforeLocal":1567468907.6467602,"realtimeAfterLocal":1567468907.7177293,"realtimeDuration":0.0709691047668457,"realtimeSinceSessionStart":312.4155373573303,"realtimeSinceBoot":44042.99553735733,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"} -{"cmdLine":"cat /tmp/extensions | grep '^\\.' | cut -f1 |tr '[:upper:]' '[:lower:]' ","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"f044cdbf-fd51-4c37-8528-dcd98fc7b6d9","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":6887,"sessionPid":6887,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567461722.813049,"realtimeAfter":1567461722.8280325,"realtimeBeforeLocal":1567468922.813049,"realtimeAfterLocal":1567468922.8280325,"realtimeDuration":0.014983415603637695,"realtimeSinceSessionStart":327.581826210022,"realtimeSinceBoot":44058.161826210024,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"} -{"cmdLine":"tig","exitCode":127,"shell":"bash","uname":"Linux","sessionId":"f044cdbf-fd51-4c37-8528-dcd98fc7b6d9","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":6887,"sessionPid":6887,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567461906.3896828,"realtimeAfter":1567461906.4084594,"realtimeBeforeLocal":1567469106.3896828,"realtimeAfterLocal":1567469106.4084594,"realtimeDuration":0.018776655197143555,"realtimeSinceSessionStart":511.1584599018097,"realtimeSinceBoot":44241.73845990181,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"} -{"cmdLine":"resh-sanitize-history | jq","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"a3318c80-3521-4b22-aa64-ea0f6c641410","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon","pwdAfter":"/home/simon","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon","realPwdAfter":"/home/simon","pid":14601,"sessionPid":14601,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567547116.2430356,"realtimeAfter":1567547116.7547352,"realtimeBeforeLocal":1567554316.2430356,"realtimeAfterLocal":1567554316.7547352,"realtimeDuration":0.5116996765136719,"realtimeSinceSessionStart":15.841878414154053,"realtimeSinceBoot":30527.201878414155,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} -{"cmdLine":"sudo pacman -S ansible","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"64154f2d-a4bc-4463-a690-520080b61ead","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/kristin","pwdAfter":"/home/simon/git/kristin","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/kristin","realPwdAfter":"/home/simon/git/kristin","pid":5663,"sessionPid":5663,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567609042.0166302,"realtimeAfter":1567609076.9726007,"realtimeBeforeLocal":1567616242.0166302,"realtimeAfterLocal":1567616276.9726007,"realtimeDuration":34.95597052574158,"realtimeSinceSessionStart":1617.0794131755829,"realtimeSinceBoot":6120.029413175583,"gitDir":"/home/simon/git/kristin","gitRealDir":"/home/simon/git/kristin","gitOriginRemote":"git@gitlab.com:sucvut/kristin.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} -{"cmdLine":"vagrant up","exitCode":1,"shell":"bash","uname":"Linux","sessionId":"64154f2d-a4bc-4463-a690-520080b61ead","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/kristin","pwdAfter":"/home/simon/git/kristin","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/kristin","realPwdAfter":"/home/simon/git/kristin","pid":5663,"sessionPid":5663,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567609090.7359188,"realtimeAfter":1567609098.3125577,"realtimeBeforeLocal":1567616290.7359188,"realtimeAfterLocal":1567616298.3125577,"realtimeDuration":7.57663893699646,"realtimeSinceSessionStart":1665.798701763153,"realtimeSinceBoot":6168.748701763153,"gitDir":"/home/simon/git/kristin","gitRealDir":"/home/simon/git/kristin","gitOriginRemote":"git@gitlab.com:sucvut/kristin.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} -{"cmdLine":"sudo modprobe vboxnetflt","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"64154f2d-a4bc-4463-a690-520080b61ead","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/kristin","pwdAfter":"/home/simon/git/kristin","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/kristin","realPwdAfter":"/home/simon/git/kristin","pid":5663,"sessionPid":5663,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567609143.2847652,"realtimeAfter":1567609143.3116078,"realtimeBeforeLocal":1567616343.2847652,"realtimeAfterLocal":1567616343.3116078,"realtimeDuration":0.026842594146728516,"realtimeSinceSessionStart":1718.3475482463837,"realtimeSinceBoot":6221.2975482463835,"gitDir":"/home/simon/git/kristin","gitRealDir":"/home/simon/git/kristin","gitOriginRemote":"git@gitlab.com:sucvut/kristin.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} -{"cmdLine":"echo $RANDOM","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"8ddacadc-6e73-483c-b347-4e18df204466","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon","pwdAfter":"/home/simon","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon","realPwdAfter":"/home/simon","pid":31387,"sessionPid":31387,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567727039.6540458,"realtimeAfter":1567727039.6629689,"realtimeBeforeLocal":1567734239.6540458,"realtimeAfterLocal":1567734239.6629689,"realtimeDuration":0.008923053741455078,"realtimeSinceSessionStart":1470.7667458057404,"realtimeSinceBoot":18495.01674580574,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} -{"cmdLine":"make resh-evaluate ","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567977478.9672194,"realtimeAfter":1567977479.5449634,"realtimeBeforeLocal":1567984678.9672194,"realtimeAfterLocal":1567984679.5449634,"realtimeDuration":0.5777440071105957,"realtimeSinceSessionStart":5738.577540636063,"realtimeSinceBoot":20980.42754063606,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} -{"cmdLine":"cat ~/.resh_history.json | grep \"./resh-eval\" | jq","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"105","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567986105.3988302,"realtimeAfter":1567986105.4809113,"realtimeBeforeLocal":1567993305.3988302,"realtimeAfterLocal":1567993305.4809113,"realtimeDuration":0.08208107948303223,"realtimeSinceSessionStart":14365.00915145874,"realtimeSinceBoot":29606.85915145874,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} -{"cmdLine":"git c \"add sanitized flag to record, add Enrich() to record\"","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1568063976.9103937,"realtimeAfter":1568063976.9326868,"realtimeBeforeLocal":1568071176.9103937,"realtimeAfterLocal":1568071176.9326868,"realtimeDuration":0.0222930908203125,"realtimeSinceSessionStart":92236.52071499825,"realtimeSinceBoot":107478.37071499825,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} -{"cmdLine":"git s","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1568063978.2340608,"realtimeAfter":1568063978.252463,"realtimeBeforeLocal":1568071178.2340608,"realtimeAfterLocal":1568071178.252463,"realtimeDuration":0.0184023380279541,"realtimeSinceSessionStart":92237.84438204765,"realtimeSinceBoot":107479.69438204766,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} -{"cmdLine":"git a evaluate/results.go ","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1568063989.0446353,"realtimeAfter":1568063989.2452207,"realtimeBeforeLocal":1568071189.0446353,"realtimeAfterLocal":1568071189.2452207,"realtimeDuration":0.20058536529541016,"realtimeSinceSessionStart":92248.65495657921,"realtimeSinceBoot":107490.50495657921,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} -{"cmdLine":"sudo pacman -S python-pip","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1568072068.3557143,"realtimeAfter":1568072070.7509863,"realtimeBeforeLocal":1568079268.3557143,"realtimeAfterLocal":1568079270.7509863,"realtimeDuration":2.3952720165252686,"realtimeSinceSessionStart":100327.96603560448,"realtimeSinceBoot":115569.81603560448,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} -{"cmdLine":"pip3 install matplotlib","exitCode":1,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1568072088.5575967,"realtimeAfter":1568072094.372314,"realtimeBeforeLocal":1568079288.5575967,"realtimeAfterLocal":1568079294.372314,"realtimeDuration":5.8147172927856445,"realtimeSinceSessionStart":100348.16791796684,"realtimeSinceBoot":115590.01791796685,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} -{"cmdLine":"sudo pip3 install matplotlib","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1568072106.138616,"realtimeAfter":1568072115.1124601,"realtimeBeforeLocal":1568079306.138616,"realtimeAfterLocal":1568079315.1124601,"realtimeDuration":8.973844051361084,"realtimeSinceSessionStart":100365.7489373684,"realtimeSinceBoot":115607.5989373684,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} -{"cmdLine":"./resh-evaluate --plotting-script evaluate/resh-evaluate-plot.py --input ~/git/resh_private/history_data/simon/dell/resh_history.json ","exitCode":130,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1568076266.9364285,"realtimeAfter":1568076288.1131275,"realtimeBeforeLocal":1568083466.9364285,"realtimeAfterLocal":1568083488.1131275,"realtimeDuration":21.176698923110962,"realtimeSinceSessionStart":104526.54674983025,"realtimeSinceBoot":119768.39674983025,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} -{"cmdLine":"git c \"Add a bunch of useless comments to make linter happy\"","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"04050353-a97d-4435-9248-f47dd08b2f2a","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":14702,"sessionPid":14702,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1569456045.8763022,"realtimeAfter":1569456045.9030173,"realtimeBeforeLocal":1569463245.8763022,"realtimeAfterLocal":1569463245.9030173,"realtimeDuration":0.02671504020690918,"realtimeSinceSessionStart":2289.789242744446,"realtimeSinceBoot":143217.91924274445,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.3","reshRevision":"188d8b420493","sanitized":false} -{"cmdLine":"fuck","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"a4aadf03-610d-4731-ba94-5b7ce21e7bb9","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":3413,"sessionPid":3413,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1569687682.4250975,"realtimeAfter":1569687682.5877323,"realtimeBeforeLocal":1569694882.4250975,"realtimeAfterLocal":1569694882.5877323,"realtimeDuration":0.16263484954833984,"realtimeSinceSessionStart":264603.49496507645,"realtimeSinceBoot":374854.48496507644,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.3","reshRevision":"188d8b420493","sanitized":false} -{"cmdLine":"code .","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"87c7ab14-ae51-408d-adbc-fc4f9d28de6e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":31947,"sessionPid":31947,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1569709366.523767,"realtimeAfter":1569709367.516908,"realtimeBeforeLocal":1569716566.523767,"realtimeAfterLocal":1569716567.516908,"realtimeDuration":0.9931409358978271,"realtimeSinceSessionStart":23846.908839941025,"realtimeSinceBoot":396539.888839941,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.3","reshRevision":"188d8b420493","sanitized":false} -{"cmdLine":"make test","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"87c7ab14-ae51-408d-adbc-fc4f9d28de6e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":31947,"sessionPid":31947,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1569709371.89966,"realtimeAfter":1569709377.430194,"realtimeBeforeLocal":1569716571.89966,"realtimeAfterLocal":1569716577.430194,"realtimeDuration":5.530533790588379,"realtimeSinceSessionStart":23852.284733057022,"realtimeSinceBoot":396545.264733057,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.3","reshRevision":"188d8b420493","sanitized":false} -{"cmdLine":"mkdir ~/git/resh/testdata","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"71529b60-2e7b-4d5b-8dc1-6d0740b58e9e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon","pwdAfter":"/home/simon","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon","realPwdAfter":"/home/simon","pid":21224,"sessionPid":21224,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1569709838.4642656,"realtimeAfter":1569709838.4718792,"realtimeBeforeLocal":1569717038.4642656,"realtimeAfterLocal":1569717038.4718792,"realtimeDuration":0.007613658905029297,"realtimeSinceSessionStart":9.437154054641724,"realtimeSinceBoot":397011.02715405467,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.3","reshRevision":"188d8b420493","sanitized":false} diff --git a/pkg/searchapp/test.go b/pkg/searchapp/test.go deleted file mode 100644 index e33e2f77..00000000 --- a/pkg/searchapp/test.go +++ /dev/null @@ -1,23 +0,0 @@ -package searchapp - -import ( - "math" - - "github.com/curusarn/resh/pkg/histcli" - "github.com/curusarn/resh/pkg/msg" - "github.com/curusarn/resh/pkg/records" -) - -// LoadHistoryFromFile ... -func LoadHistoryFromFile(historyPath string, numLines int) msg.CliResponse { - recs := records.LoadFromFile(historyPath, math.MaxInt32) - if numLines != 0 && numLines < len(recs) { - recs = recs[:numLines] - } - cliRecords := histcli.New() - for i := len(recs) - 1; i >= 0; i-- { - rec := recs[i] - cliRecords.AddRecord(rec) - } - return msg.CliResponse{CliRecords: cliRecords.List} -} diff --git a/pkg/sesshist/sesshist.go b/pkg/sesshist/sesshist.go deleted file mode 100644 index f7227642..00000000 --- a/pkg/sesshist/sesshist.go +++ /dev/null @@ -1,243 +0,0 @@ -package sesshist - -import ( - "errors" - "log" - "strconv" - "strings" - "sync" - - "github.com/curusarn/resh/pkg/histfile" - "github.com/curusarn/resh/pkg/histlist" - "github.com/curusarn/resh/pkg/records" -) - -// Dispatch Recall() calls to an apropriate session history (sesshist) -type Dispatch struct { - sessions map[string]*sesshist - mutex sync.RWMutex - - history *histfile.Histfile - historyInitSize int -} - -// NewDispatch creates a new sesshist.Dispatch and starts necessary gorutines -func NewDispatch(sessionsToInit chan records.Record, sessionsToDrop chan string, - recordsToAdd chan records.Record, history *histfile.Histfile, historyInitSize int) *Dispatch { - - s := Dispatch{ - sessions: map[string]*sesshist{}, - history: history, - historyInitSize: historyInitSize, - } - go s.sessionInitializer(sessionsToInit) - go s.sessionDropper(sessionsToDrop) - go s.recordAdder(recordsToAdd) - return &s -} - -func (s *Dispatch) sessionInitializer(sessionsToInit chan records.Record) { - for { - record := <-sessionsToInit - log.Println("sesshist: got session to init - " + record.SessionID) - s.initSession(record.SessionID, record.Shell) - } -} - -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) - } else { - // this inits session on RESH update - s.checkSession(record.SessionID, record.Shell) - } - // TODO: we will need to handle part2 as well eventually - } -} - -func (s *Dispatch) checkSession(sessionID, shell string) { - s.mutex.RLock() - _, found := s.sessions[sessionID] - s.mutex.RUnlock() - if found == false { - err := s.initSession(sessionID, shell) - if err != nil { - log.Println("sesshist: Error while checking session:", err) - } - } -} - -// InitSession struct -func (s *Dispatch) initSession(sessionID, shell string) error { - log.Println("sesshist: initializing session - " + sessionID) - s.mutex.RLock() - _, found := s.sessions[sessionID] - s.mutex.RUnlock() - - if found == true { - return errors.New("sesshist ERROR: Can't INIT already existing session " + sessionID) - } - - log.Println("sesshist: loading history to populate session - " + sessionID) - historyCmdLines := s.history.GetRecentCmdLines(shell, s.historyInitSize) - - s.mutex.Lock() - defer s.mutex.Unlock() - // init sesshist and populate it with history loaded from file - s.sessions[sessionID] = &sesshist{ - recentCmdLines: historyCmdLines, - } - log.Println("sesshist: session init done - " + sessionID) - return nil -} - -// DropSession struct -func (s *Dispatch) dropSession(sessionID string) error { - s.mutex.RLock() - _, found := s.sessions[sessionID] - s.mutex.RUnlock() - - if found == false { - return errors.New("sesshist ERROR: Can't DROP not existing session " + sessionID) - } - - s.mutex.Lock() - defer s.mutex.Unlock() - delete(s.sessions, sessionID) - return nil -} - -// AddRecent record to session -func (s *Dispatch) addRecentRecord(sessionID string, record records.Record) error { - log.Println("sesshist: Adding a record, RLocking main lock ...") - s.mutex.RLock() - log.Println("sesshist: Getting a session ...") - session, found := s.sessions[sessionID] - log.Println("sesshist: RUnlocking main lock ...") - s.mutex.RUnlock() - - if found == false { - log.Println("sesshist ERROR: addRecentRecord(): No session history for SessionID " + sessionID + " - creating session history.") - s.initSession(sessionID, record.Shell) - return s.addRecentRecord(sessionID, record) - } - log.Println("sesshist: RLocking session lock (w/ defer) ...") - session.mutex.Lock() - defer session.mutex.Unlock() - session.recentRecords = append(session.recentRecords, record) - session.recentCmdLines.AddCmdLine(record.CmdLine) - log.Println("sesshist: record:", record.CmdLine, "; added to session:", sessionID, - "; session len:", len(session.recentCmdLines.List), "; session len (records):", len(session.recentRecords)) - return nil -} - -// Recall command from recent session history -func (s *Dispatch) Recall(sessionID string, histno int, prefix string) (string, error) { - log.Println("sesshist - recall: RLocking main lock ...") - s.mutex.RLock() - log.Println("sesshist - recall: Getting session history struct ...") - session, found := s.sessions[sessionID] - s.mutex.RUnlock() - - if found == false { - // TODO: propagate actual shell here so we can use it - go s.initSession(sessionID, "bash") - return "", errors.New("sesshist ERROR: No session history for SessionID " + sessionID + " - creating one ...") - } - log.Println("sesshist - recall: Locking session lock ...") - session.mutex.Lock() - defer session.mutex.Unlock() - if prefix == "" { - log.Println("sesshist - recall: Getting records by histno ...") - return session.getRecordByHistno(histno) - } - log.Println("sesshist - recall: Searching for records by prefix ...") - return session.searchRecordByPrefix(prefix, histno) -} - -// Inspect commands in recent session history -func (s *Dispatch) Inspect(sessionID string, count int) ([]string, error) { - prefix := "" - log.Println("sesshist - inspect: RLocking main lock ...") - s.mutex.RLock() - log.Println("sesshist - inspect: Getting session history struct ...") - session, found := s.sessions[sessionID] - s.mutex.RUnlock() - - if found == false { - // go s.initSession(sessionID) - return nil, errors.New("sesshist ERROR: No session history for SessionID " + sessionID + " - should we create one?") - } - log.Println("sesshist - inspect: Locking session lock ...") - session.mutex.Lock() - defer session.mutex.Unlock() - if prefix == "" { - log.Println("sesshist - inspect: Getting records by histno ...") - idx := len(session.recentCmdLines.List) - count - if idx < 0 { - idx = 0 - } - return session.recentCmdLines.List[idx:], nil - } - log.Println("sesshist - inspect: Searching for records by prefix ... ERROR - Not implemented") - return nil, errors.New("sesshist ERROR: Inspect - Searching for records by prefix Not implemented yet") -} - -type sesshist struct { - mutex sync.Mutex - recentRecords []records.Record - recentCmdLines histlist.Histlist -} - -func (s *sesshist) getRecordByHistno(histno int) (string, error) { - // addRecords() appends records to the end of the slice - // -> this func handles the indexing - if histno == 0 { - return "", errors.New("sesshist ERROR: 'histno == 0' is not a record from history") - } - if histno < 0 { - return "", errors.New("sesshist ERROR: 'histno < 0' is a command from future (not supperted yet)") - } - index := len(s.recentCmdLines.List) - histno - if index < 0 { - return "", errors.New("sesshist ERROR: 'histno > number of commands in the session' (" + strconv.Itoa(len(s.recentCmdLines.List)) + ")") - } - return s.recentCmdLines.List[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.List) - histno - if index < 0 { - return "", errors.New("sesshist ERROR: 'histno > number of commands in the session' (" + strconv.Itoa(len(s.recentCmdLines.List)) + ")") - } - cmdLines := []string{} - for i := len(s.recentCmdLines.List) - 1; i >= 0; i-- { - if strings.HasPrefix(s.recentCmdLines.List[i], prefix) { - cmdLines = append(cmdLines, s.recentCmdLines.List[i]) - if len(cmdLines) >= histno { - break - } - } - } - if len(cmdLines) < histno { - return "", errors.New("sesshist ERROR: 'histno > number of commands matching with given prefix' (" + strconv.Itoa(len(cmdLines)) + ")") - } - return cmdLines[histno-1], nil -} diff --git a/pkg/sesswatch/sesswatch.go b/pkg/sesswatch/sesswatch.go deleted file mode 100644 index ae32bc49..00000000 --- a/pkg/sesswatch/sesswatch.go +++ /dev/null @@ -1,78 +0,0 @@ -package sesswatch - -import ( - "log" - "sync" - "time" - - "github.com/curusarn/resh/pkg/records" - "github.com/mitchellh/go-ps" -) - -type sesswatch struct { - sessionsToDrop []chan string - sleepSeconds uint - - watchedSessions map[string]bool - mutex sync.Mutex -} - -// Go runs the session watcher - watches sessions and sends -func Go(sessionsToWatch chan records.Record, sessionsToWatchRecords chan records.Record, sessionsToDrop []chan string, sleepSeconds uint) { - sw := sesswatch{sessionsToDrop: sessionsToDrop, sleepSeconds: sleepSeconds, watchedSessions: map[string]bool{}} - go sw.waiter(sessionsToWatch, sessionsToWatchRecords) -} - -func (s *sesswatch) waiter(sessionsToWatch chan records.Record, sessionsToWatchRecords chan records.Record) { - for { - func() { - select { - case record := <-sessionsToWatch: - // normal way to start watching a session - id := record.SessionID - pid := record.SessionPID - s.mutex.Lock() - defer s.mutex.Unlock() - if s.watchedSessions[id] == false { - log.Println("sesswatch: start watching NEW session ~ pid:", id, "~", pid) - s.watchedSessions[id] = true - go s.watcher(id, pid) - } - case record := <-sessionsToWatchRecords: - // additional safety - watch sessions that were never properly initialized - id := record.SessionID - pid := record.SessionPID - s.mutex.Lock() - defer s.mutex.Unlock() - if s.watchedSessions[id] == false { - log.Println("sesswatch WARN: start watching NEW session (based on /record) ~ pid:", id, "~", pid) - s.watchedSessions[id] = true - go s.watcher(id, pid) - } - } - }() - } -} - -func (s *sesswatch) watcher(sessionID string, sessionPID int) { - for { - time.Sleep(time.Duration(s.sleepSeconds) * time.Second) - proc, err := ps.FindProcess(sessionPID) - if err != nil { - log.Println("sesswatch ERROR: error while finding process:", sessionPID) - } else if proc == nil { - log.Println("sesswatch: Dropping session ~ pid:", sessionID, "~", sessionPID) - func() { - s.mutex.Lock() - defer s.mutex.Unlock() - s.watchedSessions[sessionID] = false - }() - for _, ch := range s.sessionsToDrop { - log.Println("sesswatch: sending 'drop session' message ...") - ch <- sessionID - log.Println("sesswatch: sending 'drop session' message DONE") - } - break - } - } -} diff --git a/pkg/signalhandler/signalhander.go b/pkg/signalhandler/signalhander.go deleted file mode 100644 index 5d2233e4..00000000 --- a/pkg/signalhandler/signalhander.go +++ /dev/null @@ -1,65 +0,0 @@ -package signalhandler - -import ( - "context" - "log" - "net/http" - "os" - "os/signal" - "strconv" - "syscall" - "time" -) - -func sendSignals(sig os.Signal, subscribers []chan os.Signal, done chan string) { - for _, sub := range subscribers { - sub <- sig - } - chanCount := len(subscribers) - start := time.Now() - delay := time.Millisecond * 100 - timeout := time.Millisecond * 2000 - - for { - select { - case _ = <-done: - chanCount-- - if chanCount == 0 { - log.Println("signalhandler: All components shut down successfully") - return - } - default: - time.Sleep(delay) - } - if time.Since(start) > timeout { - log.Println("signalhandler: Timouted while waiting for proper shutdown - " + strconv.Itoa(chanCount) + " boxes are up after " + timeout.String()) - return - } - } -} - -// Run catches and handles signals -func Run(subscribers []chan os.Signal, done chan string, server *http.Server) { - signals := make(chan os.Signal, 1) - - signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) - - var sig os.Signal - for { - sig := <-signals - log.Printf("signalhandler: Got signal '%s'\n", sig.String()) - if sig == syscall.SIGTERM { - // Shutdown daemon on SIGTERM - break - } - log.Printf("signalhandler: Ignoring signal '%s'. Send SIGTERM to trigger shutdown.\n", sig.String()) - } - - log.Println("signalhandler: Sending shutdown signals to components") - sendSignals(sig, subscribers, done) - - log.Println("signalhandler: Shutting down the server") - if err := server.Shutdown(context.Background()); err != nil { - log.Printf("HTTP server Shutdown: %v", err) - } -} diff --git a/pkg/strat/directory-sensitive.go b/pkg/strat/directory-sensitive.go deleted file mode 100644 index 89d030e7..00000000 --- a/pkg/strat/directory-sensitive.go +++ /dev/null @@ -1,47 +0,0 @@ -package strat - -import "github.com/curusarn/resh/pkg/records" - -// DirectorySensitive prediction/recommendation strategy -type DirectorySensitive struct { - history map[string][]string - lastPwd string -} - -// Init see name -func (s *DirectorySensitive) Init() { - s.history = map[string][]string{} -} - -// GetTitleAndDescription see name -func (s *DirectorySensitive) GetTitleAndDescription() (string, string) { - return "directory sensitive (recent)", "Use recent commands executed is the same directory" -} - -// GetCandidates see name -func (s *DirectorySensitive) GetCandidates() []string { - return s.history[s.lastPwd] -} - -// AddHistoryRecord see name -func (s *DirectorySensitive) AddHistoryRecord(record *records.EnrichedRecord) error { - // work on history for PWD - pwd := record.Pwd - // remove previous occurance of record - for i, cmd := range s.history[pwd] { - if cmd == record.CmdLine { - s.history[pwd] = append(s.history[pwd][:i], s.history[pwd][i+1:]...) - } - } - // append new record - s.history[pwd] = append([]string{record.CmdLine}, s.history[pwd]...) - s.lastPwd = record.PwdAfter - return nil -} - -// ResetHistory see name -func (s *DirectorySensitive) ResetHistory() error { - s.Init() - s.history = map[string][]string{} - return nil -} diff --git a/pkg/strat/dummy.go b/pkg/strat/dummy.go deleted file mode 100644 index fc813f2a..00000000 --- a/pkg/strat/dummy.go +++ /dev/null @@ -1,29 +0,0 @@ -package strat - -import "github.com/curusarn/resh/pkg/records" - -// Dummy prediction/recommendation strategy -type Dummy struct { - history []string -} - -// GetTitleAndDescription see name -func (s *Dummy) GetTitleAndDescription() (string, string) { - return "dummy", "Return empty candidate list" -} - -// GetCandidates see name -func (s *Dummy) GetCandidates() []string { - return nil -} - -// AddHistoryRecord see name -func (s *Dummy) AddHistoryRecord(record *records.EnrichedRecord) error { - s.history = append(s.history, record.CmdLine) - return nil -} - -// ResetHistory see name -func (s *Dummy) ResetHistory() error { - return nil -} diff --git a/pkg/strat/dynamic-record-distance.go b/pkg/strat/dynamic-record-distance.go deleted file mode 100644 index 1f779c27..00000000 --- a/pkg/strat/dynamic-record-distance.go +++ /dev/null @@ -1,91 +0,0 @@ -package strat - -import ( - "math" - "sort" - "strconv" - - "github.com/curusarn/resh/pkg/records" -) - -// DynamicRecordDistance prediction/recommendation strategy -type DynamicRecordDistance struct { - history []records.EnrichedRecord - DistParams records.DistParams - pwdHistogram map[string]int - realPwdHistogram map[string]int - gitOriginHistogram map[string]int - MaxDepth int - Label string -} - -type strDynDistEntry struct { - cmdLine string - distance float64 -} - -// Init see name -func (s *DynamicRecordDistance) Init() { - s.history = nil - s.pwdHistogram = map[string]int{} - s.realPwdHistogram = map[string]int{} - s.gitOriginHistogram = map[string]int{} -} - -// GetTitleAndDescription see name -func (s *DynamicRecordDistance) GetTitleAndDescription() (string, string) { - return "dynamic record distance (depth:" + strconv.Itoa(s.MaxDepth) + ";" + s.Label + ")", "Use TF-IDF record distance to recommend commands" -} - -func (s *DynamicRecordDistance) idf(count int) float64 { - return math.Log(float64(len(s.history)) / float64(count)) -} - -// GetCandidates see name -func (s *DynamicRecordDistance) GetCandidates(strippedRecord records.EnrichedRecord) []string { - if len(s.history) == 0 { - return nil - } - var mapItems []strDynDistEntry - for i, record := range s.history { - if s.MaxDepth != 0 && i > s.MaxDepth { - break - } - distParams := records.DistParams{ - Pwd: s.DistParams.Pwd * s.idf(s.pwdHistogram[strippedRecord.PwdAfter]), - RealPwd: s.DistParams.RealPwd * s.idf(s.realPwdHistogram[strippedRecord.RealPwdAfter]), - Git: s.DistParams.Git * s.idf(s.gitOriginHistogram[strippedRecord.GitOriginRemote]), - Time: s.DistParams.Time, - SessionID: s.DistParams.SessionID, - } - distance := record.DistanceTo(strippedRecord, distParams) - mapItems = append(mapItems, strDynDistEntry{record.CmdLine, distance}) - } - sort.SliceStable(mapItems, func(i int, j int) bool { return mapItems[i].distance < mapItems[j].distance }) - var hist []string - histSet := map[string]bool{} - for _, item := range mapItems { - if histSet[item.cmdLine] { - continue - } - histSet[item.cmdLine] = true - hist = append(hist, item.cmdLine) - } - return hist -} - -// AddHistoryRecord see name -func (s *DynamicRecordDistance) AddHistoryRecord(record *records.EnrichedRecord) error { - // append record to front - s.history = append([]records.EnrichedRecord{*record}, s.history...) - s.pwdHistogram[record.Pwd]++ - s.realPwdHistogram[record.RealPwd]++ - s.gitOriginHistogram[record.GitOriginRemote]++ - return nil -} - -// ResetHistory see name -func (s *DynamicRecordDistance) ResetHistory() error { - s.Init() - return nil -} diff --git a/pkg/strat/frequent.go b/pkg/strat/frequent.go deleted file mode 100644 index ff3b9125..00000000 --- a/pkg/strat/frequent.go +++ /dev/null @@ -1,53 +0,0 @@ -package strat - -import ( - "sort" - - "github.com/curusarn/resh/pkg/records" -) - -// Frequent prediction/recommendation strategy -type Frequent struct { - history map[string]int -} - -type strFrqEntry struct { - cmdLine string - count int -} - -// Init see name -func (s *Frequent) Init() { - s.history = map[string]int{} -} - -// GetTitleAndDescription see name -func (s *Frequent) GetTitleAndDescription() (string, string) { - return "frequent", "Use frequent commands" -} - -// GetCandidates see name -func (s *Frequent) GetCandidates() []string { - var mapItems []strFrqEntry - for cmdLine, count := range s.history { - mapItems = append(mapItems, strFrqEntry{cmdLine, count}) - } - sort.Slice(mapItems, func(i int, j int) bool { return mapItems[i].count > mapItems[j].count }) - var hist []string - for _, item := range mapItems { - hist = append(hist, item.cmdLine) - } - return hist -} - -// AddHistoryRecord see name -func (s *Frequent) AddHistoryRecord(record *records.EnrichedRecord) error { - s.history[record.CmdLine]++ - return nil -} - -// ResetHistory see name -func (s *Frequent) ResetHistory() error { - s.Init() - return nil -} diff --git a/pkg/strat/markov-chain-cmd.go b/pkg/strat/markov-chain-cmd.go deleted file mode 100644 index b1fa2f5b..00000000 --- a/pkg/strat/markov-chain-cmd.go +++ /dev/null @@ -1,97 +0,0 @@ -package strat - -import ( - "sort" - "strconv" - - "github.com/curusarn/resh/pkg/records" - "github.com/mb-14/gomarkov" -) - -// MarkovChainCmd prediction/recommendation strategy -type MarkovChainCmd struct { - Order int - history []strMarkCmdHistoryEntry - historyCmds []string -} - -type strMarkCmdHistoryEntry struct { - cmd string - cmdLine string -} - -type strMarkCmdEntry struct { - cmd string - transProb float64 -} - -// Init see name -func (s *MarkovChainCmd) Init() { - s.history = nil - s.historyCmds = nil -} - -// GetTitleAndDescription see name -func (s *MarkovChainCmd) GetTitleAndDescription() (string, string) { - return "command-based markov chain (order " + strconv.Itoa(s.Order) + ")", "Use command-based markov chain to recommend commands" -} - -// GetCandidates see name -func (s *MarkovChainCmd) GetCandidates() []string { - if len(s.history) < s.Order { - var hist []string - for _, item := range s.history { - hist = append(hist, item.cmdLine) - } - return hist - } - chain := gomarkov.NewChain(s.Order) - - chain.Add(s.historyCmds) - - cmdsSet := map[string]bool{} - var entries []strMarkCmdEntry - for _, cmd := range s.historyCmds { - if cmdsSet[cmd] { - continue - } - cmdsSet[cmd] = true - prob, _ := chain.TransitionProbability(cmd, s.historyCmds[len(s.historyCmds)-s.Order:]) - entries = append(entries, strMarkCmdEntry{cmd: cmd, transProb: prob}) - } - sort.Slice(entries, func(i int, j int) bool { return entries[i].transProb > entries[j].transProb }) - var hist []string - histSet := map[string]bool{} - for i := len(s.history) - 1; i >= 0; i-- { - if histSet[s.history[i].cmdLine] { - continue - } - histSet[s.history[i].cmdLine] = true - if s.history[i].cmd == entries[0].cmd { - hist = append(hist, s.history[i].cmdLine) - } - } - // log.Println("################") - // log.Println(s.history[len(s.history)-s.order:]) - // log.Println(" -> ") - // x := math.Min(float64(len(hist)), 3) - // log.Println(entries[:int(x)]) - // x = math.Min(float64(len(hist)), 5) - // log.Println(hist[:int(x)]) - // log.Println("################") - return hist -} - -// AddHistoryRecord see name -func (s *MarkovChainCmd) AddHistoryRecord(record *records.EnrichedRecord) error { - s.history = append(s.history, strMarkCmdHistoryEntry{cmdLine: record.CmdLine, cmd: record.Command}) - s.historyCmds = append(s.historyCmds, record.Command) - // s.historySet[record.CmdLine] = true - return nil -} - -// ResetHistory see name -func (s *MarkovChainCmd) ResetHistory() error { - s.Init() - return nil -} diff --git a/pkg/strat/markov-chain.go b/pkg/strat/markov-chain.go deleted file mode 100644 index 50c7fdce..00000000 --- a/pkg/strat/markov-chain.go +++ /dev/null @@ -1,76 +0,0 @@ -package strat - -import ( - "sort" - "strconv" - - "github.com/curusarn/resh/pkg/records" - "github.com/mb-14/gomarkov" -) - -// MarkovChain prediction/recommendation strategy -type MarkovChain struct { - Order int - history []string -} - -type strMarkEntry struct { - cmdLine string - transProb float64 -} - -// Init see name -func (s *MarkovChain) Init() { - s.history = nil -} - -// GetTitleAndDescription see name -func (s *MarkovChain) GetTitleAndDescription() (string, string) { - return "markov chain (order " + strconv.Itoa(s.Order) + ")", "Use markov chain to recommend commands" -} - -// GetCandidates see name -func (s *MarkovChain) GetCandidates() []string { - if len(s.history) < s.Order { - return s.history - } - chain := gomarkov.NewChain(s.Order) - - chain.Add(s.history) - - cmdLinesSet := map[string]bool{} - var entries []strMarkEntry - for _, cmdLine := range s.history { - if cmdLinesSet[cmdLine] { - continue - } - cmdLinesSet[cmdLine] = true - prob, _ := chain.TransitionProbability(cmdLine, s.history[len(s.history)-s.Order:]) - entries = append(entries, strMarkEntry{cmdLine: cmdLine, transProb: prob}) - } - sort.Slice(entries, func(i int, j int) bool { return entries[i].transProb > entries[j].transProb }) - var hist []string - for _, item := range entries { - hist = append(hist, item.cmdLine) - } - // log.Println("################") - // log.Println(s.history[len(s.history)-s.order:]) - // log.Println(" -> ") - // x := math.Min(float64(len(hist)), 5) - // log.Println(hist[:int(x)]) - // log.Println("################") - return hist -} - -// AddHistoryRecord see name -func (s *MarkovChain) AddHistoryRecord(record *records.EnrichedRecord) error { - s.history = append(s.history, record.CmdLine) - // s.historySet[record.CmdLine] = true - return nil -} - -// ResetHistory see name -func (s *MarkovChain) ResetHistory() error { - s.Init() - return nil -} diff --git a/pkg/strat/random.go b/pkg/strat/random.go deleted file mode 100644 index 0ff52f1a..00000000 --- a/pkg/strat/random.go +++ /dev/null @@ -1,57 +0,0 @@ -package strat - -import ( - "math/rand" - "time" - - "github.com/curusarn/resh/pkg/records" -) - -// Random prediction/recommendation strategy -type Random struct { - CandidatesSize int - history []string - historySet map[string]bool -} - -// Init see name -func (s *Random) Init() { - s.history = nil - s.historySet = map[string]bool{} -} - -// GetTitleAndDescription see name -func (s *Random) GetTitleAndDescription() (string, string) { - return "random", "Use random commands" -} - -// GetCandidates see name -func (s *Random) GetCandidates() []string { - seed := time.Now().UnixNano() - rand.Seed(seed) - var candidates []string - candidateSet := map[string]bool{} - for len(candidates) < s.CandidatesSize && len(candidates)*2 < len(s.historySet) { - x := rand.Intn(len(s.history)) - candidate := s.history[x] - if candidateSet[candidate] == false { - candidateSet[candidate] = true - candidates = append(candidates, candidate) - continue - } - } - return candidates -} - -// AddHistoryRecord see name -func (s *Random) AddHistoryRecord(record *records.EnrichedRecord) error { - s.history = append([]string{record.CmdLine}, s.history...) - s.historySet[record.CmdLine] = true - return nil -} - -// ResetHistory see name -func (s *Random) ResetHistory() error { - s.Init() - return nil -} diff --git a/pkg/strat/recent-bash.go b/pkg/strat/recent-bash.go deleted file mode 100644 index ace35711..00000000 --- a/pkg/strat/recent-bash.go +++ /dev/null @@ -1,56 +0,0 @@ -package strat - -import "github.com/curusarn/resh/pkg/records" - -// RecentBash prediction/recommendation strategy -type RecentBash struct { - histfile []string - histfileSnapshot map[string][]string - history map[string][]string -} - -// Init see name -func (s *RecentBash) Init() { - s.histfileSnapshot = map[string][]string{} - s.history = map[string][]string{} -} - -// GetTitleAndDescription see name -func (s *RecentBash) GetTitleAndDescription() (string, string) { - return "recent (bash-like)", "Behave like bash" -} - -// GetCandidates see name -func (s *RecentBash) GetCandidates(strippedRecord records.EnrichedRecord) []string { - // populate the local history from histfile - if s.histfileSnapshot[strippedRecord.SessionID] == nil { - s.histfileSnapshot[strippedRecord.SessionID] = s.histfile - } - return append(s.history[strippedRecord.SessionID], s.histfileSnapshot[strippedRecord.SessionID]...) -} - -// AddHistoryRecord see name -func (s *RecentBash) AddHistoryRecord(record *records.EnrichedRecord) error { - // remove previous occurance of record - for i, cmd := range s.history[record.SessionID] { - if cmd == record.CmdLine { - s.history[record.SessionID] = append(s.history[record.SessionID][:i], s.history[record.SessionID][i+1:]...) - } - } - // append new record - s.history[record.SessionID] = append([]string{record.CmdLine}, s.history[record.SessionID]...) - - if record.LastRecordOfSession { - // append history of the session to histfile and clear session history - s.histfile = append(s.history[record.SessionID], s.histfile...) - s.histfileSnapshot[record.SessionID] = nil - s.history[record.SessionID] = nil - } - return nil -} - -// ResetHistory see name -func (s *RecentBash) ResetHistory() error { - s.Init() - return nil -} diff --git a/pkg/strat/recent.go b/pkg/strat/recent.go deleted file mode 100644 index 157b52c8..00000000 --- a/pkg/strat/recent.go +++ /dev/null @@ -1,37 +0,0 @@ -package strat - -import "github.com/curusarn/resh/pkg/records" - -// Recent prediction/recommendation strategy -type Recent struct { - history []string -} - -// GetTitleAndDescription see name -func (s *Recent) GetTitleAndDescription() (string, string) { - return "recent", "Use recent commands" -} - -// GetCandidates see name -func (s *Recent) GetCandidates() []string { - return s.history -} - -// AddHistoryRecord see name -func (s *Recent) AddHistoryRecord(record *records.EnrichedRecord) error { - // remove previous occurance of record - for i, cmd := range s.history { - if cmd == record.CmdLine { - s.history = append(s.history[:i], s.history[i+1:]...) - } - } - // append new record - s.history = append([]string{record.CmdLine}, s.history...) - return nil -} - -// ResetHistory see name -func (s *Recent) ResetHistory() error { - s.history = nil - return nil -} diff --git a/pkg/strat/record-distance.go b/pkg/strat/record-distance.go deleted file mode 100644 index e582584c..00000000 --- a/pkg/strat/record-distance.go +++ /dev/null @@ -1,70 +0,0 @@ -package strat - -import ( - "sort" - "strconv" - - "github.com/curusarn/resh/pkg/records" -) - -// RecordDistance prediction/recommendation strategy -type RecordDistance struct { - history []records.EnrichedRecord - DistParams records.DistParams - MaxDepth int - Label string -} - -type strDistEntry struct { - cmdLine string - distance float64 -} - -// Init see name -func (s *RecordDistance) Init() { - s.history = nil -} - -// GetTitleAndDescription see name -func (s *RecordDistance) GetTitleAndDescription() (string, string) { - return "record distance (depth:" + strconv.Itoa(s.MaxDepth) + ";" + s.Label + ")", "Use record distance to recommend commands" -} - -// GetCandidates see name -func (s *RecordDistance) GetCandidates(strippedRecord records.EnrichedRecord) []string { - if len(s.history) == 0 { - return nil - } - var mapItems []strDistEntry - for i, record := range s.history { - if s.MaxDepth != 0 && i > s.MaxDepth { - break - } - distance := record.DistanceTo(strippedRecord, s.DistParams) - mapItems = append(mapItems, strDistEntry{record.CmdLine, distance}) - } - sort.SliceStable(mapItems, func(i int, j int) bool { return mapItems[i].distance < mapItems[j].distance }) - var hist []string - histSet := map[string]bool{} - for _, item := range mapItems { - if histSet[item.cmdLine] { - continue - } - histSet[item.cmdLine] = true - hist = append(hist, item.cmdLine) - } - return hist -} - -// AddHistoryRecord see name -func (s *RecordDistance) AddHistoryRecord(record *records.EnrichedRecord) error { - // append record to front - s.history = append([]records.EnrichedRecord{*record}, s.history...) - return nil -} - -// ResetHistory see name -func (s *RecordDistance) ResetHistory() error { - s.Init() - return nil -} diff --git a/pkg/strat/strat.go b/pkg/strat/strat.go deleted file mode 100644 index 28ac0151..00000000 --- a/pkg/strat/strat.go +++ /dev/null @@ -1,46 +0,0 @@ -package strat - -import ( - "github.com/curusarn/resh/pkg/records" -) - -// ISimpleStrategy interface -type ISimpleStrategy interface { - GetTitleAndDescription() (string, string) - GetCandidates() []string - AddHistoryRecord(record *records.EnrichedRecord) error - ResetHistory() error -} - -// IStrategy interface -type IStrategy interface { - GetTitleAndDescription() (string, string) - GetCandidates(r records.EnrichedRecord) []string - AddHistoryRecord(record *records.EnrichedRecord) error - ResetHistory() error -} - -type simpleStrategyWrapper struct { - strategy ISimpleStrategy -} - -// NewSimpleStrategyWrapper returns IStrategy created by wrapping given ISimpleStrategy -func NewSimpleStrategyWrapper(strategy ISimpleStrategy) *simpleStrategyWrapper { - return &simpleStrategyWrapper{strategy: strategy} -} - -func (s *simpleStrategyWrapper) GetTitleAndDescription() (string, string) { - return s.strategy.GetTitleAndDescription() -} - -func (s *simpleStrategyWrapper) GetCandidates(r records.EnrichedRecord) []string { - return s.strategy.GetCandidates() -} - -func (s *simpleStrategyWrapper) AddHistoryRecord(r *records.EnrichedRecord) error { - return s.strategy.AddHistoryRecord(r) -} - -func (s *simpleStrategyWrapper) ResetHistory() error { - return s.strategy.ResetHistory() -} diff --git a/record/legacy.go b/record/legacy.go new file mode 100644 index 00000000..3b913fb2 --- /dev/null +++ b/record/legacy.go @@ -0,0 +1,88 @@ +package record + +type Legacy struct { + // core + CmdLine string `json:"cmdLine"` + ExitCode int `json:"exitCode"` + Shell string `json:"shell"` + Uname string `json:"uname"` + SessionID string `json:"sessionId"` + RecordID string `json:"recordId"` + + // posix + Home string `json:"home"` + Lang string `json:"lang"` + LcAll string `json:"lcAll"` + Login string `json:"login"` + Pwd string `json:"pwd"` + PwdAfter string `json:"pwdAfter"` + ShellEnv string `json:"shellEnv"` + Term string `json:"term"` + + // non-posix"` + RealPwd string `json:"realPwd"` + RealPwdAfter string `json:"realPwdAfter"` + Pid int `json:"pid"` + SessionPID int `json:"sessionPid"` + Host string `json:"host"` + Hosttype string `json:"hosttype"` + Ostype string `json:"ostype"` + Machtype string `json:"machtype"` + Shlvl int `json:"shlvl"` + + // before after + TimezoneBefore string `json:"timezoneBefore"` + TimezoneAfter string `json:"timezoneAfter"` + + RealtimeBefore float64 `json:"realtimeBefore"` + RealtimeAfter float64 `json:"realtimeAfter"` + RealtimeBeforeLocal float64 `json:"realtimeBeforeLocal"` + RealtimeAfterLocal float64 `json:"realtimeAfterLocal"` + + RealtimeDuration float64 `json:"realtimeDuration"` + RealtimeSinceSessionStart float64 `json:"realtimeSinceSessionStart"` + RealtimeSinceBoot float64 `json:"realtimeSinceBoot"` + + 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"` + OsReleaseIDLike string `json:"osReleaseIdLike"` + OsReleaseName string `json:"osReleaseName"` + OsReleasePrettyName string `json:"osReleasePrettyName"` + + ReshUUID string `json:"reshUuid"` + ReshVersion string `json:"reshVersion"` + ReshRevision string `json:"reshRevision"` + + // records come in two parts (collect and postcollect) + PartOne bool `json:"partOne,omitempty"` // false => part two + PartsMerged bool `json:"partsMerged"` + // special flag -> not an actual record but an session end + SessionExit bool `json:"sessionExit,omitempty"` + + // recall metadata + Recalled bool `json:"recalled"` + RecallHistno int `json:"recallHistno,omitempty"` + RecallStrategy string `json:"recallStrategy,omitempty"` + RecallActionsRaw string `json:"recallActionsRaw,omitempty"` + RecallActions []string `json:"recallActions,omitempty"` + RecallLastCmdLine string `json:"recallLastCmdLine"` + + // recall command + RecallPrefix string `json:"recallPrefix,omitempty"` + + // added by sanitizatizer + Sanitized bool `json:"sanitized,omitempty"` + CmdLength int `json:"cmdLength,omitempty"` + + // fields that are string here and int in older resh verisons + Cols interface{} `json:"cols"` + Lines interface{} `json:"lines"` +} diff --git a/record/record.go b/record/record.go new file mode 100644 index 00000000..07ad798d --- /dev/null +++ b/record/record.go @@ -0,0 +1,2 @@ +// Package record provides record types that are used in resh history files +package record diff --git a/record/v1.go b/record/v1.go new file mode 100644 index 00000000..5b512985 --- /dev/null +++ b/record/v1.go @@ -0,0 +1,65 @@ +package record + +type V1 struct { + // future-proofing so that we can add this later without version bump + // deleted, favorite + Deleted bool `json:"deleted,omitempty"` + Favorite bool `json:"favorite,omitempty"` + + // cmdline, exitcode + CmdLine string `json:"cmdLine"` + ExitCode int `json:"exitCode"` + + DeviceID string `json:"deviceID"` + SessionID string `json:"sessionID"` + // can we have a shorter uuid for record + RecordID string `json:"recordID"` + + // paths + // TODO: Do we need both pwd and real pwd? + Home string `json:"home"` + Pwd string `json:"pwd"` + RealPwd string `json:"realPwd"` + + // Device is set during installation/setup + // It is stored in RESH configuration + Device string `json:"device"` + + // git info + // origin is the most important + GitOriginRemote string `json:"gitOriginRemote"` + // TODO: add GitBranch (v2 ?) + // maybe branch could be useful - e.g. in monorepo ?? + // GitBranch string `json:"gitBranch"` + + // what is this for ?? + // session watching needs this + // but I'm not sure if we need to save it + // records belong to sessions + // PID int `json:"pid"` + // needed for tracking of sessions but I think it shouldn't be part of V1 + // SessionPID int `json:"sessionPID"` + + // needed to because records are merged with parts with same "SessionID + Shlvl" + // I don't think we need to save it + // Shlvl int `json:"shlvl"` + + // time (before), duration of command + // time and duration are strings because we don't want unnecessary precision when they get serialized into json + // we could implement custom (un)marshalling but I don't see downsides of directly representing the values as strings + Time string `json:"time"` + Duration string `json:"duration"` + + // these look like internal stuff + + // TODO: rethink + // I don't really like this :/ + // Maybe just one field 'NotMerged' with 'partOne' and 'partTwo' as values and empty string for merged + // records come in two parts (collect and postcollect) + PartOne bool `json:"partOne,omitempty"` // false => part two + PartsNotMerged bool `json:"partsNotMerged,omitempty"` + + // special flag -> not an actual record but an session end + // TODO: this shouldn't be part of serializable V1 record + SessionExit bool `json:"sessionExit,omitempty"` +} diff --git a/roadmap.md b/roadmap.md deleted file mode 100644 index efcc2d51..00000000 --- a/roadmap.md +++ /dev/null @@ -1,45 +0,0 @@ - -# RESH Roadmap - -| | Legend | -| --- | --- | -| :heavy_check_mark: | Implemented | -| :white_check_mark: | Implemented but I'm not happy with it | -| :x: | Not implemented | - -*NOTE: Features can change in the future* - -- :heavy_check_mark: Record shell history with metadata - - :heavy_check_mark: save it as JSON to `~/.resh_history.json` - -- :white_check_mark: Provide an app to search the history - - :heavy_check_mark: launch with CTRL+R (enable it using `reshctl enable ctrl_r_binding_global`) - - :heavy_check_mark: search by keywords - - :heavy_check_mark: relevant results show up first based on context (host, directory, git, exit status) - - :heavy_check_mark: allow searching completely without context ("raw" mode) - - :heavy_check_mark: import and search history from before RESH was installed - - :white_check_mark: include a help with keybindings - - :x: allow listing details for individual commands - - :x: allow explicitly searching by metadata - -- :heavy_check_mark: Provide a `reshctl` utility to control and interact with the project - - :heavy_check_mark: turn on/off resh key bindings - - :heavy_check_mark: zsh completion - - :heavy_check_mark: bash completion - -- :x: Multi-device history - - :x: Synchronize recorded history between devices - - :x: Allow proxying history when ssh'ing into remote servers - -- :x: Provide a stable API to make resh extensible - -- :heavy_check_mark: Support zsh and bash - -- :heavy_check_mark: Support Linux and macOS - -- :white_check_mark: Require only essential prerequisite software - - :heavy_check_mark: Linux - - :white_check_mark: MacOS *(requires coreutils - `brew install coreutils`)* - -- :heavy_check_mark: Provide a tool to sanitize the recorded history - diff --git a/scripts/hooks.sh b/scripts/hooks.sh index 02e74c34..c71468ff 100644 --- a/scripts/hooks.sh +++ b/scripts/hooks.sh @@ -1,179 +1,218 @@ +#!/hint/sh -__resh_reset_variables() { - __RESH_HISTNO=0 - __RESH_HISTNO_MAX="" - __RESH_HISTNO_ZERO_LINE="" - __RESH_HIST_PREV_LINE="" - __RESH_HIST_PREV_CURSOR="" # deprecated - __RESH_HIST_PREV_PREFIX="" - __RESH_HIST_RECALL_ACTIONS="" - __RESH_HIST_NO_PREFIX_MODE=0 - __RESH_HIST_RECALL_STRATEGY="" - __RESH_RECORD_ID=$(__resh_get_uuid) +# BACKWARDS COMPATIBILITY NOTES: +# +# Stable names and options: +# * `resh-collect -version` / `resh-postcollect -version` is used to detect version mismatch. +# => The go-like/short `-version` option needs to exist for new resh-(post)collect commands in all future version. +# => Prefer using go-like/short `-version` option so that we don't have more options to support indefinitely. +# * `__resh_preexec ` with `__RESH_NO_RELOAD=1` is called on version mismatch. +# => The `__resh_preexec` function needs to exist in all future versions. +# => Make sure that `__RESH_NO_RELOAD` behavior is not broken in any future version. +# => Prefer only testing `__RESH_NO_RELOAD` for emptyness instead of specific value +# * `__resh_reload_msg` is called *after* shell files reload +# => The function shows a message from the already updated shell files +# => We can drop this function at any time - the old version will be used + +# Backwards compatibilty: Please see notes above before making any changes here. +__resh_reload_msg() { + printf '\n' + printf '+--------------------------------------------------------------+\n' + printf '| New version of RESH shell files was loaded in this terminal. |\n' + printf '| This is an informative message - no action is necessary. |\n' + printf '| Please restart this terminal if you encounter any issues. |\n' + printf '+--------------------------------------------------------------+\n' + printf '\n' } +# (pre)collect +# Backwards compatibilty: Please see notes above before making any changes here. __resh_preexec() { - # core + if [ "$(resh-collect -version)" != "$__RESH_VERSION" ] && [ -z "${__RESH_NO_RELOAD-}" ]; then + # Reload shell files and restart __resh_preexec - i.e. the full command will be recorded only with a slight delay. + # This should happens in every already open terminal after resh update. + # __RESH_NO_RELOAD prevents recursive reloads + # We never repeatadly reload the shell files to prevent potentially infinite recursion. + # If the version is still wrong then error will be raised by `resh-collect -requiredVersion`. + + source ~/.resh/shellrc + # Show reload message from the updated shell files + __resh_reload_msg + # Rerun self but prevent another reload. Extra protection against infinite recursion. + __RESH_NO_RELOAD=1 __resh_preexec "$@" + return $? + fi __RESH_COLLECT=1 - __RESH_CMDLINE="$1" # not local to preserve it for postcollect (useful as sanity check) - local fpath_last_run="$__RESH_XDG_CACHE_HOME/collect_last_run_out.txt" - __resh_collect --cmdLine "$__RESH_CMDLINE" \ - --recall-actions "$__RESH_HIST_RECALL_ACTIONS" \ - --recall-strategy "$__RESH_HIST_RECALL_STRATEGY" \ - --recall-last-cmdline "$__RESH_HIST_PREV_LINE" \ - >| "$fpath_last_run" 2>&1 || echo "resh-collect ERROR: $(head -n 1 $fpath_last_run)" + __RESH_RECORD_ID=$(resh-generate-uuid) + # TODO: do this in resh-collect + # shellcheck disable=2155 + local git_remote="$(git remote get-url origin 2>/dev/null)" + # TODO: do this in resh-collect + __RESH_RT_BEFORE=$(resh-get-epochtime) + resh-collect -requireVersion "$__RESH_VERSION" \ + --git-remote "$git_remote" \ + --home "$HOME" \ + --pwd "$PWD" \ + --record-id "$__RESH_RECORD_ID" \ + --session-id "$__RESH_SESSION_ID" \ + --session-pid "$$" \ + --shell "$__RESH_SHELL" \ + --shlvl "$SHLVL" \ + --time "$__RESH_RT_BEFORE" \ + --cmd-line "$1" + return $? } -# 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 - if [ "${BASH_VERSINFO[0]}" -ge "4" ]; then - # $BASHPID is only available in bash4+ - # $$ is fairly similar so it should not be an issue - local __RESH_PID="$BASHPID" # current pid - else - local __RESH_PID="$$" # current pid - fi +# postcollect +# Backwards compatibilty: Please see notes above before making any changes here. +__resh_precmd() { + # Get status first before it gets overriden by another command. + local exit_code=$? + # Don't do anything if __resh_preexec was not called. + # There are situations (in bash) where no command was submitted but __resh_precmd gets called anyway. + [ -n "${__RESH_COLLECT-}" ] || return + if [ "$(resh-postcollect -version)" != "$__RESH_VERSION" ]; then + # Reload shell files and return - i.e. skip recording part2 for this command. + # We don't call __resh_precmd because the new __resh_preexec might not be backwards compatible with variables set by old __resh_preexec. + # This should happen only in the one terminal where resh update was executed. + # And the resh-daemon was likely restarted so we likely don't even have the matching part1 of the comand in the resh-daemon memory. + source ~/.resh/shellrc + # Show reload message from the updated shell files + __resh_reload_msg + return 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." + unset __RESH_COLLECT + + # do this in resh-postcollect + # shellcheck disable=2155 + local rt_after=$(resh-get-epochtime) + resh-postcollect -requireVersion "$__RESH_VERSION" \ + --exit-code "$exit_code" \ + --record-id "$__RESH_RECORD_ID" \ + --session-id "$__RESH_SESSION_ID" \ + --shlvl "$SHLVL" \ + --time-after "$rt_after" \ + --time-before "$__RESH_RT_BEFORE" + return $? +} + +# Backwards compatibilty: No restrictions. This is only used at the start of the session. +__resh_session_init() { + resh-session-init -requireVersion "$__RESH_VERSION" \ + --session-id "$__RESH_SESSION_ID" \ + --session-pid "$$" + return $? +} + +# Backwards compatibilty: Please see notes above before making any changes here. +__resh_widget_control_R() { + # This is a very bad workaround. + # Force bash-preexec to run repeatedly because otherwise premature run of bash-preexec overshadows the next proper 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" + + local PREVBUFFER=$BUFFER + + local status_code + local git_remote; git_remote="$(git remote get-url origin 2>/dev/null)" + if [ "$(resh-cli -version)" != "$__RESH_VERSION" ] && [ -z "${__RESH_NO_RELOAD-}" ]; then + source ~/.resh/shellrc + # Show reload message from the updated shell files + __resh_reload_msg + # Rerun self but prevent another reload. Extra protection against infinite recursion. + __RESH_NO_RELOAD=1 __resh_widget_control_R "$@" + return $? + fi + BUFFER=$(resh-cli -requireVersion "$__RESH_VERSION" \ + --git-remote "$git_remote" \ + --pwd "$PWD" \ + --query "$BUFFER" \ + --session-id "$__RESH_SESSION_ID" \ + ) + status_code=$? + if [ $status_code = 111 ]; then + # execute + if [ -n "${ZSH_VERSION-}" ]; then + # zsh + zle accept-line + elif [ -n "${BASH_VERSION-}" ]; then + # bash + # set chained keyseq to accept-line + bind '"\u[32~": accept-line' 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})" + elif [ $status_code = 0 ]; then + if [ -n "${BASH_VERSION-}" ]; then + # bash + # set chained keyseq to nothing + bind -x '"\u[32~": __resh_nop' fi + else + echo "RESH SEARCH APP failed" + printf "%s" "$buffer" >&2 + BUFFER="$PREVBUFFER" 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" \ - -recordId "$__RESH_RECORD_ID" \ - -cols "$__RESH_COLS" \ - -home "$__RESH_HOME" \ - -lang "$__RESH_LANG" \ - -lcAll "$__RESH_LC_ALL" \ - -lines "$__RESH_LINES" \ - -login "$__RESH_LOGIN" \ - -pwd "$__RESH_PWD" \ - -shellEnv "$__RESH_SHELL_ENV" \ - -term "$__RESH_TERM" \ - -pid "$__RESH_PID" \ - -sessionPid "$__RESH_SESSION_PID" \ - -host "$__RESH_HOST" \ - -hosttype "$__RESH_HOSTTYPE" \ - -ostype "$__RESH_OSTYPE" \ - -machtype "$__RESH_MACHTYPE" \ - -shlvl "$__RESH_SHLVL" \ - -gitCdup "$__RESH_GIT_CDUP" \ - -gitCdupExitCode "$__RESH_GIT_CDUP_EXIT_CODE" \ - -gitRemote "$__RESH_GIT_REMOTE" \ - -gitRemoteExitCode "$__RESH_GIT_REMOTE_EXIT_CODE" \ - -realtimeBefore "$__RESH_RT_BEFORE" \ - -realtimeSession "$__RESH_RT_SESSION" \ - -realtimeSessSinceBoot "$__RESH_RT_SESS_SINCE_BOOT" \ - -timezoneBefore "$__RESH_TZ_BEFORE" \ - -osReleaseId "$__RESH_OS_RELEASE_ID" \ - -osReleaseVersionId "$__RESH_OS_RELEASE_VERSION_ID" \ - -osReleaseIdLike "$__RESH_OS_RELEASE_ID_LIKE" \ - -osReleaseName "$__RESH_OS_RELEASE_NAME" \ - -osReleasePrettyName "$__RESH_OS_RELEASE_PRETTY_NAME" \ - -histno "$__RESH_HISTNO" \ - "$@" - return $? - fi - return 1 + CURSOR=${#BUFFER} } -__resh_precmd() { - local __RESH_EXIT_CODE=$? - local __RESH_RT_AFTER - local __RESH_TZ_AFTER - local __RESH_PWD_AFTER - local __RESH_GIT_CDUP_AFTER - local __RESH_GIT_CDUP_EXIT_CODE_AFTER - local __RESH_GIT_REMOTE_AFTER - local __RESH_GIT_REMOTE_EXIT_CODE_AFTER - local __RESH_SHLVL="$SHLVL" - __RESH_RT_AFTER=$(__resh_get_epochrealtime) - __RESH_TZ_AFTER=$(date +%z) - __RESH_PWD_AFTER="$PWD" - __RESH_GIT_CDUP_AFTER="$(git rev-parse --show-cdup 2>/dev/null)" - __RESH_GIT_CDUP_EXIT_CODE_AFTER=$? - __RESH_GIT_REMOTE_AFTER="$(git remote get-url origin 2>/dev/null)" - __RESH_GIT_REMOTE_EXIT_CODE_AFTER=$? - if [ -n "${__RESH_COLLECT}" ]; then - if [ "$__RESH_VERSION" != "$(resh-postcollect -version)" ]; then - # shellcheck source=shellrc.sh - source ~/.resh/shellrc - if [ "$__RESH_VERSION" != "$(resh-postcollect -version)" ]; then - echo "RESH WARNING: You probably just updated RESH - PLEASE RESTART OR RELOAD THIS TERMINAL SESSION (resh version: $(resh-collect -version); resh version of this terminal session: ${__RESH_VERSION})" - else - echo "RESH INFO: New RESH shellrc script was loaded - if you encounter any issues please restart this terminal session." - fi - elif [ "$__RESH_REVISION" != "$(resh-postcollect -revision)" ]; then - # shellcheck source=shellrc.sh - source ~/.resh/shellrc - if [ "$__RESH_REVISION" != "$(resh-postcollect -revision)" ]; then - echo "RESH WARNING: You probably just updated RESH - PLEASE RESTART OR RELOAD THIS TERMINAL SESSION (resh revision: $(resh-collect -revision); resh revision of this terminal session: ${__RESH_REVISION})" - fi - fi - if [ "$__RESH_VERSION" = "$(resh-postcollect -version)" ] && [ "$__RESH_REVISION" = "$(resh-postcollect -revision)" ]; then - local fpath_last_run="$__RESH_XDG_CACHE_HOME/postcollect_last_run_out.txt" - resh-postcollect -requireVersion "$__RESH_VERSION" \ - -requireRevision "$__RESH_REVISION" \ - -cmdLine "$__RESH_CMDLINE" \ - -realtimeBefore "$__RESH_RT_BEFORE" \ - -exitCode "$__RESH_EXIT_CODE" \ - -sessionId "$__RESH_SESSION_ID" \ - -recordId "$__RESH_RECORD_ID" \ - -shell "$__RESH_SHELL" \ - -shlvl "$__RESH_SHLVL" \ - -pwdAfter "$__RESH_PWD_AFTER" \ - -gitCdupAfter "$__RESH_GIT_CDUP_AFTER" \ - -gitCdupExitCodeAfter "$__RESH_GIT_CDUP_EXIT_CODE_AFTER" \ - -gitRemoteAfter "$__RESH_GIT_REMOTE_AFTER" \ - -gitRemoteExitCodeAfter "$__RESH_GIT_REMOTE_EXIT_CODE_AFTER" \ - -realtimeAfter "$__RESH_RT_AFTER" \ - -timezoneAfter "$__RESH_TZ_AFTER" \ - >| "$fpath_last_run" 2>&1 || echo "resh-postcollect ERROR: $(head -n 1 $fpath_last_run)" - fi - __resh_reset_variables +# Wrapper for resh-cli for calling resh directly +resh() { + if [ "$(resh-cli -version)" != "$__RESH_VERSION" ] && [ -z "${__RESH_NO_RELOAD-}" ]; then + source ~/.resh/shellrc + # Show reload message from the updated shell files + __resh_reload_msg + # Rerun self but prevent another reload. Extra protection against infinite recursion. + __RESH_NO_RELOAD=1 resh "$@" + return $? fi - unset __RESH_COLLECT + local buffer + local git_remote; git_remote="$(git remote get-url origin 2>/dev/null)" + buffer=$(resh-cli -requireVersion "$__RESH_VERSION" \ + --git-origin-remote "$git_remote" \ + --pwd "$PWD" \ + --session-id "$__RESH_SESSION_ID" \ + "$@" + ) + status_code=$? + if [ $status_code = 111 ]; then + # execute + echo "$buffer" + eval "$buffer" + elif [ $status_code = 0 ]; then + # paste + echo "$buffer" + elif [ $status_code = 130 ]; then + true + else + printf "%s" "$buffer" >&2 + fi +} + +__resh_widget_control_R_compat() { + __bindfunc_compat_wrapper __resh_widget_control_R +} + +__resh_nop() { + # does nothing + true +} + +# shellcheck source=../submodules/bash-zsh-compat-widgets/bindfunc.sh +. ~/.resh/bindfunc.sh + +__resh_bind_control_R() { + bindfunc '\C-r' __resh_widget_control_R_compat + if [ -n "${BASH_VERSION-}" ]; then + # fuck bash + bind '"\C-r": "\u[31~\u[32~"' + bind -x '"\u[31~": __resh_widget_control_R_compat' + + # execute + # bind '"\u[32~": accept-line' + + # just paste + # bind -x '"\u[32~": __resh_nop' + true + fi + return 0 } diff --git a/scripts/install.sh b/scripts/install.sh index 1b22d156..8e7059e0 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,264 +1,196 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh set -euo pipefail +# TODO: There is a lot of hardcoded stuff here (paths mostly) +# TODO: Split this into installation and setup because we want to suport package manager installation eventually +# TODO: "installation" should stay here and be simple, "setup" should be moved behind "reshctl setup" + echo echo "Checking your system ..." +printf '\e[31;1m' # red color on + +cleanup() { + printf '\e[0m' # reset + exit +} +trap cleanup EXIT INT TERM # /usr/bin/zsh -> zsh login_shell=$(echo "$SHELL" | rev | cut -d'/' -f1 | rev) if [ "$login_shell" != bash ] && [ "$login_shell" != zsh ]; then - echo "ERROR: Unsupported/unknown login shell: $login_shell" - exit 1 + echo "* UNSUPPORTED login shell: $login_shell" + echo " -> RESH supports zsh and bash" + echo + if [ -z "${RESH_INSTALL_IGNORE_LOGIN_SHELL-}" ]; then + echo 'EXITING!' + echo ' -> You can skip this check with `export RESH_INSTALL_IGNORE_LOGIN_SHELL=1`' + exit 1 + fi fi -echo " * Login shell: $login_shell - OK" +# TODO: Explicitly ask users if they want to enable RESH in shells +# Only offer shells with supported versions +# E.g. Enable RESH in: Zsh (your login shell), Bash, Both shells -# check like we are not running bash bash_version=$(bash -c 'echo ${BASH_VERSION}') bash_version_major=$(bash -c 'echo ${BASH_VERSINFO[0]}') bash_version_minor=$(bash -c 'echo ${BASH_VERSINFO[1]}') -bash_too_old="" -if [ "$bash_version_major" -le 3 ]; then - bash_too_old=true -elif [ "$bash_version_major" -eq 4 ] && [ "$bash_version_minor" -lt 3 ]; then - bash_too_old=true +bash_ok=1 +if [ "$bash_version_major" -le 3 ]; then + bash_ok=0 +elif [ "$bash_version_major" -eq 4 ] && [ "$bash_version_minor" -lt 3 ]; then + bash_ok=0 fi -if [ "$bash_too_old" = true ]; then - echo " * Bash version: $bash_version - WARNING!" - if [ "$login_shell" = bash ]; then - echo " > Your bash version is old." - echo " > Bash is also your login shell." - echo " > Updating to bash 4.3+ is strongly RECOMMENDED!" - else - echo " > Your bash version is old" - echo " > Bash is not your login shell so it should not be an issue." - echo " > Updating to bash 4.3+ is recommended." - fi -else - echo " * Bash version: $bash_version - OK" +if [ "$bash_ok" != 1 ]; then + echo "* UNSUPPORTED bash version: $bash_version" + echo " -> Update to bash 4.3+ if you want to use RESH in bash" + echo fi - +zsh_ok=1 if ! zsh --version >/dev/null 2>&1; then - echo " * Zsh version: ? - not installed!" + echo "* Zsh not installed" + zsh_ok=0 else zsh_version=$(zsh -c 'echo ${ZSH_VERSION}') zsh_version_major=$(echo "$zsh_version" | cut -d'.' -f1) - if [ "$zsh_version_major" -lt 5 ]; then - echo " * Zsh version: $zsh_version - UNSUPPORTED!" - if [ "$login_shell" = zsh ]; then - echo " > Your zsh version is old." - echo " > Zsh is also your login shell." - echo " > Updating to Zsh 5.0+ is strongly RECOMMENDED!" - else - echo " > Your zsh version is old" - echo " > Zsh is not your login shell so it should not be an issue." - echo " > Updating to zsh 5.0+ is recommended." - fi - else - echo " * Zsh version: $zsh_version - OK" + if [ "$zsh_version_major" -lt 5 ]; then + echo "* UNSUPPORTED zsh version: $zsh_version" + echo " -> Updatie to zsh 5.0+ if you want to use RESH in zsh" + echo + zsh_ok=0 fi fi - -if [ "$(uname)" = Darwin ]; then - if gnohup --version >/dev/null 2>&1; then - echo " * Nohup installed: OK" - else - echo " * Nohup installed: NOT INSTALLED!" - echo " > You don't have nohup" - echo " > Please install GNU coreutils" - echo - echo " $ brew install coreutils" - echo - exit 1 - fi -else - if setsid --version >/dev/null 2>&1; then - echo " * Setsid installed: OK" - else - echo " * Setsid installed: NOT INSTALLED!" - echo " > You don't have setsid" - echo " > Please install unix-util" +if [ "$bash_ok" != 1 ] && [ "$zsh_ok" != 1 ]; then + echo "* You have no shell that is supported by RESH!" + echo " -> Please install/update zsh or bash and run this installation again" + echo + if [ -z "${RESH_INSTALL_IGNORE_NO_SHELL-}" ]; then + echo 'EXITING!' + echo ' -> You can prevent this check by setting `export RESH_INSTALL_IGNORE_NO_SHELL=1`' echo exit 1 fi fi -# echo +printf '\e[0m' # reset # echo "Continue with installation? (Any key to CONTINUE / Ctrl+C to ABORT)" # # shellcheck disable=2034 # read -r x -echo -echo "Creating directories ..." +if [ -z "${__RESH_VERSION-}" ]; then + # First installation + # Stop the daemon anyway just to be sure + # But don't output anything + ./scripts/resh-daemon-stop.sh -q +else + ./scripts/resh-daemon-stop.sh +fi + +echo "Installing ..." +# Crete dirs first to get rid of edge-cases +# If we fail we don't roll back - directories are harmless mkdir_if_not_exists() { if [ ! -d "$1" ]; then - mkdir "$1" + mkdir "$1" fi } mkdir_if_not_exists ~/.resh mkdir_if_not_exists ~/.resh/bin -mkdir_if_not_exists ~/.resh/bash_completion.d -mkdir_if_not_exists ~/.resh/zsh_completion.d mkdir_if_not_exists ~/.config -echo "Copying files ..." +# Run setup and update tasks + +./bin/resh-install-utils setup-device +# migrate-all updates format of config and history +# migrate-all restores original config and history on error +# There is no need to roll back anything else because we haven't replaced +# anything in the previous installation. +./bin/resh-install-utils migrate-all + + +# Copy files + +# echo "Copying files ..." 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 scripts/shellrc.sh ~/.resh/shellrc -cp -f scripts/reshctl.sh scripts/widgets.sh scripts/hooks.sh scripts/util.sh ~/.resh/ +cp -f scripts/resh-daemon-start.sh ~/.resh/bin/resh-daemon-start +cp -f scripts/resh-daemon-stop.sh ~/.resh/bin/resh-daemon-stop +cp -f scripts/resh-daemon-restart.sh ~/.resh/bin/resh-daemon-restart +cp -f scripts/hooks.sh ~/.resh/ cp -f scripts/rawinstall.sh ~/.resh/ -update_config() { - version=$1 - key=$2 - value=$3 - # TODO: create bin/semver-lt - if bin/semver-lt "${__RESH_VERSION:-0.0.0}" "$1" && [ "$(bin/resh-config -key $key)" != "$value" ] ; then - echo " * config option $key was updated to $value" - # TODO: enable resh-config value setting - # resh-config -key "$key" -value "$value" - fi -} +# echo "Copying more files ..." +# Copy all go executables. We don't really need to omit install-utils from the bin directory +cp -f bin/resh-* ~/.resh/bin/ +# Rename reshctl +mv ~/.resh/bin/resh-control ~/.resh/bin/reshctl -# Do not overwrite config if it exists -if [ ! -f ~/.config/resh.toml ]; then - echo "Copying config file ..." - cp -f conf/config.toml ~/.config/resh.toml -# else - # echo "Merging config files ..." - # NOTE: This is where we will merge configs when we make changes to the upstream config - # HINT: check which version are we updating FROM and make changes to config based on that +echo "Handling shell files ..." +# Only add shell directives into bash if it passed version checks +if [ "$bash_ok" = 1 ]; then + if [ ! -f ~/.bashrc ]; then + touch ~/.bashrc + fi + # Adding resh shellrc to .bashrc ... + grep -q '[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc' ~/.bashrc ||\ + echo -e '\n[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc # this line was added by RESH' >> ~/.bashrc + # Adding bash-preexec to .bashrc ... + grep -q '[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh' ~/.bashrc ||\ + echo -e '\n[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh # this line was added by RESH' >> ~/.bashrc fi -echo "Generating completions ..." -bin/resh-control completion bash > ~/.resh/bash_completion.d/_reshctl -bin/resh-control completion zsh > ~/.resh/zsh_completion.d/_reshctl - -echo "Copying more files ..." -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/sanitizer_data - -# backward compatibility: We have a new location for resh history file -[ ! -f ~/.resh/history.json ] || mv ~/.resh/history.json ~/.resh_history.json - -echo "Finishing up ..." -# Adding resh shellrc to .bashrc ... -if [ ! -f ~/.bashrc ]; then - touch ~/.bashrc -fi -grep -q '[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc' ~/.bashrc ||\ - echo -e '\n[[ -f ~/.resh/shellrc ]] && source ~/.resh/shellrc # this line was added by RESH (Rich Enchanced Shell History)' >> ~/.bashrc -# Adding bash-preexec to .bashrc ... -grep -q '[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh' ~/.bashrc ||\ - echo -e '\n[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh # this line was added by RESH (Rich Enchanced Shell History)' >> ~/.bashrc -# Adding resh shellrc to .zshrc ... -if [ -f ~/.zshrc ]; then - grep -q '[ -f ~/.resh/shellrc ] && source ~/.resh/shellrc' ~/.zshrc ||\ - echo -e '\n[ -f ~/.resh/shellrc ] && source ~/.resh/shellrc # this line was added by RESH (Rich Enchanced Shell History)' >> ~/.zshrc +# Only add shell directives into zsh if it passed version checks +if [ "$zsh_ok" = 1 ]; then + # Adding resh shellrc to .zshrc ... + if [ -f ~/.zshrc ]; then + grep -q '[ -f ~/.resh/shellrc ] && source ~/.resh/shellrc' ~/.zshrc ||\ + echo -e '\n[ -f ~/.resh/shellrc ] && source ~/.resh/shellrc # this line was added by RESH' >> ~/.zshrc + fi fi -# Deleting zsh completion cache - for future use -# [ ! -e ~/.zcompdump ] || rm ~/.zcompdump +~/.resh/bin/resh-daemon-start -# Final touch -touch ~/.resh_history.json +# bright green +high='\e[1m' +reset='\e[0m' -# Generating resh-uuid ... -[ -e ~/.resh/resh-uuid ] \ - || cat /proc/sys/kernel/random/uuid > ~/.resh/resh-uuid 2>/dev/null \ - || scripts/uuid.sh > ~/.resh/resh-uuid 2>/dev/null +printf ' +Installation finished successfully. -# Source utils to get __resh_run_daemon function -# shellcheck source=util.sh -. ~/.resh/util.sh -# Restarting resh daemon ... -if [ -f ~/.resh/resh.pid ]; then - kill -SIGTERM "$(cat ~/.resh/resh.pid)" || true - rm ~/.resh/resh.pid -else - pkill -SIGTERM "resh-daemon" || true +QUICK START +\e[32;1m Press CTRL+R to launch RESH SEARCH \e[0m +' +if [ -z "${__RESH_VERSION-}" ]; then + printf 'You will need to restart your terminal first!\n' fi -# daemon uses xdg path variables -__resh_set_xdg_home_paths -__resh_run_daemon - - -info="---- Scroll down using arrow keys ---- -##################################### -# ____ _____ ____ _ _ # -# | _ \| ____/ ___|| | | | # -# | |_) | _| \___ \| |_| | # -# | _ <| |___ ___) | _ | # -# |_| \_\_____|____/|_| |_| # -# Rich Enhanced Shell History # -##################################### -" - -info="$info -RESH SEARCH APPLICATION = Redesigned reverse search that actually works - - >>> Launch RESH SEARCH app by pressing CTRL+R <<< - (you will need to restart your terminal first) - - Search your history by commands. - Host, directories, git remote, and exit status is used to display relevant results first. - - At first, the search application will use the standard shell history without context. - All history recorded from now on will have context which will by the RESH SEARCH app. - - Enable/disable Ctrl+R binding using reshctl command: - $ reshctl enable ctrl_r_binding - $ reshctl disable ctrl_r_binding - -CHECK FOR UPDATES - To check for (and install) updates use reshctl command: - $ reshctl update - -HISTORY - Your resh history will be recorded to '~/.resh_history.json' - Look at it using e.g. following command (you might need to install jq) - $ tail -f ~/.resh_history.json | jq - -ISSUES & FEEDBACK - Please report issues to: https://github.com/curusarn/resh/issues - Feedback and suggestions are very welcome! -" -if [ -z "${__RESH_VERSION:-}" ]; then info="$info -############################################################## -# # -# Finish the installation by RESTARTING this terminal! # -# # -##############################################################" -fi - -info="$info ----- Close this by pressing Q ----" +printf ' + Full-text search your shell history. + Relevant results are displayed first based on current directory, git repo, and exit status. + RESH will locally record and save shell history with context (directory, time, exit status, ...) + Start using RESH right away because bash and zsh history are also searched. -echo "$info" | ${PAGER:-less} + Update RESH by running: reshctl update + Thank you for using RESH! +' -echo -echo "All done!" -echo "Thank you for using RESH" -echo "Issues go here: https://github.com/curusarn/resh/issues" -echo "Ctrl+R launches the RESH SEARCH app" -# echo "Do not forget to restart your terminal" -if [ -z "${__RESH_VERSION:-}" ]; then echo " +# Show banner if RESH is not loaded in the terminal +if [ -z "${__RESH_VERSION-}" ]; then printf ' ############################################################## # # # Finish the installation by RESTARTING this terminal! # # # -##############################################################" +############################################################## +' fi diff --git a/scripts/resh-daemon-restart.sh b/scripts/resh-daemon-restart.sh new file mode 100755 index 00000000..42ed1610 --- /dev/null +++ b/scripts/resh-daemon-restart.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +resh-daemon-stop "$@" +resh-daemon-start "$@" \ No newline at end of file diff --git a/scripts/resh-daemon-start.sh b/scripts/resh-daemon-start.sh new file mode 100755 index 00000000..ecb75435 --- /dev/null +++ b/scripts/resh-daemon-start.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +if [ "${1-}" != "-q" ]; then + echo "Starting RESH daemon ..." + printf "Logs are in: %s\n" "${XDG_DATA_HOME-~/.local/share}/resh/log.json" +fi +# Run daemon in background - don't block +# Redirect stdin, stdout, and stderr to /dev/null - detach all I/O +resh-daemon /dev/null 2>/dev/null & + +# After resh-daemon-start.sh exits the resh-daemon process loses its parent +# and it gets adopted by init + +# NOTES: +# No disown - job control of this shell doesn't affect the parent shell +# No nohup - SIGHUP signals won't be sent to orphaned resh-daemon (plus the daemon ignores them) +# No setsid - SIGINT signals won't be sent to orphaned resh-daemon (plus the daemon ignores them) \ No newline at end of file diff --git a/scripts/resh-daemon-stop.sh b/scripts/resh-daemon-stop.sh new file mode 100755 index 00000000..742c2d36 --- /dev/null +++ b/scripts/resh-daemon-stop.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env sh + +failed_to_kill() { + [ "${1-}" != "-q" ] && echo "Failed to kill the RESH daemon - it probably isn't running" +} + +xdg_pid() { + local path="${XDG_DATA_HOME-}"/resh/daemon.pid + [ -n "${XDG_DATA_HOME-}" ] && [ -f "$path" ] || return 1 + cat "$path" +} +default_pid() { + local path=~/.local/share/resh/daemon.pid + [ -f "$path" ] || return 1 + cat "$path" +} +legacy_pid() { + local path=~/.resh/resh.pid + [ -f "$path" ] || return 1 + cat "$path" +} +pid=$(xdg_pid || default_pid || legacy_pid) + +if [ -n "$pid" ]; then + [ "${1-}" != "-q" ] && printf "Stopping RESH daemon ... (PID: %s)\n" "$pid" + kill "$pid" || failed_to_kill +else + [ "${1-}" != "-q" ] && printf "Stopping RESH daemon ...\n" + killall -q resh-daemon || failed_to_kill +fi + diff --git a/scripts/resh-evaluate-plot.py b/scripts/resh-evaluate-plot.py deleted file mode 100755 index 89792cbb..00000000 --- a/scripts/resh-evaluate-plot.py +++ /dev/null @@ -1,1218 +0,0 @@ -#!/usr/bin/env python3 - - -import traceback -import sys -import json -from collections import defaultdict -import numpy as np -from graphviz import Digraph -from datetime import datetime - -from matplotlib import rcParams -rcParams['font.family'] = 'serif' -# rcParams['font.serif'] = [''] - -import matplotlib.pyplot as plt -import matplotlib.path as mpath -import matplotlib.patches as mpatches - -PLOT_WIDTH = 10 # inches -PLOT_HEIGHT = 7 # inches - -PLOT_SIZE_zipf = 20 - -data = json.load(sys.stdin) - -DATA_records = [] -DATA_records_by_session = defaultdict(list) -DATA_records_by_user = defaultdict(list) -for user in data["UsersRecords"]: - if user["Devices"] is None: - continue - for device in user["Devices"]: - if device["Records"] is None: - continue - for record in device["Records"]: - if "invalid" in record and record["invalid"]: - continue - - DATA_records.append(record) - DATA_records_by_session[record["seqSessionId"]].append(record) - DATA_records_by_user[user["Name"] + ":" + device["Name"]].append(record) - -DATA_records = list(sorted(DATA_records, key=lambda x: x["realtimeAfterLocal"])) - -for pid, session in DATA_records_by_session.items(): - session = list(sorted(session, key=lambda x: x["realtimeAfterLocal"])) - -# TODO: this should be a cmdline option -async_draw = True - -# for strategy in data["Strategies"]: -# print(json.dumps(strategy)) - -def zipf(length): - return list(map(lambda x: 1/2**x, range(0, length))) - - -def trim(text, length, add_elipse=True): - if add_elipse and len(text) > length: - return text[:length-1] + "…" - return text[:length] - - -# Figure 3.1. The normalized command frequency, compared with Zipf. -def plot_cmdLineFrq_rank(plotSize=PLOT_SIZE_zipf, show_labels=False): - cmdLine_count = defaultdict(int) - for record in DATA_records: - cmdLine_count[record["cmdLine"]] += 1 - - tmp = sorted(cmdLine_count.items(), key=lambda x: x[1], reverse=True)[:plotSize] - cmdLineFrq = list(map(lambda x: x[1] / tmp[0][1], tmp)) - labels = list(map(lambda x: trim(x[0], 7), tmp)) - - ranks = range(1, len(cmdLineFrq)+1) - plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT)) - plt.plot(ranks, zipf(len(ranks)), '-') - plt.plot(ranks, cmdLineFrq, 'o-') - plt.title("Commandline frequency / rank") - plt.ylabel("Normalized commandline frequency") - plt.xlabel("Commandline rank") - plt.legend(("Zipf", "Commandline"), loc="best") - if show_labels: - plt.xticks(ranks, labels, rotation=-60) - # TODO: make xticks integral - if async_draw: - plt.draw() - else: - plt.show() - - -# similar to ~ Figure 3.1. The normalized command frequency, compared with Zipf. -def plot_cmdFrq_rank(plotSize=PLOT_SIZE_zipf, show_labels=False): - plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT)) - plt.title("Command frequency / rank") - plt.ylabel("Normalized command frequency") - plt.xlabel("Command rank") - legend = [] - - - cmd_count = defaultdict(int) - len_records = 0 - for record in DATA_records: - cmd = record["command"] - if cmd == "": - continue - cmd_count[cmd] += 1 - len_records += 1 - - tmp = sorted(cmd_count.items(), key=lambda x: x[1], reverse=True)[:plotSize] - cmdFrq = list(map(lambda x: x[1] / tmp[0][1], tmp)) - labels = list(map(lambda x: trim(x[0], 7), tmp)) - - top100percent = 100 * sum(map(lambda x: x[1], list(cmd_count.items())[:int(1 * len(cmd_count))])) / len_records - top10percent = 100 * sum(map(lambda x: x[1], list(cmd_count.items())[:int(0.1 * len(cmd_count))])) / len_records - top20percent = 100 * sum(map(lambda x: x[1], list(cmd_count.items())[:int(0.2 * len(cmd_count))])) / len_records - print("% ALL: Top {} %% of cmds amounts for {} %% of all command lines".format(100, top100percent)) - print("% ALL: Top {} %% of cmds amounts for {} %% of all command lines".format(10, top10percent)) - print("% ALL: Top {} %% of cmds amounts for {} %% of all command lines".format(20, top20percent)) - ranks = range(1, len(cmdFrq)+1) - plt.plot(ranks, zipf(len(ranks)), '-') - legend.append("Zipf distribution") - plt.plot(ranks, cmdFrq, 'o-') - legend.append("All subjects") - - - for user in DATA_records_by_user.items(): - cmd_count = defaultdict(int) - len_records = 0 - name, records = user - for record in records: - cmd = record["command"] - if cmd == "": - continue - cmd_count[cmd] += 1 - len_records += 1 - - tmp = sorted(cmd_count.items(), key=lambda x: x[1], reverse=True)[:plotSize] - cmdFrq = list(map(lambda x: x[1] / tmp[0][1], tmp)) - labels = list(map(lambda x: trim(x[0], 7), tmp)) - - top100percent = 100 * sum(map(lambda x: x[1], list(cmd_count.items())[:int(1 * len(cmd_count))])) / len_records - top10percent = 100 * sum(map(lambda x: x[1], list(cmd_count.items())[:int(0.1 * len(cmd_count))])) / len_records - top20percent = 100 * sum(map(lambda x: x[1], list(cmd_count.items())[:int(0.2 * len(cmd_count))])) / len_records - print("% {}: Top {} %% of cmds amounts for {} %% of all command lines".format(name, 100, top100percent)) - print("% {}: Top {} %% of cmds amounts for {} %% of all command lines".format(name, 10, top10percent)) - print("% {}: Top {} %% of cmds amounts for {} %% of all command lines".format(name, 20, top20percent)) - ranks = range(1, len(cmdFrq)+1) - plt.plot(ranks, cmdFrq, 'o-') - legend.append("{} (sanitize!)".format(name)) - - plt.legend(legend, loc="best") - - if show_labels: - plt.xticks(ranks, labels, rotation=-60) - # TODO: make xticks integral - if async_draw: - plt.draw() - else: - plt.show() - -# Figure 3.2. Command vocabulary size vs. the number of command lines entered for four individuals. -def plot_cmdVocabularySize_cmdLinesEntered(): - plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT)) - plt.title("Command vocabulary size vs. the number of command lines entered") - plt.ylabel("Command vocabulary size") - plt.xlabel("# of command lines entered") - legend = [] - - # x_count = max(map(lambda x: len(x[1]), DATA_records_by_user.items())) - # x_values = range(0, x_count) - for user in DATA_records_by_user.items(): - new_cmds_after_1k = 0 - new_cmds_after_2k = 0 - new_cmds_after_3k = 0 - cmd_vocabulary = set() - y_cmd_count = [0] - name, records = user - for record in records: - cmd = record["command"] - if cmd == "": - continue - if cmd in cmd_vocabulary: - # repeat last value - y_cmd_count.append(y_cmd_count[-1]) - else: - cmd_vocabulary.add(cmd) - # append last value +1 - y_cmd_count.append(y_cmd_count[-1] + 1) - if len(y_cmd_count) > 1000: - new_cmds_after_1k+=1 - if len(y_cmd_count) > 2000: - new_cmds_after_2k+=1 - if len(y_cmd_count) > 3000: - new_cmds_after_3k+=1 - - if len(y_cmd_count) == 1000: - print("% {}: Cmd adoption rate at 1k (between 0 and 1k) cmdlines = {}".format(name ,len(cmd_vocabulary) / (len(y_cmd_count)))) - if len(y_cmd_count) == 2000: - print("% {}: Cmd adoption rate at 2k cmdlines = {}".format(name ,len(cmd_vocabulary) / (len(y_cmd_count)))) - print("% {}: Cmd adoption rate between 1k and 2k cmdlines = {}".format(name ,new_cmds_after_1k / (len(y_cmd_count) - 1000))) - if len(y_cmd_count) == 3000: - print("% {}: Cmd adoption rate between 2k and 3k cmdlines = {}".format(name ,new_cmds_after_2k / (len(y_cmd_count) - 2000))) - - print("% {}: New cmd adoption rate after 1k cmdlines = {}".format(name ,new_cmds_after_1k / (len(y_cmd_count) - 1000))) - print("% {}: New cmd adoption rate after 2k cmdlines = {}".format(name ,new_cmds_after_2k / (len(y_cmd_count) - 2000))) - print("% {}: New cmd adoption rate after 3k cmdlines = {}".format(name ,new_cmds_after_3k / (len(y_cmd_count) - 3000))) - x_cmds_entered = range(0, len(y_cmd_count)) - plt.plot(x_cmds_entered, y_cmd_count, '-') - legend.append(name + " (TODO: sanitize!)") - - # print(cmd_vocabulary) - - plt.legend(legend, loc="best") - - if async_draw: - plt.draw() - else: - plt.show() - - -def plot_cmdVocabularySize_daily(): - SECONDS_IN_A_DAY = 86400 - plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT)) - plt.title("Command vocabulary size in days") - plt.ylabel("Command vocabulary size") - plt.xlabel("Days") - legend = [] - - # x_count = max(map(lambda x: len(x[1]), DATA_records_by_user.items())) - # x_values = range(0, x_count) - for user in DATA_records_by_user.items(): - new_cmds_after_100 = 0 - new_cmds_after_200 = 0 - new_cmds_after_300 = 0 - cmd_vocabulary = set() - y_cmd_count = [0] - name, records = user - - cmd_fail_count = 0 - - if not len(records): - print("ERROR: no records for user {}".format(name)) - continue - - first_day = records[0]["realtimeAfter"] - this_day = first_day - - for record in records: - cmd = record["command"] - timestamp = record["realtimeAfter"] - - if cmd == "": - cmd_fail_count += 1 - continue - - if timestamp >= this_day + SECONDS_IN_A_DAY: - this_day += SECONDS_IN_A_DAY - while timestamp >= this_day + SECONDS_IN_A_DAY: - y_cmd_count.append(-10) - this_day += SECONDS_IN_A_DAY - - y_cmd_count.append(len(cmd_vocabulary)) - cmd_vocabulary = set() # wipes the vocabulary each day - - if len(y_cmd_count) > 100: - new_cmds_after_100+=1 - if len(y_cmd_count) > 200: - new_cmds_after_200+=1 - if len(y_cmd_count) > 300: - new_cmds_after_300+=1 - - if len(y_cmd_count) == 100: - print("% {}: Cmd adoption rate at 100 days (between 0 and 100 days) = {}".format(name, len(cmd_vocabulary) / (len(y_cmd_count)))) - if len(y_cmd_count) == 200: - print("% {}: Cmd adoption rate at 200 days days = {}".format(name, len(cmd_vocabulary) / (len(y_cmd_count)))) - print("% {}: Cmd adoption rate between 100 and 200 days = {}".format(name, new_cmds_after_100 / (len(y_cmd_count) - 100))) - if len(y_cmd_count) == 300: - print("% {}: Cmd adoption rate between 200 and 300 days = {}".format(name, new_cmds_after_200 / (len(y_cmd_count) - 200))) - - if cmd not in cmd_vocabulary: - cmd_vocabulary.add(cmd) - - - print("% {}: New cmd adoption rate after 100 days = {}".format(name, new_cmds_after_100 / (len(y_cmd_count) - 100))) - print("% {}: New cmd adoption rate after 200 days = {}".format(name, new_cmds_after_200 / (len(y_cmd_count) - 200))) - print("% {}: New cmd adoption rate after 300 days = {}".format(name, new_cmds_after_300 / (len(y_cmd_count) - 300))) - print("% {}: cmd_fail_count = {}".format(name, cmd_fail_count)) - x_cmds_entered = range(0, len(y_cmd_count)) - plt.plot(x_cmds_entered, y_cmd_count, 'o', markersize=2) - legend.append(name + " (TODO: sanitize!)") - - # print(cmd_vocabulary) - - plt.legend(legend, loc="best") - plt.ylim(bottom=-5) - - if async_draw: - plt.draw() - else: - plt.show() - - -def matplotlib_escape(ss): - ss = ss.replace('$', '\\$') - return ss - - -def plot_cmdUsage_in_time(sort_cmds=False, num_cmds=None): - SECONDS_IN_A_DAY = 86400 - tab_colors = ("tab:blue", "tab:orange", "tab:green", "tab:red", "tab:purple", "tab:brown", "tab:pink", "tab:gray") - plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT)) - plt.title("Command use in time") - plt.ylabel("Commands") - plt.xlabel("Days") - legend_patches = [] - - cmd_ids = {} - y_labels = [] - - all_x_values = [] - all_y_values = [] - all_s_values = [] # size - all_c_values = [] # color - - x_values = [] - y_values = [] - s_values = [] # size - c_values = [] # color - - if sort_cmds: - cmd_count = defaultdict(int) - for user in DATA_records_by_user.items(): - name, records = user - for record in records: - cmd = record["command"] - cmd_count[cmd] += 1 - - sorted_cmds = map(lambda x: x[0], sorted(cmd_count.items(), key=lambda x: x[1], reverse=True)) - - for cmd in sorted_cmds: - cmd_ids[cmd] = len(cmd_ids) - y_labels.append(matplotlib_escape(cmd)) - - - for user_idx, user in enumerate(DATA_records_by_user.items()): - name, records = user - - if not len(records): - print("ERROR: no records for user {}".format(name)) - continue - - - first_day = records[0]["realtimeAfter"] - this_day = first_day - day_no = 0 - today_cmds = defaultdict(int) - - for record in records: - cmd = record["command"] - timestamp = record["realtimeAfter"] - - if cmd == "": - print("NOTICE: Empty cmd for {}".format(record["cmdLine"])) - continue - - if timestamp >= this_day + SECONDS_IN_A_DAY: - for item in today_cmds.items(): - cmd, count = item - cmd_id = cmd_ids[cmd] - # skip commands with high ids - if num_cmds is not None and cmd_id >= num_cmds: - continue - - x_values.append(day_no) - y_values.append(cmd_id) - s_values.append(count) - c_values.append(tab_colors[user_idx]) - - today_cmds = defaultdict(int) - - this_day += SECONDS_IN_A_DAY - day_no += 1 - while timestamp >= this_day + SECONDS_IN_A_DAY: - this_day += SECONDS_IN_A_DAY - day_no += 1 - - if cmd not in cmd_ids: - cmd_ids[cmd] = len(cmd_ids) - y_labels.append(matplotlib_escape(cmd)) - - today_cmds[cmd] += 1 - - all_x_values.extend(x_values) - all_y_values.extend(y_values) - all_s_values.extend(s_values) - all_c_values.extend(c_values) - x_values = [] - y_values = [] - s_values = [] - c_values = [] - legend_patches.append(mpatches.Patch(color=tab_colors[user_idx], label="{} ({}) (TODO: sanitize!)".format(name, user_idx))) - - if num_cmds is not None and len(y_labels) > num_cmds: - y_labels = y_labels[:num_cmds] - plt.yticks(ticks=range(0, len(y_labels)), labels=y_labels, fontsize=6) - plt.scatter(all_x_values, all_y_values, s=all_s_values, c=all_c_values, marker='o') - plt.legend(handles=legend_patches, loc="best") - - if async_draw: - plt.draw() - else: - plt.show() - - -# Figure 5.6. Command line vocabulary size vs. the number of commands entered for four typical individuals. -def plot_cmdVocabularySize_time(): - SECONDS_IN_A_DAY = 86400 - plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT)) - plt.title("Command vocabulary size growth in time") - plt.ylabel("Command vocabulary size") - plt.xlabel("Days") - legend = [] - - # x_count = max(map(lambda x: len(x[1]), DATA_records_by_user.items())) - # x_values = range(0, x_count) - for user in DATA_records_by_user.items(): - new_cmds_after_100 = 0 - new_cmds_after_200 = 0 - new_cmds_after_300 = 0 - cmd_vocabulary = set() - y_cmd_count = [0] - name, records = user - - cmd_fail_count = 0 - - if not len(records): - print("ERROR: no records for user {}".format(name)) - continue - - first_day = records[0]["realtimeAfter"] - this_day = first_day - - for record in records: - cmd = record["command"] - timestamp = record["realtimeAfter"] - - if cmd == "": - cmd_fail_count += 1 - continue - - if timestamp >= this_day + SECONDS_IN_A_DAY: - this_day += SECONDS_IN_A_DAY - while timestamp >= this_day + SECONDS_IN_A_DAY: - y_cmd_count.append(-10) - this_day += SECONDS_IN_A_DAY - - y_cmd_count.append(len(cmd_vocabulary)) - - if len(y_cmd_count) > 100: - new_cmds_after_100+=1 - if len(y_cmd_count) > 200: - new_cmds_after_200+=1 - if len(y_cmd_count) > 300: - new_cmds_after_300+=1 - - if len(y_cmd_count) == 100: - print("% {}: Cmd adoption rate at 100 days (between 0 and 100 days) = {}".format(name, len(cmd_vocabulary) / (len(y_cmd_count)))) - if len(y_cmd_count) == 200: - print("% {}: Cmd adoption rate at 200 days days = {}".format(name, len(cmd_vocabulary) / (len(y_cmd_count)))) - print("% {}: Cmd adoption rate between 100 and 200 days = {}".format(name, new_cmds_after_100 / (len(y_cmd_count) - 100))) - if len(y_cmd_count) == 300: - print("% {}: Cmd adoption rate between 200 and 300 days = {}".format(name, new_cmds_after_200 / (len(y_cmd_count) - 200))) - - if cmd not in cmd_vocabulary: - cmd_vocabulary.add(cmd) - - - print("% {}: New cmd adoption rate after 100 days = {}".format(name, new_cmds_after_100 / (len(y_cmd_count) - 100))) - print("% {}: New cmd adoption rate after 200 days = {}".format(name, new_cmds_after_200 / (len(y_cmd_count) - 200))) - print("% {}: New cmd adoption rate after 300 days = {}".format(name, new_cmds_after_300 / (len(y_cmd_count) - 300))) - print("% {}: cmd_fail_count = {}".format(name, cmd_fail_count)) - x_cmds_entered = range(0, len(y_cmd_count)) - plt.plot(x_cmds_entered, y_cmd_count, 'o', markersize=2) - legend.append(name + " (TODO: sanitize!)") - - # print(cmd_vocabulary) - - plt.legend(legend, loc="best") - plt.ylim(bottom=0) - - if async_draw: - plt.draw() - else: - plt.show() - - -# Figure 5.6. Command line vocabulary size vs. the number of commands entered for four typical individuals. -def plot_cmdLineVocabularySize_cmdLinesEntered(): - plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT)) - plt.title("Command line vocabulary size vs. the number of command lines entered") - plt.ylabel("Command line vocabulary size") - plt.xlabel("# of command lines entered") - legend = [] - - for user in DATA_records_by_user.items(): - cmdLine_vocabulary = set() - y_cmdLine_count = [0] - name, records = user - for record in records: - cmdLine = record["cmdLine"] - if cmdLine in cmdLine_vocabulary: - # repeat last value - y_cmdLine_count.append(y_cmdLine_count[-1]) - else: - cmdLine_vocabulary.add(cmdLine) - # append last value +1 - y_cmdLine_count.append(y_cmdLine_count[-1] + 1) - - # print(cmdLine_vocabulary) - x_cmdLines_entered = range(0, len(y_cmdLine_count)) - plt.plot(x_cmdLines_entered, y_cmdLine_count, '-') - legend.append(name + " (TODO: sanitize!)") - - plt.legend(legend, loc="best") - - if async_draw: - plt.draw() - else: - plt.show() - -# Figure 3.3. Sequential structure of UNIX command usage, from Figure 4 in Hanson et al. (1984). -# Ball diameters are proportional to stationary probability. Lines indicate significant dependencies, -# solid ones being more probable (p < .0001) and dashed ones less probable (.005 < p < .0001). -def graph_cmdSequences(node_count=33, edge_minValue=0.05, view_graph=True): - START_CMD = "_start_" - END_CMD = "_end_" - cmd_count = defaultdict(int) - cmdSeq_count = defaultdict(lambda: defaultdict(int)) - cmd_id = dict() - x = 0 - cmd_id[START_CMD] = str(x) - x += 1 - cmd_id[END_CMD] = str(x) - for pid, session in DATA_records_by_session.items(): - cmd_count[START_CMD] += 1 - prev_cmd = START_CMD - for record in session: - cmd = record["command"] - if cmd == "": - continue - cmdSeq_count[prev_cmd][cmd] += 1 - cmd_count[cmd] += 1 - if cmd not in cmd_id: - x += 1 - cmd_id[cmd] = str(x) - prev_cmd = cmd - # end the session - cmdSeq_count[prev_cmd][END_CMD] += 1 - cmd_count[END_CMD] += 1 - - - # get `node_count` of largest nodes - sorted_cmd_count = sorted(cmd_count.items(), key=lambda x: x[1], reverse=True) - print(sorted_cmd_count) - cmds_to_graph = list(map(lambda x: x[0], sorted_cmd_count))[:node_count] - - # use 3 biggest nodes as a reference point for scaling - biggest_node = cmd_count[cmds_to_graph[0]] - nd_biggest_node = cmd_count[cmds_to_graph[1]] - rd_biggest_node = cmd_count[cmds_to_graph[1]] - count2scale_coef = 3 / (biggest_node + nd_biggest_node + rd_biggest_node) - - # scaling constant - # affects node size and node label - base_scaling_factor = 21 - # extra scaling for experiments - not really useful imho - # affects everything nodes, edges, node labels, treshold for turning label into xlabel, xlabel size, ... - extra_scaling_factor = 1.0 - for x in range(0, 10): - # graphviz is not the most reliable piece of software - # -> retry on fail but scale nodes down by 1% - scaling_factor = base_scaling_factor * (1 - x * 0.01) - - # overlap: scale -> solve overlap by scaling the graph - # overlap_shrink -> try to shrink the graph a bit after you are done - # splines -> don't draw edges over nodes - # sep: 2.5 -> assume that nodes are 2.5 inches larger - graph_attr={'overlap':'scale', 'overlap_shrink':'true', - 'splines':'true', 'sep':'0.25'} - graph = Digraph(name='command_sequentiality', engine='neato', graph_attr=graph_attr) - - # iterate over all nodes - for cmd in cmds_to_graph: - seq = cmdSeq_count[cmd] - count = cmd_count[cmd] - - # iterate over all "following" commands (for each node) - for seq_entry in seq.items(): - cmd2, seq_count = seq_entry - relative_seq_count = seq_count / count - - # check if "follow" command is supposed to be in the graph - if cmd2 not in cmds_to_graph: - continue - # check if the edge value is high enough - if relative_seq_count < edge_minValue: - continue - - # create starting node and end node for the edge - # duplicates don't matter - for id_, cmd_ in ((cmd_id[cmd], cmd), (cmd_id[cmd2], cmd2)): - count_ = cmd_count[cmd_] - scale_ = count_ * count2scale_coef * scaling_factor * extra_scaling_factor - width_ = 0.08 * scale_ - fontsize_ = 8.5 * scale_ / (len(cmd_) + 3) - - width_ = str(width_) - if fontsize_ < 12 * extra_scaling_factor: - graph.node(id_, ' ', shape='circle', fixedsize='true', fontname='monospace bold', - width=width_, fontsize=str(12 * extra_scaling_factor), forcelabels='true', xlabel=cmd_) - else: - fontsize_ = str(fontsize_) - graph.node(id_, cmd_, shape='circle', fixedsize='true', fontname='monospace bold', - width=width_, fontsize=fontsize_, forcelabels='true', labelloc='c') - - # value of the edge (percentage) 1.0 is max - scale_ = seq_count / cmd_count[cmd] - penwidth_ = str((0.5 + 4.5 * scale_) * extra_scaling_factor) - #penwidth_bold_ = str(8 * scale_) - # if scale_ > 0.5: - # graph.edge(cmd_id[cmd], cmd_id[cmd2], constraint='true', splines='curved', - # penwidth=penwidth_, style='bold', arrowhead='diamond') - # elif scale_ > 0.2: - if scale_ > 0.3: - scale_ = str(int(scale_ * 100)/100) - graph.edge(cmd_id[cmd], cmd_id[cmd2], constraint='true', splines='curved', - penwidth=penwidth_, forcelables='true', label=scale_) - elif scale_ > 0.2: - graph.edge(cmd_id[cmd], cmd_id[cmd2], constraint='true', splines='curved', - penwidth=penwidth_, style='dashed') - # elif scale_ > 0.1: - else: - graph.edge(cmd_id[cmd], cmd_id[cmd2], constraint='false', splines='curved', - penwidth=penwidth_, style='dotted', arrowhead='empty') - - # graphviz sometimes fails - see above - try: - # graph.view() - graph.render('/tmp/resh-graph-command_sequence-nodeCount_{}-edgeMinVal_{}.gv'.format(node_count, edge_minValue), view=view_graph) - break - except Exception as e: - trace = traceback.format_exc() - print("GRAPHVIZ EXCEPTION: <{}>\nGRAPHVIZ TRACE: <{}>".format(str(e), trace)) - - -def plot_strategies_matches(plot_size=50, selected_strategies=[], show_strat_title=True, force_strat_title=None): - plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT)) - plt.title("Matches at distance <{}>".format(datetime.now().strftime('%H:%M:%S'))) - plt.ylabel('%' + " of matches") - plt.xlabel("Distance") - legend = [] - x_values = range(1, plot_size+1) - saved_matches_total = None - saved_dataPoint_count = None - for strategy in data["Strategies"]: - strategy_title = strategy["Title"] - # strategy_description = strategy["Description"] - - dataPoint_count = 0 - matches = [0] * plot_size - matches_total = 0 - charsRecalled = [0] * plot_size - charsRecalled_total = 0 - - for match in strategy["Matches"]: - dataPoint_count += 1 - - if not match["Match"]: - continue - - chars = match["CharsRecalled"] - charsRecalled_total += chars - matches_total += 1 - - dist = match["Distance"] - if dist > plot_size: - continue - - matches[dist-1] += 1 - charsRecalled[dist-1] += chars - - # recent is very simple strategy so we will believe - # that there is no bug in it and we can use it to determine total - if strategy_title == "recent": - saved_matches_total = matches_total - saved_dataPoint_count = dataPoint_count - - if len(selected_strategies) and strategy_title not in selected_strategies: - continue - - acc = 0 - matches_cumulative = [] - for x in matches: - acc += x - matches_cumulative.append(acc) - # matches_cumulative.append(matches_total) - matches_percent = list(map(lambda x: 100 * x / dataPoint_count, matches_cumulative)) - - plt.plot(x_values, matches_percent, 'o-') - if force_strat_title is not None: - legend.append(force_strat_title) - else: - legend.append(strategy_title) - - - assert(saved_matches_total is not None) - assert(saved_dataPoint_count is not None) - max_values = [100 * saved_matches_total / saved_dataPoint_count] * len(x_values) - print("% >>> Avg recurrence rate = {}".format(max_values[0])) - plt.plot(x_values, max_values, 'r-') - legend.append("maximum possible") - - x_ticks = list(range(1, plot_size+1, 2)) - x_labels = x_ticks[:] - plt.xticks(x_ticks, x_labels) - plt.ylim(bottom=0) - if show_strat_title: - plt.legend(legend, loc="best") - if async_draw: - plt.draw() - else: - plt.show() - - -def plot_strategies_charsRecalled(plot_size=50, selected_strategies=[]): - plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT)) - plt.title("Average characters recalled at distance <{}>".format(datetime.now().strftime('%H:%M:%S'))) - plt.ylabel("Average characters recalled") - plt.xlabel("Distance") - x_values = range(1, plot_size+1) - legend = [] - saved_charsRecalled_total = None - saved_dataPoint_count = None - for strategy in data["Strategies"]: - strategy_title = strategy["Title"] - # strategy_description = strategy["Description"] - - dataPoint_count = 0 - matches = [0] * plot_size - matches_total = 0 - charsRecalled = [0] * plot_size - charsRecalled_total = 0 - - for match in strategy["Matches"]: - dataPoint_count += 1 - - if not match["Match"]: - continue - - chars = match["CharsRecalled"] - charsRecalled_total += chars - matches_total += 1 - - dist = match["Distance"] - if dist > plot_size: - continue - - matches[dist-1] += 1 - charsRecalled[dist-1] += chars - - # recent is very simple strategy so we will believe - # that there is no bug in it and we can use it to determine total - if strategy_title == "recent": - saved_charsRecalled_total = charsRecalled_total - saved_dataPoint_count = dataPoint_count - - if len(selected_strategies) and strategy_title not in selected_strategies: - continue - - acc = 0 - charsRecalled_cumulative = [] - for x in charsRecalled: - acc += x - charsRecalled_cumulative.append(acc) - charsRecalled_average = list(map(lambda x: x / dataPoint_count, charsRecalled_cumulative)) - - plt.plot(x_values, charsRecalled_average, 'o-') - legend.append(strategy_title) - - assert(saved_charsRecalled_total is not None) - assert(saved_dataPoint_count is not None) - max_values = [saved_charsRecalled_total / saved_dataPoint_count] * len(x_values) - print("% >>> Max avg recalled characters = {}".format(max_values[0])) - plt.plot(x_values, max_values, 'r-') - legend.append("maximum possible") - - x_ticks = list(range(1, plot_size+1, 2)) - x_labels = x_ticks[:] - plt.xticks(x_ticks, x_labels) - plt.ylim(bottom=0) - plt.legend(legend, loc="best") - if async_draw: - plt.draw() - else: - plt.show() - - -def plot_strategies_charsRecalled_prefix(plot_size=50, selected_strategies=[]): - plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT)) - plt.title("Average characters recalled at distance (including prefix matches) <{}>".format(datetime.now().strftime('%H:%M:%S'))) - plt.ylabel("Average characters recalled (including prefix matches)") - plt.xlabel("Distance") - x_values = range(1, plot_size+1) - legend = [] - saved_charsRecalled_total = None - saved_dataPoint_count = None - for strategy in data["Strategies"]: - strategy_title = strategy["Title"] - # strategy_description = strategy["Description"] - - dataPoint_count = 0 - matches_total = 0 - charsRecalled = [0] * plot_size - charsRecalled_total = 0 - - for multiMatch in strategy["PrefixMatches"]: - dataPoint_count += 1 - - if not multiMatch["Match"]: - continue - matches_total += 1 - - last_charsRecalled = 0 - for match in multiMatch["Entries"]: - - chars = match["CharsRecalled"] - charsIncrease = chars - last_charsRecalled - assert(charsIncrease > 0) - charsRecalled_total += charsIncrease - - dist = match["Distance"] - if dist > plot_size: - continue - - charsRecalled[dist-1] += charsIncrease - last_charsRecalled = chars - - # recent is very simple strategy so we will believe - # that there is no bug in it and we can use it to determine total - if strategy_title == "recent": - saved_charsRecalled_total = charsRecalled_total - saved_dataPoint_count = dataPoint_count - - if len(selected_strategies) and strategy_title not in selected_strategies: - continue - - acc = 0 - charsRecalled_cumulative = [] - for x in charsRecalled: - acc += x - charsRecalled_cumulative.append(acc) - charsRecalled_average = list(map(lambda x: x / dataPoint_count, charsRecalled_cumulative)) - - plt.plot(x_values, charsRecalled_average, 'o-') - legend.append(strategy_title) - - assert(saved_charsRecalled_total is not None) - assert(saved_dataPoint_count is not None) - max_values = [saved_charsRecalled_total / saved_dataPoint_count] * len(x_values) - print("% >>> Max avg recalled characters (including prefix matches) = {}".format(max_values[0])) - plt.plot(x_values, max_values, 'r-') - legend.append("maximum possible") - - x_ticks = list(range(1, plot_size+1, 2)) - x_labels = x_ticks[:] - plt.xticks(x_ticks, x_labels) - plt.ylim(bottom=0) - plt.legend(legend, loc="best") - if async_draw: - plt.draw() - else: - plt.show() - - -def plot_strategies_matches_noncummulative(plot_size=50, selected_strategies=["recent (bash-like)"], show_strat_title=False, force_strat_title=None): - plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT)) - plt.title("Matches at distance (noncumulative) <{}>".format(datetime.now().strftime('%H:%M:%S'))) - plt.ylabel('%' + " of matches") - plt.xlabel("Distance") - legend = [] - x_values = range(1, plot_size+1) - saved_matches_total = None - saved_dataPoint_count = None - for strategy in data["Strategies"]: - strategy_title = strategy["Title"] - # strategy_description = strategy["Description"] - - dataPoint_count = 0 - matches = [0] * plot_size - matches_total = 0 - charsRecalled = [0] * plot_size - charsRecalled_total = 0 - - for match in strategy["Matches"]: - dataPoint_count += 1 - - if not match["Match"]: - continue - - chars = match["CharsRecalled"] - charsRecalled_total += chars - matches_total += 1 - - dist = match["Distance"] - if dist > plot_size: - continue - - matches[dist-1] += 1 - charsRecalled[dist-1] += chars - - # recent is very simple strategy so we will believe - # that there is no bug in it and we can use it to determine total - if strategy_title == "recent": - saved_matches_total = matches_total - saved_dataPoint_count = dataPoint_count - - if len(selected_strategies) and strategy_title not in selected_strategies: - continue - - # acc = 0 - # matches_cumulative = [] - # for x in matches: - # acc += x - # matches_cumulative.append(acc) - # # matches_cumulative.append(matches_total) - matches_percent = list(map(lambda x: 100 * x / dataPoint_count, matches)) - - plt.plot(x_values, matches_percent, 'o-') - if force_strat_title is not None: - legend.append(force_strat_title) - else: - legend.append(strategy_title) - - assert(saved_matches_total is not None) - assert(saved_dataPoint_count is not None) - # max_values = [100 * saved_matches_total / saved_dataPoint_count] * len(x_values) - # print("% >>> Avg recurrence rate = {}".format(max_values[0])) - # plt.plot(x_values, max_values, 'r-') - # legend.append("maximum possible") - - x_ticks = list(range(1, plot_size+1, 2)) - x_labels = x_ticks[:] - plt.xticks(x_ticks, x_labels) - # plt.ylim(bottom=0) - if show_strat_title: - plt.legend(legend, loc="best") - if async_draw: - plt.draw() - else: - plt.show() - - -def plot_strategies_charsRecalled_noncummulative(plot_size=50, selected_strategies=["recent (bash-like)"], show_strat_title=False): - plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT)) - plt.title("Average characters recalled at distance (noncumulative) <{}>".format(datetime.now().strftime('%H:%M:%S'))) - plt.ylabel("Average characters recalled") - plt.xlabel("Distance") - x_values = range(1, plot_size+1) - legend = [] - saved_charsRecalled_total = None - saved_dataPoint_count = None - for strategy in data["Strategies"]: - strategy_title = strategy["Title"] - # strategy_description = strategy["Description"] - - dataPoint_count = 0 - matches = [0] * plot_size - matches_total = 0 - charsRecalled = [0] * plot_size - charsRecalled_total = 0 - - for match in strategy["Matches"]: - dataPoint_count += 1 - - if not match["Match"]: - continue - - chars = match["CharsRecalled"] - charsRecalled_total += chars - matches_total += 1 - - dist = match["Distance"] - if dist > plot_size: - continue - - matches[dist-1] += 1 - charsRecalled[dist-1] += chars - - # recent is very simple strategy so we will believe - # that there is no bug in it and we can use it to determine total - if strategy_title == "recent": - saved_charsRecalled_total = charsRecalled_total - saved_dataPoint_count = dataPoint_count - - if len(selected_strategies) and strategy_title not in selected_strategies: - continue - - # acc = 0 - # charsRecalled_cumulative = [] - # for x in charsRecalled: - # acc += x - # charsRecalled_cumulative.append(acc) - # charsRecalled_average = list(map(lambda x: x / dataPoint_count, charsRecalled_cumulative)) - charsRecalled_average = list(map(lambda x: x / dataPoint_count, charsRecalled)) - - plt.plot(x_values, charsRecalled_average, 'o-') - legend.append(strategy_title) - - assert(saved_charsRecalled_total is not None) - assert(saved_dataPoint_count is not None) - # max_values = [saved_charsRecalled_total / saved_dataPoint_count] * len(x_values) - # print("% >>> Max avg recalled characters = {}".format(max_values[0])) - # plt.plot(x_values, max_values, 'r-') - # legend.append("maximum possible") - - x_ticks = list(range(1, plot_size+1, 2)) - x_labels = x_ticks[:] - plt.xticks(x_ticks, x_labels) - # plt.ylim(bottom=0) - if show_strat_title: - plt.legend(legend, loc="best") - if async_draw: - plt.draw() - else: - plt.show() - - -def plot_strategies_charsRecalled_prefix_noncummulative(plot_size=50, selected_strategies=["recent (bash-like)"], show_strat_title=False): - plt.figure(figsize=(PLOT_WIDTH, PLOT_HEIGHT)) - plt.title("Average characters recalled at distance (including prefix matches) (noncummulative) <{}>".format(datetime.now().strftime('%H:%M:%S'))) - plt.ylabel("Average characters recalled (including prefix matches)") - plt.xlabel("Distance") - x_values = range(1, plot_size+1) - legend = [] - saved_charsRecalled_total = None - saved_dataPoint_count = None - for strategy in data["Strategies"]: - strategy_title = strategy["Title"] - # strategy_description = strategy["Description"] - - dataPoint_count = 0 - matches_total = 0 - charsRecalled = [0] * plot_size - charsRecalled_total = 0 - - for multiMatch in strategy["PrefixMatches"]: - dataPoint_count += 1 - - if not multiMatch["Match"]: - continue - matches_total += 1 - - last_charsRecalled = 0 - for match in multiMatch["Entries"]: - - chars = match["CharsRecalled"] - charsIncrease = chars - last_charsRecalled - assert(charsIncrease > 0) - charsRecalled_total += charsIncrease - - dist = match["Distance"] - if dist > plot_size: - continue - - charsRecalled[dist-1] += charsIncrease - last_charsRecalled = chars - - # recent is very simple strategy so we will believe - # that there is no bug in it and we can use it to determine total - if strategy_title == "recent": - saved_charsRecalled_total = charsRecalled_total - saved_dataPoint_count = dataPoint_count - - if len(selected_strategies) and strategy_title not in selected_strategies: - continue - - # acc = 0 - # charsRecalled_cumulative = [] - # for x in charsRecalled: - # acc += x - # charsRecalled_cumulative.append(acc) - # charsRecalled_average = list(map(lambda x: x / dataPoint_count, charsRecalled_cumulative)) - charsRecalled_average = list(map(lambda x: x / dataPoint_count, charsRecalled)) - - plt.plot(x_values, charsRecalled_average, 'o-') - legend.append(strategy_title) - - assert(saved_charsRecalled_total is not None) - assert(saved_dataPoint_count is not None) - # max_values = [saved_charsRecalled_total / saved_dataPoint_count] * len(x_values) - # print("% >>> Max avg recalled characters (including prefix matches) = {}".format(max_values[0])) - # plt.plot(x_values, max_values, 'r-') - # legend.append("maximum possible") - - x_ticks = list(range(1, plot_size+1, 2)) - x_labels = x_ticks[:] - plt.xticks(x_ticks, x_labels) - # plt.ylim(bottom=0) - if show_strat_title: - plt.legend(legend, loc="best") - if async_draw: - plt.draw() - else: - plt.show() - - -def print_top_cmds(num_cmds=20): - cmd_count = defaultdict(int) - cmd_total = 0 - for pid, session in DATA_records_by_session.items(): - for record in session: - cmd = record["command"] - if cmd == "": - continue - cmd_count[cmd] += 1 - cmd_total += 1 - - # get `node_count` of largest nodes - sorted_cmd_count = list(sorted(cmd_count.items(), key=lambda x: x[1], reverse=True)) - print("\n\n% All subjects: Top commands") - for cmd, count in sorted_cmd_count[:num_cmds]: - print("{} {}".format(cmd, count)) - # print(sorted_cmd_count) - # cmds_to_graph = list(map(lambda x: x[0], sorted_cmd_count))[:cmd_count] - - -def print_top_cmds_by_user(num_cmds=20): - for user in DATA_records_by_user.items(): - name, records = user - cmd_count = defaultdict(int) - cmd_total = 0 - for record in records: - cmd = record["command"] - if cmd == "": - continue - cmd_count[cmd] += 1 - cmd_total += 1 - - # get `node_count` of largest nodes - sorted_cmd_count = list(sorted(cmd_count.items(), key=lambda x: x[1], reverse=True)) - print("\n\n% {}: Top commands".format(name)) - for cmd, count in sorted_cmd_count[:num_cmds]: - print("{} {}".format(cmd, count)) - # print(sorted_cmd_count) - # cmds_to_graph = list(map(lambda x: x[0], sorted_cmd_count))[:cmd_count] - - -def print_avg_cmdline_length(): - cmd_len_total = 0 - cmd_total = 0 - for pid, session in DATA_records_by_session.items(): - for record in session: - cmd = record["cmdLine"] - if cmd == "": - continue - cmd_len_total += len(cmd) - cmd_total += 1 - - print("% ALL avg cmdline = {}".format(cmd_len_total / cmd_total)) - # print(sorted_cmd_count) - # cmds_to_graph = list(map(lambda x: x[0], sorted_cmd_count))[:cmd_count] - - -# plot_cmdLineFrq_rank() -# plot_cmdFrq_rank() -print_top_cmds(30) -print_top_cmds_by_user(30) -# print_avg_cmdline_length() -# -# plot_cmdLineVocabularySize_cmdLinesEntered() -plot_cmdVocabularySize_cmdLinesEntered() -plot_cmdVocabularySize_time() -# plot_cmdVocabularySize_daily() -plot_cmdUsage_in_time(num_cmds=100) -plot_cmdUsage_in_time(sort_cmds=True, num_cmds=100) -# -recent_strats=("recent", "recent (bash-like)") -recurrence_strat=("recent (bash-like)",) -# plot_strategies_matches(20) -# plot_strategies_charsRecalled(20) -# plot_strategies_charsRecalled_prefix(20) -# plot_strategies_charsRecalled_noncummulative(20, selected_strategies=recent_strats) -# plot_strategies_matches_noncummulative(20) -# plot_strategies_charsRecalled_noncummulative(20) -# plot_strategies_charsRecalled_prefix_noncummulative(20) -# plot_strategies_matches(20, selected_strategies=recurrence_strat, show_strat_title=True, force_strat_title="recurrence rate") -# plot_strategies_matches_noncummulative(20, selected_strategies=recurrence_strat, show_strat_title=True, force_strat_title="recurrence rate") - -# graph_cmdSequences(node_count=33, edge_minValue=0.048) - -# graph_cmdSequences(node_count=28, edge_minValue=0.06) - -# new improved -# for n in range(40, 43): -# for e in range(94, 106, 2): -# e *= 0.001 -# graph_cmdSequences(node_count=n, edge_minValue=e, view_graph=False) - -#for n in range(29, 35): -# for e in range(44, 56, 2): -# e *= 0.001 -# graph_cmdSequences(node_count=n, edge_minValue=e, view_graph=False) - -# be careful and check if labels fit the display - -if async_draw: - plt.show() diff --git a/scripts/reshctl.sh b/scripts/reshctl.sh deleted file mode 100644 index db021470..00000000 --- a/scripts/reshctl.sh +++ /dev/null @@ -1,155 +0,0 @@ - -# shellcheck source=../submodules/bash-zsh-compat-widgets/bindfunc.sh -. ~/.resh/bindfunc.sh -# shellcheck source=widgets.sh -. ~/.resh/widgets.sh - -__resh_nop() { - # does nothing - true -} - -__resh_bind_control_R() { - bindfunc --revert '\C-r' __resh_widget_control_R_compat - if [ "${__RESH_control_R_bind_enabled-0}" != 0 ]; then - # Re-binding is a valid usecase but it shouldn't happen much - # so this is a warning - echo "Re-binding RESH SEARCH app to Ctrl+R ..." - else - # Only save original binding if resh binding was not enabled - __RESH_bindfunc_revert_control_R_bind=$_bindfunc_revert - fi - __RESH_control_R_bind_enabled=1 - if [ -n "${BASH_VERSION-}" ]; then - # fuck bash - bind '"\C-r": "\u[31~\u[32~"' - bind -x '"\u[31~": __resh_widget_control_R_compat' - - # execute - # bind '"\u[32~": accept-line' - - # just paste - # bind -x '"\u[32~": __resh_nop' - true - fi - return 0 -} - -__resh_unbind_control_R() { - if [ "${__RESH_control_R_bind_enabled-0}" != 1 ]; then - echo "RESH SEARCH app Ctrl+R binding is already disabled!" - return 1 - fi - if [ -z "${__RESH_bindfunc_revert_control_R_bind+x}" ]; then - echo "Warn: Couldn't revert Ctrl+R binding because 'revert command' is empty." - else - eval "$__RESH_bindfunc_revert_control_R_bind" - fi - __RESH_control_R_bind_enabled=0 - return 0 -} - -__resh_bind_all() { - __resh_bind_control_R -} - -__resh_unbind_all() { - __resh_unbind_control_R -} - -# wrapper for resh-cli for calling resh directly -resh() { - local buffer - local git_remote; git_remote="$(git remote get-url origin 2>/dev/null)" - buffer=$(resh-cli --sessionID "$__RESH_SESSION_ID" --host "$__RESH_HOST" --pwd "$PWD" --gitOriginRemote "$git_remote" "$@") - status_code=$? - if [ $status_code = 111 ]; then - # execute - echo "$buffer" - eval "$buffer" - elif [ $status_code = 0 ]; then - # paste - echo "$buffer" - elif [ $status_code = 130 ]; then - true - else - local fpath_last_run="$__RESH_XDG_CACHE_HOME/cli_last_run_out.txt" - echo "$buffer" >| "$fpath_last_run" - echo "resh-cli failed - check '$fpath_last_run' and '~/.resh/cli.log'" - fi -} - -reshctl() { - # export current shell because resh-control needs to know - export __RESH_ctl_shell=$__RESH_SHELL - # run resh-control aka the real reshctl - resh-control "$@" - - # modify current shell session based on exit status - local _status=$? - # echo $_status - # unexport current shell - unset __RESH_ctl_shell - case "$_status" in - 0|1) - # success | fail - return "$_status" - ;; - # enable - # 30) - # # enable all - # __resh_bind_all - # return 0 - # ;; - 32) - # enable control R - __resh_bind_control_R - return 0 - ;; - # disable - # 40) - # # disable all - # __resh_unbind_all - # return 0 - # ;; - 42) - # disable control R - __resh_unbind_control_R - return 0 - ;; - 50) - # reload rc files - . ~/.resh/shellrc - return 0 - ;; - 51) - # inspect session history - # reshctl debug inspect N - resh-inspect --sessionID "$__RESH_SESSION_ID" --count "${3-10}" - return 0 - ;; - 52) - # show status - echo - echo 'Control R binding ...' - if [ "$(resh-config --key BindControlR)" = true ]; then - echo ' * future sessions: ENABLED' - else - echo ' * future sessions: DISABLED' - fi - if [ "${__RESH_control_R_bind_enabled-0}" != 0 ]; then - echo ' * this session: ENABLED' - else - echo ' * this session: DISABLED' - fi - return 0 - ;; - *) - echo "reshctl() FATAL ERROR: unknown status ($_status)" >&2 - echo "Possibly caused by version mismatch between installed resh and resh in this session." >&2 - echo "Please REPORT this issue here: https://github.com/curusarn/resh/issues" >&2 - echo "Please RESTART your terminal window." >&2 - return "$_status" - ;; - esac -} diff --git a/scripts/shellrc.sh b/scripts/shellrc.sh index c21f3358..b6a02984 100644 --- a/scripts/shellrc.sh +++ b/scripts/shellrc.sh @@ -1,96 +1,42 @@ +#!/hint/sh PATH=$PATH:~/.resh/bin -# if [ -n "$ZSH_VERSION" ]; then -# 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_MACOS=0 -__RESH_LINUX=0 -__RESH_UNAME=$(uname) - -if [ "$__RESH_UNAME" = "Darwin" ]; then - __RESH_MACOS=1 -elif [ "$__RESH_UNAME" = "Linux" ]; then - __RESH_LINUX=1 -else - echo "resh PANIC unrecognized OS" -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 - -# posix -__RESH_HOME="$HOME" -__RESH_LOGIN="$LOGNAME" -__RESH_SHELL_ENV="$SHELL" -__RESH_TERM="$TERM" - -# non-posix -__RESH_RT_SESSION=$(__resh_get_epochrealtime) -__RESH_OSTYPE="$OSTYPE" -__RESH_MACHTYPE="$MACHTYPE" - -if [ $__RESH_LINUX -eq 1 ]; then - __RESH_OS_RELEASE_ID=$(. /etc/os-release; echo "$ID") - __RESH_OS_RELEASE_VERSION_ID=$(. /etc/os-release; echo "$VERSION_ID") - __RESH_OS_RELEASE_ID_LIKE=$(. /etc/os-release; echo "$ID_LIKE") - __RESH_OS_RELEASE_NAME=$(. /etc/os-release; echo "$NAME") - __RESH_OS_RELEASE_PRETTY_NAME=$(. /etc/os-release; echo "$PRETTY_NAME") - __RESH_RT_SESS_SINCE_BOOT=$(cut -d' ' -f1 /proc/uptime) -elif [ $__RESH_MACOS -eq 1 ]; then - __RESH_OS_RELEASE_ID="macos" - __RESH_OS_RELEASE_VERSION_ID=$(sw_vers -productVersion 2>/dev/null) - __RESH_OS_RELEASE_NAME="macOS" - __RESH_OS_RELEASE_PRETTY_NAME="Mac OS X" - __RESH_RT_SESS_SINCE_BOOT=$(sysctl -n kern.boottime | awk '{print $4}' | sed 's/,//g') + echo "RESH PANIC: unrecognized shell - please report this to https://github.com/curusarn/resh/issues" fi # shellcheck disable=2155 export __RESH_VERSION=$(resh-collect -version) -# shellcheck disable=2155 -export __RESH_REVISION=$(resh-collect -revision) -__resh_set_xdg_home_paths - -__resh_run_daemon +resh-daemon-start -q [ "$(resh-config --key BindControlR)" = true ] && __resh_bind_control_R # block for anything we only want to do once per session # NOTE: nested shells are still the same session +# i.e. $__RESH_SESSION_ID will be set in nested shells 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 + # shellcheck disable=2155 + export __RESH_SESSION_ID=$(resh-generate-uuid) + __resh_session_init fi # block for anything we only want to do once per shell +# NOTE: nested shells are new shells +# i.e. $__RESH_INIT_DONE will NOT be set in nested shells if [ -z "${__RESH_INIT_DONE+x}" ]; then preexec_functions+=(__resh_preexec) precmd_functions+=(__resh_precmd) - __resh_reset_variables - __RESH_INIT_DONE=1 fi \ No newline at end of file diff --git a/scripts/test.sh b/scripts/test.sh index 6c23446b..942e9ae1 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,20 +1,17 @@ #!/usr/bin/env bash # very simple tests to catch simple errors in scripts -# shellcheck disable=SC2016 -[ "${BASH_SOURCE[0]}" != "scripts/test.sh" ] && echo 'Run this script using `make test`' && exit 1 - for f in scripts/*.sh; do echo "Running shellcheck on $f ..." - shellcheck "$f" --shell=bash --severity=error || exit 1 + shellcheck "$f" --shell=sh --severity=error || exit 1 done -for f in scripts/{shellrc,util,reshctl,hooks}.sh; do +for f in scripts/{shellrc,hooks}.sh; do echo "Checking Zsh syntax of $f ..." ! zsh -n "$f" && echo "Zsh syntax check failed!" && exit 1 done -if [ "$1" == "--all" ]; then +if [ "$1" = "--all" ]; then for sh in bash zsh; do echo "Running functions in scripts/shellrc.sh using $sh ..." ! $sh -c ". scripts/shellrc.sh; __resh_preexec; __resh_precmd" && echo "Error while running functions!" && exit 1 diff --git a/scripts/util.sh b/scripts/util.sh deleted file mode 100644 index 4746eb03..00000000 --- a/scripts/util.sh +++ /dev/null @@ -1,197 +0,0 @@ -# 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 - if [ "${BASH_VERSINFO[0]}" -ge "4" ]; then - # $BASHPID is only available in bash4+ - # $$ is fairly similar so it should not be an issue - local __RESH_PID="$BASHPID" # current pid - else - local __RESH_PID="$$" # current pid - fi - 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 - local fpath_last_run="$__RESH_XDG_CACHE_HOME/daemon_last_run_out.txt" - if [ "$(uname)" = Darwin ]; then - # hotfix - gnohup resh-daemon >| "$fpath_last_run" 2>&1 & disown - else - # TODO: switch to nohup for consistency once you confirm that daemon is - # not getting killed anymore on macOS - # nohup resh-daemon >| "$fpath_last_run" 2>&1 & disown - setsid resh-daemon >| "$fpath_last_run" 2>&1 & disown - fi -} - -__resh_bash_completion_init() { - # primitive check to find out if bash_completions are installed - # skip completion init if they are not - _get_comp_words_by_ref >/dev/null 2>/dev/null - [[ $? == 127 ]] && return - local bash_completion_dir=~/.resh/bash_completion.d - 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 - [[ -f "$i" && -r "$i" ]] && . "$i" - done - fi -} - -__resh_zsh_completion_init() { - # NOTE: this is hacky - each completion needs to be added individually - # TODO: fix later - # fpath=(~/.resh/zsh_completion.d $fpath) - # we should be using fpath but that doesn't work well with oh-my-zsh - # so we are just adding it manually - # shellcheck disable=1090 - if typeset -f compdef >/dev/null 2>&1; then - source ~/.resh/zsh_completion.d/_reshctl && compdef _reshctl reshctl - else - # fallback I guess - fpath=(~/.resh/zsh_completion.d $fpath) - __RESH_zsh_no_compdef=1 - fi - - # TODO: test and use this - # NOTE: this is not how globbing works - # for f in ~/.resh/zsh_completion.d/_*; do - # source ~/.resh/zsh_completion.d/_$f && compdef _$f $f - # done -} - -__resh_session_init() { - # 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 - local fpath_last_run="$__RESH_XDG_CACHE_HOME/session_init_last_run_out.txt" - 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" \ - >| "$fpath_last_run" 2>&1 || echo "resh-session-init ERROR: $(head -n 1 $fpath_last_run)" - fi -} - -__resh_set_xdg_home_paths() { - if [ -z "${XDG_CONFIG_HOME-}" ]; then - __RESH_XDG_CONFIG_FILE="$HOME/.config" - else - __RESH_XDG_CONFIG_FILE="$XDG_CONFIG_HOME" - fi - mkdir -p "$__RESH_XDG_CONFIG_FILE" >/dev/null 2>/dev/null - __RESH_XDG_CONFIG_FILE="$__RESH_XDG_CONFIG_FILE/resh.toml" - - - if [ -z "${XDG_CACHE_HOME-}" ]; then - __RESH_XDG_CACHE_HOME="$HOME/.cache/resh" - else - __RESH_XDG_CACHE_HOME="$XDG_CACHE_HOME/resh" - fi - mkdir -p "$__RESH_XDG_CACHE_HOME" >/dev/null 2>/dev/null - export __RESH_XDG_CACHE_HOME - - - if [ -z "${XDG_DATA_HOME-}" ]; then - __RESH_XDG_DATA_HOME="$HOME/.local/share/resh" - else - __RESH_XDG_DATA_HOME="$XDG_DATA_HOME/resh" - fi - mkdir -p "$__RESH_XDG_DATA_HOME" >/dev/null 2>/dev/null -} diff --git a/scripts/uuid.sh b/scripts/uuid.sh deleted file mode 100755 index 8b4527b9..00000000 --- a/scripts/uuid.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# https://gist.github.com/markusfisch/6110640 - -# Generate a pseudo UUID -uuid() -{ - local N B C='89ab' - - for (( N=0; N < 16; ++N )) - do - B=$(( $RANDOM%256 )) - - case $N in - 6) - printf '4%x' $(( B%16 )) - ;; - 8) - printf '%c%x' ${C:$RANDOM%${#C}:1} $(( B%16 )) - ;; - 3 | 5 | 7 | 9) - printf '%02x-' $B - ;; - *) - printf '%02x' $B - ;; - esac - done - - echo -} - -if [ "$BASH_SOURCE" == "$0" ] -then - uuid -fi diff --git a/scripts/widgets.sh b/scripts/widgets.sh deleted file mode 100644 index defc6059..00000000 --- a/scripts/widgets.sh +++ /dev/null @@ -1,51 +0,0 @@ - -# shellcheck source=hooks.sh -. ~/.resh/hooks.sh - -__resh_widget_control_R() { - # 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" - - # local __RESH_PREFIX=${BUFFER:0:CURSOR} - # __RESH_HIST_RECALL_ACTIONS="$__RESH_HIST_RECALL_ACTIONS;control_R:$__RESH_PREFIX" - local PREVBUFFER=$BUFFER - __RESH_HIST_RECALL_ACTIONS="$__RESH_HIST_RECALL_ACTIONS|||control_R:$BUFFER" - - local status_code - local git_remote; git_remote="$(git remote get-url origin 2>/dev/null)" - BUFFER=$(resh-cli --sessionID "$__RESH_SESSION_ID" --host "$__RESH_HOST" --pwd "$PWD" --gitOriginRemote "$git_remote" --query "$BUFFER") - status_code=$? - local fpath_last_run="$__RESH_XDG_CACHE_HOME/cli_last_run_out.txt" - touch "$fpath_last_run" - if [ $status_code = 111 ]; then - # execute - if [ -n "${ZSH_VERSION-}" ]; then - # zsh - zle accept-line - elif [ -n "${BASH_VERSION-}" ]; then - # bash - # set chained keyseq to accept-line - bind '"\u[32~": accept-line' - fi - elif [ $status_code = 0 ]; then - if [ -n "${BASH_VERSION-}" ]; then - # bash - # set chained keyseq to nothing - bind -x '"\u[32~": __resh_nop' - fi - else - echo "$BUFFER" >| "$fpath_last_run" - echo "# RESH SEARCH APP failed - sorry for the inconvinience - check '$fpath_last_run' and '~/.resh/cli.log'" - BUFFER="$PREVBUFFER" - fi - CURSOR=${#BUFFER} - # recorded to history - __RESH_HIST_PREV_LINE=${BUFFER} -} - -__resh_widget_control_R_compat() { - __bindfunc_compat_wrapper __resh_widget_control_R -} diff --git a/test/Dockefile_macoslike b/test/Dockefile_macoslike deleted file mode 100644 index 6e8c524f..00000000 --- a/test/Dockefile_macoslike +++ /dev/null @@ -1,12 +0,0 @@ -FROM debian:jessie - -RUN apt-get update && apt-get install -y wget tar gcc automake make bison flex curl vim makeinfo -RUN wget https://ftp.gnu.org/gnu/bash/bash-3.2.57.tar.gz && \ - tar -xvzf bash-3.2.57.tar.gz && \ - cd bash-3.2.57 && \ - ./configure && \ - make && \ - make install -RUN echo /usr/local/bin/bash >> /etc/shells && chsh -s /usr/local/bin/bash - -CMD bash diff --git a/troubleshooting.md b/troubleshooting.md new file mode 100644 index 00000000..60a45de0 --- /dev/null +++ b/troubleshooting.md @@ -0,0 +1,72 @@ +# Troubleshooting + +## First help + +Run RESH doctor to detect common issues: +```sh +reshctl doctor +``` + +## Restarting RESH daemon + +Sometimes restarting RESH daemon can help: +```sh +resh-daemon-restart +``` + +You can also start and stop RESH daemon with: +```sh +resh-daemon-start +resh-daemon-stop +``` + +:warning: You will get error messages in your shell when RESH daemon is not running. + +## Recorded history + +Your RESH history is saved in one of: +- `~/.local/share/resh/history.reshjson` +- `$XDG_DATA_HOME/resh/history.reshjson` + +The format is JSON prefixed by version. Display it as json using: + +```sh +cat ~/.local/share/resh/history.reshjson | sed 's/^v[^{]*{/{/' | jq . +``` + +ℹ️ You will need `jq` installed. + +## Configuration + +RESH config is read from one of: +- `~/.config/resh.toml` +- `$XDG_CONFIG_HOME/resh.toml` + +## Logs + +Logs can be useful for troubleshooting issues. + +Find RESH logs in one of: +- `~/.local/share//resh/log.json` +- `$XDG_DATA_HOME/resh/log.json` + +### Log verbosity + +Get more detailed logs by setting `LogLevel = "debug"` in [RESH config](#configuration). +Restart RESH daemon for the config change to take effect: `resh-daemon-restart` + +## Common problems + +### Using RESH with bash on macOS + +ℹ️ It is recommended to use zsh on macOS. + +MacOS comes with really old bash (`bash 3.2`). +Update it using: `brew install bash` + +On macOS, bash shell does not load `~/.bashrc` because every shell runs as login shell. +Fix it by running: `echo '[ -f ~/.bashrc ] && . ~/.bashrc' >> ~/.bash_profile` + +## GitHub issues + +Problem persists? [Create an issue ⇗](https://github.com/curusarn/resh/issues)