structured logging, refactoring, improvements

pull/179/head
Simon Let 4 years ago
parent e06fe26375
commit f5349fc45b
No known key found for this signature in database
GPG Key ID: D650C65DD46D431D
  1. 5
      Makefile
  2. 242
      cmd/cli/main.go
  3. 54
      cmd/collect/main.go
  4. 26
      cmd/config/main.go
  5. 3
      cmd/control/cmd/completion.go
  6. 66
      cmd/control/cmd/debug.go
  7. 46
      cmd/control/cmd/root.go
  8. 16
      cmd/control/cmd/update.go
  9. 11
      cmd/control/cmd/version.go
  10. 4
      cmd/control/main.go
  11. 25
      cmd/control/status/status.go
  12. 35
      cmd/daemon/dump.go
  13. 28
      cmd/daemon/kill.go
  14. 160
      cmd/daemon/main.go
  15. 35
      cmd/daemon/record.go
  16. 48
      cmd/daemon/run-server.go
  17. 24
      cmd/daemon/session-init.go
  18. 36
      cmd/daemon/status.go
  19. 50
      cmd/postcollect/main.go
  20. 523
      cmd/sanitize/main.go
  21. 49
      cmd/session-init/main.go
  22. 2
      conf/config.toml
  23. 20
      go.mod
  24. 18
      go.sum
  25. 117
      internal/cfg/cfg.go
  26. 31
      internal/collect/collect.go
  27. 2
      internal/histcli/histcli.go
  28. 148
      internal/histfile/histfile.go
  29. 28
      internal/histlist/histlist.go
  30. 0
      internal/httpclient/httpclient.go
  31. 28
      internal/logger/logger.go
  32. 2
      internal/msg/msg.go
  33. 69
      internal/output/output.go
  34. 89
      internal/records/records.go
  35. 5
      internal/records/records_test.go
  36. 0
      internal/records/testdata/resh_history.json
  37. 0
      internal/searchapp/highlight.go
  38. 6
      internal/searchapp/item.go
  39. 0
      internal/searchapp/item_test.go
  40. 17
      internal/searchapp/query.go
  41. 22
      internal/searchapp/test.go
  42. 0
      internal/searchapp/time.go
  43. 0
      internal/sess/sess.go
  44. 44
      internal/sesswatch/sesswatch.go
  45. 74
      internal/signalhandler/signalhander.go
  46. 12
      pkg/cfg/cfg.go
  47. 21
      pkg/searchapp/test.go
  48. 65
      pkg/signalhandler/signalhander.go
  49. 2
      scripts/util.sh

@ -2,7 +2,7 @@ SHELL=/bin/bash
LATEST_TAG=$(shell git describe --tags) LATEST_TAG=$(shell git describe --tags)
COMMIT=$(shell [ -z "$(git status --untracked-files=no --porcelain)" ] && git rev-parse --short=12 HEAD || echo "no_commit") COMMIT=$(shell [ -z "$(git status --untracked-files=no --porcelain)" ] && git rev-parse --short=12 HEAD || echo "no_commit")
VERSION="${LATEST_TAG}-DEV" VERSION="${LATEST_TAG}-DEV"
GOFLAGS=-ldflags "-X main.version=${VERSION} -X main.commit=${COMMIT}" GOFLAGS=-ldflags "-X main.version=${VERSION} -X main.commit=${COMMIT} -X main.development=true"
build: submodules bin/resh-session-init bin/resh-collect bin/resh-postcollect bin/resh-daemon\ build: submodules bin/resh-session-init bin/resh-collect bin/resh-postcollect bin/resh-daemon\
@ -27,7 +27,8 @@ 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

@ -7,7 +7,6 @@ import (
"flag" "flag"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"os" "os"
"sort" "sort"
@ -15,59 +14,41 @@ 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/logger"
"github.com/curusarn/resh/pkg/records" "github.com/curusarn/resh/internal/msg"
"github.com/curusarn/resh/pkg/searchapp" "github.com/curusarn/resh/internal/output"
"github.com/curusarn/resh/internal/records"
"github.com/curusarn/resh/internal/searchapp"
"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 developement bool
// 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, _ := logger.New("search-app", config.LogLevel, developement)
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()
dir := usr.HomeDir
configPath := filepath.Join(dir, "/.config/resh.toml")
logPath := filepath.Join(dir, ".resh/cli.log")
f, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Fatal("Error opening file:", err)
}
defer f.Close()
log.SetOutput(f)
var config cfg.Config
if _, err := toml.DecodeFile(configPath, &config); err != nil {
log.Fatal("Error reading config:", err)
}
if config.Debug {
debug = true
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
log.Println("DEBUG is ON")
}
sessionID := flag.String("sessionID", "", "resh generated session id") sessionID := flag.String("sessionID", "", "resh generated session id")
host := flag.String("host", "", "host") host := flag.String("host", "", "host")
pwd := flag.String("pwd", "", "present working directory") pwd := flag.String("pwd", "", "present working directory")
@ -77,22 +58,23 @@ func runReshCli() (string, int) {
testHistoryLines := flag.Int("test-lines", 0, "the number of lines to load from a file passed with --test-history (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() flag.Parse()
errMsg := "Failed to get necessary command-line arguments"
if *sessionID == "" { if *sessionID == "" {
log.Println("Error: you need to specify sessionId") out.Fatal(errMsg, errors.New("missing option --sessionId"))
} }
if *host == "" { if *host == "" {
log.Println("Error: you need to specify HOST") out.Fatal(errMsg, errors.New("missing option --host"))
} }
if *pwd == "" { if *pwd == "" {
log.Println("Error: you need to specify PWD") out.Fatal(errMsg, errors.New("missing option --pwd"))
} }
if *gitOriginRemote == "DEFAULT" { if *gitOriginRemote == "DEFAULT" {
log.Println("Error: you need to specify gitOriginRemote") out.Fatal(errMsg, errors.New("missing option --gitOriginRemote"))
} }
g, err := gocui.NewGui(gocui.OutputNormal, false) g, err := gocui.NewGui(gocui.OutputNormal, false)
if err != nil { if err != nil {
log.Panicln(err) out.Fatal("Failed to launch TUI", err)
} }
defer g.Close() defer g.Close()
@ -107,9 +89,9 @@ func runReshCli() (string, int) {
SessionID: *sessionID, SessionID: *sessionID,
PWD: *pwd, PWD: *pwd,
} }
resp = SendCliMsg(mess, strconv.Itoa(config.Port)) resp = SendCliMsg(out, mess, strconv.Itoa(config.Port))
} else { } else {
resp = searchapp.LoadHistoryFromFile(*testHistory, *testHistoryLines) resp = searchapp.LoadHistoryFromFile(out.Logger.Sugar(), *testHistory, *testHistoryLines)
} }
st := state{ st := state{
@ -128,47 +110,48 @@ func runReshCli() (string, int) {
} }
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.Fatal(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.Fatal(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.Fatal(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.Fatal(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.Fatal(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.Fatal(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.Fatal(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.Fatal(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.Fatal(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.Fatal(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.Fatal(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.Fatal("Main application loop finished with error", err)
} }
return layout.s.output, layout.s.exitCode return layout.s.output, layout.s.exitCode
} }
@ -190,11 +173,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
} }
@ -254,11 +239,11 @@ type dedupRecord struct {
} }
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(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)
@ -268,7 +253,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 +270,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 +284,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 +305,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 +330,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 +381,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.Fatal("Failed to set view 'input'", err)
} }
v.Editable = true v.Editable = true
@ -421,7 +404,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.Fatal("Failed to set view 'body'", err)
} }
v.Frame = false v.Frame = false
v.Autoscroll = false v.Autoscroll = false
@ -441,6 +424,7 @@ func quit(g *gocui.Gui, v *gocui.View) error {
const smallTerminalTresholdWidth = 110 const smallTerminalTresholdWidth = 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
@ -459,7 +443,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 +489,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 +497,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 +528,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 requried 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.Fatal("Failed to marshal message", err)
} }
req, err := http.NewRequest( req, err := http.NewRequest(
@ -613,7 +575,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.Fatal("Failed to build request", err)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
@ -622,22 +584,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 := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
log.Fatalf("Read response error: %v\n", err) out.Fatal("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.Fatal("Failed decode response", err)
}
if debug {
log.Printf("Recieved %d records from daemon\n", len(response.CliRecords))
} }
sugar.Debug("Recieved records from daemon",
"recordCount", len(response.CliRecords),
)
return response return response
} }

