Merge pull request #184 from curusarn/reduction

v3 prerelease
pull/195/head
Šimon Let 3 years ago committed by GitHub
commit ebfc4565d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      .github/workflows/go.yaml
  2. 16
      .github/workflows/release.yaml
  3. 8
      .github/workflows/sh.yaml
  4. 39
      .goreleaser.yml
  5. 29
      Makefile
  6. 129
      README.md
  7. 316
      cmd/cli/main.go
  8. 262
      cmd/collect/main.go
  9. 30
      cmd/config/main.go
  10. 48
      cmd/control/cmd/completion.go
  11. 66
      cmd/control/cmd/debug.go
  12. 147
      cmd/control/cmd/doctor.go
  13. 80
      cmd/control/cmd/enable.go
  14. 70
      cmd/control/cmd/root.go
  15. 54
      cmd/control/cmd/sanitize.go
  16. 72
      cmd/control/cmd/status.go
  17. 16
      cmd/control/cmd/update.go
  18. 45
      cmd/control/cmd/version.go
  19. 8
      cmd/control/main.go
  20. 25
      cmd/control/status/status.go
  21. 41
      cmd/daemon/dump.go
  22. 28
      cmd/daemon/kill.go
  23. 184
      cmd/daemon/main.go
  24. 109
      cmd/daemon/recall.go
  25. 58
      cmd/daemon/record.go
  26. 89
      cmd/daemon/run-server.go
  27. 36
      cmd/daemon/session-init.go
  28. 36
      cmd/daemon/status.go
  29. 152
      cmd/evaluate/main.go
  30. 7
      cmd/event/main.go
  31. 26
      cmd/generate-uuid/main.go
  32. 14
      cmd/get-epochtime/main.go
  33. 87
      cmd/inspect/main.go
  34. 28
      cmd/install-utils/device.go
  35. 67
      cmd/install-utils/main.go
  36. 195
      cmd/install-utils/migrate.go
  37. 166
      cmd/postcollect/main.go
  38. 523
      cmd/sanitize/main.go
  39. 188
      cmd/session-init/main.go
  40. 5
      conf/config.toml
  41. 7
      data/sanitizer/copyright_information.md
  42. 1195
      data/sanitizer/whitelist.txt
  43. 39
      go.mod
  44. 75
      go.sum
  45. 77
      installation.md
  46. 191
      internal/cfg/cfg.go
  47. 100
      internal/cfg/migrate.go
  48. 92
      internal/check/check.go
  49. 116
      internal/collect/collect.go
  50. 36
      internal/datadir/datadir.go
  51. 145
      internal/device/device.go
  52. 14
      internal/epochtime/epochtime.go
  53. 18
      internal/epochtime/epochtime_test.go
  54. 113
      internal/futil/futil.go
  55. 17
      internal/histcli/histcli.go
  56. 283
      internal/histfile/histfile.go
  57. 56
      internal/histio/file.go
  58. 43
      internal/histio/histio.go
  59. 28
      internal/histlist/histlist.go
  60. 0
      internal/httpclient/httpclient.go
  61. 27
      internal/logger/logger.go
  62. 21
      internal/msg/msg.go
  63. 31
      internal/normalize/normailze.go
  64. 51
      internal/normalize/normalize_test.go
  65. 36
      internal/opt/opt.go
  66. 143
      internal/output/output.go
  67. 37
      internal/recconv/recconv.go
  68. 144
      internal/recio/read.go
  69. 13
      internal/recio/recio.go
  70. 64
      internal/recio/write.go
  71. 34
      internal/recordint/collect.go
  72. 2
      internal/recordint/recordint.go
  73. 56
      internal/recordint/searchapp.go
  74. 84
      internal/records/records.go
  75. 51
      internal/recutil/recutil.go
  76. 0
      internal/searchapp/highlight.go
  77. 83
      internal/searchapp/item.go
  78. 42
      internal/searchapp/item_test.go
  79. 24
      internal/searchapp/query.go
  80. 0
      internal/searchapp/time.go
  81. 0
      internal/sess/sess.go
  82. 96
      internal/sesswatch/sesswatch.go
  83. 74
      internal/signalhandler/signalhander.go
  84. 49
      internal/status/status.go
  85. 12
      pkg/cfg/cfg.go
  86. 120
      pkg/collect/collect.go
  87. 246
      pkg/histanal/histeval.go
  88. 180
      pkg/histanal/histload.go
  89. 262
      pkg/histfile/histfile.go
  90. 32
      pkg/msg/msg.go
  91. 689
      pkg/records/records.go
  92. 152
      pkg/records/records_test.go
  93. 27
      pkg/records/testdata/resh_history.json
  94. 23
      pkg/searchapp/test.go
  95. 243
      pkg/sesshist/sesshist.go
  96. 78
      pkg/sesswatch/sesswatch.go
  97. 65
      pkg/signalhandler/signalhander.go
  98. 47
      pkg/strat/directory-sensitive.go
  99. 29
      pkg/strat/dummy.go
  100. 91
      pkg/strat/dynamic-record-distance.go
  101. Some files were not shown because too many files have changed in this diff Show More

@ -7,13 +7,18 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Set up Go - name: Checkout
uses: actions/setup-go@v2 uses: actions/checkout@v3
with: 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 - name: Set up Go
uses: actions/checkout@v2 uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Test - name: Test
run: go test -v ./... run: go test -v ./...

@ -8,19 +8,25 @@ on:
jobs: jobs:
goreleaser: goreleaser:
name: Goreleaser
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Checkout submodules - name: Get Go version
run: git submodule update --init --recursive run: echo "GO_VERSION=$(grep '^go ' go.mod | cut -d ' ' -f 2)" >> $GITHUB_ENV && cat $GITHUB_ENV
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v3
with: with:
go-version: 1.16 go-version: ${{ env.GO_VERSION }}
- name: Checkout submodules
run: git submodule update --init --recursive
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2 uses: goreleaser/goreleaser-action@v2

@ -7,11 +7,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up - name: Set up
run: sudo apt-get install -y shellcheck zsh run: sudo apt-get install -y shellcheck zsh
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Test - name: Test
run: scripts/test.sh run: scripts/test.sh

@ -76,54 +76,45 @@ builds:
- arm - arm
- arm64 - arm64
- -
id: "evaluate" id: "postcollect"
main: ./cmd/evaluate main: ./cmd/postcollect
binary: bin/resh-evaluate binary: bin/resh-postcollect
goarch:
- 386
- amd64
- arm
- arm64
-
id: "event"
main: ./cmd/event
binary: bin/resh-event
goarch: goarch:
- 386 - 386
- amd64 - amd64
- arm - arm
- arm64 - arm64
- -
id: "inspect" id: "session-init"
main: ./cmd/inspect main: ./cmd/session-init
binary: bin/resh-inspect binary: bin/resh-session-init
goarch: goarch:
- 386 - 386
- amd64 - amd64
- arm - arm
- arm64 - arm64
- -
id: "postcollect" id: "install-utils"
main: ./cmd/postcollect main: ./cmd/install-utils
binary: bin/resh-postcollect binary: bin/resh-install-utils
goarch: goarch:
- 386 - 386
- amd64 - amd64
- arm - arm
- arm64 - arm64
- -
id: "sanitize" id: "generate-uuid"
main: ./cmd/sanitize main: ./cmd/generate-uuid
binary: bin/resh-sanitize binary: bin/resh-generate-uuid
goarch: goarch:
- 386 - 386
- amd64 - amd64
- arm - arm
- arm64 - arm64
- -
id: "session-init" id: "get-epochtime"
main: ./cmd/session-init main: ./cmd/get-epochtime
binary: bin/resh-session-init binary: bin/resh-get-epochtime
goarch: goarch:
- 386 - 386
- amd64 - amd64

