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)
COMMIT=$(shell [ -z "$(git status --untracked-files=no --porcelain)" ] && git rev-parse --short=12 HEAD || echo "no_commit")
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\
@ -27,7 +27,8 @@ uninstall:
# Uninstalling ...
-rm -rf ~/.resh/
bin/resh-%: cmd/%/*.go pkg/*/*.go cmd/control/cmd/*.go cmd/control/status/status.go
go_files = $(shell find -name '*.go')
bin/resh-%: $(go_files)
grep $@ .goreleaser.yml -q # all build targets need to be included in .goreleaser.yml
go build ${GOFLAGS} -o $@ cmd/$*/*.go

@ -7,7 +7,6 @@ import (
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"sort"
@ -15,59 +14,41 @@ import (
"sync"
"time"
"github.com/BurntSushi/toml"
"github.com/awesome-gocui/gocui"
"github.com/curusarn/resh/pkg/cfg"
"github.com/curusarn/resh/pkg/msg"
"github.com/curusarn/resh/pkg/records"
"github.com/curusarn/resh/pkg/searchapp"
"github.com/curusarn/resh/internal/cfg"
"github.com/curusarn/resh/internal/logger"
"github.com/curusarn/resh/internal/msg"
"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"
)
// version from git set during build
// info passed during build
var version string
// commit from git set during build
var commit string
var developement bool
// special constant recognized by RESH wrappers
const exitCodeExecute = 111
var debug bool
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)
os.Exit(exitCode)
}
func runReshCli() (string, int) {
usr, _ := user.Current()
dir := usr.HomeDir
configPath := filepath.Join(dir, "/.config/resh.toml")
logPath := filepath.Join(dir, ".resh/cli.log")
f, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Fatal("Error opening file:", err)
}
defer f.Close()
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")
}
func runReshCli(out *output.Output, config cfg.Config) (string, int) {
sessionID := flag.String("sessionID", "", "resh generated session id")
host := flag.String("host", "", "host")
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!)")
flag.Parse()
errMsg := "Failed to get necessary command-line arguments"
if *sessionID == "" {
log.Println("Error: you need to specify sessionId")
out.Fatal(errMsg, errors.New("missing option --sessionId"))
}
if *host == "" {
log.Println("Error: you need to specify HOST")
out.Fatal(errMsg, errors.New("missing option --host"))
}
if *pwd == "" {
log.Println("Error: you need to specify PWD")
out.Fatal(errMsg, errors.New("missing option --pwd"))
}
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)
if err != nil {
log.Panicln(err)
out.Fatal("Failed to launch TUI", err)
}
defer g.Close()
@ -107,9 +89,9 @@ func runReshCli() (string, int) {
SessionID: *sessionID,
PWD: *pwd,
}
resp = SendCliMsg(mess, strconv.Itoa(config.Port))
resp = SendCliMsg(out, mess, strconv.Itoa(config.Port))
} else {
resp = searchapp.LoadHistoryFromFile(*testHistory, *testHistoryLines)
resp = searchapp.LoadHistoryFromFile(out.Logger.Sugar(), *testHistory, *testHistoryLines)
}
st := state{
@ -128,47 +110,48 @@ func runReshCli() (string, int) {
}
g.SetManager(layout)
errMsg = "Failed to set keybindings"
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 {
log.Panicln(err)
out.Fatal(errMsg, err)
}
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 {
log.Panicln(err)
out.Fatal(errMsg, err)
}
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 {
log.Panicln(err)
out.Fatal(errMsg, err)
}
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 {
log.Panicln(err)
out.Fatal(errMsg, err)
}
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 {
log.Panicln(err)
out.Fatal(errMsg, err)
}
if err := g.SetKeybinding("", gocui.KeyCtrlR, gocui.ModNone, layout.SwitchModes); err != nil {
log.Panicln(err)
out.Fatal(errMsg, err)
}
layout.UpdateData(*query)
layout.UpdateRawData(*query)
err = g.MainLoop()
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
}
@ -190,11 +173,13 @@ type state struct {
}
type manager struct {
out *output.Output
config cfg.Config
sessionID string
host string
pwd string
gitOriginRemote string
config cfg.Config
s *state
}
@ -254,11 +239,11 @@ type dedupRecord struct {
}
func (m manager) UpdateData(input string) {
if debug {
log.Println("EDIT start")
log.Println("len(fullRecords) =", len(m.s.cliRecords))
log.Println("len(data) =", len(m.s.data))
}
sugar := m.out.Logger.Sugar()
sugar.Debugw("Starting data update ...",
"recordCount", len(m.s.cliRecords),
"itemCount", len(m.s.data),
)
query := searchapp.NewQueryFromString(input, m.host, m.pwd, m.gitOriginRemote, m.config.Debug)
var data []searchapp.Item
itemSet := make(map[string]int)
@ -268,7 +253,7 @@ func (m manager) UpdateData(input string) {
itm, err := searchapp.NewItemFromRecordForQuery(rec, query, m.config.Debug)
if err != nil {
// records didn't match the query
// log.Println(" * continue (no match)", rec.Pwd)
// sugar.Println(" * continue (no match)", rec.Pwd)
continue
}
if idx, ok := itemSet[itm.Key]; ok {
@ -285,9 +270,9 @@ func (m manager) UpdateData(input string) {
itemSet[itm.Key] = len(data)
data = append(data, itm)
}
if debug {
log.Println("len(tmpdata) =", len(data))
}
sugar.Debugw("Got new items from records for query, sorting items ...",
"itemCount", len(data),
)
sort.SliceStable(data, func(p, q int) bool {
return data[p].Score > data[q].Score
})
@ -299,19 +284,18 @@ func (m manager) UpdateData(input string) {
m.s.data = append(m.s.data, itm)
}
m.s.highlightedItem = 0
if debug {
log.Println("len(fullRecords) =", len(m.s.cliRecords))
log.Println("len(data) =", len(m.s.data))
log.Println("EDIT end")
}
sugar.Debugw("Done with data update",
"recordCount", len(m.s.cliRecords),
"itemCount", len(m.s.data),
)
}
func (m manager) UpdateRawData(input string) {
if m.config.Debug {
log.Println("EDIT start")
log.Println("len(fullRecords) =", len(m.s.cliRecords))
log.Println("len(data) =", len(m.s.data))
}
sugar := m.out.Logger.Sugar()
sugar.Debugw("Starting RAW data update ...",
"recordCount", len(m.s.cliRecords),
"itemCount", len(m.s.data),
)
query := searchapp.GetRawTermsFromString(input, m.config.Debug)
var data []searchapp.RawItem
itemSet := make(map[string]bool)
@ -321,20 +305,20 @@ func (m manager) UpdateRawData(input string) {
itm, err := searchapp.NewRawItemFromRecordForQuery(rec, query, m.config.Debug)
if err != nil {
// records didn't match the query
// log.Println(" * continue (no match)", rec.Pwd)
// sugar.Println(" * continue (no match)", rec.Pwd)
continue
}
if itemSet[itm.Key] {
// log.Println(" * continue (already present)", itm.key(), itm.pwd)
// sugar.Println(" * continue (already present)", itm.key(), itm.pwd)
continue
}
itemSet[itm.Key] = true
data = append(data, itm)
// log.Println("DATA =", itm.display)
}
if debug {
log.Println("len(tmpdata) =", len(data))
// sugar.Println("DATA =", itm.display)
}
sugar.Debugw("Got new RAW items from records for query, sorting items ...",
"itemCount", len(data),
)
sort.SliceStable(data, func(p, q int) bool {
return data[p].Score > data[q].Score
})
@ -346,11 +330,10 @@ func (m manager) UpdateRawData(input string) {
m.s.rawData = append(m.s.rawData, itm)
}
m.s.highlightedItem = 0
if debug {
log.Println("len(fullRecords) =", len(m.s.cliRecords))
log.Println("len(data) =", len(m.s.data))
log.Println("EDIT end")
}
sugar.Debugw("Done with RAW data update",
"recordCount", len(m.s.cliRecords),
"itemCount", len(m.s.data),
)
}
func (m manager) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
gocui.DefaultEditor.Edit(v, key, ch, mod)
@ -398,7 +381,7 @@ func (m manager) Layout(g *gocui.Gui) error {
v, err := g.SetView("input", 0, 0, maxX-1, 2, b)
if err != nil && !errors.Is(err, gocui.ErrUnknownView) {
log.Panicln(err.Error())
m.out.Fatal("Failed to set view 'input'", err)
}
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)
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.Autoscroll = false
@ -441,6 +424,7 @@ func quit(g *gocui.Gui, v *gocui.View) error {
const smallTerminalTresholdWidth = 110
func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error {
sugar := m.out.Logger.Sugar()
maxX, maxY := g.Size()
compactRenderingMode := false
@ -459,7 +443,7 @@ func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error {
if i == maxY {
break
}
ic := itm.DrawItemColumns(compactRenderingMode, debug)
ic := itm.DrawItemColumns(compactRenderingMode, m.config.Debug)
data = append(data, ic)
if i > maxPossibleMainViewHeight {
// do not stretch columns because of results that will end up outside of the page
@ -505,7 +489,7 @@ func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error {
// header
// header := getHeader()
// error is expected for header
dispStr, _, _ := header.ProduceLine(longestDateLen, longestLocationLen, longestFlagsLen, true, true, debug)
dispStr, _, _ := header.ProduceLine(longestDateLen, longestLocationLen, longestFlagsLen, true, true, m.config.Debug)
dispStr = searchapp.DoHighlightHeader(dispStr, maxX*2)
v.WriteString(dispStr + "\n")
@ -513,33 +497,24 @@ func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error {
for index < len(data) {
itm := data[index]
if index >= mainViewHeight {
if debug {
log.Printf("Finished drawing page. mainViewHeight: %v, predictedMax: %v\n",
mainViewHeight, maxPossibleMainViewHeight)
}
sugar.Debugw("Reached bottom of the page while producing lines",
"mainViewHeight", mainViewHeight,
"predictedMaxViewHeight", maxPossibleMainViewHeight,
)
// page is full
break
}
displayStr, _, err := itm.ProduceLine(longestDateLen, longestLocationLen, longestFlagsLen, false, true, debug)
displayStr, _, err := itm.ProduceLine(longestDateLen, longestLocationLen, longestFlagsLen, false, true, m.config.Debug)
if err != nil {
log.Printf("produceLine error: %v\n", err)
sugar.Error("Error while drawing item", zap.Error(err))
}
if m.s.highlightedItem == index {
// maxX * 2 because there are escape sequences that make it hard to tell the real string length
displayStr = searchapp.DoHighlightString(displayStr, maxX*3)
if debug {
log.Println("### HightlightedItem string :", displayStr)
}
} else if debug {
log.Println(displayStr)
}
if strings.Contains(displayStr, "\n") {
log.Println("display string contained \\n")
displayStr = strings.ReplaceAll(displayStr, "\n", "#")
if debug {
log.Println("display string contained \\n")
}
}
v.WriteString(displayStr + "\n")
index++
@ -553,59 +528,46 @@ func (m manager) normalMode(g *gocui.Gui, v *gocui.View) error {
v.WriteString(line)
}
v.WriteString(helpLine)
if debug {
log.Println("len(data) =", len(m.s.data))
log.Println("highlightedItem =", m.s.highlightedItem)
}
sugar.Debugw("Done drawing page",
"itemCount", len(m.s.data),
"highlightedItemIndex", m.s.highlightedItem,
)
return nil
}
func (m manager) rawMode(g *gocui.Gui, v *gocui.View) error {
sugar := m.out.Logger.Sugar()
maxX, maxY := g.Size()
topBoxSize := 3
m.s.displayedItemsCount = maxY - topBoxSize
for i, itm := range m.s.rawData {
if i == maxY {
if debug {
log.Println(maxY)
}
break
}
displayStr := itm.CmdLineWithColor
if m.s.highlightedItem == i {
// use actual min requried length instead of 420 constant
displayStr = searchapp.DoHighlightString(displayStr, maxX*2)
if debug {
log.Println("### HightlightedItem string :", displayStr)
}
} else if debug {
log.Println(displayStr)
}
if strings.Contains(displayStr, "\n") {
log.Println("display string contained \\n")
displayStr = strings.ReplaceAll(displayStr, "\n", "#")
if debug {
log.Println("display string contained \\n")
}
}
v.WriteString(displayStr + "\n")
// if m.s.highlightedItem == i {
// v.SetHighlight(m.s.highlightedItem, true)
// }
}
if debug {
log.Println("len(data) =", len(m.s.data))
log.Println("highlightedItem =", m.s.highlightedItem)
}
sugar.Debugw("Done drawing page in RAW mode",
"itemCount", len(m.s.data),
"highlightedItemIndex", m.s.highlightedItem,
)
return nil
}
// SendCliMsg to daemon
func SendCliMsg(m msg.CliMsg, port string) msg.CliResponse {
func SendCliMsg(out *output.Output, m msg.CliMsg, port string) msg.CliResponse {
sugar := out.Logger.Sugar()
recJSON, err := json.Marshal(m)
if err != nil {
log.Fatalf("Failed to marshal message: %v\n", err)
out.Fatal("Failed to marshal message", err)
}
req, err := http.NewRequest(
@ -613,7 +575,7 @@ func SendCliMsg(m msg.CliMsg, port string) msg.CliResponse {
"http://localhost:"+port+"/dump",
bytes.NewBuffer(recJSON))
if err != nil {
log.Fatalf("Failed to build request: %v\n", err)
out.Fatal("Failed to build request", err)
}
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)
if err != nil {
log.Fatal("resh-daemon is not running - try restarting this terminal")
out.FatalDaemonNotRunning(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
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{}
err = json.Unmarshal(body, &response)
if err != nil {
log.Fatalf("Unmarshal resp error: %v\n", err)
}
if debug {
log.Printf("Recieved %d records from daemon\n", len(response.CliRecords))
out.Fatal("Failed decode response", err)
}
sugar.Debug("Recieved records from daemon",
"recordCount", len(response.CliRecords),
)
return response
}

@ -3,39 +3,43 @@ package main
import (
"flag"
"fmt"
"log"
"os"
"github.com/BurntSushi/toml"
"github.com/curusarn/resh/pkg/cfg"
"github.com/curusarn/resh/pkg/collect"
"github.com/curusarn/resh/pkg/records"
"github.com/curusarn/resh/internal/cfg"
"github.com/curusarn/resh/internal/collect"
"github.com/curusarn/resh/internal/logger"
"github.com/curusarn/resh/internal/output"
"github.com/curusarn/resh/internal/records"
"go.uber.org/zap"
// "os/exec"
"os/user"
"path/filepath"
"strconv"
)
// version tag from git set during build
// info passed during build
var version string
// Commit hash from git set during build
var commit string
var developement bool
func main() {
usr, _ := user.Current()
dir := usr.HomeDir
configPath := filepath.Join(dir, "/.config/resh.toml")
reshUUIDPath := filepath.Join(dir, "/.resh/resh-uuid")
machineIDPath := "/etc/machine-id"
config, errCfg := cfg.New()
logger, _ := logger.New("collect", 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-collect ERROR")
var config cfg.Config
if _, err := toml.DecodeFile(configPath, &config); err != nil {
log.Fatal("Error reading config:", err)
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"
// version
showVersion := flag.Bool("version", false, "Show version and exit")
showRevision := flag.Bool("revision", false, "Show git revision and exit")
@ -121,29 +125,29 @@ func main() {
realtimeBefore, err := strconv.ParseFloat(*rtb, 64)
if err != nil {
log.Fatal("Flag Parsing error (rtb):", err)
out.Fatal("Error while parsing flag --realtimeBefore", err)
}
realtimeSessionStart, err := strconv.ParseFloat(*rtsess, 64)
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)
if err != nil {
log.Fatal("Flag Parsing error (rt sess boot):", err)
out.Fatal("Error while parsing flag --realtimeSessSinceBoot", err)
}
realtimeSinceSessionStart := realtimeBefore - realtimeSessionStart
realtimeSinceBoot := realtimeSessSinceBoot + realtimeSinceSessionStart
timezoneBeforeOffset := collect.GetTimezoneOffsetInSeconds(*timezoneBefore)
timezoneBeforeOffset := collect.GetTimezoneOffsetInSeconds(logger, *timezoneBefore)
realtimeBeforeLocal := realtimeBefore + timezoneBeforeOffset
realPwd, err := filepath.EvalSymlinks(*pwd)
if err != nil {
log.Println("err while handling pwd realpath:", err)
logger.Error("Error while handling pwd realpath", zap.Error(err))
realPwd = ""
}
gitDir, gitRealDir := collect.GetGitDirs(*gitCdup, *gitCdupExitCode, *pwd)
gitDir, gitRealDir := collect.GetGitDirs(logger, *gitCdup, *gitCdupExitCode, *pwd)
if *gitRemoteExitCode != 0 {
*gitRemote = ""
}
@ -218,5 +222,5 @@ func main() {
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"
"fmt"
"os"
"os/user"
"path/filepath"
"strings"
"github.com/BurntSushi/toml"
"github.com/curusarn/resh/pkg/cfg"
"github.com/curusarn/resh/internal/cfg"
"github.com/curusarn/resh/internal/logger"
"go.uber.org/zap"
)
// info passed during build
var version string
var commit string
var developement bool
func main() {
usr, _ := user.Current()
dir := usr.HomeDir
configPath := filepath.Join(dir, ".config/resh.toml")
var config cfg.Config
_, err := toml.DecodeFile(configPath, &config)
if err != nil {
fmt.Println("Error reading config", err)
os.Exit(1)
config, errCfg := cfg.New()
logger, _ := logger.New("config", config.LogLevel, developement)
defer logger.Sync() // flushes buffer, if any
if errCfg != nil {
logger.Error("Error while getting configuration", zap.Error(errCfg))
}
configKey := flag.String("key", "", "Key of the requested config entry")

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

@ -1,66 +0,0 @@
package cmd
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/curusarn/resh/cmd/control/status"
"github.com/spf13/cobra"
)
var debugCmd = &cobra.Command{
Use: "debug",
Short: "debug utils for resh",
Long: "Reloads resh rc files. Shows logs and output from last runs of resh",
}
var debugReloadCmd = &cobra.Command{
Use: "reload",
Short: "reload resh rc files",
Long: "Reload resh rc files",
Run: func(cmd *cobra.Command, args []string) {
exitCode = status.ReloadRcFiles
},
}
var debugInspectCmd = &cobra.Command{
Use: "inspect",
Short: "inspect session history",
Run: func(cmd *cobra.Command, args []string) {
exitCode = status.InspectSessionHistory
},
}
var debugOutputCmd = &cobra.Command{
Use: "output",
Short: "shows output from last runs of resh",
Long: "Shows output from last runs of resh",
Run: func(cmd *cobra.Command, args []string) {
files := []string{
"daemon_last_run_out.txt",
"collect_last_run_out.txt",
"postcollect_last_run_out.txt",
"session_init_last_run_out.txt",
"cli_last_run_out.txt",
}
dir := os.Getenv("__RESH_XDG_CACHE_HOME")
for _, fpath := range files {
fpath := filepath.Join(dir, fpath)
debugReadFile(fpath)
}
exitCode = status.Success
},
}
func debugReadFile(path string) {
fmt.Println("============================================================")
fmt.Println(" filepath:", path)
fmt.Println("============================================================")
dat, err := ioutil.ReadFile(path)
if err != nil {
fmt.Println("ERROR while reading file:", err)
}
fmt.Println(string(dat))
}

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

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

@ -4,13 +4,11 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"github.com/curusarn/resh/cmd/control/status"
"github.com/curusarn/resh/pkg/msg"
"github.com/curusarn/resh/internal/msg"
"github.com/spf13/cobra"
)
@ -24,13 +22,13 @@ var versionCmd = &cobra.Command{
commitEnv := getEnvVarWithDefault("__RESH_REVISION", "<unknown>")
printVersion("This terminal session", versionEnv, commitEnv)
// TODO: use output.Output.Error... for these
resp, err := getDaemonStatus(config.Port)
if err != nil {
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, "-> 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")
exitCode = status.Fail
return
}
printVersion("Currently running daemon", resp.Version, resp.Commit)
@ -48,7 +46,6 @@ var versionCmd = &cobra.Command{
return
}
exitCode = status.ReshStatus
},
}
@ -74,11 +71,11 @@ func getDaemonStatus(port int) (msg.StatusResponse, error) {
defer resp.Body.Close()
jsn, err := ioutil.ReadAll(resp.Body)
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)
if err != nil {
log.Fatal("Error while decoding 'daemon /status' response:", err)
out.Fatal("Error while decoding 'daemon /status' response", err)
}
return mess, nil
}

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

@ -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
import (
//"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/user"
"os/exec"
"path/filepath"
"strconv"
"strings"
"github.com/BurntSushi/toml"
"github.com/curusarn/resh/pkg/cfg"
"github.com/curusarn/resh/internal/cfg"
"github.com/curusarn/resh/internal/httpclient"
"github.com/curusarn/resh/internal/logger"
"go.uber.org/zap"
)
// version from git set during build
// info passed during build
var version string
// commit from git set during build
var commit string
// Debug switch
var Debug = false
var developement bool
func main() {
log.Println("Daemon starting... \n" +
"version: " + version +
" commit: " + commit)
usr, _ := user.Current()
dir := usr.HomeDir
pidfilePath := filepath.Join(dir, ".resh/resh.pid")
configPath := filepath.Join(dir, ".config/resh.toml")
reshHistoryPath := filepath.Join(dir, ".resh_history.json")
bashHistoryPath := filepath.Join(dir, ".bash_history")
zshHistoryPath := filepath.Join(dir, ".zsh_history")
logPath := filepath.Join(dir, ".resh/daemon.log")
f, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Fatalf("Error opening file: %v\n", err)
config, errCfg := cfg.New()
logger, _ := logger.New("daemon", config.LogLevel, developement)
defer logger.Sync() // flushes buffer, if any
if errCfg != nil {
logger.Error("Error while getting configuration", zap.Error(errCfg))
}
defer f.Close()
sugar := logger.Sugar()
d := daemon{sugar: sugar}
sugar.Infow("Deamon starting ...",
"version", version,
"commit", commit,
)
log.SetOutput(f)
log.SetPrefix(strconv.Itoa(os.Getpid()) + " | ")
// xdgCacheHome := d.getEnvOrPanic("__RESH_XDG_CACHE_HOME")
// xdgDataHome := d.getEnvOrPanic("__RESH_XDG_DATA_HOME")
var config cfg.Config
if _, err := toml.DecodeFile(configPath, &config); err != nil {
log.Printf("Error reading config: %v\n", err)
return
}
if config.Debug {
Debug = true
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
// TODO: rethink PID file and logs location
homeDir, err := os.UserHomeDir()
if err != nil {
sugar.Fatalw("Could not get user home dir", zap.Error(err))
}
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 {
log.Printf("Error while checking if the daemon is runnnig"+
" - it's probably not running: %v\n", err)
sugar.Errorw("Error while checking daemon status - "+
"it's probably not running", "error", err)
}
if res {
log.Println("Daemon is already running - exiting!")
sugar.Errorw("Daemon is already running - exiting!")
return
}
_, err = os.Stat(pidfilePath)
_, err = os.Stat(PIDFile)
if err == nil {
log.Println("Pidfile exists")
sugar.Warn("Pidfile exists")
// kill daemon
err = killDaemon(pidfilePath)
err = d.killDaemon(PIDFile)
if err != nil {
log.Printf("Error while killing daemon: %v\n", err)
sugar.Errorw("Could not kill daemon",
"error", err,
)
}
}
err = ioutil.WriteFile(pidfilePath, []byte(strconv.Itoa(os.Getpid())), 0644)
err = 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 {
log.Fatalf("Could not create pidfile: %v\n", err)
return fmt.Errorf("could not parse PID file contents: %w", err)
}
runServer(config, reshHistoryPath, bashHistoryPath, zshHistoryPath)
log.Println("main: Removing pidfile ...")
err = os.Remove(pidfilePath)
d.sugar.Infow("Successfully parsed PID", "PID", pid)
cmd := exec.Command("kill", "-s", "sigint", strconv.Itoa(pid))
err = cmd.Run()
if err != nil {
log.Printf("Could not delete pidfile: %v\n", err)
return fmt.Errorf("kill command finished with error: %w", err)
}
log.Println("main: Shutdown - bye")
return nil
}

@ -3,45 +3,58 @@ package main
import (
"encoding/json"
"io/ioutil"
"log"
"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 {
sugar *zap.SugaredLogger
subscribers []chan records.Record
}
func (h *recordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
sugar := h.sugar.With(zap.String("endpoint", "/record"))
sugar.Debugw("Handling request, sending response, reading body ...")
w.Write([]byte("OK\n"))
jsn, err := ioutil.ReadAll(r.Body)
// run rest of the handler as goroutine to prevent any hangups
go func() {
if err != nil {
log.Println("Error reading the body", err)
sugar.Errorw("Error reading body", "error", err)
return
}
sugar.Debugw("Unmarshaling record ...")
record := records.Record{}
err = json.Unmarshal(jsn, &record)
if err != nil {
log.Println("Decoding error: ", err)
log.Println("Payload: ", jsn)
sugar.Errorw("Error during unmarshaling",
"error", err,
"payload", jsn,
)
return
}
part := "2"
if record.PartOne {
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 {
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"
"strconv"
"github.com/curusarn/resh/pkg/cfg"
"github.com/curusarn/resh/pkg/histfile"
"github.com/curusarn/resh/pkg/records"
"github.com/curusarn/resh/pkg/sesswatch"
"github.com/curusarn/resh/pkg/signalhandler"
"github.com/curusarn/resh/internal/cfg"
"github.com/curusarn/resh/internal/histfile"
"github.com/curusarn/resh/internal/records"
"github.com/curusarn/resh/internal/sesswatch"
"github.com/curusarn/resh/internal/signalhandler"
"go.uber.org/zap"
)
func runServer(config cfg.Config, reshHistoryPath, bashHistoryPath, zshHistoryPath string) {
// 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 sessionInitSubscribers []chan records.Record
var sessionDropSubscribers []chan string
@ -29,8 +41,8 @@ func runServer(config cfg.Config, reshHistoryPath, bashHistoryPath, zshHistoryPa
signalSubscribers = append(signalSubscribers, histfileSignals)
maxHistSize := 10000 // lines
minHistSizeKB := 2000 // roughly lines
histfileBox := histfile.New(histfileRecords, histfileSessionsToDrop,
reshHistoryPath, bashHistoryPath, zshHistoryPath,
histfileBox := histfile.New(s.sugar, histfileRecords, histfileSessionsToDrop,
s.reshHistoryPath, s.bashHistoryPath, s.zshHistoryPath,
maxHistSize, minHistSizeKB,
histfileSignals, shutdown)
@ -39,21 +51,27 @@ func runServer(config cfg.Config, reshHistoryPath, bashHistoryPath, zshHistoryPa
recordSubscribers = append(recordSubscribers, sesswatchRecords)
sesswatchSessionsToWatch := make(chan records.Record)
sessionInitSubscribers = append(sessionInitSubscribers, sesswatchSessionsToWatch)
sesswatch.Go(sesswatchSessionsToWatch, sesswatchRecords, sessionDropSubscribers, config.SesswatchPeriodSeconds)
sesswatch.Go(
s.sugar,
sesswatchSessionsToWatch,
sesswatchRecords,
sessionDropSubscribers,
s.config.SesswatchPeriodSeconds,
)
// handlers
mux := http.NewServeMux()
mux.HandleFunc("/status", statusHandler)
mux.Handle("/record", &recordHandler{subscribers: recordSubscribers})
mux.Handle("/session_init", &sessionInitHandler{subscribers: sessionInitSubscribers})
mux.Handle("/dump", &dumpHandler{histfileBox: histfileBox})
mux.Handle("/status", &statusHandler{sugar: s.sugar})
mux.Handle("/record", &recordHandler{sugar: s.sugar, subscribers: recordSubscribers})
mux.Handle("/session_init", &sessionInitHandler{sugar: s.sugar, subscribers: sessionInitSubscribers})
mux.Handle("/dump", &dumpHandler{sugar: s.sugar, histfileBox: histfileBox})
server := &http.Server{
Addr: "localhost:" + strconv.Itoa(config.Port),
Addr: "localhost:" + strconv.Itoa(s.config.Port),
Handler: mux,
}
go server.ListenAndServe()
// signalhandler - takes over the main goroutine so when signal handler exists the whole program exits
signalhandler.Run(signalSubscribers, shutdown, server)
signalhandler.Run(s.sugar, signalSubscribers, shutdown, server)
}

@ -3,36 +3,48 @@ package main
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
"github.com/curusarn/resh/pkg/records"
"github.com/curusarn/resh/internal/records"
"go.uber.org/zap"
)
type sessionInitHandler struct {
sugar *zap.SugaredLogger
subscribers []chan records.Record
}
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"))
// TODO: should we somehow check for errors here?
jsn, err := ioutil.ReadAll(r.Body)
// run rest of the handler as goroutine to prevent any hangups
go func() {
if err != nil {
log.Println("Error reading the body", err)
sugar.Errorw("Error reading body", "error", err)
return
}
sugar.Debugw("Unmarshaling record ...")
record := records.Record{}
err = json.Unmarshal(jsn, &record)
if err != nil {
log.Println("Decoding error: ", err)
log.Println("Payload: ", jsn)
sugar.Errorw("Error during unmarshaling",
"error", err,
"payload", jsn,
)
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 {
sub <- record
}
sugar.Debugw("Session sent to subscribers")
}()
}

@ -2,16 +2,19 @@ package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"github.com/curusarn/resh/pkg/httpclient"
"github.com/curusarn/resh/pkg/msg"
"github.com/curusarn/resh/internal/msg"
"go.uber.org/zap"
)
func statusHandler(w http.ResponseWriter, r *http.Request) {
log.Println("/status START")
type statusHandler struct {
sugar *zap.SugaredLogger
}
func (h *statusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
sugar := h.sugar.With(zap.String("endpoint", "/status"))
sugar.Debugw("Handling request ...")
resp := msg.StatusResponse{
Status: true,
Version: version,
@ -19,23 +22,12 @@ func statusHandler(w http.ResponseWriter, r *http.Request) {
}
jsn, err := json.Marshal(&resp)
if err != nil {
log.Println("Encoding error:", err)
log.Println("Response:", resp)
sugar.Errorw("Error when marshaling",
"error", err,
"response", resp,
)
return
}
w.Write(jsn)
log.Println("/status END")
}
func isDaemonRunning(port int) (bool, error) {
url := "http://localhost:" + strconv.Itoa(port) + "/status"
client := httpclient.New()
resp, err := client.Get(url)
if err != nil {
log.Printf("Error while checking daemon status - "+
"it's probably not running: %v\n", err)
return false, err
}
defer resp.Body.Close()
return true, nil
sugar.Infow("Request handled")
}

@ -3,38 +3,42 @@ package main
import (
"flag"
"fmt"
"log"
"os"
"github.com/BurntSushi/toml"
"github.com/curusarn/resh/pkg/cfg"
"github.com/curusarn/resh/pkg/collect"
"github.com/curusarn/resh/pkg/records"
"github.com/curusarn/resh/internal/cfg"
"github.com/curusarn/resh/internal/collect"
"github.com/curusarn/resh/internal/logger"
"github.com/curusarn/resh/internal/output"
"github.com/curusarn/resh/internal/records"
"go.uber.org/zap"
// "os/exec"
"os/user"
"path/filepath"
"strconv"
)
// version from git set during build
// info passed during build
var version string
// commit from git set during build
var commit string
var developement bool
func main() {
usr, _ := user.Current()
dir := usr.HomeDir
configPath := filepath.Join(dir, "/.config/resh.toml")
reshUUIDPath := filepath.Join(dir, "/.resh/resh-uuid")
config, errCfg := cfg.New()
logger, _ := logger.New("postcollect", 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-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"
var config cfg.Config
if _, err := toml.DecodeFile(configPath, &config); err != nil {
log.Fatal("Error reading config:", err)
}
showVersion := flag.Bool("version", false, "Show version and exit")
showRevision := flag.Bool("revision", false, "Show git revision and exit")
@ -92,24 +96,24 @@ func main() {
}
realtimeAfter, err := strconv.ParseFloat(*rta, 64)
if err != nil {
log.Fatal("Flag Parsing error (rta):", err)
out.Fatal("Error while parsing flag --realtimeAfter", err)
}
realtimeBefore, err := strconv.ParseFloat(*rtb, 64)
if err != nil {
log.Fatal("Flag Parsing error (rtb):", err)
out.Fatal("Error while parsing flag --realtimeBefore", err)
}
realtimeDuration := realtimeAfter - realtimeBefore
timezoneAfterOffset := collect.GetTimezoneOffsetInSeconds(*timezoneAfter)
timezoneAfterOffset := collect.GetTimezoneOffsetInSeconds(logger, *timezoneAfter)
realtimeAfterLocal := realtimeAfter + timezoneAfterOffset
realPwdAfter, err := filepath.EvalSymlinks(*pwdAfter)
if err != nil {
log.Println("err while handling pwdAfter realpath:", err)
logger.Error("Error while handling pwdAfter realpath", zap.Error(err))
realPwdAfter = ""
}
gitDirAfter, gitRealDirAfter := collect.GetGitDirs(*gitCdupAfter, *gitCdupExitCodeAfter, *pwdAfter)
gitDirAfter, gitRealDirAfter := collect.GetGitDirs(logger, *gitCdupAfter, *gitCdupExitCodeAfter, *pwdAfter)
if *gitRemoteExitCodeAfter != 0 {
*gitRemoteAfter = ""
}
@ -150,5 +154,5 @@ func main() {
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 (
"flag"
"fmt"
"log"
"os"
"github.com/BurntSushi/toml"
"github.com/curusarn/resh/pkg/cfg"
"github.com/curusarn/resh/pkg/collect"
"github.com/curusarn/resh/pkg/records"
"github.com/curusarn/resh/internal/cfg"
"github.com/curusarn/resh/internal/collect"
"github.com/curusarn/resh/internal/logger"
"github.com/curusarn/resh/internal/output"
"github.com/curusarn/resh/internal/records"
"go.uber.org/zap"
"os/user"
"path/filepath"
"strconv"
)
// version from git set during build
// info passed during build
var version string
// commit from git set during build
var commit string
var developement bool
func main() {
usr, _ := user.Current()
dir := usr.HomeDir
configPath := filepath.Join(dir, "/.config/resh.toml")
reshUUIDPath := filepath.Join(dir, "/.resh/resh-uuid")
config, errCfg := cfg.New()
logger, _ := logger.New("collect", 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-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"
var config cfg.Config
if _, err := toml.DecodeFile(configPath, &config); err != nil {
log.Fatal("Error reading config:", err)
}
showVersion := flag.Bool("version", false, "Show version and exit")
showRevision := flag.Bool("revision", false, "Show git revision and exit")
@ -106,20 +111,20 @@ func main() {
}
realtimeBefore, err := strconv.ParseFloat(*rtb, 64)
if err != nil {
log.Fatal("Flag Parsing error (rtb):", err)
out.Fatal("Error while parsing flag --realtimeBefore", err)
}
realtimeSessionStart, err := strconv.ParseFloat(*rtsess, 64)
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)
if err != nil {
log.Fatal("Flag Parsing error (rt sess boot):", err)
out.Fatal("Error while parsing flag --realtimeSessSinceBoot", err)
}
realtimeSinceSessionStart := realtimeBefore - realtimeSessionStart
realtimeSinceBoot := realtimeSessSinceBoot + realtimeSinceSessionStart
timezoneBeforeOffset := collect.GetTimezoneOffsetInSeconds(*timezoneBefore)
timezoneBeforeOffset := collect.GetTimezoneOffsetInSeconds(logger, *timezoneBefore)
realtimeBeforeLocal := realtimeBefore + timezoneBeforeOffset
if *osReleaseID == "" {
@ -182,5 +187,5 @@ func main() {
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
sesswatchPeriodSeconds = 120
sesshistInitHistorySize = 1000
debug = false
bindControlR = true
logVerbosity = info

@ -1,19 +1,27 @@
module github.com/curusarn/resh
go 1.16
go 1.18
require (
github.com/BurntSushi/toml v0.4.1
github.com/awesome-gocui/gocui v1.0.0
github.com/coreos/go-semver v0.3.0
github.com/gdamore/tcell/v2 v2.4.0 // indirect
github.com/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/mitchellh/go-ps v1.0.0
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
)
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/term v0.0.0-20210615171337-6886f2dfbf5b // 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/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/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@ -57,11 +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-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -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/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@ -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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/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.27/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.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/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/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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210903071746-97244b99971b h1:3Dq0eVHn0uaQJmPO+/aYPI/fRMqdrVDbu7MQcku54gg=
golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/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.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -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/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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=

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

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

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

@ -1,9 +1,11 @@
package histlist
import "log"
import "go.uber.org/zap"
// Histlist is a deduplicated list of cmdLines
type Histlist struct {
// TODO: I'm not excited about logger being passed here
sugar *zap.SugaredLogger
// list of commands lines (deduplicated)
List []string
// lookup: cmdLine -> last index
@ -11,13 +13,16 @@ type Histlist struct {
}
// New Histlist
func New() Histlist {
return Histlist{LastIndex: make(map[string]int)}
func New(sugar *zap.SugaredLogger) Histlist {
return Histlist{
sugar: sugar.With("component", "histlist"),
LastIndex: make(map[string]int),
}
}
// Copy Histlist
func Copy(hl Histlist) Histlist {
newHl := New()
newHl := New(hl.sugar)
// copy list
newHl.List = make([]string, len(hl.List))
copy(newHl.List, hl.List)
@ -36,7 +41,10 @@ func (h *Histlist) AddCmdLine(cmdLine string) {
if found {
// remove duplicate
if cmdLine != h.List[idx] {
log.Println("histlist ERROR: Adding cmdLine:", cmdLine, " != LastIndex[cmdLine]:", h.List[idx])
h.sugar.DPanicw("Index key is different than actual cmd line in the list",
"indexKeyCmdLine", cmdLine,
"actualCmdLine", h.List[idx],
)
}
h.List = append(h.List[:idx], h.List[idx+1:]...)
// idx++
@ -44,7 +52,10 @@ func (h *Histlist) AddCmdLine(cmdLine string) {
cmdLn := h.List[idx]
h.LastIndex[cmdLn]--
if idx != h.LastIndex[cmdLn] {
log.Println("histlist ERROR: Shifting LastIndex idx:", idx, " != LastIndex[cmdLn]:", h.LastIndex[cmdLn])
h.sugar.DPanicw("Index position is different than actual position of the cmd line",
"actualPosition", idx,
"indexedPosition", h.LastIndex[cmdLn],
)
}
idx++
}
@ -53,7 +64,10 @@ func (h *Histlist) AddCmdLine(cmdLine string) {
h.LastIndex[cmdLine] = len(h.List)
// append new cmdline
h.List = append(h.List, cmdLine)
// log.Println("histlist: Added cmdLine:", cmdLine, "; history length:", lenBefore, "->", len(h.List))
h.sugar.Debugw("Added cmdLine",
"cmdLine", cmdLine,
"historyLength", len(h.List),
)
}
// AddHistlist contents of another histlist to this histlist

@ -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
import "github.com/curusarn/resh/pkg/records"
import "github.com/curusarn/resh/internal/records"
// 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"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"math"
"os"
"strconv"
"strings"
"github.com/curusarn/resh/pkg/histlist"
"github.com/curusarn/resh/internal/histlist"
"github.com/mattn/go-shellwords"
"go.uber.org/zap"
)
// BaseRecord - common base for Record and FallbackRecord
@ -219,14 +220,14 @@ func Enriched(r Record) EnrichedRecord {
if err != nil {
record.Errors = append(record.Errors, "Validate error:"+err.Error())
// rec, _ := record.ToString()
// log.Println("Invalid command:", rec)
// sugar.Println("Invalid command:", rec)
record.Invalid = true
}
record.Command, record.FirstWord, err = GetCommandAndFirstWord(r.CmdLine)
if err != nil {
record.Errors = append(record.Errors, "GetCommandAndFirstWord error:"+err.Error())
// rec, _ := record.ToString()
// log.Println("Invalid command:", rec)
// sugar.Println("Invalid command:", rec)
record.Invalid = true // should this be really invalid ?
}
return record
@ -327,7 +328,7 @@ func (r *EnrichedRecord) SetCmdLine(cmdLine string) {
r.Command, r.FirstWord, err = GetCommandAndFirstWord(cmdLine)
if err != nil {
r.Errors = append(r.Errors, "GetCommandAndFirstWord error:"+err.Error())
// log.Println("Invalid command:", r.CmdLine)
// sugar.Println("Invalid command:", r.CmdLine)
r.Invalid = true
}
}
@ -352,7 +353,7 @@ func Stripped(r EnrichedRecord) EnrichedRecord {
func GetCommandAndFirstWord(cmdLine string) (string, string, error) {
args, err := shellwords.Parse(cmdLine)
if err != nil {
// log.Println("shellwords Error:", err, " (cmdLine: <", cmdLine, "> )")
// Println("shellwords Error:", err, " (cmdLine: <", cmdLine, "> )")
return "", "", err
}
if len(args) == 0 {
@ -361,15 +362,14 @@ func GetCommandAndFirstWord(cmdLine string) (string, string, error) {
i := 0
for true {
// commands in shell sometimes look like this `variable=something command argument otherArgument --option`
// to get the command we skip over tokens that contain '='
// to get the command we skip over tokens that contain '='
if strings.ContainsRune(args[i], '=') && len(args) > i+1 {
i++
continue
}
return args[i], args[0], nil
}
log.Fatal("GetCommandAndFirstWord error: this should not happen!")
return "ERROR", "ERROR", errors.New("this should not happen - contact developer ;)")
return "ERROR", "ERROR", errors.New("failed to retrieve first word of command")
}
// NormalizeGitRemote func
@ -511,22 +511,19 @@ func (r *EnrichedRecord) DistanceTo(r2 EnrichedRecord, p DistParams) float64 {
}
// LoadFromFile loads records from 'fname' file
func LoadFromFile(fname string) []Record {
const allowedErrors = 2
func LoadFromFile(sugar *zap.SugaredLogger, fname string) []Record {
const allowedErrors = 3
var encounteredErrors int
// NOTE: limit does nothing atm
var recs []Record
file, err := os.Open(fname)
if err != nil {
log.Println("Open() resh history file error:", err)
log.Println("WARN: Skipping reading resh history!")
sugar.Error("Failed to open resh history file - skipping reading resh history", zap.Error(err))
return recs
}
defer file.Close()
reader := bufio.NewReader(file)
var i int
var firstErrLine int
for {
var line string
line, err = reader.ReadString('\n')
@ -540,14 +537,16 @@ func LoadFromFile(fname string) []Record {
if err != nil {
err = json.Unmarshal([]byte(line), &fallbackRecord)
if err != nil {
if encounteredErrors == 0 {
firstErrLine = i
}
encounteredErrors++
log.Println("Line:", line)
log.Println("Decoding error:", err)
sugar.Error("Could not decode line in resh history file",
"lineContents", line,
"lineNumber", i,
zap.Error(err),
)
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)
@ -555,32 +554,43 @@ func LoadFromFile(fname string) []Record {
recs = append(recs, record)
}
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 {
// 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"
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)
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)
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
}
func copyFile(source, dest string) error {
from, err := os.Open(source)
if err != nil {
// log.Println("Open() resh history file error:", err)
return err
}
defer from.Close()
@ -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.Create(dest)
if err != nil {
// log.Println("Create() resh history backup error:", err)
return err
}
defer to.Close()
_, err = io.Copy(to, from)
if err != nil {
// log.Println("Copy() resh history to backup error:", err)
return err
}
return nil
@ -604,14 +612,13 @@ func copyFile(source, dest string) error {
func writeHistory(fname string, history []Record) error {
file, err := os.Create(fname)
if err != nil {
// log.Println("Create() resh history error:", err)
return err
}
defer file.Close()
for _, rec := range history {
jsn, err := json.Marshal(rec)
if err != nil {
log.Fatalln("Encode error!")
return fmt.Errorf("failed to encode record: %w", err)
}
file.Write(append(jsn, []byte("\n")...))
}
@ -619,12 +626,11 @@ func writeHistory(fname string, history []Record) error {
}
// LoadCmdLinesFromZshFile loads cmdlines from zsh history file
func LoadCmdLinesFromZshFile(fname string) histlist.Histlist {
hl := histlist.New()
func LoadCmdLinesFromZshFile(sugar *zap.SugaredLogger, fname string) histlist.Histlist {
hl := histlist.New(sugar)
file, err := os.Open(fname)
if err != nil {
log.Println("Open() zsh history file error:", err)
log.Println("WARN: Skipping reading zsh history!")
sugar.Error("Failed to open zsh history file - skipping reading zsh history", zap.Error(err))
return hl
}
defer file.Close()
@ -656,12 +662,11 @@ func LoadCmdLinesFromZshFile(fname string) histlist.Histlist {
}
// LoadCmdLinesFromBashFile loads cmdlines from bash history file
func LoadCmdLinesFromBashFile(fname string) histlist.Histlist {
hl := histlist.New()
func LoadCmdLinesFromBashFile(sugar *zap.SugaredLogger, fname string) histlist.Histlist {
hl := histlist.New(sugar)
file, err := os.Open(fname)
if err != nil {
log.Println("Open() bash history file error:", err)
log.Println("WARN: Skipping reading bash history!")
sugar.Error("Failed to open bash history file - skipping reading bash history", zap.Error(err))
return hl
}
defer file.Close()

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

@ -2,13 +2,12 @@ package searchapp
import (
"fmt"
"log"
"math"
"strconv"
"strings"
"time"
"github.com/curusarn/resh/pkg/records"
"github.com/curusarn/resh/internal/records"
"golang.org/x/exp/utf8string"
)
@ -228,9 +227,6 @@ func produceLocation(length int, host string, pwdTilde string, differentHost boo
shrinkFactor := float64(length) / float64(totalLen)
shrinkedHostLen := int(math.Ceil(float64(hostLen) * shrinkFactor))
if debug {
log.Printf("shrinkFactor: %f\n", shrinkFactor)
}
halfLocationLen := length/2 - colonLen
newHostLen = minInt(hostLen, shrinkedHostLen, halfLocationLen)

@ -1,7 +1,6 @@
package searchapp
import (
"log"
"sort"
"strings"
)
@ -37,26 +36,16 @@ func filterTerms(terms []string) []string {
// NewQueryFromString .
func NewQueryFromString(queryInput string, host string, pwd string, gitOriginRemote string, debug bool) Query {
if debug {
log.Println("QUERY input = <" + queryInput + ">")
}
terms := strings.Fields(queryInput)
var logStr string
for _, term := range terms {
logStr += " <" + term + ">"
}
if debug {
log.Println("QUERY raw terms =" + logStr)
}
terms = filterTerms(terms)
logStr = ""
for _, term := range terms {
logStr += " <" + term + ">"
}
if debug {
log.Println("QUERY filtered terms =" + logStr)
log.Println("QUERY pwd =" + pwd)
}
sort.SliceStable(terms, func(i, j int) bool { return len(terms[i]) < len(terms[j]) })
return Query{
terms: terms,
@ -68,17 +57,11 @@ func NewQueryFromString(queryInput string, host string, pwd string, gitOriginRem
// GetRawTermsFromString .
func GetRawTermsFromString(queryInput string, debug bool) []string {
if debug {
log.Println("QUERY input = <" + queryInput + ">")
}
terms := strings.Fields(queryInput)
var logStr string
for _, term := range terms {
logStr += " <" + term + ">"
}
if debug {
log.Println("QUERY raw terms =" + logStr)
}
terms = filterTerms(terms)
logStr = ""
for _, term := range terms {

@ -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
import (
"log"
"sync"
"time"
"github.com/curusarn/resh/pkg/records"
"github.com/curusarn/resh/internal/records"
"github.com/mitchellh/go-ps"
"go.uber.org/zap"
)
type sesswatch struct {
sugar *zap.SugaredLogger
sessionsToDrop []chan string
sleepSeconds uint
@ -18,8 +20,16 @@ type sesswatch struct {
}
// Go runs the session watcher - watches sessions and sends
func Go(sessionsToWatch chan records.Record, sessionsToWatchRecords chan records.Record, sessionsToDrop []chan string, sleepSeconds uint) {
sw := sesswatch{sessionsToDrop: sessionsToDrop, sleepSeconds: sleepSeconds, watchedSessions: map[string]bool{}}
func Go(sugar *zap.SugaredLogger,
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)
}
@ -31,46 +41,54 @@ func (s *sesswatch) waiter(sessionsToWatch chan records.Record, sessionsToWatchR
// normal way to start watching a session
id := record.SessionID
pid := record.SessionPID
sugar := s.sugar.With(
"sessionID", record.SessionID,
"sessionPID", record.SessionPID,
)
s.mutex.Lock()
defer s.mutex.Unlock()
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
go s.watcher(id, pid)
go s.watcher(sugar, id, pid)
}
case record := <-sessionsToWatchRecords:
// additional safety - watch sessions that were never properly initialized
id := record.SessionID
pid := record.SessionPID
sugar := s.sugar.With(
"sessionID", record.SessionID,
"sessionPID", record.SessionPID,
)
s.mutex.Lock()
defer s.mutex.Unlock()
if s.watchedSessions[id] == false {
log.Println("sesswatch WARN: start watching NEW session (based on /record) - id:", id, "; pid:", pid)
sugar.Warnw("Starting watching new session based on '/record'")
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 {
time.Sleep(time.Duration(s.sleepSeconds) * time.Second)
proc, err := ps.FindProcess(sessionPID)
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 {
log.Println("sesswatch: Dropping session - id:", sessionID, "; pid:", sessionPID)
sugar.Infow("Dropping session")
func() {
s.mutex.Lock()
defer s.mutex.Unlock()
s.watchedSessions[sessionID] = false
}()
for _, ch := range s.sessionsToDrop {
log.Println("sesswatch: sending 'drop session' message ...")
sugar.Debugw("Sending 'drop session' message ...")
ch <- sessionID
log.Println("sesswatch: sending 'drop session' message DONE")
sugar.Debugw("Sending 'drop session' message DONE")
}
break
}

@ -0,0 +1,74 @@
package signalhandler
import (
"context"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"go.uber.org/zap"
)
func sendSignals(sugar *zap.SugaredLogger, sig os.Signal, subscribers []chan os.Signal, done chan string) {
for _, sub := range subscribers {
sub <- sig
}
sugar.Warnw("Sent shutdown signals to components")
chanCount := len(subscribers)
start := time.Now()
delay := time.Millisecond * 100
timeout := time.Millisecond * 2000
for {
select {
case _ = <-done:
chanCount--
if chanCount == 0 {
sugar.Warnw("All components shut down successfully")
return
}
default:
time.Sleep(delay)
}
if time.Since(start) > timeout {
sugar.Errorw("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
mkdir -p "$__RESH_XDG_CONFIG_FILE" >/dev/null 2>/dev/null
__RESH_XDG_CONFIG_FILE="$__RESH_XDG_CONFIG_FILE/resh.toml"
export __RESH_XDG_CONFIG_FILE
if [ -z "${XDG_CACHE_HOME-}" ]; then
@ -194,4 +195,5 @@ __resh_set_xdg_home_paths() {
__RESH_XDG_DATA_HOME="$XDG_DATA_HOME/resh"
fi
mkdir -p "$__RESH_XDG_DATA_HOME" >/dev/null 2>/dev/null
export __RESH_XDG_CONFIG_FILE
}

Loading…
Cancel
Save