@ -3,39 +3,43 @@ package main
import ( import (
"flag" "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/output"
"github.com/curusarn/resh/internal/records"
"go.uber.org/zap"
// "os/exec" // "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 developement bool
func main() { func main() {
usr, _ := user.Current() config, errCfg := cfg.New()
dir := usr.HomeDir logger, _ := logger.New("collect", config.LogLevel, developement)
configPath := filepath.Join(dir, "/.config/resh.toml") defer logger.Sync() // flushes buffer, if any
reshUUIDPath := filepath.Join(dir, "/.resh/resh-uuid") if errCfg != nil {
logger.Error("Error while getting configuration", zap.Error(errCfg))
machineIDPath := "/etc/machine-id" }
out := output.New(logger, "resh-collect ERROR")
var config cfg.Config homeDir, err := os.UserHomeDir()
if _, err := toml.DecodeFile(configPath, &config); err != nil { if err != nil {
log.Fatal("Error reading config:", err) out.Fatal("Could not get user home dir", err)
} }
reshUUIDPath := filepath.Join(homeDir, "/.resh/resh-uuid")
machineIDPath := "/etc/machine-id"
// version // version
showVersion := flag.Bool("version", false, "Show version and exit") showVersion := flag.Bool("version", false, "Show version and exit")
showRevision := flag.Bool("revision", false, "Show git revision and exit") showRevision := flag.Bool("revision", false, "Show git revision and exit")
@ -121,29 +125,29 @@ func main() {
realtimeBefore, err := strconv.ParseFloat(*rtb, 64) realtimeBefore, err := strconv.ParseFloat(*rtb, 64)
if err != nil { if err != nil {
log.Fatal("Flag Parsing error (rtb):", err) out.Fatal("Error while parsing flag --realtimeBefore", err)
} }
realtimeSessionStart, err := strconv.ParseFloat(*rtsess, 64) realtimeSessionStart, err := strconv.ParseFloat(*rtsess, 64)
if err != nil { if err != nil {
log.Fatal("Flag Parsing error (rt sess):", err) out.Fatal("Error while parsing flag --realtimeSession", err)
} }
realtimeSessSinceBoot, err := strconv.ParseFloat(*rtsessboot, 64) realtimeSessSinceBoot, err := strconv.ParseFloat(*rtsessboot, 64)
if err != nil { if err != nil {
log.Fatal("Flag Parsing error (rt sess boot):", err) out.Fatal("Error while parsing flag --realtimeSessSinceBoot", err)
} }
realtimeSinceSessionStart := realtimeBefore - realtimeSessionStart realtimeSinceSessionStart := realtimeBefore - realtimeSessionStart
realtimeSinceBoot := realtimeSessSinceBoot + realtimeSinceSessionStart realtimeSinceBoot := realtimeSessSinceBoot + realtimeSinceSessionStart
timezoneBeforeOffset := collect.GetTimezoneOffsetInSeconds(*timezoneBefore) timezoneBeforeOffset := collect.GetTimezoneOffsetInSeconds(logger, *timezoneBefore)
realtimeBeforeLocal := realtimeBefore + timezoneBeforeOffset 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) logger.Error("Error while handling pwd realpath", zap.Error(err))
realPwd = "" realPwd = ""
} }
gitDir, gitRealDir := collect.GetGitDirs(*gitCdup, *gitCdupExitCode, *pwd) gitDir, gitRealDir := collect.GetGitDirs(logger, *gitCdup, *gitCdupExitCode, *pwd)
if *gitRemoteExitCode != 0 { if *gitRemoteExitCode != 0 {
*gitRemote = "" *gitRemote = ""
} }
@ -218,5 +222,5 @@ func main() {
ReshRevision: commit, ReshRevision: commit,
}, },
} }
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 developement bool
func main() { func main() {
usr, _ := user.Current() config, errCfg := cfg.New()
dir := usr.HomeDir logger, _ := logger.New("config", config.LogLevel, developement)
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")

@ -3,7 +3,6 @@ package cmd
import ( import (
"os" "os"
"github.com/curusarn/resh/cmd/control/status"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -30,7 +29,6 @@ var completionBashCmd = &cobra.Command{
`, `,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
rootCmd.GenBashCompletion(os.Stdout) rootCmd.GenBashCompletion(os.Stdout)
exitCode = status.Success
}, },
} }
@ -43,6 +41,5 @@ var completionZshCmd = &cobra.Command{
`, `,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
rootCmd.GenZshCompletion(os.Stdout) 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))
}

@ -1,23 +1,20 @@
package cmd package cmd
import ( import (
"fmt" "github.com/curusarn/resh/internal/cfg"
"log" "github.com/curusarn/resh/internal/logger"
"os/user" "github.com/curusarn/resh/internal/output"
"path/filepath"
"github.com/BurntSushi/toml"
"github.com/curusarn/resh/cmd/control/status"
"github.com/curusarn/resh/pkg/cfg"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// globals // info passed during build
var exitCode status.Code
var version string var version string
var commit string var commit string
var debug = false var developement bool
// globals
var config cfg.Config var config cfg.Config
var out *output.Output
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "reshctl", Use: "reshctl",
@ -25,39 +22,28 @@ var rootCmd = &cobra.Command{
} }
// Execute reshctl // Execute reshctl
func Execute(ver, com string) status.Code { func Execute(ver, com string) {
version = ver version = ver
commit = com commit = com
usr, _ := user.Current() config, errCfg := cfg.New()
dir := usr.HomeDir logger, _ := logger.New("reshctl", config.LogLevel, developement)
configPath := filepath.Join(dir, ".config/resh.toml") defer logger.Sync() // flushes buffer, if any
if _, err := toml.DecodeFile(configPath, &config); err != nil { out = output.New(logger, "ERROR")
log.Println("Error reading config", err) if errCfg != nil {
return status.Fail out.Error("Error while getting configuration", errCfg)
}
if config.Debug {
debug = true
// log.SetFlags(log.LstdFlags | log.Lmicroseconds)
} }
rootCmd.AddCommand(completionCmd) rootCmd.AddCommand(completionCmd)
completionCmd.AddCommand(completionBashCmd) completionCmd.AddCommand(completionBashCmd)
completionCmd.AddCommand(completionZshCmd) completionCmd.AddCommand(completionZshCmd)
rootCmd.AddCommand(debugCmd)
debugCmd.AddCommand(debugReloadCmd)
debugCmd.AddCommand(debugInspectCmd)
debugCmd.AddCommand(debugOutputCmd)
rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(versionCmd)
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)
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
fmt.Println(err) out.Fatal("Command ended with error", err)
return status.Fail
} }
return exitCode
} }

@ -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.Fatal("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.Fatal("Update ended with error", err)
} }
}, },
} }

@ -4,13 +4,11 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"github.com/curusarn/resh/cmd/control/status" "github.com/curusarn/resh/internal/msg"
"github.com/curusarn/resh/pkg/msg"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -24,13 +22,13 @@ var versionCmd = &cobra.Command{
commitEnv := getEnvVarWithDefault("__RESH_REVISION", "<unknown>") commitEnv := getEnvVarWithDefault("__RESH_REVISION", "<unknown>")
printVersion("This terminal session", versionEnv, commitEnv) printVersion("This terminal session", versionEnv, commitEnv)
// TODO: use output.Output.Error... for these
resp, err := getDaemonStatus(config.Port) resp, err := getDaemonStatus(config.Port)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "\nERROR: Resh-daemon didn't respond - it's probably not running.\n\n") fmt.Fprintf(os.Stderr, "\nERROR: Resh-daemon didn't respond - it's probably not running.\n\n")
fmt.Fprintf(os.Stderr, "-> Try restarting this terminal window to bring resh-daemon back up.\n") fmt.Fprintf(os.Stderr, "-> Try restarting this terminal window to bring resh-daemon back up.\n")
fmt.Fprintf(os.Stderr, "-> If the problem persists you can check resh-daemon logs: ~/.resh/daemon.log\n") fmt.Fprintf(os.Stderr, "-> If the problem persists you can check resh-daemon logs: ~/.resh/daemon.log\n")
fmt.Fprintf(os.Stderr, "-> You can file an issue at: https://github.com/curusarn/resh/issues\n") fmt.Fprintf(os.Stderr, "-> You can file an issue at: https://github.com/curusarn/resh/issues\n")
exitCode = status.Fail
return return
} }
printVersion("Currently running daemon", resp.Version, resp.Commit) printVersion("Currently running daemon", resp.Version, resp.Commit)
@ -48,7 +46,6 @@ var versionCmd = &cobra.Command{
return return
} }
exitCode = status.ReshStatus
}, },
} }
@ -74,11 +71,11 @@ func getDaemonStatus(port int) (msg.StatusResponse, error) {
defer resp.Body.Close() defer resp.Body.Close()
jsn, err := ioutil.ReadAll(resp.Body) jsn, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
log.Fatal("Error while reading 'daemon /status' response:", err) out.Fatal("Error while reading 'daemon /status' response", err)
} }
err = json.Unmarshal(jsn, &mess) err = json.Unmarshal(jsn, &mess)
if err != nil { if err != nil {
log.Fatal("Error while decoding 'daemon /status' response:", err) out.Fatal("Error while decoding 'daemon /status' response", err)
} }
return mess, nil return mess, nil
} }

@ -1,8 +1,6 @@
package main package main
import ( import (
"os"
"github.com/curusarn/resh/cmd/control/cmd" "github.com/curusarn/resh/cmd/control/cmd"
) )
@ -13,5 +11,5 @@ var version string
var commit string var commit string
func main() { func main() {
os.Exit(int(cmd.Execute(version, commit))) cmd.Execute(version, commit)
} }

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