@ -1,16 +1,25 @@
SHELL=/bin/bash SHELL=/bin/bash
LATEST_TAG=$(shell git describe --tags) 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}-$(shell date +%s)"
VERSION="${LATEST_TAG}-DEV" 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=${REVISION}" 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\ # We disable jobserver for the actual installation because we want it to run serially
bin/resh-evaluate bin/resh-sanitize bin/resh-control bin/resh-config bin/resh-inspect bin/resh-cli # Make waits to the daemon process we launch during install and hangs
install: build install: build
scripts/install.sh 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: test:
go test -v ./... go test -v ./...
go vet ./... go vet ./...
@ -21,19 +30,19 @@ rebuild:
make build make build
clean: clean:
rm -f bin/resh-* rm -f -- bin/*
uninstall: uninstall:
# Uninstalling ... # 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 grep $@ .goreleaser.yml -q # all build targets need to be included in .goreleaser.yml
go build ${GOFLAGS} -o $@ cmd/$*/*.go go build ${GOFLAGS} -o $@ cmd/$*/*.go
.PHONY: submodules build install rebuild uninstall clean test .PHONY: submodules build install rebuild uninstall clean test
submodules: | submodules/bash-preexec/bash-preexec.sh submodules/bash-zsh-compat-widgets/bindfunc.sh submodules: | submodules/bash-preexec/bash-preexec.sh submodules/bash-zsh-compat-widgets/bindfunc.sh
@# sets submodule.recurse to true if unset @# sets submodule.recurse to true if unset
@# sets status.submoduleSummary to true if unset @# sets status.submoduleSummary to true if unset

@ -1,11 +1,16 @@
![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/curusarn/resh?sort=semver) [![Latest version](https://img.shields.io/github/v/tag/curusarn/resh?sort=semver)](https://github.com/curusarn/resh/releases)
![Go test](https://github.com/curusarn/resh/actions/workflows/go.yaml/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/curusarn/resh)](https://goreportcard.com/report/github.com/curusarn/resh)
![Shell test](https://github.com/curusarn/resh/actions/workflows/sh.yaml/badge.svg) [![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
<!-- Contextual shell history --> <!-- Contextual shell history -->
<!-- Contextual bash history --> <!-- Contextual bash history -->
<!-- Contextual zsh history --> <!-- Contextual zsh history -->
@ -17,114 +22,36 @@ Context-based replacement/enhancement for zsh and bash shell history
<!-- Better zsh history --> <!-- Better zsh history -->
<!-- PWD Directory --> <!-- PWD Directory -->
**Search your history by commands and get relevant results based on current directory, git repo, exit status, and host.** ## Install
## Installation
### Prerequisites
Standard stuff: `bash(4.3+)`, `curl`, `tar`, ...
Bash completions will only work if you have `bash-completion` installed
MacOS: `coreutils` (`brew install coreutils`) Install RESH with one command:
### Simplest installation
Run this command.
```sh ```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 You will need to have `curl` and `tar` installed.
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.
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 <kbd>Ctrl</kbd> + <kbd>R</kbd> to search:
Eventually most of your history will have context and RESH SEARCH app will get more useful. <img width="906" alt="RESH search app screenshot" src="https://user-images.githubusercontent.com/10132717/221371937-d4ba64e0-ede6-4bfa-8b74-529252bf73a3.png">
![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
```
### In-app key bindings ### In-app key bindings
- Type to search/filter - Type to search
- Up/Down or Ctrl+P/Ctrl+N to select results - <kbd>Up</kbd> / <kbd>Down</kbd> or <kbd>Ctrl</kbd> + <kbd>P</kbd> / <kbd>Ctrl</kbd> + <kbd>N</kbd> to select results
- Right to paste selected command onto the command line so you can edit it before execution - <kbd>Enter</kbd> to execute selected command
- Enter to execute - <kbd>Right</kbd> to paste selected command onto the command line so you can edit it before execution
- Ctrl+C/Ctrl+D to quit - <kbd>Ctrl</kbd> + <kbd>C</kbd> or <kbd>Ctrl</kbd> + <kbd>D</kbd> to quit
- Ctrl+G to abort and paste the current query onto the command line - <kbd>Ctrl</kbd> + <kbd>G</kbd> to abort and paste the current query onto the command line
- Ctrl+R to switch between RAW and NORMAL mode - <kbd>Ctrl</kbd> + <kbd>R</kbd> to search without context (toggle)
### 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
## 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)

@ -4,10 +4,8 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"flag"
"fmt" "fmt"
"io/ioutil" "io"
"log"
"net/http" "net/http"
"os" "os"
"sort" "sort"
@ -15,84 +13,81 @@ import (
"sync" "sync"
"time" "time"
"github.com/BurntSushi/toml"
"github.com/awesome-gocui/gocui" "github.com/awesome-gocui/gocui"
"github.com/curusarn/resh/pkg/cfg" "github.com/curusarn/resh/internal/cfg"
"github.com/curusarn/resh/pkg/msg" "github.com/curusarn/resh/internal/datadir"
"github.com/curusarn/resh/pkg/records" "github.com/curusarn/resh/internal/device"
"github.com/curusarn/resh/pkg/searchapp" "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" "strconv"
) )
// version from git set during build // info passed during build
var version string var version string
// commit from git set during build
var commit string var commit string
var development string
// special constant recognized by RESH wrappers // special constant recognized by RESH wrappers
const exitCodeExecute = 111 const exitCodeExecute = 111
var debug bool
func main() { 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) fmt.Print(output)
os.Exit(exitCode) os.Exit(exitCode)
} }
func runReshCli() (string, int) { func runReshCli(out *output.Output, config cfg.Config) (string, int) {
usr, _ := user.Current() args := opt.HandleVersionOpts(out, os.Args, version, commit)
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) const missing = "<missing cdxgtcpboqwrdom>"
if err != nil { flags := pflag.NewFlagSet("", pflag.ExitOnError)
log.Fatal("Error opening file:", err) sessionID := flags.String("session-id", missing, "Resh generated session ID")
} pwd := flags.String("pwd", missing, "$PWD - present working directory")
defer f.Close() gitOriginRemote := flags.String("git-remote", missing, "> git remote get-url origin")
query := flags.String("query", "", "Search query")
log.SetOutput(f) flags.Parse(args)
var config cfg.Config // TODO: These errors should tell the user that they should not be running the command directly
if _, err := toml.DecodeFile(configPath, &config); err != nil { errMsg := "Failed to get required command-line arguments"
log.Fatal("Error reading config:", err) if *sessionID == missing {
out.FatalE(errMsg, errors.New("missing required option --session-id"))
} }
if config.Debug { if *pwd == missing {
debug = true out.FatalE(errMsg, errors.New("missing required option --pwd"))
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
log.Println("DEBUG is ON")
}
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 == "" { if *gitOriginRemote == missing {
log.Println("Error: you need to specify HOST") out.FatalE(errMsg, errors.New("missing required option --git-origin-remote"))
} }
if *pwd == "" { dataDir, err := datadir.GetPath()
log.Println("Error: you need to specify PWD") if err != nil {
out.FatalE("Could not get user data directory", err)
} }
if *gitOriginRemote == "DEFAULT" { deviceName, err := device.GetName(dataDir)
log.Println("Error: you need to specify gitOriginRemote") if err != nil {
out.FatalE("Could not get device name", err)
} }
g, err := gocui.NewGui(gocui.OutputNormal, false) g, err := gocui.NewGui(gocui.OutputNormal, false)
if err != nil { if err != nil {
log.Panicln(err) out.FatalE("Failed to launch TUI", err)
} }
defer g.Close() defer g.Close()
@ -102,80 +97,79 @@ func runReshCli() (string, int) {
g.Highlight = true g.Highlight = true
var resp msg.CliResponse var resp msg.CliResponse
if *testHistory == "" {
mess := msg.CliMsg{ mess := msg.CliMsg{
SessionID: *sessionID, SessionID: *sessionID,
PWD: *pwd, PWD: *pwd,
} }
resp = SendCliMsg(mess, strconv.Itoa(config.Port)) resp = SendCliMsg(out, mess, strconv.Itoa(config.Port))
} else {
resp = searchapp.LoadHistoryFromFile(*testHistory, *testHistoryLines)
}
st := state{ st := state{
// lock sync.Mutex // lock sync.Mutex
cliRecords: resp.CliRecords, cliRecords: resp.Records,
initialQuery: *query, initialQuery: *query,
} }
// TODO: Use device ID
layout := manager{ layout := manager{
out: out,
config: config,
sessionID: *sessionID, sessionID: *sessionID,
host: *host, host: deviceName,
pwd: *pwd, pwd: *pwd,
gitOriginRemote: records.NormalizeGitRemote(*gitOriginRemote), gitOriginRemote: *gitOriginRemote,
config: config,
s: &st, s: &st,
} }
g.SetManager(layout) g.SetManager(layout)
errMsg = "Failed to set keybindings"
if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, layout.Next); err != nil { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { if err := g.SetKeybinding("", gocui.KeyCtrlR, gocui.ModNone, layout.SwitchModes); err != nil {
log.Panicln(err) out.FatalE(errMsg, err)
} }
layout.UpdateData(*query) layout.UpdateData(*query)
layout.UpdateRawData(*query) layout.UpdateRawData(*query)
err = g.MainLoop() err = g.MainLoop()
if err != nil && !errors.Is(err, gocui.ErrQuit) { 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 return layout.s.output, layout.s.exitCode
} }
type state struct { type state struct {
lock sync.Mutex lock sync.Mutex
cliRecords []records.CliRecord cliRecords []recordint.SearchApp
data []searchapp.Item data []searchapp.Item
rawData []searchapp.RawItem rawData []searchapp.RawItem
highlightedItem int highlightedItem int
@ -190,11 +184,13 @@ type state struct {
} }
type manager struct { type manager struct {
out *output.Output
config cfg.Config
sessionID string sessionID string
host string host string
pwd string pwd string
gitOriginRemote string gitOriginRemote string
config cfg.Config
s *state s *state
} }
@ -204,13 +200,13 @@ func (m manager) SelectExecute(g *gocui.Gui, v *gocui.View) error {
defer m.s.lock.Unlock() defer m.s.lock.Unlock()
if m.s.rawMode { if m.s.rawMode {
if m.s.highlightedItem < len(m.s.rawData) { 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 m.s.exitCode = exitCodeExecute
return gocui.ErrQuit return gocui.ErrQuit
} }
} else { } else {
if m.s.highlightedItem < len(m.s.data) { 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 m.s.exitCode = exitCodeExecute
return gocui.ErrQuit return gocui.ErrQuit
} }
@ -223,13 +219,13 @@ func (m manager) SelectPaste(g *gocui.Gui, v *gocui.View) error {
defer m.s.lock.Unlock() defer m.s.lock.Unlock()
if m.s.rawMode { if m.s.rawMode {
if m.s.highlightedItem < len(m.s.rawData) { 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 m.s.exitCode = 0 // success
return gocui.ErrQuit return gocui.ErrQuit
} }
} else { } else {
if m.s.highlightedItem < len(m.s.data) { 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 m.s.exitCode = 0 // success
return gocui.ErrQuit return gocui.ErrQuit
} }
@ -248,18 +244,13 @@ func (m manager) AbortPaste(g *gocui.Gui, v *gocui.View) error {
return nil return nil
} }
type dedupRecord struct {
dataIndex int
score float32
}
func (m manager) UpdateData(input string) { func (m manager) UpdateData(input string) {
if debug { sugar := m.out.Logger.Sugar()
log.Println("EDIT start") sugar.Debugw("Starting data update ...",
log.Println("len(fullRecords) =", len(m.s.cliRecords)) "recordCount", len(m.s.cliRecords),
log.Println("len(data) =", len(m.s.data)) "itemCount", len(m.s.data),
} )
query := searchapp.NewQueryFromString(input, m.host, m.pwd, m.gitOriginRemote, m.config.Debug) query := searchapp.NewQueryFromString(sugar, input, m.host, m.pwd, m.gitOriginRemote, m.config.Debug)
var data []searchapp.Item var data []searchapp.Item
itemSet := make(map[string]int) itemSet := make(map[string]int)
m.s.lock.Lock() m.s.lock.Lock()
@ -268,7 +259,7 @@ func (m manager) UpdateData(input string) {
itm, err := searchapp.NewItemFromRecordForQuery(rec, query, m.config.Debug) itm, err := searchapp.NewItemFromRecordForQuery(rec, query, m.config.Debug)
if err != nil { if err != nil {
// records didn't match the query // records didn't match the query
// log.Println(" * continue (no match)", rec.Pwd) // sugar.Println(" * continue (no match)", rec.Pwd)
continue continue
} }
if idx, ok := itemSet[itm.Key]; ok { if idx, ok := itemSet[itm.Key]; ok {
@ -285,9 +276,9 @@ func (m manager) UpdateData(input string) {
itemSet[itm.Key] = len(data) itemSet[itm.Key] = len(data)
data = append(data, itm) data = append(data, itm)
} }
if debug { sugar.Debugw("Got new items from records for query, sorting items ...",
log.Println("len(tmpdata) =", len(data)) "itemCount", len(data),
} )
sort.SliceStable(data, func(p, q int) bool { sort.SliceStable(data, func(p, q int) bool {
return data[p].Score > data[q].Score 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.data = append(m.s.data, itm)
} }
m.s.highlightedItem = 0 m.s.highlightedItem = 0
if debug { sugar.Debugw("Done with data update",
log.Println("len(fullRecords) =", len(m.s.cliRecords)) "recordCount", len(m.s.cliRecords),
log.Println("len(data) =", len(m.s.data)) "itemCount", len(m.s.data),
log.Println("EDIT end") )
}
} }
func (m manager) UpdateRawData(input string) { func (m manager) UpdateRawData(input string) {
if m.config.Debug { sugar := m.out.Logger.Sugar()
log.Println("EDIT start") sugar.Debugw("Starting RAW data update ...",
log.Println("len(fullRecords) =", len(m.s.cliRecords)) "recordCount", len(m.s.cliRecords),
log.Println("len(data) =", len(m.s.data)) "itemCount", len(m.s.data),
} )
query := searchapp.GetRawTermsFromString(input, m.config.Debug) query := searchapp.GetRawTermsFromString(input, m.config.Debug)
var data []searchapp.RawItem var data []searchapp.RawItem
itemSet := make(map[string]bool) itemSet := make(map[string]bool)
@ -321,20 +311,20 @@ func (m manager) UpdateRawData(input string) {
itm, err := searchapp.NewRawItemFromRecordForQuery(rec, query, m.config.Debug) itm, err := searchapp.NewRawItemFromRecordForQuery(rec, query, m.config.Debug)
if err != nil { if err != nil {
// records didn't match the query // records didn't match the query
// log.Println(" * continue (no match)", rec.Pwd) // sugar.Println(" * continue (no match)", rec.Pwd)
continue continue
} }
if itemSet[itm.Key] { if itemSet[itm.Key] {
// log.Println(" * continue (already present)", itm.key(), itm.pwd) // sugar.Println(" * continue (already present)", itm.key(), itm.pwd)
continue continue
} }
itemSet[itm.Key] = true itemSet[itm.Key] = true
data = append(data, itm) data = append(data, itm)
// log.Println("DATA =", itm.display) // sugar.Println("DATA =", itm.display)
}
if debug {
log.Println("len(tmpdata) =", len(data))
} }
sugar.Debugw("Got new RAW items from records for query, sorting items ...",
"itemCount", len(data),
)
sort.SliceStable(data, func(p, q int) bool { sort.SliceStable(data, func(p, q int) bool {
return data[p].Score > data[q].Score 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.rawData = append(m.s.rawData, itm)
} }
m.s.highlightedItem = 0 m.s.highlightedItem = 0
if debug { sugar.Debugw("Done with RAW data update",
log.Println("len(fullRecords) =", len(m.s.cliRecords)) "recordCount", len(m.s.cliRecords),
log.Println("len(data) =", len(m.s.data)) "itemCount", len(m.s.data),
log.Println("EDIT end") )
}
} }
func (m manager) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) { func (m manager) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
gocui.DefaultEditor.Edit(v, key, ch, mod) 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) v, err := g.SetView("input", 0, 0, maxX-1, 2, b)
if err != nil && !errors.Is(err, gocui.ErrUnknownView) { if err != nil && !errors.Is(err, gocui.ErrUnknownView) {
log.Panicln(err.Error()) m.out.FatalE("Failed to set view 'input'", err)
} }
v.Editable = true 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) v, err = g.SetView("body", 0, 2, maxX-1, maxY, b)
if err != nil && !errors.Is(err, gocui.ErrUnknownView) { 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.Frame = false
v.Autoscroll = false v.Autoscroll = false
@ -438,13 +427,14 @@ func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit return gocui.ErrQuit
} }
const smallTerminalTresholdWidth = 110 const smallTerminalThresholdWidth = 110
func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error { func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error {
sugar := m.out.Logger.Sugar()
maxX, maxY := g.Size() maxX, maxY := g.Size()
compactRenderingMode := false compactRenderingMode := false
if maxX < smallTerminalTresholdWidth { if maxX < smallTerminalThresholdWidth {
compactRenderingMode = true compactRenderingMode = true
} }
@ -459,7 +449,7 @@ func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error {
if i == maxY { if i == maxY {
break break
} }
ic := itm.DrawItemColumns(compactRenderingMode, debug) ic := itm.DrawItemColumns(compactRenderingMode, m.config.Debug)
data = append(data, ic) data = append(data, ic)
if i > maxPossibleMainViewHeight { if i > maxPossibleMainViewHeight {
// do not stretch columns because of results that will end up outside of the page // 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
// header := getHeader() // header := getHeader()
// error is expected for header // 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) dispStr = searchapp.DoHighlightHeader(dispStr, maxX*2)
v.WriteString(dispStr + "\n") v.WriteString(dispStr + "\n")
@ -513,33 +503,24 @@ func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error {
for index < len(data) { for index < len(data) {
itm := data[index] itm := data[index]
if index >= mainViewHeight { if index >= mainViewHeight {
if debug { sugar.Debugw("Reached bottom of the page while producing lines",
log.Printf("Finished drawing page. mainViewHeight: %v, predictedMax: %v\n", "mainViewHeight", mainViewHeight,
mainViewHeight, maxPossibleMainViewHeight) "predictedMaxViewHeight", maxPossibleMainViewHeight,
} )
// page is full // page is full
break 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 { if err != nil {
log.Printf("produceLine error: %v\n", err) sugar.Error("Error while drawing item", zap.Error(err))
} }
if m.s.highlightedItem == index { if m.s.highlightedItem == index {
// maxX * 2 because there are escape sequences that make it hard to tell the real string length // maxX * 2 because there are escape sequences that make it hard to tell the real string length
displayStr = searchapp.DoHighlightString(displayStr, maxX*3) displayStr = searchapp.DoHighlightString(displayStr, maxX*3)
if debug {
log.Println("### HightlightedItem string :", displayStr)
}
} else if debug {
log.Println(displayStr)
} }
if strings.Contains(displayStr, "\n") { if strings.Contains(displayStr, "\n") {
log.Println("display string contained \\n")
displayStr = strings.ReplaceAll(displayStr, "\n", "#") displayStr = strings.ReplaceAll(displayStr, "\n", "#")
if debug {
log.Println("display string contained \\n")
}
} }
v.WriteString(displayStr + "\n") v.WriteString(displayStr + "\n")
index++ index++
@ -553,59 +534,46 @@ func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error {
v.WriteString(line) v.WriteString(line)
} }
v.WriteString(helpLine) v.WriteString(helpLine)
if debug { sugar.Debugw("Done drawing page",
log.Println("len(data) =", len(m.s.data)) "itemCount", len(m.s.data),
log.Println("highlightedItem =", m.s.highlightedItem) "highlightedItemIndex", m.s.highlightedItem,
} )
return nil return nil
} }
func (m manager) rawMode(g *gocui.Gui, v *gocui.View) error { func (m manager) rawMode(g *gocui.Gui, v *gocui.View) error {
sugar := m.out.Logger.Sugar()
maxX, maxY := g.Size() maxX, maxY := g.Size()
topBoxSize := 3 topBoxSize := 3
m.s.displayedItemsCount = maxY - topBoxSize m.s.displayedItemsCount = maxY - topBoxSize
for i, itm := range m.s.rawData { for i, itm := range m.s.rawData {
if i == maxY { if i == maxY {
if debug {
log.Println(maxY)
}
break break
} }
displayStr := itm.CmdLineWithColor displayStr := itm.CmdLineWithColor
if m.s.highlightedItem == i { 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) displayStr = searchapp.DoHighlightString(displayStr, maxX*2)
if debug {
log.Println("### HightlightedItem string :", displayStr)
}
} else if debug {
log.Println(displayStr)
} }
if strings.Contains(displayStr, "\n") { if strings.Contains(displayStr, "\n") {
log.Println("display string contained \\n")
displayStr = strings.ReplaceAll(displayStr, "\n", "#") displayStr = strings.ReplaceAll(displayStr, "\n", "#")
if debug {
log.Println("display string contained \\n")
}
} }
v.WriteString(displayStr + "\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 return nil
} }
// SendCliMsg to daemon // 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) recJSON, err := json.Marshal(m)
if err != nil { if err != nil {
log.Fatalf("Failed to marshal message: %v\n", err) out.FatalE("Failed to marshal message", err)
} }
req, err := http.NewRequest( req, err := http.NewRequest(
@ -613,7 +581,7 @@ func SendCliMsg(m msg.CliMsg, port string) msg.CliResponse {
"http://localhost:"+port+"/dump", "http://localhost:"+port+"/dump",
bytes.NewBuffer(recJSON)) bytes.NewBuffer(recJSON))
if err != nil { 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") 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) resp, err := client.Do(req)
if err != nil { if err != nil {
log.Fatal("resh-daemon is not running - try restarting this terminal") out.FatalDaemonNotRunning(err)
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { 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{} response := msg.CliResponse{}
err = json.Unmarshal(body, &response) err = json.Unmarshal(body, &response)
if err != nil { if err != nil {
log.Fatalf("Unmarshal resp error: %v\n", err) out.FatalE("Failed decode response", err)
}
if debug {
log.Printf("Recieved %d records from daemon\n", len(response.CliRecords))
} }
sugar.Debugw("Received records from daemon",
"recordCount", len(response.Records),
)
return response return response
} }

@ -1,255 +1,91 @@
package main package main
import ( import (
"flag"
"fmt" "fmt"
"log"
"os" "os"
"github.com/BurntSushi/toml" "github.com/curusarn/resh/internal/cfg"
"github.com/curusarn/resh/pkg/cfg" "github.com/curusarn/resh/internal/collect"
"github.com/curusarn/resh/pkg/collect" "github.com/curusarn/resh/internal/logger"
"github.com/curusarn/resh/pkg/records" "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" "path/filepath"
"strconv" "strconv"
) )
// version tag from git set during build // info passed during build
var version string var version string
// Commit hash from git set during build
var commit string var commit string
var development string
func main() { func main() {
usr, _ := user.Current() config, errCfg := cfg.New()
dir := usr.HomeDir logger, err := logger.New("collect", config.LogLevel, development)
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)
if err != nil {
log.Fatal("Flag Parsing error (rtb):", err)
}
realtimeSessionStart, err := strconv.ParseFloat(*rtsess, 64)
if err != nil { if err != nil {
log.Fatal("Flag Parsing error (rt sess):", err) fmt.Printf("Error while creating logger: %v", err)
} }
realtimeSessSinceBoot, err := strconv.ParseFloat(*rtsessboot, 64) defer logger.Sync() // flushes buffer, if any
if errCfg != nil {
logger.Error("Error while getting configuration", zap.Error(errCfg))
}
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 { 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) realPwd, err := filepath.EvalSymlinks(*pwd)
if err != nil { if err != nil {
log.Println("err while handling pwd realpath:", err) out.ErrorE("Error while evaluating symlinks in PWD", err)
realPwd = "" realPwd = ""
} }
gitDir, gitRealDir := collect.GetGitDirs(*gitCdup, *gitCdupExitCode, *pwd) rec := recordint.Collect{
if *gitRemoteExitCode != 0 {
*gitRemote = ""
}
// if *osReleaseID == "" {
// *osReleaseID = "linux"
// }
// if *osReleaseName == "" {
// *osReleaseName = "Linux"
// }
// if *osReleasePrettyName == "" {
// *osReleasePrettyName = "Linux"
// }
if *recall {
rec := records.SlimRecord{
SessionID: *sessionID, SessionID: *sessionID,
RecallHistno: *recallHistno, Shlvl: *shlvl,
RecallPrefix: *recallPrefix, SessionPID: *sessionPID,
}
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, Shell: *shell,
Uname: *uname,
Rec: record.V1{
SessionID: *sessionID, SessionID: *sessionID,
RecordID: *recordID, RecordID: *recordID,
CmdLine: *cmdLine,
// posix // posix
Home: *home, Home: *home,
Lang: *lang,
LcAll: *lcAll,
Login: *login,
// Path: *path,
Pwd: *pwd, Pwd: *pwd,
ShellEnv: *shellEnv,
Term: *term,
// non-posix
RealPwd: realPwd, 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, GitOriginRemote: *gitRemote,
MachineID: collect.ReadFileContent(machineIDPath),
OsReleaseID: *osReleaseID, Time: fmt.Sprintf("%.4f", time),
OsReleaseVersionID: *osReleaseVersionID,
OsReleaseIDLike: *osReleaseIDLike,
OsReleaseName: *osReleaseName,
OsReleasePrettyName: *osReleasePrettyName,
PartOne: true, PartOne: true,
PartsNotMerged: 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") collect.SendRecord(out, rec, strconv.Itoa(config.Port), "/record")
}
} }

@ -4,24 +4,24 @@ import (
"flag" "flag"
"fmt" "fmt"
"os" "os"
"os/user"
"path/filepath"
"strings" "strings"
"github.com/BurntSushi/toml" "github.com/curusarn/resh/internal/cfg"
"github.com/curusarn/resh/pkg/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() { func main() {
usr, _ := user.Current() config, errCfg := cfg.New()
dir := usr.HomeDir logger, _ := logger.New("config", config.LogLevel, development)
configPath := filepath.Join(dir, ".config/resh.toml") defer logger.Sync() // flushes buffer, if any
if errCfg != nil {
var config cfg.Config logger.Error("Error while getting configuration", zap.Error(errCfg))
_, err := toml.DecodeFile(configPath, &config)
if err != nil {
fmt.Println("Error reading config", err)
os.Exit(1)
} }
configKey := flag.String("key", "", "Key of the requested config entry") configKey := flag.String("key", "", "Key of the requested config entry")
@ -39,9 +39,9 @@ func main() {
case "port": case "port":
fmt.Println(config.Port) fmt.Println(config.Port)
case "sesswatchperiodseconds": case "sesswatchperiodseconds":
fmt.Println(config.SesswatchPeriodSeconds) fmt.Println(config.SessionWatchPeriodSeconds)
case "sesshistinithistorysize": case "sesshistinithistorysize":
fmt.Println(config.SesshistInitHistorySize) fmt.Println(config.ReshHistoryMinSize)
default: default:
fmt.Println("Error: illegal --key!") fmt.Println("Error: illegal --key!")
os.Exit(1) os.Exit(1)

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

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

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

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

@ -2,70 +2,58 @@ package cmd
import ( import (
"fmt" "fmt"
"log"
"os/user"
"path/filepath"
"github.com/BurntSushi/toml" "github.com/curusarn/resh/internal/cfg"
"github.com/curusarn/resh/cmd/control/status" "github.com/curusarn/resh/internal/logger"
"github.com/curusarn/resh/pkg/cfg" "github.com/curusarn/resh/internal/output"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// globals
var exitCode status.Code
var version string var version string
var commit string var commit string
var debug = false
var config cfg.Config // globals
var out *output.Output
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "reshctl", 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 // Execute reshctl
func Execute(ver, com string) status.Code { func Execute(ver, com, development string) {
version = ver version = ver
commit = com commit = com
usr, _ := user.Current() config, errCfg := cfg.New()
dir := usr.HomeDir logger, err := logger.New("reshctl", config.LogLevel, development)
configPath := filepath.Join(dir, ".config/resh.toml") if err != nil {
if _, err := toml.DecodeFile(configPath, &config); err != nil { fmt.Printf("Error while creating logger: %v", err)
log.Println("Error reading config", err)
return status.Fail
} }
if config.Debug { defer logger.Sync() // flushes buffer, if any
debug = true out = output.New(logger, "ERROR")
// log.SetFlags(log.LstdFlags | log.Lmicroseconds) if errCfg != nil {
out.ErrorE("Error while getting configuration", errCfg)
} }
rootCmd.AddCommand(enableCmd) var versionCmd = cobra.Command{
enableCmd.AddCommand(enableControlRBindingCmd) Use: "version",
Short: "show RESH version",
rootCmd.AddCommand(disableCmd) Run: versionCmdFunc(config),
disableCmd.AddCommand(disableControlRBindingCmd) }
rootCmd.AddCommand(&versionCmd)
rootCmd.AddCommand(completionCmd)
completionCmd.AddCommand(completionBashCmd)
completionCmd.AddCommand(completionZshCmd)
rootCmd.AddCommand(debugCmd)
debugCmd.AddCommand(debugReloadCmd)
debugCmd.AddCommand(debugInspectCmd)
debugCmd.AddCommand(debugOutputCmd)
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.") updateCmd.Flags().BoolVar(&betaFlag, "beta", false, "Update to latest version even if it's beta.")
rootCmd.AddCommand(updateCmd) rootCmd.AddCommand(updateCmd)
rootCmd.AddCommand(sanitizeCmd)
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
fmt.Println(err) out.FatalE("Command ended with error", err)
return status.Fail
} }
return exitCode
} }

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

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

@ -3,10 +3,8 @@ package cmd
import ( import (
"os" "os"
"os/exec" "os/exec"
"os/user"
"path/filepath" "path/filepath"
"github.com/curusarn/resh/cmd/control/status"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -15,9 +13,11 @@ var updateCmd = &cobra.Command{
Use: "update", Use: "update",
Short: "check for updates and update RESH", Short: "check for updates and update RESH",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
usr, _ := user.Current() homeDir, err := os.UserHomeDir()
dir := usr.HomeDir if err != nil {
rawinstallPath := filepath.Join(dir, ".resh/rawinstall.sh") out.FatalE("Could not get user home dir", err)
}
rawinstallPath := filepath.Join(homeDir, ".resh/rawinstall.sh")
execArgs := []string{rawinstallPath} execArgs := []string{rawinstallPath}
if betaFlag { if betaFlag {
execArgs = append(execArgs, "--beta") execArgs = append(execArgs, "--beta")
@ -25,9 +25,9 @@ var updateCmd = &cobra.Command{
execCmd := exec.Command("bash", execArgs...) execCmd := exec.Command("bash", execArgs...)
execCmd.Stdout = os.Stdout execCmd.Stdout = os.Stdout
execCmd.Stderr = os.Stderr execCmd.Stderr = os.Stderr
err := execCmd.Run() err = execCmd.Run()
if err == nil { if err != nil {
exitCode = status.Success out.FatalE("Update ended with error", err)
} }
}, },
} }

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

@ -1,17 +1,13 @@
package main package main
import ( import (
"os"
"github.com/curusarn/resh/cmd/control/cmd" "github.com/curusarn/resh/cmd/control/cmd"
) )
// version from git set during build
var version string var version string
// commit from git set during build
var commit string var commit string
var development string
func main() { func main() {
os.Exit(int(cmd.Execute(version, commit))) cmd.Execute(version, commit, development)
} }

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

@ -2,53 +2,50 @@ package main
import ( import (
"encoding/json" "encoding/json"
"io/ioutil" "io"
"log"
"net/http" "net/http"
"github.com/curusarn/resh/pkg/histfile" "github.com/curusarn/resh/internal/histfile"
"github.com/curusarn/resh/pkg/msg" "github.com/curusarn/resh/internal/msg"
"go.uber.org/zap"
) )
type dumpHandler struct { type dumpHandler struct {
sugar *zap.SugaredLogger
histfileBox *histfile.Histfile histfileBox *histfile.Histfile
} }
func (h *dumpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *dumpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if Debug { sugar := h.sugar.With(zap.String("endpoint", "/dump"))
log.Println("/dump START") sugar.Debugw("Handling request, reading body ...")
log.Println("/dump reading body ...") jsn, err := io.ReadAll(r.Body)
}
jsn, err := ioutil.ReadAll(r.Body)
if err != nil { if err != nil {
log.Println("Error reading the body", err) sugar.Errorw("Error reading body", "error", err)
return return
} }
sugar.Debugw("Unmarshaling record ...")
mess := msg.CliMsg{} mess := msg.CliMsg{}
if Debug {
log.Println("/dump unmarshaling record ...")
}
err = json.Unmarshal(jsn, &mess) err = json.Unmarshal(jsn, &mess)
if err != nil { if err != nil {
log.Println("Decoding error:", err) sugar.Errorw("Error during unmarshaling",
log.Println("Payload:", jsn) "error", err,
"payload", jsn,
)
return return
} }
if Debug { sugar.Debugw("Getting records to send ...")
log.Println("/dump dumping ...")
}
fullRecords := h.histfileBox.DumpCliRecords() fullRecords := h.histfileBox.DumpCliRecords()
if err != nil { 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) jsn, err = json.Marshal(&resp)
if err != nil { if err != nil {
log.Println("Encoding error:", err) sugar.Errorw("Error when marshaling", "error", err)
return return
} }
w.Write(jsn) w.Write(jsn)
log.Println("/dump END") sugar.Infow("Request handled")
} }

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

@ -1,87 +1,165 @@
package main package main
import ( import (
//"flag" "fmt"
"io/ioutil"
"log"
"os" "os"
"os/user" "os/exec"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings"
"github.com/BurntSushi/toml" "github.com/curusarn/resh/internal/cfg"
"github.com/curusarn/resh/pkg/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 var version string
// commit from git set during build
var commit string 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.
// Debug switch $ resh-daemon-start
var Debug = false 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
MORE INFO:
https://github.com/curusarn/resh/
`
func main() { func main() {
log.Println("Daemon starting... \n" + if len(os.Args) > 1 {
"version: " + version + fmt.Fprint(os.Stderr, helpMsg)
" commit: " + commit) os.Exit(1)
usr, _ := user.Current() }
dir := usr.HomeDir config, errCfg := cfg.New()
pidfilePath := filepath.Join(dir, ".resh/resh.pid") logger, err := logger.New("daemon", config.LogLevel, development)
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 err != nil { if err != nil {
log.Fatalf("Error opening file: %v\n", err) fmt.Printf("Error while creating logger: %v", err)
} }
defer f.Close() defer logger.Sync() // flushes buffer, if any
if errCfg != nil {
log.SetOutput(f) logger.Error("Error while getting configuration", zap.Error(errCfg))
log.SetPrefix(strconv.Itoa(os.Getpid()) + " | ") }
sugar := logger.Sugar()
var config cfg.Config d := daemon{sugar: sugar}
if _, err := toml.DecodeFile(configPath, &config); err != nil { sugar.Infow("Daemon starting ...",
log.Printf("Error reading config: %v\n", err) "version", version,
return "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))
}
// 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))
} }
if config.Debug { deviceName, err := device.GetName(dataDir)
Debug = true if err != nil {
log.SetFlags(log.LstdFlags | log.Lmicroseconds) 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 { if err != nil {
log.Printf("Error while checking if the daemon is runnnig"+ sugar.Errorw("Error while checking daemon status - it's probably not running",
" - it's probably not running: %v\n", err) "error", err)
} }
if res { if res {
log.Println("Daemon is already running - exiting!") sugar.Errorw("Daemon is already running - exiting!")
return return
} }
_, err = os.Stat(pidfilePath) _, err = os.Stat(pidFile)
if err == nil { if err == nil {
log.Println("Pidfile exists") sugar.Warnw("PID file exists",
"PIDFile", pidFile)
// kill daemon // kill daemon
err = killDaemon(pidfilePath) err = d.killDaemon(pidFile)
if err != nil {
sugar.Errorw("Could not kill daemon",
"error", err,
)
}
}
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 { if err != nil {
log.Printf("Error while killing daemon: %v\n", err) 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)
} }
err = ioutil.WriteFile(pidfilePath, []byte(strconv.Itoa(os.Getpid())), 0644) d.sugar.Infow("Successfully read PID file", "contents", string(dat))
pid, err := strconv.Atoi(strings.TrimSuffix(string(dat), "\n"))
if err != nil { 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) d.sugar.Infow("Successfully parsed PID", "PID", pid)
log.Println("main: Removing pidfile ...") err = exec.Command("kill", "-SIGTERM", fmt.Sprintf("%d", pid)).Run()
err = os.Remove(pidfilePath)
if err != nil { 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
} }

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

@ -2,46 +2,64 @@ package main
import ( import (
"encoding/json" "encoding/json"
"io/ioutil" "io"
"log"
"net/http" "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 { 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) { 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")) 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 // run rest of the handler as goroutine to prevent any hangups
go func() { go func() {
if err != nil { if err != nil {
log.Println("Error reading the body", err) sugar.Errorw("Error reading body", "error", err)
return return
} }
record := records.Record{} sugar.Debugw("Unmarshaling record ...")
err = json.Unmarshal(jsn, &record) rec := recordint.Collect{}
err = json.Unmarshal(jsn, &rec)
if err != nil { if err != nil {
log.Println("Decoding error: ", err) sugar.Errorw("Error during unmarshaling",
log.Println("Payload: ", jsn) "error", err,
"payload", jsn,
)
return return
} }
for _, sub := range h.subscribers {
sub <- record
}
part := "2" part := "2"
if record.PartOne { if rec.Rec.PartOne {
part = "1" 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)
} }

@ -4,33 +4,40 @@ import (
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"time"
"github.com/curusarn/resh/pkg/cfg" "github.com/curusarn/resh/internal/cfg"
"github.com/curusarn/resh/pkg/histfile" "github.com/curusarn/resh/internal/histfile"
"github.com/curusarn/resh/pkg/records" "github.com/curusarn/resh/internal/recordint"
"github.com/curusarn/resh/pkg/sesshist" "github.com/curusarn/resh/internal/sesswatch"
"github.com/curusarn/resh/pkg/sesswatch" "github.com/curusarn/resh/internal/signalhandler"
"github.com/curusarn/resh/pkg/signalhandler" "go.uber.org/zap"
) )
func runServer(config cfg.Config, reshHistoryPath, bashHistoryPath, zshHistoryPath string) { // TODO: turn server and handlers into package
var recordSubscribers []chan records.Record
var sessionInitSubscribers []chan records.Record 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 sessionDropSubscribers []chan string
var signalSubscribers []chan os.Signal var signalSubscribers []chan os.Signal
shutdown := make(chan string) 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 // histfile
histfileRecords := make(chan records.Record) histfileRecords := make(chan recordint.Collect)
recordSubscribers = append(recordSubscribers, histfileRecords) recordSubscribers = append(recordSubscribers, histfileRecords)
histfileSessionsToDrop := make(chan string) histfileSessionsToDrop := make(chan string)
sessionDropSubscribers = append(sessionDropSubscribers, histfileSessionsToDrop) sessionDropSubscribers = append(sessionDropSubscribers, histfileSessionsToDrop)
@ -38,38 +45,46 @@ func runServer(config cfg.Config, reshHistoryPath, bashHistoryPath, zshHistoryPa
signalSubscribers = append(signalSubscribers, histfileSignals) signalSubscribers = append(signalSubscribers, histfileSignals)
maxHistSize := 10000 // lines maxHistSize := 10000 // lines
minHistSizeKB := 2000 // roughly lines minHistSizeKB := 2000 // roughly lines
histfileBox := histfile.New(histfileRecords, histfileSessionsToDrop, histfileBox := histfile.New(s.sugar, histfileRecords, histfileSessionsToDrop,
reshHistoryPath, bashHistoryPath, zshHistoryPath, s.reshHistoryPath, s.bashHistoryPath, s.zshHistoryPath,
maxHistSize, minHistSizeKB, maxHistSize, minHistSizeKB,
histfileSignals, shutdown) histfileSignals, shutdown)
// sesshist New
sesshistDispatch := sesshist.NewDispatch(sesshistSessionsToInit, sesshistSessionsToDrop,
sesshistRecords, histfileBox,
config.SesshistInitHistorySize)
// sesswatch // sesswatch
sesswatchRecords := make(chan records.Record) sesswatchRecords := make(chan recordint.Collect)
recordSubscribers = append(recordSubscribers, sesswatchRecords) recordSubscribers = append(recordSubscribers, sesswatchRecords)
sesswatchSessionsToWatch := make(chan records.Record) sesswatchSessionsToWatch := make(chan recordint.SessionInit)
sessionInitSubscribers = append(sessionInitSubscribers, sesswatchRecords, sesswatchSessionsToWatch) sessionInitSubscribers = append(sessionInitSubscribers, sesswatchSessionsToWatch)
sesswatch.Go(sesswatchSessionsToWatch, sesswatchRecords, sessionDropSubscribers, config.SesswatchPeriodSeconds) sesswatch.Go(
s.sugar,
sesswatchSessionsToWatch,
sesswatchRecords,
sessionDropSubscribers,
s.config.SessionWatchPeriodSeconds,
)
// handlers // handlers
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/status", statusHandler) mux.Handle("/status", &statusHandler{sugar: s.sugar})
mux.Handle("/record", &recordHandler{subscribers: recordSubscribers}) mux.Handle("/record", &recordHandler{
mux.Handle("/session_init", &sessionInitHandler{subscribers: sessionInitSubscribers}) sugar: s.sugar,
mux.Handle("/recall", &recallHandler{sesshistDispatch: sesshistDispatch}) subscribers: recordSubscribers,
mux.Handle("/inspect", &inspectHandler{sesshistDispatch: sesshistDispatch}) deviceID: s.deviceID,
mux.Handle("/dump", &dumpHandler{histfileBox: histfileBox}) 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{ server := &http.Server{
Addr: "localhost:" + strconv.Itoa(config.Port), Addr: "localhost:" + strconv.Itoa(s.config.Port),
Handler: mux, Handler: mux,
ReadTimeout: 1 * time.Second,
WriteTimeout: 1 * time.Second,
ReadHeaderTimeout: 1 * time.Second,
IdleTimeout: 30 * time.Second,
} }
go server.ListenAndServe() go server.ListenAndServe()
// signalhandler - takes over the main goroutine so when signal handler exists the whole program exits // 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)
} }

@ -2,37 +2,49 @@ package main
import ( import (
"encoding/json" "encoding/json"
"io/ioutil" "io"
"log"
"net/http" "net/http"
"github.com/curusarn/resh/pkg/records" "github.com/curusarn/resh/internal/recordint"
"go.uber.org/zap"
) )
type sessionInitHandler struct { type sessionInitHandler struct {
subscribers []chan records.Record sugar *zap.SugaredLogger
subscribers []chan recordint.SessionInit
} }
func (h *sessionInitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 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")) 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 // run rest of the handler as goroutine to prevent any hangups
go func() { go func() {
if err != nil { if err != nil {
log.Println("Error reading the body", err) sugar.Errorw("Error reading body", "error", err)
return return
} }
record := records.Record{} sugar.Debugw("Unmarshaling record ...")
err = json.Unmarshal(jsn, &record) rec := recordint.SessionInit{}
err = json.Unmarshal(jsn, &rec)
if err != nil { if err != nil {
log.Println("Decoding error: ", err) sugar.Errorw("Error during unmarshaling",
log.Println("Payload: ", jsn) "error", err,
"payload", jsn,
)
return return
} }
sugar := sugar.With(
"sessionID", rec.SessionID,
"sessionPID", rec.SessionPID,
)
sugar.Infow("Got session, sending to subscribers ...")
for _, sub := range h.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")
}() }()
} }

@ -2,16 +2,19 @@ package main
import ( import (
"encoding/json" "encoding/json"
"log"
"net/http" "net/http"
"strconv"
"github.com/curusarn/resh/pkg/httpclient" "github.com/curusarn/resh/internal/msg"
"github.com/curusarn/resh/pkg/msg" "go.uber.org/zap"
) )
func statusHandler(w http.ResponseWriter, r *http.Request) { type statusHandler struct {
log.Println("/status START") 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{ resp := msg.StatusResponse{
Status: true, Status: true,
Version: version, Version: version,
@ -19,23 +22,12 @@ func statusHandler(w http.ResponseWriter, r *http.Request) {
} }
jsn, err := json.Marshal(&resp) jsn, err := json.Marshal(&resp)
if err != nil { if err != nil {
log.Println("Encoding error:", err) sugar.Errorw("Error when marshaling",
log.Println("Response:", resp) "error", err,
"response", resp,
)
return return
} }
w.Write(jsn) w.Write(jsn)
log.Println("/status END") sugar.Infow("Request handled")
}
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
} }

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

@ -1,7 +0,0 @@
package main
import "fmt"
func main() {
fmt.Println("Hell world")
}

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

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

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

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

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

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

@ -1,154 +1,74 @@
package main package main
import ( import (
"flag"
"fmt" "fmt"
"log"
"os" "os"
"github.com/BurntSushi/toml" "github.com/curusarn/resh/internal/cfg"
"github.com/curusarn/resh/pkg/cfg" "github.com/curusarn/resh/internal/collect"
"github.com/curusarn/resh/pkg/collect" "github.com/curusarn/resh/internal/logger"
"github.com/curusarn/resh/pkg/records" "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" "strconv"
) )
// version from git set during build // info passed during build
var version string var version string
// commit from git set during build
var commit string var commit string
var development string
func main() { func main() {
usr, _ := user.Current() config, errCfg := cfg.New()
dir := usr.HomeDir logger, err := logger.New("postcollect", config.LogLevel, development)
configPath := filepath.Join(dir, "/.config/resh.toml")
reshUUIDPath := filepath.Join(dir, "/.resh/resh-uuid")
machineIDPath := "/etc/machine-id"
var config cfg.Config
if _, err := toml.DecodeFile(configPath, &config); err != nil {
log.Fatal("Error reading config:", err)
}
showVersion := flag.Bool("version", false, "Show version and exit")
showRevision := flag.Bool("revision", false, "Show git revision and exit")
requireVersion := flag.String("requireVersion", "", "abort if version doesn't match")
requireRevision := flag.String("requireRevision", "", "abort if revision doesn't match")
cmdLine := flag.String("cmdLine", "", "command line")
exitCode := flag.Int("exitCode", -1, "exit code")
sessionID := flag.String("sessionId", "", "resh generated session id")
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, "... $?")
// before after
timezoneAfter := flag.String("timezoneAfter", "", "")
rtb := flag.String("realtimeBefore", "-1", "before $EPOCHREALTIME")
rta := flag.String("realtimeAfter", "-1", "after $EPOCHREALTIME")
flag.Parse()
if *showVersion == true {
fmt.Println(version)
os.Exit(0)
}
if *showRevision == true {
fmt.Println(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)
if err != nil { if err != nil {
log.Fatal("Flag Parsing error (rta):", err) fmt.Printf("Error while creating logger: %v", err)
} }
realtimeBefore, err := strconv.ParseFloat(*rtb, 64) defer logger.Sync() // flushes buffer, if any
if err != nil { if errCfg != nil {
log.Fatal("Flag Parsing error (rtb):", err) logger.Error("Error while getting configuration", zap.Error(errCfg))
} }
realtimeDuration := realtimeAfter - realtimeBefore out := output.New(logger, "resh-postcollect ERROR")
args := opt.HandleVersionOpts(out, os.Args, version, commit)
timezoneAfterOffset := collect.GetTimezoneOffsetInSeconds(*timezoneAfter) flags := pflag.NewFlagSet("", pflag.ExitOnError)
realtimeAfterLocal := realtimeAfter + timezoneAfterOffset 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)
realPwdAfter, err := filepath.EvalSymlinks(*pwdAfter) timeAfter, err := strconv.ParseFloat(*rta, 64)
if err != nil { if err != nil {
log.Println("err while handling pwdAfter realpath:", err) out.FatalE("Error while parsing flag --time-after", err)
realPwdAfter = ""
} }
timeBefore, err := strconv.ParseFloat(*rtb, 64)
gitDirAfter, gitRealDirAfter := collect.GetGitDirs(*gitCdupAfter, *gitCdupExitCodeAfter, *pwdAfter) if err != nil {
if *gitRemoteExitCodeAfter != 0 { out.FatalE("Error while parsing flag --time-before", err)
*gitRemoteAfter = ""
} }
duration := timeAfter - timeBefore
rec := records.Record{ // FIXME: use recordint.Postcollect
// core rec := recordint.Collect{
BaseRecord: records.BaseRecord{
CmdLine: *cmdLine,
ExitCode: *exitCode,
SessionID: *sessionID, SessionID: *sessionID,
RecordID: *recordID,
Shlvl: *shlvl, Shlvl: *shlvl,
Shell: *shell,
PwdAfter: *pwdAfter, Rec: record.V1{
RecordID: *recordID,
// non-posix SessionID: *sessionID,
RealPwdAfter: realPwdAfter,
// before after
TimezoneAfter: *timezoneAfter,
RealtimeBefore: realtimeBefore,
RealtimeAfter: realtimeAfter,
RealtimeAfterLocal: realtimeAfterLocal,
RealtimeDuration: realtimeDuration,
GitDirAfter: gitDirAfter,
GitRealDirAfter: gitRealDirAfter,
GitOriginRemoteAfter: *gitRemoteAfter,
MachineID: collect.ReadFileContent(machineIDPath),
PartOne: false, ExitCode: *exitCode,
Duration: fmt.Sprintf("%.4f", duration),
ReshUUID: collect.ReadFileContent(reshUUIDPath), PartsNotMerged: true,
ReshVersion: version,
ReshRevision: commit,
}, },
} }
collect.SendRecord(rec, strconv.Itoa(config.Port), "/record") collect.SendRecord(out, rec, strconv.Itoa(config.Port), "/record")
} }

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

@ -1,186 +1,48 @@
package main package main
import ( import (
"flag"
"fmt" "fmt"
"log"
"os" "os"
"github.com/BurntSushi/toml" "github.com/curusarn/resh/internal/cfg"
"github.com/curusarn/resh/pkg/cfg" "github.com/curusarn/resh/internal/collect"
"github.com/curusarn/resh/pkg/collect" "github.com/curusarn/resh/internal/logger"
"github.com/curusarn/resh/pkg/records" "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" "strconv"
) )
// version from git set during build // info passed during build
var version string var version string
// commit from git set during build
var commit string var commit string
var development string
func main() { func main() {
usr, _ := user.Current() config, errCfg := cfg.New()
dir := usr.HomeDir logger, err := logger.New("session-init", config.LogLevel, development)
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 { if err != nil {
log.Fatal("Flag Parsing error (rt sess):", err) fmt.Printf("Error while creating logger: %v", err)
} }
realtimeSessSinceBoot, err := strconv.ParseFloat(*rtsessboot, 64) defer logger.Sync() // flushes buffer, if any
if err != nil { if errCfg != nil {
log.Fatal("Flag Parsing error (rt sess boot):", err) logger.Error("Error while getting configuration", zap.Error(errCfg))
} }
realtimeSinceSessionStart := realtimeBefore - realtimeSessionStart out := output.New(logger, "resh-collect ERROR")
realtimeSinceBoot := realtimeSessSinceBoot + realtimeSinceSessionStart
timezoneBeforeOffset := collect.GetTimezoneOffsetInSeconds(*timezoneBefore) args := opt.HandleVersionOpts(out, os.Args, version, commit)
realtimeBeforeLocal := realtimeBefore + timezoneBeforeOffset
if *osReleaseID == "" { flags := pflag.NewFlagSet("", pflag.ExitOnError)
*osReleaseID = "linux" sessionID := flags.String("session-id", "", "Resh generated session ID")
} sessionPID := flags.Int("session-pid", -1, "$$ - Shell session PID")
if *osReleaseName == "" { flags.Parse(args)
*osReleaseName = "Linux"
}
if *osReleasePrettyName == "" {
*osReleasePrettyName = "Linux"
}
rec := records.Record{ rec := recordint.SessionInit{
// posix
Cols: *cols,
Lines: *lines,
// core
BaseRecord: records.BaseRecord{
Shell: *shell,
Uname: *uname,
SessionID: *sessionID, SessionID: *sessionID,
SessionPID: *sessionPID,
// posix
Home: *home,
Lang: *lang,
LcAll: *lcAll,
Login: *login,
// Path: *path,
ShellEnv: *shellEnv,
Term: *term,
// non-posix
Pid: *pid,
SessionPID: *sessionPid,
Host: *host,
Hosttype: *hosttype,
Ostype: *ostype,
Machtype: *machtype,
Shlvl: *shlvl,
// before after
TimezoneBefore: *timezoneBefore,
RealtimeBefore: realtimeBefore,
RealtimeBeforeLocal: realtimeBeforeLocal,
RealtimeSinceSessionStart: realtimeSinceSessionStart,
RealtimeSinceBoot: realtimeSinceBoot,
MachineID: collect.ReadFileContent(machineIDPath),
OsReleaseID: *osReleaseID,
OsReleaseVersionID: *osReleaseVersionID,
OsReleaseIDLike: *osReleaseIDLike,
OsReleaseName: *osReleaseName,
OsReleasePrettyName: *osReleasePrettyName,
ReshUUID: collect.ReadFileContent(reshUUIDPath),
ReshVersion: version,
ReshRevision: commit,
},
} }
collect.SendRecord(rec, strconv.Itoa(config.Port), "/session_init") collect.SendSessionInit(out, rec, strconv.Itoa(config.Port))
} }

@ -1,5 +0,0 @@
port = 2627
sesswatchPeriodSeconds = 120
sesshistInitHistorySize = 1000
debug = false
bindControlR = true

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

File diff suppressed because it is too large Load Diff

@ -1,23 +1,30 @@
module github.com/curusarn/resh module github.com/curusarn/resh
go 1.16 go 1.19
require ( require (
github.com/BurntSushi/toml v0.4.1 github.com/BurntSushi/toml v1.2.1
github.com/awesome-gocui/gocui v1.0.0 github.com/awesome-gocui/gocui v1.1.0
github.com/coreos/go-semver v0.3.0 github.com/google/uuid v1.3.0
github.com/gdamore/tcell/v2 v2.4.0 // indirect github.com/mattn/go-isatty v0.0.17
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/mitchellh/go-ps v1.0.0 github.com/mitchellh/go-ps v1.0.0
github.com/schollz/progressbar v1.0.0 github.com/spf13/cobra v1.6.1
github.com/spf13/cobra v1.2.1 github.com/spf13/pflag v1.0.5
github.com/whilp/git-urls v1.0.0 github.com/whilp/git-urls v1.0.0
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 go.uber.org/zap v1.24.0
golang.org/x/sys v0.0.0-20210903071746-97244b99971b // indirect golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect )
golang.org/x/text v0.3.7 // indirect
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
) )

@ -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.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= 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 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/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/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= 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/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 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.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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= 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= 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-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-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/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-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/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.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.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/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.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= 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.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 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM=
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= 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/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 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= 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-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/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/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.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.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 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= 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/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 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629 h1:1dSBUfGlorLAua2CRx0zFN7kQsTpE2DQSmr7rrTNgY8= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629/go.mod h1:mb5nS4uRANwOJSZj8rlCWAfAcGi72GGMIXx+xGOjA7M= 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/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.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 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/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/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-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.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.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.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.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 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-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
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/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 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/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 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/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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/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/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/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/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= 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/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/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/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.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 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.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/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/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.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/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/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/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= 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/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 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= 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/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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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.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.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.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.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/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 h1:95f6UMWN5FKW71ECsXRUd3FVYiXdrE7aX4NZKcPmIjU=
github.com/whilp/git-urls v1.0.0/go.mod h1:J16SAmobsqc3Qcy98brfl5f5+e0clUvg1krgwk/qCfE= 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.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/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.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/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/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= 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.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= 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.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.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.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-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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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-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-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-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-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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 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-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 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-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-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/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= 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.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/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.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-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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/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-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-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-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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/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-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-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-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-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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/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-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 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-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-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-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 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-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.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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 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.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-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-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/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.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.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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-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.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-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-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

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

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

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

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

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

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

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

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

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

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

@ -1,31 +1,34 @@
package histcli package histcli
import ( 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 // Histcli is a dump of history preprocessed for resh cli purposes
type Histcli struct { type Histcli struct {
// list of records // list of records
List []records.CliRecord List []recordint.SearchApp
sugar *zap.SugaredLogger
} }
// New Histcli // New Histcli
func New() Histcli { func New(sugar *zap.SugaredLogger) Histcli {
return Histcli{} return Histcli{}
} }
// AddRecord to the histcli // AddRecord to the histcli
func (h *Histcli) AddRecord(record records.Record) { func (h *Histcli) AddRecord(rec *record.V1) {
enriched := records.Enriched(record) cli := recordint.NewSearchApp(h.sugar, rec)
cli := records.NewCliRecord(enriched)
h.List = append(h.List, cli) h.List = append(h.List, cli)
} }
// AddCmdLine to the histcli // AddCmdLine to the histcli
func (h *Histcli) AddCmdLine(cmdline string) { func (h *Histcli) AddCmdLine(cmdline string) {
cli := records.NewCliRecordFromCmdLine(cmdline) cli := recordint.NewSearchAppFromCmdLine(cmdline)
h.List = append(h.List, cli) h.List = append(h.List, cli)
} }

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

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

@ -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? (<id>.reshjson vs. <id>)
// 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) {
}

@ -1,9 +1,11 @@
package histlist package histlist
import "log" import "go.uber.org/zap"
// Histlist is a deduplicated list of cmdLines // Histlist is a deduplicated list of cmdLines
type Histlist struct { type Histlist struct {
// TODO: I'm not excited about logger being passed here
sugar *zap.SugaredLogger
// list of commands lines (deduplicated) // list of commands lines (deduplicated)
List []string List []string
// lookup: cmdLine -> last index // lookup: cmdLine -> last index
@ -11,13 +13,16 @@ type Histlist struct {
} }
// New Histlist // New Histlist
func New() Histlist { func New(sugar *zap.SugaredLogger) Histlist {
return Histlist{LastIndex: make(map[string]int)} return Histlist{
sugar: sugar.With("component", "histlist"),
LastIndex: make(map[string]int),
}
} }
// Copy Histlist // Copy Histlist
func Copy(hl Histlist) Histlist { func Copy(hl Histlist) Histlist {
newHl := New() newHl := New(hl.sugar)
// copy list // copy list
newHl.List = make([]string, len(hl.List)) newHl.List = make([]string, len(hl.List))
copy(newHl.List, hl.List) copy(newHl.List, hl.List)
@ -36,7 +41,10 @@ func (h *Histlist) AddCmdLine(cmdLine string) {
if found { if found {
// remove duplicate // remove duplicate
if cmdLine != h.List[idx] { 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:]...) h.List = append(h.List[:idx], h.List[idx+1:]...)
// idx++ // idx++
@ -44,7 +52,10 @@ func (h *Histlist) AddCmdLine(cmdLine string) {
cmdLn := h.List[idx] cmdLn := h.List[idx]
h.LastIndex[cmdLn]-- h.LastIndex[cmdLn]--
if 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++ idx++
} }
@ -53,7 +64,10 @@ func (h *Histlist) AddCmdLine(cmdLine string) {
h.LastIndex[cmdLine] = len(h.List) h.LastIndex[cmdLine] = len(h.List)
// append new cmdline // append new cmdline
h.List = append(h.List, 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 // AddHistlist contents of another histlist to this histlist

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

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

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

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

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

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

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

@ -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", "<Search 'Error while decoding line' to see individual errors>",
)
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", "<Search 'Error while decoding line' to see individual errors>",
)
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
}

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

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

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

@ -0,0 +1,2 @@
// Package recordint provides internal record types that are passed between resh components
package recordint

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

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

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

@ -2,13 +2,13 @@ package searchapp
import ( import (
"fmt" "fmt"
"log"
"math" "math"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"unicode"
"github.com/curusarn/resh/pkg/records" "github.com/curusarn/resh/internal/recordint"
"golang.org/x/exp/utf8string" "golang.org/x/exp/utf8string"
) )
@ -19,7 +19,7 @@ const dots = "…"
type Item struct { type Item struct {
isRaw bool isRaw bool
realtimeBefore float64 time float64
// [host:]pwd // [host:]pwd
differentHost bool differentHost bool
@ -32,8 +32,11 @@ type Item struct {
sameGitRepo bool sameGitRepo bool
exitCode int exitCode int
// Shown in TUI
CmdLineWithColor string CmdLineWithColor string
CmdLine string CmdLine string
// Unchanged cmdline to paste to command line
CmdLineOut string
Score float64 Score float64
@ -106,8 +109,8 @@ func (i Item) DrawStatusLine(compactRendering bool, printedLineLength, realLineL
if i.isRaw { if i.isRaw {
return splitStatusLineToLines(i.CmdLine, printedLineLength, realLineLength) return splitStatusLineToLines(i.CmdLine, printedLineLength, realLineLength)
} }
secs := int64(i.realtimeBefore) secs := int64(i.time)
nsecs := int64((i.realtimeBefore - float64(secs)) * 1e9) nsecs := int64((i.time - float64(secs)) * 1e9)
tm := time.Unix(secs, nsecs) tm := time.Unix(secs, nsecs)
const timeFormat = "2006-01-02 15:04:05" const timeFormat = "2006-01-02 15:04:05"
timeString := tm.Format(timeFormat) timeString := tm.Format(timeFormat)
@ -143,8 +146,8 @@ func (i Item) DrawItemColumns(compactRendering bool, debug bool) ItemColumns {
// DISPLAY // DISPLAY
// DISPLAY > date // DISPLAY > date
secs := int64(i.realtimeBefore) secs := int64(i.time)
nsecs := int64((i.realtimeBefore - float64(secs)) * 1e9) nsecs := int64((i.time - float64(secs)) * 1e9)
tm := time.Unix(secs, nsecs) tm := time.Unix(secs, nsecs)
var date string var date string
@ -228,9 +231,6 @@ func produceLocation(length int, host string, pwdTilde string, differentHost boo
shrinkFactor := float64(length) / float64(totalLen) shrinkFactor := float64(length) / float64(totalLen)
shrinkedHostLen := int(math.Ceil(float64(hostLen) * shrinkFactor)) shrinkedHostLen := int(math.Ceil(float64(hostLen) * shrinkFactor))
if debug {
log.Printf("shrinkFactor: %f\n", shrinkFactor)
}
halfLocationLen := length/2 - colonLen halfLocationLen := length/2 - colonLen
newHostLen = minInt(hostLen, shrinkedHostLen, halfLocationLen) 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 // pwd length is the rest of the length
newPwdLen := length - colonLen - newHostLen 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 { if differentHost {
hostWithColor = highlightHost(hostWithColor) hostWithColor = highlightHost(hostWithColor)
} }
@ -279,6 +286,20 @@ func (ic ItemColumns) ProduceLine(dateLength int, locationLength int, flagsLengt
return line, length, err 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 { func leftCutPadString(str string, newLen int) string {
if newLen <= 0 { if newLen <= 0 {
return "" 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 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 { func properMatch(str, term, padChar string) bool {
if strings.Contains(padChar+str+padChar, padChar+term+padChar) { return strings.Contains(padChar+str+padChar, padChar+term+padChar)
return true
}
return false
} }
// NewItemFromRecordForQuery creates new item from record based on given query // NewItemFromRecordForQuery creates new item from record based on given query
//
// returns error if the query doesn't match the record // returns error if the query doesn't match the record
func NewItemFromRecordForQuery(record records.CliRecord, query Query, debug bool) (Item, error) { 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 // 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 hitScore = 1.517 // 1 * 1.51
const properMatchScore = 0.501 // 0.33 * 1.51 const properMatchScore = 0.501 // 0.33 * 1.51
const hitScoreConsecutive = 0.00302 // 0.002 * 1.51 const hitScoreConsecutive = 0.00302 // 0.002 * 1.51
// context score weigth 1 // context score weight 1
// Host penalty // Host penalty
var actualPwdScore = 0.9 var actualPwdScore = 0.9
var sameGitRepoScore = 0.8 var sameGitRepoScore = 0.8
@ -360,22 +379,17 @@ func NewItemFromRecordForQuery(record records.CliRecord, query Query, debug bool
// DISPLAY > cmdline // DISPLAY > cmdline
// cmd := "<" + strings.ReplaceAll(record.CmdLine, "\n", ";") + ">" // cmd := "<" + strings.ReplaceAll(record.CmdLine, "\n", ";") + ">"
cmdLine := strings.ReplaceAll(record.CmdLine, "\n", ";") cmdLine := strings.ReplaceAll(record.CmdLine, "\n", "\\n ")
cmdLineWithColor := strings.ReplaceAll(cmd, "\n", ";") cmdLineWithColor := strings.ReplaceAll(cmd, "\n", "\\n ")
// KEY for deduplication // 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 { if record.IsRaw {
return Item{ return Item{
isRaw: true, isRaw: true,
CmdLineOut: record.CmdLine,
CmdLine: cmdLine, CmdLine: cmdLine,
CmdLineWithColor: cmdLineWithColor, CmdLineWithColor: cmdLineWithColor,
Score: score, Score: score,
@ -387,7 +401,7 @@ func NewItemFromRecordForQuery(record records.CliRecord, query Query, debug bool
// -> N matches against the command // -> N matches against the command
// -> 1 extra match for the actual directory match // -> 1 extra match for the actual directory match
sameGitRepo := false sameGitRepo := false
if query.gitOriginRemote != "" && query.gitOriginRemote == record.GitOriginRemote { if len(query.gitOriginRemote) != 0 && query.gitOriginRemote == record.GitOriginRemote {
sameGitRepo = true sameGitRepo = true
} }
@ -415,10 +429,10 @@ func NewItemFromRecordForQuery(record records.CliRecord, query Query, debug bool
// if score <= 0 && !anyHit { // if score <= 0 && !anyHit {
// return Item{}, errors.New("no match for given record and query") // return Item{}, errors.New("no match for given record and query")
// } // }
score += record.RealtimeBefore * timeScoreCoef score += record.Time * timeScoreCoef
it := Item{ it := Item{
realtimeBefore: record.RealtimeBefore, time: record.Time,
differentHost: differentHost, differentHost: differentHost,
host: record.Host, host: record.Host,
@ -428,6 +442,7 @@ func NewItemFromRecordForQuery(record records.CliRecord, query Query, debug bool
sameGitRepo: sameGitRepo, sameGitRepo: sameGitRepo,
exitCode: record.ExitCode, exitCode: record.ExitCode,
CmdLineOut: record.CmdLine,
CmdLine: cmdLine, CmdLine: cmdLine,
CmdLineWithColor: cmdLineWithColor, CmdLineWithColor: cmdLineWithColor,
Score: score, Score: score,
@ -465,6 +480,7 @@ func GetHeader(compactRendering bool) ItemColumns {
type RawItem struct { type RawItem struct {
CmdLineWithColor string CmdLineWithColor string
CmdLine string CmdLine string
CmdLineOut string
Score float64 Score float64
@ -473,8 +489,9 @@ type RawItem struct {
} }
// NewRawItemFromRecordForQuery creates new item from record based on given query // NewRawItemFromRecordForQuery creates new item from record based on given query
//
// returns error if the query doesn't match the record // returns error if the query doesn't match the record
func NewRawItemFromRecordForQuery(record records.CliRecord, terms []string, debug bool) (RawItem, error) { func NewRawItemFromRecordForQuery(record recordint.SearchApp, terms []string, debug bool) (RawItem, error) {
const hitScore = 1.0 const hitScore = 1.0
const hitScoreConsecutive = 0.01 const hitScoreConsecutive = 0.01
const properMatchScore = 0.3 const properMatchScore = 0.3
@ -493,7 +510,7 @@ func NewRawItemFromRecordForQuery(record records.CliRecord, terms []string, debu
cmd = strings.ReplaceAll(cmd, term, highlightMatch(term)) cmd = strings.ReplaceAll(cmd, term, highlightMatch(term))
} }
} }
score += record.RealtimeBefore * timeScoreCoef score += record.Time * timeScoreCoef
// KEY for deduplication // KEY for deduplication
key := record.CmdLine key := record.CmdLine

@ -87,3 +87,45 @@ func TestRightCutPadString(t *testing.T) {
t.Fatal("Incorrect right pad from ♥♥♥♥ to '♥♥♥♥ '") 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 ' ♥♥♥♥'")
}
}

@ -1,9 +1,11 @@
package searchapp package searchapp
import ( import (
"log"
"sort" "sort"
"strings" "strings"
"github.com/curusarn/resh/internal/normalize"
"go.uber.org/zap"
) )
// Query holds information that is used for result scoring // Query holds information that is used for result scoring
@ -36,49 +38,33 @@ func filterTerms(terms []string) []string {
} }
// NewQueryFromString . // NewQueryFromString .
func NewQueryFromString(queryInput string, host string, pwd string, gitOriginRemote string, debug bool) Query { func NewQueryFromString(sugar *zap.SugaredLogger, queryInput string, host string, pwd string, gitOriginRemote string, debug bool) Query {
if debug {
log.Println("QUERY input = <" + queryInput + ">")
}
terms := strings.Fields(queryInput) terms := strings.Fields(queryInput)
var logStr string var logStr string
for _, term := range terms { for _, term := range terms {
logStr += " <" + term + ">" logStr += " <" + term + ">"
} }
if debug {
log.Println("QUERY raw terms =" + logStr)
}
terms = filterTerms(terms) terms = filterTerms(terms)
logStr = "" logStr = ""
for _, term := range terms { for _, term := range terms {
logStr += " <" + term + ">" 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]) }) sort.SliceStable(terms, func(i, j int) bool { return len(terms[i]) < len(terms[j]) })
return Query{ return Query{
terms: terms, terms: terms,
host: host, host: host,
pwd: pwd, pwd: pwd,
gitOriginRemote: gitOriginRemote, gitOriginRemote: normalize.GitRemote(sugar, gitOriginRemote),
} }
} }
// GetRawTermsFromString . // GetRawTermsFromString .
func GetRawTermsFromString(queryInput string, debug bool) []string { func GetRawTermsFromString(queryInput string, debug bool) []string {
if debug {
log.Println("QUERY input = <" + queryInput + ">")
}
terms := strings.Fields(queryInput) terms := strings.Fields(queryInput)
var logStr string var logStr string
for _, term := range terms { for _, term := range terms {
logStr += " <" + term + ">" logStr += " <" + term + ">"
} }
if debug {
log.Println("QUERY raw terms =" + logStr)
}
terms = filterTerms(terms) terms = filterTerms(terms)
logStr = "" logStr = ""
for _, term := range terms { for _, term := range terms {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save