@ -3,52 +3,49 @@ package main
import ( import (
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"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 := ioutil.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{CliRecords: 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,141 @@
package main package main
import ( import (
//"flag" "fmt"
"io/ioutil" "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/httpclient"
"github.com/curusarn/resh/internal/logger"
"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 developement bool
// Debug switch
var Debug = false
func main() { func main() {
log.Println("Daemon starting... \n" + config, errCfg := cfg.New()
"version: " + version + logger, _ := logger.New("daemon", config.LogLevel, developement)
" commit: " + commit) defer logger.Sync() // flushes buffer, if any
usr, _ := user.Current() if errCfg != nil {
dir := usr.HomeDir logger.Error("Error while getting configuration", zap.Error(errCfg))
pidfilePath := filepath.Join(dir, ".resh/resh.pid")
configPath := filepath.Join(dir, ".config/resh.toml")
reshHistoryPath := filepath.Join(dir, ".resh_history.json")
bashHistoryPath := filepath.Join(dir, ".bash_history")
zshHistoryPath := filepath.Join(dir, ".zsh_history")
logPath := filepath.Join(dir, ".resh/daemon.log")
f, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Fatalf("Error opening file: %v\n", err)
} }
defer f.Close() sugar := logger.Sugar()
d := daemon{sugar: sugar}
sugar.Infow("Deamon starting ...",
"version", version,
"commit", commit,
)
log.SetOutput(f) // xdgCacheHome := d.getEnvOrPanic("__RESH_XDG_CACHE_HOME")
log.SetPrefix(strconv.Itoa(os.Getpid()) + " | ") // xdgDataHome := d.getEnvOrPanic("__RESH_XDG_DATA_HOME")
var config cfg.Config // TODO: rethink PID file and logs location
if _, err := toml.DecodeFile(configPath, &config); err != nil { homeDir, err := os.UserHomeDir()
log.Printf("Error reading config: %v\n", err) if err != nil {
return sugar.Fatalw("Could not get user home dir", zap.Error(err))
}
if config.Debug {
Debug = true
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
} }
PIDFile := filepath.Join(homeDir, ".resh/resh.pid")
reshHistoryPath := filepath.Join(homeDir, ".resh_history.json")
bashHistoryPath := filepath.Join(homeDir, ".bash_history")
zshHistoryPath := filepath.Join(homeDir, ".zsh_history")
sugar = sugar.With(zap.Int("daemonPID", os.Getpid()))
res, err := isDaemonRunning(config.Port) res, err := d.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: %v\n", err) "it's probably not running", "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.Warn("Pidfile exists")
// kill daemon // kill daemon
err = killDaemon(pidfilePath) err = d.killDaemon(PIDFile)
if err != nil { if err != nil {
log.Printf("Error while killing daemon: %v\n", err) sugar.Errorw("Could not kill daemon",
"error", err,
)
} }
} }
err = ioutil.WriteFile(pidfilePath, []byte(strconv.Itoa(os.Getpid())), 0644) err = ioutil.WriteFile(PIDFile, []byte(strconv.Itoa(os.Getpid())), 0644)
if err != nil {
sugar.Fatalw("Could not create pidfile",
"error", err,
"PIDFile", PIDFile,
)
}
server := Server{
sugar: sugar,
config: config,
reshHistoryPath: reshHistoryPath,
bashHistoryPath: bashHistoryPath,
zshHistoryPath: zshHistoryPath,
}
server.Run()
sugar.Infow("Removing PID file ...",
"PIDFile", PIDFile,
)
err = os.Remove(PIDFile)
if err != nil {
sugar.Errorw("Could not delete PID file", "error", err)
}
sugar.Info("Shutting down ...")
}
type daemon struct {
sugar *zap.SugaredLogger
}
func (d *daemon) getEnvOrPanic(envVar string) string {
val, found := os.LookupEnv(envVar)
if !found {
d.sugar.Fatalw("Required env variable is not set",
"variableName", envVar,
)
}
return val
}
func (d *daemon) isDaemonRunning(port int) (bool, error) {
url := "http://localhost:" + strconv.Itoa(port) + "/status"
client := httpclient.New()
resp, err := client.Get(url)
if err != nil {
return false, err
}
defer resp.Body.Close()
return true, nil
}
func (d *daemon) killDaemon(pidfile string) error {
dat, err := ioutil.ReadFile(pidfile)
if err != nil {
d.sugar.Errorw("Reading pid file failed",
"PIDFile", pidfile,
"error", err)
}
d.sugar.Infow("Succesfully 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 ...") cmd := exec.Command("kill", "-s", "sigint", strconv.Itoa(pid))
err = os.Remove(pidfilePath) err = cmd.Run()
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
} }

@ -3,45 +3,58 @@ package main
import ( import (
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"github.com/curusarn/resh/pkg/records" "github.com/curusarn/resh/internal/records"
"go.uber.org/zap"
) )
func NewRecordHandler(sugar *zap.SugaredLogger, subscribers []chan records.Record) recordHandler {
return recordHandler{
sugar: sugar.With(zap.String("endpoint", "/record")),
subscribers: subscribers,
}
}
type recordHandler struct { type recordHandler struct {
sugar *zap.SugaredLogger
subscribers []chan records.Record subscribers []chan records.Record
} }
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 := ioutil.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
} }
sugar.Debugw("Unmarshaling record ...")
record := records.Record{} record := records.Record{}
err = json.Unmarshal(jsn, &record) err = json.Unmarshal(jsn, &record)
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
} }
part := "2" part := "2"
if record.PartOne { if record.PartOne {
part = "1" part = "1"
} }
log.Println("/record - ", record.CmdLine, " - part", part) sugar := sugar.With(
"cmdLine", record.CmdLine,
"part", part,
)
sugar.Debugw("Got record, sending to subscribers ...")
for _, sub := range h.subscribers { for _, sub := range h.subscribers {
sub <- record sub <- record
} }
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)
} }

@ -5,14 +5,26 @@ import (
"os" "os"
"strconv" "strconv"
"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/records"
"github.com/curusarn/resh/pkg/sesswatch" "github.com/curusarn/resh/internal/sesswatch"
"github.com/curusarn/resh/pkg/signalhandler" "github.com/curusarn/resh/internal/signalhandler"
"go.uber.org/zap"
) )
func runServer(config cfg.Config, reshHistoryPath, bashHistoryPath, zshHistoryPath string) { // TODO: turn server and handlers into package
type Server struct {
sugar *zap.SugaredLogger
config cfg.Config
reshHistoryPath string
bashHistoryPath string
zshHistoryPath string
}
func (s *Server) Run() {
var recordSubscribers []chan records.Record var recordSubscribers []chan records.Record
var sessionInitSubscribers []chan records.Record var sessionInitSubscribers []chan records.Record
var sessionDropSubscribers []chan string var sessionDropSubscribers []chan string
@ -29,8 +41,8 @@ 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)
@ -39,21 +51,27 @@ func runServer(config cfg.Config, reshHistoryPath, bashHistoryPath, zshHistoryPa
recordSubscribers = append(recordSubscribers, sesswatchRecords) recordSubscribers = append(recordSubscribers, sesswatchRecords)
sesswatchSessionsToWatch := make(chan records.Record) sesswatchSessionsToWatch := make(chan records.Record)
sessionInitSubscribers = append(sessionInitSubscribers, sesswatchSessionsToWatch) sessionInitSubscribers = append(sessionInitSubscribers, sesswatchSessionsToWatch)
sesswatch.Go(sesswatchSessionsToWatch, sesswatchRecords, sessionDropSubscribers, config.SesswatchPeriodSeconds) sesswatch.Go(
s.sugar,
sesswatchSessionsToWatch,
sesswatchRecords,
sessionDropSubscribers,
s.config.SesswatchPeriodSeconds,
)
// 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{sugar: s.sugar, subscribers: recordSubscribers})
mux.Handle("/session_init", &sessionInitHandler{subscribers: sessionInitSubscribers}) mux.Handle("/session_init", &sessionInitHandler{sugar: s.sugar, subscribers: sessionInitSubscribers})
mux.Handle("/dump", &dumpHandler{histfileBox: histfileBox}) 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,
} }
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)
} }

@ -3,36 +3,48 @@ package main
import ( import (
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"github.com/curusarn/resh/pkg/records" "github.com/curusarn/resh/internal/records"
"go.uber.org/zap"
) )
type sessionInitHandler struct { type sessionInitHandler struct {
sugar *zap.SugaredLogger
subscribers []chan records.Record subscribers []chan records.Record
} }
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"))
// TODO: should we somehow check for errors here?
jsn, err := ioutil.ReadAll(r.Body) jsn, err := ioutil.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
} }
sugar.Debugw("Unmarshaling record ...")
record := records.Record{} record := records.Record{}
err = json.Unmarshal(jsn, &record) err = json.Unmarshal(jsn, &record)
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
} }
log.Println("/session_init - id:", record.SessionID, " - pid:", record.SessionPID) sugar := sugar.With(
"sessionID", record.SessionID,
"sessionPID", record.SessionPID,
)
sugar.Infow("Got session, sending to subscribers ...")
for _, sub := range h.subscribers { for _, sub := range h.subscribers {
sub <- record sub <- record
} }
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
} }

@ -3,38 +3,42 @@ package main
import ( import (
"flag" "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/output"
"github.com/curusarn/resh/internal/records"
"go.uber.org/zap"
// "os/exec" // "os/exec"
"os/user"
"path/filepath" "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 developement bool
func main() { func main() {
usr, _ := user.Current() config, errCfg := cfg.New()
dir := usr.HomeDir logger, _ := logger.New("postcollect", config.LogLevel, developement)
configPath := filepath.Join(dir, "/.config/resh.toml") defer logger.Sync() // flushes buffer, if any
reshUUIDPath := filepath.Join(dir, "/.resh/resh-uuid") if errCfg != nil {
logger.Error("Error while getting configuration", zap.Error(errCfg))
}
out := output.New(logger, "resh-postcollect ERROR")
homeDir, err := os.UserHomeDir()
if err != nil {
out.Fatal("Could not get user home dir", err)
}
reshUUIDPath := filepath.Join(homeDir, "/.resh/resh-uuid")
machineIDPath := "/etc/machine-id" 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") showVersion := flag.Bool("version", false, "Show version and exit")
showRevision := flag.Bool("revision", false, "Show git revision and exit") showRevision := flag.Bool("revision", false, "Show git revision and exit")
@ -92,24 +96,24 @@ func main() {
} }
realtimeAfter, err := strconv.ParseFloat(*rta, 64) realtimeAfter, err := strconv.ParseFloat(*rta, 64)
if err != nil { if err != nil {
log.Fatal("Flag Parsing error (rta):", err) out.Fatal("Error while parsing flag --realtimeAfter", err)
} }
realtimeBefore, err := strconv.ParseFloat(*rtb, 64) realtimeBefore, err := strconv.ParseFloat(*rtb, 64)
if err != nil { if err != nil {
log.Fatal("Flag Parsing error (rtb):", err) out.Fatal("Error while parsing flag --realtimeBefore", err)
} }
realtimeDuration := realtimeAfter - realtimeBefore realtimeDuration := realtimeAfter - realtimeBefore
timezoneAfterOffset := collect.GetTimezoneOffsetInSeconds(*timezoneAfter) timezoneAfterOffset := collect.GetTimezoneOffsetInSeconds(logger, *timezoneAfter)
realtimeAfterLocal := realtimeAfter + timezoneAfterOffset realtimeAfterLocal := realtimeAfter + timezoneAfterOffset
realPwdAfter, err := filepath.EvalSymlinks(*pwdAfter) realPwdAfter, err := filepath.EvalSymlinks(*pwdAfter)
if err != nil { if err != nil {
log.Println("err while handling pwdAfter realpath:", err) logger.Error("Error while handling pwdAfter realpath", zap.Error(err))
realPwdAfter = "" realPwdAfter = ""
} }
gitDirAfter, gitRealDirAfter := collect.GetGitDirs(*gitCdupAfter, *gitCdupExitCodeAfter, *pwdAfter) gitDirAfter, gitRealDirAfter := collect.GetGitDirs(logger, *gitCdupAfter, *gitCdupExitCodeAfter, *pwdAfter)
if *gitRemoteExitCodeAfter != 0 { if *gitRemoteExitCodeAfter != 0 {
*gitRemoteAfter = "" *gitRemoteAfter = ""
} }
@ -150,5 +154,5 @@ func main() {
ReshRevision: commit, 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
}

@ -3,37 +3,42 @@ package main
import ( import (
"flag" "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/output"
"github.com/curusarn/resh/internal/records"
"go.uber.org/zap"
"os/user"
"path/filepath" "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 developement bool
func main() { func main() {
usr, _ := user.Current() config, errCfg := cfg.New()
dir := usr.HomeDir logger, _ := logger.New("collect", config.LogLevel, developement)
configPath := filepath.Join(dir, "/.config/resh.toml") defer logger.Sync() // flushes buffer, if any
reshUUIDPath := filepath.Join(dir, "/.resh/resh-uuid") if errCfg != nil {
logger.Error("Error while getting configuration", zap.Error(errCfg))
}
out := output.New(logger, "resh-collect ERROR")
homeDir, err := os.UserHomeDir()
if err != nil {
out.Fatal("Could not get user home dir", err)
}
reshUUIDPath := filepath.Join(homeDir, "/.resh/resh-uuid")
machineIDPath := "/etc/machine-id" 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") showVersion := flag.Bool("version", false, "Show version and exit")
showRevision := flag.Bool("revision", false, "Show git revision and exit") showRevision := flag.Bool("revision", false, "Show git revision and exit")
@ -106,20 +111,20 @@ func main() {
} }
realtimeBefore, err := strconv.ParseFloat(*rtb, 64) realtimeBefore, err := strconv.ParseFloat(*rtb, 64)
if err != nil { if err != nil {
log.Fatal("Flag Parsing error (rtb):", err) out.Fatal("Error while parsing flag --realtimeBefore", err)
} }
realtimeSessionStart, err := strconv.ParseFloat(*rtsess, 64) realtimeSessionStart, err := strconv.ParseFloat(*rtsess, 64)
if err != nil { if err != nil {
log.Fatal("Flag Parsing error (rt sess):", err) out.Fatal("Error while parsing flag --realtimeSession", err)
} }
realtimeSessSinceBoot, err := strconv.ParseFloat(*rtsessboot, 64) realtimeSessSinceBoot, err := strconv.ParseFloat(*rtsessboot, 64)
if err != nil { if err != nil {
log.Fatal("Flag Parsing error (rt sess boot):", err) out.Fatal("Error while parsing flag --realtimeSessSinceBoot", err)
} }
realtimeSinceSessionStart := realtimeBefore - realtimeSessionStart realtimeSinceSessionStart := realtimeBefore - realtimeSessionStart
realtimeSinceBoot := realtimeSessSinceBoot + realtimeSinceSessionStart realtimeSinceBoot := realtimeSessSinceBoot + realtimeSinceSessionStart
timezoneBeforeOffset := collect.GetTimezoneOffsetInSeconds(*timezoneBefore) timezoneBeforeOffset := collect.GetTimezoneOffsetInSeconds(logger, *timezoneBefore)
realtimeBeforeLocal := realtimeBefore + timezoneBeforeOffset realtimeBeforeLocal := realtimeBefore + timezoneBeforeOffset
if *osReleaseID == "" { if *osReleaseID == "" {
@ -182,5 +187,5 @@ func main() {
ReshRevision: commit, ReshRevision: commit,
}, },
} }
collect.SendRecord(rec, strconv.Itoa(config.Port), "/session_init") collect.SendRecord(out, rec, strconv.Itoa(config.Port), "/session_init")
} }

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

@ -1,19 +1,27 @@
module github.com/curusarn/resh module github.com/curusarn/resh
go 1.16 go 1.18
require ( require (
github.com/BurntSushi/toml v0.4.1 github.com/BurntSushi/toml v0.4.1
github.com/awesome-gocui/gocui v1.0.0 github.com/awesome-gocui/gocui v1.0.0
github.com/coreos/go-semver v0.3.0
github.com/gdamore/tcell/v2 v2.4.0 // indirect
github.com/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/mattn/go-shellwords v1.0.12
github.com/mitchellh/go-ps v1.0.0 github.com/mitchellh/go-ps v1.0.0
github.com/spf13/cobra v1.2.1 github.com/spf13/cobra v1.2.1
github.com/whilp/git-urls v1.0.0 go.uber.org/zap v1.21.0
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6
)
require (
github.com/gdamore/encoding v1.0.0 // indirect
github.com/gdamore/tcell/v2 v2.4.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/sys v0.0.0-20210903071746-97244b99971b // indirect golang.org/x/sys v0.0.0-20210903071746-97244b99971b // indirect
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect

@ -47,6 +47,8 @@ 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/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 +59,11 @@ 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/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=
@ -209,8 +211,10 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
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/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=
@ -239,10 +243,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/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/go.mod h1:J16SAmobsqc3Qcy98brfl5f5+e0clUvg1krgwk/qCfE=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -258,9 +261,15 @@ 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/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/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=
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=
@ -405,7 +414,6 @@ golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@ -475,6 +483,7 @@ 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/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=
@ -591,6 +600,7 @@ 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=
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=

@ -0,0 +1,117 @@
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 {
Port *int
SesswatchPeriodSeconds *uint
SesshistInitHistorySize *int
LogLevel *string
BindControlR *bool
// deprecated
BindArrowKeysBash *bool
BindArrowKeysZsh *bool
Debug *bool
}
// Config returned by this package to be used in the rest of the project
type Config struct {
Port int
SesswatchPeriodSeconds uint
SesshistInitHistorySize int
LogLevel zapcore.Level
Debug bool
BindControlR bool
}
// defaults for config
var defaults = Config{
Port: 2627,
SesswatchPeriodSeconds: 120,
SesshistInitHistorySize: 1000,
LogLevel: zap.InfoLevel,
Debug: false,
BindControlR: true,
}
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() (*configFile, error) {
var config configFile
path, err := getConfigPath()
if err != nil {
return &config, fmt.Errorf("could not get config file path: %w", err)
}
if _, err := toml.DecodeFile(path, &config); err != nil {
return &config, fmt.Errorf("could not decode config: %w", err)
}
return &config, nil
}
func processAndFillDefaults(configF *configFile) (Config, error) {
config := defaults
if configF.Port != nil {
config.Port = *configF.Port
}
if configF.SesswatchPeriodSeconds != nil {
config.SesswatchPeriodSeconds = *configF.SesswatchPeriodSeconds
}
if configF.SesshistInitHistorySize != nil {
config.SesshistInitHistorySize = *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 := readConfig()
if err != nil {
return defaults, fmt.Errorf("using default config because of error while getting config: %w", err)
}
config, err := processAndFillDefaults(configF)
if err != nil {
return config, fmt.Errorf("errors while processing config: %w", err)
}
return config, nil
}

@ -4,14 +4,15 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"github.com/curusarn/resh/pkg/httpclient" "github.com/curusarn/resh/internal/httpclient"
"github.com/curusarn/resh/pkg/records" "github.com/curusarn/resh/internal/output"
"github.com/curusarn/resh/internal/records"
"go.uber.org/zap"
) )
// SingleResponse json struct // SingleResponse json struct
@ -21,23 +22,27 @@ type SingleResponse struct {
} }
// SendRecord to daemon // SendRecord to daemon
func SendRecord(r records.Record, port, path string) { func SendRecord(out *output.Output, r records.Record, port, path string) {
out.Logger.Debug("Sending record ...",
zap.String("cmdLine", r.CmdLine),
zap.String("sessionID", r.SessionID),
)
recJSON, err := json.Marshal(r) recJSON, err := json.Marshal(r)
if err != nil { if err != nil {
log.Fatal("send err 1", err) out.Fatal("Error while encoding record", err)
} }
req, err := http.NewRequest("POST", "http://localhost:"+port+path, req, err := http.NewRequest("POST", "http://localhost:"+port+path,
bytes.NewBuffer(recJSON)) bytes.NewBuffer(recJSON))
if err != nil { if err != nil {
log.Fatal("send err 2", err) out.Fatal("Error while sending record", err)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
client := httpclient.New() client := httpclient.New()
_, err = client.Do(req) _, err = client.Do(req)
if err != nil { if err != nil {
log.Fatal("resh-daemon is not running - try restarting this terminal") out.FatalDaemonNotRunning(err)
} }
} }
@ -46,38 +51,38 @@ func ReadFileContent(path string) string {
dat, err := ioutil.ReadFile(path) dat, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
return "" return ""
//log.Fatal("failed to open " + path) //sugar.Fatal("failed to open " + path)
} }
return strings.TrimSuffix(string(dat), "\n") return strings.TrimSuffix(string(dat), "\n")
} }
// GetGitDirs based on result of git "cdup" command // GetGitDirs based on result of git "cdup" command
func GetGitDirs(cdup string, exitCode int, pwd string) (string, string) { func GetGitDirs(logger *zap.Logger, cdup string, exitCode int, pwd string) (string, string) {
if exitCode != 0 { if exitCode != 0 {
return "", "" return "", ""
} }
abspath := filepath.Clean(filepath.Join(pwd, cdup)) abspath := filepath.Clean(filepath.Join(pwd, cdup))
realpath, err := filepath.EvalSymlinks(abspath) realpath, err := filepath.EvalSymlinks(abspath)
if err != nil { if err != nil {
log.Println("err while handling git dir paths:", err) logger.Error("Error while handling git dir paths", zap.Error(err))
return "", "" return "", ""
} }
return abspath, realpath return abspath, realpath
} }
// GetTimezoneOffsetInSeconds based on zone returned by date command // GetTimezoneOffsetInSeconds based on zone returned by date command
func GetTimezoneOffsetInSeconds(zone string) float64 { func GetTimezoneOffsetInSeconds(logger *zap.Logger, zone string) float64 {
// date +%z -> "+0200" // date +%z -> "+0200"
hoursStr := zone[:3] hoursStr := zone[:3]
minsStr := zone[3:] minsStr := zone[3:]
hours, err := strconv.Atoi(hoursStr) hours, err := strconv.Atoi(hoursStr)
if err != nil { if err != nil {
log.Println("err while parsing hours in timezone offset:", err) logger.Error("Error while parsing hours in timezone offset", zap.Error(err))
return -1 return -1
} }
mins, err := strconv.Atoi(minsStr) mins, err := strconv.Atoi(minsStr)
if err != nil { if err != nil {
log.Println("err while parsing mins in timezone offset:", err) logger.Error("err while parsing minutes in timezone offset:", zap.Error(err))
return -1 return -1
} }
secs := ((hours * 60) + mins) * 60 secs := ((hours * 60) + mins) * 60

@ -1,7 +1,7 @@
package histcli package histcli
import ( import (
"github.com/curusarn/resh/pkg/records" "github.com/curusarn/resh/internal/records"
) )
// Histcli is a dump of history preprocessed for resh cli purposes // Histcli is a dump of history preprocessed for resh cli purposes

@ -2,19 +2,21 @@ package histfile
import ( import (
"encoding/json" "encoding/json"
"log"
"math" "math"
"os" "os"
"strconv" "strconv"
"sync" "sync"
"github.com/curusarn/resh/pkg/histcli" "github.com/curusarn/resh/internal/histcli"
"github.com/curusarn/resh/pkg/histlist" "github.com/curusarn/resh/internal/histlist"
"github.com/curusarn/resh/pkg/records" "github.com/curusarn/resh/internal/records"
"go.uber.org/zap"
) )
// Histfile writes records to histfile // Histfile writes records to histfile
type Histfile struct { type Histfile struct {
sugar *zap.SugaredLogger
sessionsMutex sync.Mutex sessionsMutex sync.Mutex
sessions map[string]records.Record sessions map[string]records.Record
historyPath string historyPath string
@ -31,16 +33,17 @@ type Histfile struct {
} }
// New creates new histfile and runs its gorutines // New creates new histfile and runs its gorutines
func New(input chan records.Record, sessionsToDrop chan string, func New(sugar *zap.SugaredLogger, input chan records.Record, sessionsToDrop chan string,
reshHistoryPath string, bashHistoryPath string, zshHistoryPath string, reshHistoryPath string, bashHistoryPath string, zshHistoryPath string,
maxInitHistSize int, minInitHistSizeKB int, maxInitHistSize int, minInitHistSizeKB int,
signals chan os.Signal, shutdownDone chan string) *Histfile { signals chan os.Signal, shutdownDone chan string) *Histfile {
hf := Histfile{ hf := Histfile{
sugar: sugar.With("module", "histfile"),
sessions: map[string]records.Record{}, sessions: map[string]records.Record{},
historyPath: reshHistoryPath, historyPath: reshHistoryPath,
bashCmdLines: histlist.New(), bashCmdLines: histlist.New(sugar),
zshCmdLines: histlist.New(), zshCmdLines: histlist.New(sugar),
cliRecords: histcli.New(), cliRecords: histcli.New(),
} }
go hf.loadHistory(bashHistoryPath, zshHistoryPath, maxInitHistSize, minInitHistSizeKB) go hf.loadHistory(bashHistoryPath, zshHistoryPath, maxInitHistSize, minInitHistSizeKB)
@ -61,49 +64,58 @@ func (h *Histfile) loadCliRecords(recs []records.Record) {
rec := recs[i] rec := recs[i]
h.cliRecords.AddRecord(rec) h.cliRecords.AddRecord(rec)
} }
log.Println("histfile: resh history loaded - history records count:", len(h.cliRecords.List)) 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 // 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) { func (h *Histfile) loadHistory(bashHistoryPath, zshHistoryPath string, maxInitHistSize, minInitHistSizeKB int) {
h.recentMutex.Lock() h.recentMutex.Lock()
defer h.recentMutex.Unlock() defer h.recentMutex.Unlock()
log.Println("histfile: Checking if resh_history is large enough ...") h.sugar.Infow("Checking if resh_history is large enough ...")
fi, err := os.Stat(h.historyPath) fi, err := os.Stat(h.historyPath)
var size int var size int
if err != nil { if err != nil {
log.Println("histfile ERROR: failed to stat resh_history file:", err) h.sugar.Errorw("Failed to stat resh_history file", "error", err)
} else { } else {
size = int(fi.Size()) size = int(fi.Size())
} }
useNativeHistories := false useNativeHistories := false
if size/1024 < minInitHistSizeKB { if size/1024 < minInitHistSizeKB {
useNativeHistories = true useNativeHistories = true
log.Println("histfile WARN: resh_history is too small - loading native bash and zsh history ...") h.sugar.Warnw("Resh_history is too small - loading native bash and zsh history ...")
h.bashCmdLines = records.LoadCmdLinesFromBashFile(bashHistoryPath) h.bashCmdLines = records.LoadCmdLinesFromBashFile(h.sugar, bashHistoryPath)
log.Println("histfile: bash history loaded - cmdLine count:", len(h.bashCmdLines.List)) h.sugar.Infow("Bash history loaded", "cmdLineCount", len(h.bashCmdLines.List))
h.zshCmdLines = records.LoadCmdLinesFromZshFile(zshHistoryPath) h.zshCmdLines = records.LoadCmdLinesFromZshFile(h.sugar, zshHistoryPath)
log.Println("histfile: zsh history loaded - cmdLine count:", len(h.zshCmdLines.List)) h.sugar.Infow("Zsh history loaded", "cmdLineCount", len(h.zshCmdLines.List))
// no maxInitHistSize when using native histories // no maxInitHistSize when using native histories
maxInitHistSize = math.MaxInt32 maxInitHistSize = math.MaxInt32
} }
log.Println("histfile: Loading resh history from file ...") h.sugar.Debugw("Loading resh history from file ...",
history := records.LoadFromFile(h.historyPath) "historyFile", h.historyPath,
log.Println("histfile: resh history loaded from file - count:", len(history)) )
history := records.LoadFromFile(h.sugar, h.historyPath)
h.sugar.Infow("Resh history loaded from file",
"historyFile", h.historyPath,
"recordCount", len(history),
)
go h.loadCliRecords(history) go h.loadCliRecords(history)
// NOTE: keeping this weird interface for now because we might use it in the future // NOTE: keeping this weird interface for now because we might use it in the future
// when we only load bash or zsh history // when we only load bash or zsh history
reshCmdLines := loadCmdLines(history) reshCmdLines := loadCmdLines(h.sugar, history)
log.Println("histfile: resh history loaded - cmdLine count:", len(reshCmdLines.List)) h.sugar.Infow("Resh history loaded and processed",
"recordCount", len(reshCmdLines.List),
)
if useNativeHistories == false { if useNativeHistories == false {
h.bashCmdLines = reshCmdLines h.bashCmdLines = reshCmdLines
h.zshCmdLines = histlist.Copy(reshCmdLines) h.zshCmdLines = histlist.Copy(reshCmdLines)
return return
} }
h.bashCmdLines.AddHistlist(reshCmdLines) h.bashCmdLines.AddHistlist(reshCmdLines)
log.Println("histfile: bash history + resh history - cmdLine count:", len(h.bashCmdLines.List)) h.sugar.Infow("Processed bash history and resh history together", "cmdLinecount", len(h.bashCmdLines.List))
h.zshCmdLines.AddHistlist(reshCmdLines) h.zshCmdLines.AddHistlist(reshCmdLines)
log.Println("histfile: zsh history + resh history - cmdLine count:", len(h.zshCmdLines.List)) 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 // sessionGC reads sessionIDs from channel and deletes them from histfile struct
@ -111,15 +123,16 @@ func (h *Histfile) sessionGC(sessionsToDrop chan string) {
for { for {
func() { func() {
session := <-sessionsToDrop session := <-sessionsToDrop
log.Println("histfile: got session to drop", session) sugar := h.sugar.With("sessionID", session)
sugar.Debugw("Got session to drop")
h.sessionsMutex.Lock() h.sessionsMutex.Lock()
defer h.sessionsMutex.Unlock() defer h.sessionsMutex.Unlock()
if part1, found := h.sessions[session]; found == true { if part1, found := h.sessions[session]; found == true {
log.Println("histfile: Dropping session:", session) sugar.Infow("Dropping session")
delete(h.sessions, session) delete(h.sessions, session)
go writeRecord(part1, h.historyPath) go writeRecord(sugar, part1, h.historyPath)
} else { } else {
log.Println("histfile: No hanging parts for session:", session) sugar.Infow("No hanging parts for session - nothing to drop")
} }
}() }()
} }
@ -131,36 +144,56 @@ func (h *Histfile) writer(input chan records.Record, signals chan os.Signal, shu
func() { func() {
select { select {
case record := <-input: case record := <-input:
part := "2"
if record.PartOne {
part = "1"
}
sugar := h.sugar.With(
"recordCmdLine", record.CmdLine,
"recordPart", part,
"recordShell", record.Shell,
)
sugar.Debugw("Got record")
h.sessionsMutex.Lock() h.sessionsMutex.Lock()
defer h.sessionsMutex.Unlock() defer h.sessionsMutex.Unlock()
// allows nested sessions to merge records properly // allows nested sessions to merge records properly
mergeID := record.SessionID + "_" + strconv.Itoa(record.Shlvl) mergeID := record.SessionID + "_" + strconv.Itoa(record.Shlvl)
sugar = sugar.With("mergeID", mergeID)
if record.PartOne { if record.PartOne {
if _, found := h.sessions[mergeID]; found { if _, found := h.sessions[mergeID]; found {
log.Println("histfile WARN: Got another first part of the records before merging the previous one - overwriting! " + msg := "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)") if record.Shell == "zsh" {
sugar.Warnw(msg)
} else {
sugar.Infow(msg + " Unfortunately this is normal in bash, it can't be prevented.")
}
} }
h.sessions[mergeID] = record h.sessions[mergeID] = record
} else { } else {
if part1, found := h.sessions[mergeID]; found == false { 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, ")") sugar.Warnw("Got second part of record and nothing to merge it with - ignoring!")
} else { } else {
delete(h.sessions, mergeID) delete(h.sessions, mergeID)
go h.mergeAndWriteRecord(part1, record) go h.mergeAndWriteRecord(sugar, part1, record)
} }
} }
case sig := <-signals: case sig := <-signals:
log.Println("histfile: Got signal " + sig.String()) sugar := h.sugar.With(
"signal", sig.String(),
)
sugar.Infow("Got signal")
h.sessionsMutex.Lock() h.sessionsMutex.Lock()
defer h.sessionsMutex.Unlock() defer h.sessionsMutex.Unlock()
log.Println("histfile DEBUG: Unlocked mutex") sugar.Debugw("Unlocked mutex")
for sessID, record := range h.sessions { for sessID, record := range h.sessions {
log.Printf("histfile WARN: Writing incomplete record for session: %v\n", sessID) sugar.Warnw("Writing incomplete record for session",
h.writeRecord(record) "sessionID", sessID,
)
h.writeRecord(sugar, record)
} }
log.Println("histfile DEBUG: Shutdown success") sugar.Debugw("Shutdown successful")
shutdownDone <- "histfile" shutdownDone <- "histfile"
return return
} }
@ -168,14 +201,14 @@ func (h *Histfile) writer(input chan records.Record, signals chan os.Signal, shu
} }
} }
func (h *Histfile) writeRecord(part1 records.Record) { func (h *Histfile) writeRecord(sugar *zap.SugaredLogger, part1 records.Record) {
writeRecord(part1, h.historyPath) writeRecord(sugar, part1, h.historyPath)
} }
func (h *Histfile) mergeAndWriteRecord(part1, part2 records.Record) { func (h *Histfile) mergeAndWriteRecord(sugar *zap.SugaredLogger, part1, part2 records.Record) {
err := part1.Merge(part2) err := part1.Merge(part2)
if err != nil { if err != nil {
log.Println("Error while merging", err) sugar.Errorw("Error while merging records", "error", err)
return return
} }
@ -189,57 +222,40 @@ func (h *Histfile) mergeAndWriteRecord(part1, part2 records.Record) {
h.cliRecords.AddRecord(part1) h.cliRecords.AddRecord(part1)
}() }()
writeRecord(part1, h.historyPath) writeRecord(sugar, part1, h.historyPath)
} }
func writeRecord(rec records.Record, outputPath string) { func writeRecord(sugar *zap.SugaredLogger, rec records.Record, outputPath string) {
recJSON, err := json.Marshal(rec) recJSON, err := json.Marshal(rec)
if err != nil { if err != nil {
log.Println("Marshalling error", err) sugar.Errorw("Marshalling error", "error", err)
return return
} }
f, err := os.OpenFile(outputPath, f, err := os.OpenFile(outputPath,
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil { if err != nil {
log.Println("Could not open file", err) sugar.Errorw("Could not open file", "error", err)
return return
} }
defer f.Close() defer f.Close()
_, err = f.Write(append(recJSON, []byte("\n")...)) _, err = f.Write(append(recJSON, []byte("\n")...))
if err != nil { if err != nil {
log.Printf("Error while writing: %v, %s\n", rec, err) sugar.Errorw("Error while writing record",
"recordRaw", rec,
"error", err,
)
return 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 // DumpCliRecords returns enriched records
func (h *Histfile) DumpCliRecords() histcli.Histcli { func (h *Histfile) DumpCliRecords() histcli.Histcli {
// don't forget locks in the future // don't forget locks in the future
return h.cliRecords return h.cliRecords
} }
func loadCmdLines(recs []records.Record) histlist.Histlist { func loadCmdLines(sugar *zap.SugaredLogger, recs []records.Record) histlist.Histlist {
hl := histlist.New() hl := histlist.New(sugar)
// go from bottom and deduplicate // go from bottom and deduplicate
var cmdLines []string var cmdLines []string
cmdLinesSet := map[string]bool{} cmdLinesSet := map[string]bool{}

@ -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,28 @@
package logger
import (
"fmt"
"os"
"path/filepath"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func New(executable string, level zapcore.Level, developement bool) (*zap.Logger, error) {
// TODO: consider getting log path from config ?
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("error while getting home dir: %w", err)
}
logPath := filepath.Join(homeDir, ".resh/log.json")
loggerConfig := zap.NewProductionConfig()
loggerConfig.OutputPaths = []string{logPath}
loggerConfig.Level.SetLevel(level)
loggerConfig.Development = developement // DPanic panics in developement
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
}

@ -1,6 +1,6 @@
package msg package msg
import "github.com/curusarn/resh/pkg/records" import "github.com/curusarn/resh/internal/records"
// CliMsg struct // CliMsg struct
type CliMsg struct { type CliMsg struct {

@ -0,0 +1,69 @@
package output
import (
"fmt"
"os"
"go.uber.org/zap"
)
// Output wrapper for writting 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,
}
}
func (f *Output) Info(msg string) {
fmt.Fprintf(os.Stdout, msg)
f.Logger.Info(msg)
}
func (f *Output) Error(msg string, err error) {
fmt.Fprintf(os.Stderr, "%s: %s: %v", f.ErrPrefix, msg, err)
f.Logger.Error(msg, zap.Error(err))
}
func (f *Output) Fatal(msg string, err error) {
fmt.Fprintf(os.Stderr, "%s: %s: %v", f.ErrPrefix, msg, err)
f.Logger.Fatal(msg, zap.Error(err))
}
var msgDeamonNotRunning = `Resh-daemon didn't respond - it's probably not running.
-> Try restarting this terminal window to bring resh-daemon back up
-> If the problem persists you can check resh-daemon logs: ~/.resh/log.json
-> You can create an issue at: https://github.com/curusarn/resh/issues
`
var msgVersionMismatch = `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
`
func (f *Output) ErrorDaemonNotRunning(err error) {
fmt.Fprintf(os.Stderr, "%s: %s", f.ErrPrefix, msgDeamonNotRunning)
f.Logger.Error("Daemon is not running", zap.Error(err))
}
func (f *Output) FatalDaemonNotRunning(err error) {
fmt.Fprintf(os.Stderr, "%s: %s", f.ErrPrefix, msgDeamonNotRunning)
f.Logger.Fatal("Daemon is not running", zap.Error(err))
}
func (f *Output) ErrorVersionMismatch(err error) {
fmt.Fprintf(os.Stderr, "%s: %s", f.ErrPrefix, msgVersionMismatch)
f.Logger.Fatal("Version mismatch", zap.Error(err))
}
func (f *Output) FatalVersionMismatch(err error) {
fmt.Fprintf(os.Stderr, "%s: %s", f.ErrPrefix, msgVersionMismatch)
f.Logger.Fatal("Version mismatch", zap.Error(err))
}

@ -4,15 +4,16 @@ import (
"bufio" "bufio"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io" "io"
"log"
"math" "math"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"github.com/curusarn/resh/pkg/histlist" "github.com/curusarn/resh/internal/histlist"
"github.com/mattn/go-shellwords" "github.com/mattn/go-shellwords"
"go.uber.org/zap"
) )
// BaseRecord - common base for Record and FallbackRecord // BaseRecord - common base for Record and FallbackRecord
@ -219,14 +220,14 @@ func Enriched(r Record) EnrichedRecord {
if err != nil { if err != nil {
record.Errors = append(record.Errors, "Validate error:"+err.Error()) record.Errors = append(record.Errors, "Validate error:"+err.Error())
// rec, _ := record.ToString() // rec, _ := record.ToString()
// log.Println("Invalid command:", rec) // sugar.Println("Invalid command:", rec)
record.Invalid = true record.Invalid = true
} }
record.Command, record.FirstWord, err = GetCommandAndFirstWord(r.CmdLine) record.Command, record.FirstWord, err = GetCommandAndFirstWord(r.CmdLine)
if err != nil { if err != nil {
record.Errors = append(record.Errors, "GetCommandAndFirstWord error:"+err.Error()) record.Errors = append(record.Errors, "GetCommandAndFirstWord error:"+err.Error())
// rec, _ := record.ToString() // rec, _ := record.ToString()
// log.Println("Invalid command:", rec) // sugar.Println("Invalid command:", rec)
record.Invalid = true // should this be really invalid ? record.Invalid = true // should this be really invalid ?
} }
return record return record
@ -327,7 +328,7 @@ func (r *EnrichedRecord) SetCmdLine(cmdLine string) {
r.Command, r.FirstWord, err = GetCommandAndFirstWord(cmdLine) r.Command, r.FirstWord, err = GetCommandAndFirstWord(cmdLine)
if err != nil { if err != nil {
r.Errors = append(r.Errors, "GetCommandAndFirstWord error:"+err.Error()) r.Errors = append(r.Errors, "GetCommandAndFirstWord error:"+err.Error())
// log.Println("Invalid command:", r.CmdLine) // sugar.Println("Invalid command:", r.CmdLine)
r.Invalid = true r.Invalid = true
} }
} }
@ -352,7 +353,7 @@ func Stripped(r EnrichedRecord) EnrichedRecord {
func GetCommandAndFirstWord(cmdLine string) (string, string, error) { func GetCommandAndFirstWord(cmdLine string) (string, string, error) {
args, err := shellwords.Parse(cmdLine) args, err := shellwords.Parse(cmdLine)
if err != nil { if err != nil {
// log.Println("shellwords Error:", err, " (cmdLine: <", cmdLine, "> )") // Println("shellwords Error:", err, " (cmdLine: <", cmdLine, "> )")
return "", "", err return "", "", err
} }
if len(args) == 0 { if len(args) == 0 {
@ -361,15 +362,14 @@ func GetCommandAndFirstWord(cmdLine string) (string, string, error) {
i := 0 i := 0
for true { for true {
// commands in shell sometimes look like this `variable=something command argument otherArgument --option` // commands in shell sometimes look like this `variable=something command argument otherArgument --option`
// to get the command we skip over tokens that contain '=' // to get the command we skip over tokens that contain '='
if strings.ContainsRune(args[i], '=') && len(args) > i+1 { if strings.ContainsRune(args[i], '=') && len(args) > i+1 {
i++ i++
continue continue
} }
return args[i], args[0], nil return args[i], args[0], nil
} }
log.Fatal("GetCommandAndFirstWord error: this should not happen!") return "ERROR", "ERROR", errors.New("failed to retrieve first word of command")
return "ERROR", "ERROR", errors.New("this should not happen - contact developer ;)")
} }
// NormalizeGitRemote func // NormalizeGitRemote func
@ -511,22 +511,19 @@ func (r *EnrichedRecord) DistanceTo(r2 EnrichedRecord, p DistParams) float64 {
} }
// LoadFromFile loads records from 'fname' file // LoadFromFile loads records from 'fname' file
func LoadFromFile(fname string) []Record { func LoadFromFile(sugar *zap.SugaredLogger, fname string) []Record {
const allowedErrors = 2 const allowedErrors = 3
var encounteredErrors int var encounteredErrors int
// NOTE: limit does nothing atm
var recs []Record var recs []Record
file, err := os.Open(fname) file, err := os.Open(fname)
if err != nil { if err != nil {
log.Println("Open() resh history file error:", err) sugar.Error("Failed to open resh history file - skipping reading resh history", zap.Error(err))
log.Println("WARN: Skipping reading resh history!")
return recs return recs
} }
defer file.Close() defer file.Close()
reader := bufio.NewReader(file) reader := bufio.NewReader(file)
var i int var i int
var firstErrLine int
for { for {
var line string var line string
line, err = reader.ReadString('\n') line, err = reader.ReadString('\n')
@ -540,14 +537,16 @@ func LoadFromFile(fname string) []Record {
if err != nil { if err != nil {
err = json.Unmarshal([]byte(line), &fallbackRecord) err = json.Unmarshal([]byte(line), &fallbackRecord)
if err != nil { if err != nil {
if encounteredErrors == 0 {
firstErrLine = i
}
encounteredErrors++ encounteredErrors++
log.Println("Line:", line) sugar.Error("Could not decode line in resh history file",
log.Println("Decoding error:", err) "lineContents", line,
"lineNumber", i,
zap.Error(err),
)
if encounteredErrors > allowedErrors { if encounteredErrors > allowedErrors {
log.Fatalf("Fatal: Encountered more than %d decoding errors (%d)", allowedErrors, encounteredErrors) sugar.Fatal("Encountered too many errors during decoding - exiting",
"allowedErrors", allowedErrors,
)
} }
} }
record = Convert(&fallbackRecord) record = Convert(&fallbackRecord)
@ -555,32 +554,43 @@ func LoadFromFile(fname string) []Record {
recs = append(recs, record) recs = append(recs, record)
} }
if err != io.EOF { if err != io.EOF {
log.Println("records: error while loading file:", err) sugar.Error("Error while loading file", zap.Error(err))
} }
// log.Println("records: Loaded lines - count:", i) sugar.Infow("Loaded resh history records",
"recordCount", len(recs),
)
if encounteredErrors > 0 { if encounteredErrors > 0 {
// fix errors in the history file // fix errors in the history file
log.Printf("There were %d decoding errors, the first error happend on line %d/%d\n", encounteredErrors, firstErrLine, i) sugar.Warnw("Some history records could not be decoded - fixing resh history file by dropping them",
"corruptedRecords", encounteredErrors,
)
fnameBak := fname + ".bak" fnameBak := fname + ".bak"
log.Printf("Backing up current history file to %s\n", fnameBak) sugar.Infow("Backing up current corrupted history file",
"backupFilename", fnameBak,
)
err := copyFile(fname, fnameBak) err := copyFile(fname, fnameBak)
if err != nil { if err != nil {
log.Fatalln("Failed to backup history file with decode errors") sugar.Errorw("Failed to create a backup history file - aborting fixing history file",
"backupFilename", fnameBak,
zap.Error(err),
)
return recs
} }
log.Println("Writing out a history file without errors ...") sugar.Info("Writing resh history file without errors ...")
err = writeHistory(fname, recs) err = writeHistory(fname, recs)
if err != nil { if err != nil {
log.Fatalln("Fatal: Failed write out new history") sugar.Errorw("Failed write fixed history file - aborting fixing history file",
"filename", fname,
zap.Error(err),
)
} }
} }
log.Println("records: Loaded records - count:", len(recs))
return recs return recs
} }
func copyFile(source, dest string) error { func copyFile(source, dest string) error {
from, err := os.Open(source) from, err := os.Open(source)
if err != nil { if err != nil {
// log.Println("Open() resh history file error:", err)
return err return err
} }
defer from.Close() defer from.Close()
@ -588,14 +598,12 @@ func copyFile(source, dest string) error {
// to, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE, 0666) // to, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE, 0666)
to, err := os.Create(dest) to, err := os.Create(dest)
if err != nil { if err != nil {
// log.Println("Create() resh history backup error:", err)
return err return err
} }
defer to.Close() defer to.Close()
_, err = io.Copy(to, from) _, err = io.Copy(to, from)
if err != nil { if err != nil {
// log.Println("Copy() resh history to backup error:", err)
return err return err
} }
return nil return nil
@ -604,14 +612,13 @@ func copyFile(source, dest string) error {
func writeHistory(fname string, history []Record) error { func writeHistory(fname string, history []Record) error {
file, err := os.Create(fname) file, err := os.Create(fname)
if err != nil { if err != nil {
// log.Println("Create() resh history error:", err)
return err return err
} }
defer file.Close() defer file.Close()
for _, rec := range history { for _, rec := range history {
jsn, err := json.Marshal(rec) jsn, err := json.Marshal(rec)
if err != nil { if err != nil {
log.Fatalln("Encode error!") return fmt.Errorf("failed to encode record: %w", err)
} }
file.Write(append(jsn, []byte("\n")...)) file.Write(append(jsn, []byte("\n")...))
} }
@ -619,12 +626,11 @@ func writeHistory(fname string, history []Record) error {
} }
// LoadCmdLinesFromZshFile loads cmdlines from zsh history file // LoadCmdLinesFromZshFile loads cmdlines from zsh history file
func LoadCmdLinesFromZshFile(fname string) histlist.Histlist { func LoadCmdLinesFromZshFile(sugar *zap.SugaredLogger, fname string) histlist.Histlist {
hl := histlist.New() hl := histlist.New(sugar)
file, err := os.Open(fname) file, err := os.Open(fname)
if err != nil { if err != nil {
log.Println("Open() zsh history file error:", err) sugar.Error("Failed to open zsh history file - skipping reading zsh history", zap.Error(err))
log.Println("WARN: Skipping reading zsh history!")
return hl return hl
} }
defer file.Close() defer file.Close()
@ -656,12 +662,11 @@ func LoadCmdLinesFromZshFile(fname string) histlist.Histlist {
} }
// LoadCmdLinesFromBashFile loads cmdlines from bash history file // LoadCmdLinesFromBashFile loads cmdlines from bash history file
func LoadCmdLinesFromBashFile(fname string) histlist.Histlist { func LoadCmdLinesFromBashFile(sugar *zap.SugaredLogger, fname string) histlist.Histlist {
hl := histlist.New() hl := histlist.New(sugar)
file, err := os.Open(fname) file, err := os.Open(fname)
if err != nil { if err != nil {
log.Println("Open() bash history file error:", err) sugar.Error("Failed to open bash history file - skipping reading bash history", zap.Error(err))
log.Println("WARN: Skipping reading bash history!")
return hl return hl
} }
defer file.Close() defer file.Close()

@ -11,7 +11,7 @@ import (
func GetTestRecords() []Record { func GetTestRecords() []Record {
file, err := os.Open("testdata/resh_history.json") file, err := os.Open("testdata/resh_history.json")
if err != nil { if err != nil {
log.Fatal("Open() resh history file error:", err) log.Fatalf("Failed to open resh history file: %v", err)
} }
defer file.Close() defer file.Close()
@ -22,8 +22,7 @@ func GetTestRecords() []Record {
line := scanner.Text() line := scanner.Text()
err = json.Unmarshal([]byte(line), &record) err = json.Unmarshal([]byte(line), &record)
if err != nil { if err != nil {
log.Println("Line:", line) log.Fatalf("Error decoding record: '%s'; err: %v", line, err)
log.Fatal("Decoding error:", err)
} }
recs = append(recs, record) recs = append(recs, record)
} }

@ -2,13 +2,12 @@ package searchapp
import ( import (
"fmt" "fmt"
"log"
"math" "math"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/curusarn/resh/pkg/records" "github.com/curusarn/resh/internal/records"
"golang.org/x/exp/utf8string" "golang.org/x/exp/utf8string"
) )
@ -228,9 +227,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)

@ -1,7 +1,6 @@
package searchapp package searchapp
import ( import (
"log"
"sort" "sort"
"strings" "strings"
) )
@ -37,26 +36,16 @@ func filterTerms(terms []string) []string {
// NewQueryFromString . // NewQueryFromString .
func NewQueryFromString(queryInput string, host string, pwd string, gitOriginRemote string, debug bool) Query { func NewQueryFromString(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,
@ -68,17 +57,11 @@ func NewQueryFromString(queryInput string, host string, pwd string, gitOriginRem
// 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,22 @@
package searchapp
import (
"github.com/curusarn/resh/internal/histcli"
"github.com/curusarn/resh/internal/msg"
"github.com/curusarn/resh/internal/records"
"go.uber.org/zap"
)
// LoadHistoryFromFile ...
func LoadHistoryFromFile(sugar *zap.SugaredLogger, historyPath string, numLines int) msg.CliResponse {
recs := records.LoadFromFile(sugar, historyPath)
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,15 +1,17 @@
package sesswatch package sesswatch
import ( import (
"log"
"sync" "sync"
"time" "time"
"github.com/curusarn/resh/pkg/records" "github.com/curusarn/resh/internal/records"
"github.com/mitchellh/go-ps" "github.com/mitchellh/go-ps"
"go.uber.org/zap"
) )
type sesswatch struct { type sesswatch struct {
sugar *zap.SugaredLogger
sessionsToDrop []chan string sessionsToDrop []chan string
sleepSeconds uint sleepSeconds uint
@ -18,8 +20,16 @@ type sesswatch struct {
} }
// Go runs the session watcher - watches sessions and sends // Go runs the session watcher - watches sessions and sends
func Go(sessionsToWatch chan records.Record, sessionsToWatchRecords chan records.Record, sessionsToDrop []chan string, sleepSeconds uint) { func Go(sugar *zap.SugaredLogger,
sw := sesswatch{sessionsToDrop: sessionsToDrop, sleepSeconds: sleepSeconds, watchedSessions: map[string]bool{}} sessionsToWatch chan records.Record, sessionsToWatchRecords chan records.Record,
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) go sw.waiter(sessionsToWatch, sessionsToWatchRecords)
} }
@ -31,46 +41,54 @@ func (s *sesswatch) waiter(sessionsToWatch chan records.Record, sessionsToWatchR
// normal way to start watching a session // normal way to start watching a session
id := record.SessionID id := record.SessionID
pid := record.SessionPID pid := record.SessionPID
sugar := s.sugar.With(
"sessionID", record.SessionID,
"sessionPID", record.SessionPID,
)
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
if s.watchedSessions[id] == false { if s.watchedSessions[id] == false {
log.Println("sesswatch: start watching NEW session - id:", id, "; pid:", pid) sugar.Infow("Starting watching new session")
s.watchedSessions[id] = true s.watchedSessions[id] = true
go s.watcher(id, pid) go s.watcher(sugar, id, pid)
} }
case record := <-sessionsToWatchRecords: case record := <-sessionsToWatchRecords:
// additional safety - watch sessions that were never properly initialized // additional safety - watch sessions that were never properly initialized
id := record.SessionID id := record.SessionID
pid := record.SessionPID pid := record.SessionPID
sugar := s.sugar.With(
"sessionID", record.SessionID,
"sessionPID", record.SessionPID,
)
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
if s.watchedSessions[id] == false { if s.watchedSessions[id] == false {
log.Println("sesswatch WARN: start watching NEW session (based on /record) - id:", id, "; pid:", pid) sugar.Warnw("Starting watching new session based on '/record'")
s.watchedSessions[id] = true s.watchedSessions[id] = true
go s.watcher(id, pid) go s.watcher(sugar, id, pid)
} }
} }
}() }()
} }
} }
func (s *sesswatch) watcher(sessionID string, sessionPID int) { func (s *sesswatch) watcher(sugar *zap.SugaredLogger, sessionID string, sessionPID int) {
for { for {
time.Sleep(time.Duration(s.sleepSeconds) * time.Second) time.Sleep(time.Duration(s.sleepSeconds) * time.Second)
proc, err := ps.FindProcess(sessionPID) proc, err := ps.FindProcess(sessionPID)
if err != nil { if err != nil {
log.Println("sesswatch ERROR: error while finding process - pid:", sessionPID) sugar.Errorw("Error while finding process", "error", err)
} else if proc == nil { } else if proc == nil {
log.Println("sesswatch: Dropping session - id:", sessionID, "; pid:", sessionPID) sugar.Infow("Dropping session")
func() { func() {
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
s.watchedSessions[sessionID] = false s.watchedSessions[sessionID] = false
}() }()
for _, ch := range s.sessionsToDrop { for _, ch := range s.sessionsToDrop {
log.Println("sesswatch: sending 'drop session' message ...") sugar.Debugw("Sending 'drop session' message ...")
ch <- sessionID ch <- sessionID
log.Println("sesswatch: sending 'drop session' message DONE") sugar.Debugw("Sending 'drop session' message DONE")
} }
break 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("Timouted 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)
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,
)
}
}

@ -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,21 +0,0 @@
package searchapp
import (
"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)
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,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)
}
}

@ -177,6 +177,7 @@ __resh_set_xdg_home_paths() {
fi fi
mkdir -p "$__RESH_XDG_CONFIG_FILE" >/dev/null 2>/dev/null mkdir -p "$__RESH_XDG_CONFIG_FILE" >/dev/null 2>/dev/null
__RESH_XDG_CONFIG_FILE="$__RESH_XDG_CONFIG_FILE/resh.toml" __RESH_XDG_CONFIG_FILE="$__RESH_XDG_CONFIG_FILE/resh.toml"
export __RESH_XDG_CONFIG_FILE
if [ -z "${XDG_CACHE_HOME-}" ]; then if [ -z "${XDG_CACHE_HOME-}" ]; then
@ -194,4 +195,5 @@ __resh_set_xdg_home_paths() {
__RESH_XDG_DATA_HOME="$XDG_DATA_HOME/resh" __RESH_XDG_DATA_HOME="$XDG_DATA_HOME/resh"
fi fi
mkdir -p "$__RESH_XDG_DATA_HOME" >/dev/null 2>/dev/null mkdir -p "$__RESH_XDG_DATA_HOME" >/dev/null 2>/dev/null
export __RESH_XDG_CONFIG_FILE
} }

Loading…
Cancel
Save