From f5349fc45bbbdbd8730f40ce936ac9b164bedf56 Mon Sep 17 00:00:00 2001 From: Simon Let Date: Tue, 3 May 2022 02:52:54 +0200 Subject: [PATCH] structured logging, refactoring, improvements --- Makefile | 5 +- cmd/cli/main.go | 242 ++++---- cmd/collect/main.go | 54 +- cmd/config/main.go | 26 +- cmd/control/cmd/completion.go | 3 - cmd/control/cmd/debug.go | 66 --- cmd/control/cmd/root.go | 46 +- cmd/control/cmd/update.go | 16 +- cmd/control/cmd/version.go | 11 +- cmd/control/main.go | 4 +- cmd/control/status/status.go | 25 - cmd/daemon/dump.go | 35 +- cmd/daemon/kill.go | 28 - cmd/daemon/main.go | 160 ++++-- cmd/daemon/record.go | 35 +- cmd/daemon/run-server.go | 48 +- cmd/daemon/session-init.go | 24 +- cmd/daemon/status.go | 36 +- cmd/postcollect/main.go | 50 +- cmd/sanitize/main.go | 523 ------------------ cmd/session-init/main.go | 49 +- conf/config.toml | 2 +- go.mod | 20 +- go.sum | 18 +- internal/cfg/cfg.go | 117 ++++ {pkg => internal}/collect/collect.go | 31 +- {pkg => internal}/histcli/histcli.go | 2 +- {pkg => internal}/histfile/histfile.go | 148 ++--- {pkg => internal}/histlist/histlist.go | 28 +- {pkg => internal}/httpclient/httpclient.go | 0 internal/logger/logger.go | 28 + {pkg => internal}/msg/msg.go | 2 +- internal/output/output.go | 69 +++ {pkg => internal}/records/records.go | 89 +-- {pkg => internal}/records/records_test.go | 5 +- .../records/testdata/resh_history.json | 0 {pkg => internal}/searchapp/highlight.go | 0 {pkg => internal}/searchapp/item.go | 6 +- {pkg => internal}/searchapp/item_test.go | 0 {pkg => internal}/searchapp/query.go | 17 - internal/searchapp/test.go | 22 + {pkg => internal}/searchapp/time.go | 0 {pkg => internal}/sess/sess.go | 0 {pkg => internal}/sesswatch/sesswatch.go | 44 +- internal/signalhandler/signalhander.go | 74 +++ pkg/cfg/cfg.go | 12 - pkg/searchapp/test.go | 21 - pkg/signalhandler/signalhander.go | 65 --- scripts/util.sh | 2 + 49 files changed, 987 insertions(+), 1321 deletions(-) delete mode 100644 cmd/control/cmd/debug.go delete mode 100644 cmd/control/status/status.go delete mode 100644 cmd/daemon/kill.go delete mode 100644 cmd/sanitize/main.go create mode 100644 internal/cfg/cfg.go rename {pkg => internal}/collect/collect.go (59%) rename {pkg => internal}/histcli/histcli.go (92%) rename {pkg => internal}/histfile/histfile.go (57%) rename {pkg => internal}/histlist/histlist.go (62%) rename {pkg => internal}/httpclient/httpclient.go (100%) create mode 100644 internal/logger/logger.go rename {pkg => internal}/msg/msg.go (92%) create mode 100644 internal/output/output.go rename {pkg => internal}/records/records.go (88%) rename {pkg => internal}/records/records_test.go (96%) rename {pkg => internal}/records/testdata/resh_history.json (100%) rename {pkg => internal}/searchapp/highlight.go (100%) rename {pkg => internal}/searchapp/item.go (99%) rename {pkg => internal}/searchapp/item_test.go (100%) rename {pkg => internal}/searchapp/query.go (80%) create mode 100644 internal/searchapp/test.go rename {pkg => internal}/searchapp/time.go (100%) rename {pkg => internal}/sess/sess.go (100%) rename {pkg => internal}/sesswatch/sesswatch.go (55%) create mode 100644 internal/signalhandler/signalhander.go delete mode 100644 pkg/cfg/cfg.go delete mode 100644 pkg/searchapp/test.go delete mode 100644 pkg/signalhandler/signalhander.go diff --git a/Makefile b/Makefile index f90918c..b269c6d 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/cmd/cli/main.go b/cmd/cli/main.go index a58ff10..9ea53ae 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.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 } diff --git a/cmd/collect/main.go b/cmd/collect/main.go index 57e6135..a0a17b2 100644 --- a/cmd/collect/main.go +++ b/cmd/collect/main.go @@ -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") } diff --git a/cmd/config/main.go b/cmd/config/main.go index e67ee7b..e908817 100644 --- a/cmd/config/main.go +++ b/cmd/config/main.go @@ -4,24 +4,24 @@ import ( "flag" "fmt" "os" - "os/user" - "path/filepath" "strings" - "github.com/BurntSushi/toml" - "github.com/curusarn/resh/pkg/cfg" + "github.com/curusarn/resh/internal/cfg" + "github.com/curusarn/resh/internal/logger" + "go.uber.org/zap" ) +// info passed during build +var version string +var commit string +var 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") diff --git a/cmd/control/cmd/completion.go b/cmd/control/cmd/completion.go index e80fa56..6c0e450 100644 --- a/cmd/control/cmd/completion.go +++ b/cmd/control/cmd/completion.go @@ -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 }, } diff --git a/cmd/control/cmd/debug.go b/cmd/control/cmd/debug.go deleted file mode 100644 index f1401c7..0000000 --- a/cmd/control/cmd/debug.go +++ /dev/null @@ -1,66 +0,0 @@ -package cmd - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - - "github.com/curusarn/resh/cmd/control/status" - "github.com/spf13/cobra" -) - -var debugCmd = &cobra.Command{ - Use: "debug", - Short: "debug utils for resh", - Long: "Reloads resh rc files. Shows logs and output from last runs of resh", -} - -var debugReloadCmd = &cobra.Command{ - Use: "reload", - Short: "reload resh rc files", - Long: "Reload resh rc files", - Run: func(cmd *cobra.Command, args []string) { - exitCode = status.ReloadRcFiles - }, -} - -var debugInspectCmd = &cobra.Command{ - Use: "inspect", - Short: "inspect session history", - Run: func(cmd *cobra.Command, args []string) { - exitCode = status.InspectSessionHistory - }, -} - -var debugOutputCmd = &cobra.Command{ - Use: "output", - Short: "shows output from last runs of resh", - Long: "Shows output from last runs of resh", - Run: func(cmd *cobra.Command, args []string) { - files := []string{ - "daemon_last_run_out.txt", - "collect_last_run_out.txt", - "postcollect_last_run_out.txt", - "session_init_last_run_out.txt", - "cli_last_run_out.txt", - } - dir := os.Getenv("__RESH_XDG_CACHE_HOME") - for _, fpath := range files { - fpath := filepath.Join(dir, fpath) - debugReadFile(fpath) - } - exitCode = status.Success - }, -} - -func debugReadFile(path string) { - fmt.Println("============================================================") - fmt.Println(" filepath:", path) - fmt.Println("============================================================") - dat, err := ioutil.ReadFile(path) - if err != nil { - fmt.Println("ERROR while reading file:", err) - } - fmt.Println(string(dat)) -} diff --git a/cmd/control/cmd/root.go b/cmd/control/cmd/root.go index 980d309..baf12b0 100644 --- a/cmd/control/cmd/root.go +++ b/cmd/control/cmd/root.go @@ -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 } diff --git a/cmd/control/cmd/update.go b/cmd/control/cmd/update.go index dcdb21d..6263656 100644 --- a/cmd/control/cmd/update.go +++ b/cmd/control/cmd/update.go @@ -3,10 +3,8 @@ package cmd import ( "os" "os/exec" - "os/user" "path/filepath" - "github.com/curusarn/resh/cmd/control/status" "github.com/spf13/cobra" ) @@ -15,9 +13,11 @@ var updateCmd = &cobra.Command{ Use: "update", Short: "check for updates and update RESH", Run: func(cmd *cobra.Command, args []string) { - usr, _ := user.Current() - dir := usr.HomeDir - rawinstallPath := filepath.Join(dir, ".resh/rawinstall.sh") + homeDir, err := os.UserHomeDir() + if err != nil { + out.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) } }, } diff --git a/cmd/control/cmd/version.go b/cmd/control/cmd/version.go index 57c776f..9d86f20 100644 --- a/cmd/control/cmd/version.go +++ b/cmd/control/cmd/version.go @@ -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", "") 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 } diff --git a/cmd/control/main.go b/cmd/control/main.go index ecae4e0..79d7289 100644 --- a/cmd/control/main.go +++ b/cmd/control/main.go @@ -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) } diff --git a/cmd/control/status/status.go b/cmd/control/status/status.go deleted file mode 100644 index 0d797d7..0000000 --- a/cmd/control/status/status.go +++ /dev/null @@ -1,25 +0,0 @@ -package status - -// Code - exit code of the resh-control command -type Code int - -const ( - // Success exit code - Success Code = 0 - // Fail exit code - Fail = 1 - // EnableResh exit code - tells reshctl() wrapper to enable resh - // EnableResh = 30 - - // EnableControlRBinding exit code - tells reshctl() wrapper to enable control R binding - EnableControlRBinding = 32 - - // DisableControlRBinding exit code - tells reshctl() wrapper to disable control R binding - DisableControlRBinding = 42 - // ReloadRcFiles exit code - tells reshctl() wrapper to reload shellrc resh file - ReloadRcFiles = 50 - // InspectSessionHistory exit code - tells reshctl() wrapper to take current sessionID and send /inspect request to daemon - InspectSessionHistory = 51 - // ReshStatus exit code - tells reshctl() wrapper to show RESH status (aka systemctl status) - ReshStatus = 52 -) diff --git a/cmd/daemon/dump.go b/cmd/daemon/dump.go index 375da76..c038705 100644 --- a/cmd/daemon/dump.go +++ b/cmd/daemon/dump.go @@ -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") } diff --git a/cmd/daemon/kill.go b/cmd/daemon/kill.go deleted file mode 100644 index 7c403a9..0000000 --- a/cmd/daemon/kill.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "io/ioutil" - "log" - "os/exec" - "strconv" - "strings" -) - -func killDaemon(pidfile string) error { - dat, err := ioutil.ReadFile(pidfile) - if err != nil { - log.Println("Reading pid file failed", err) - } - log.Print(string(dat)) - pid, err := strconv.Atoi(strings.TrimSuffix(string(dat), "\n")) - if err != nil { - log.Fatal("Pidfile contents are malformed", err) - } - cmd := exec.Command("kill", "-s", "sigint", strconv.Itoa(pid)) - err = cmd.Run() - if err != nil { - log.Printf("Command finished with error: %v", err) - return err - } - return nil -} diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index fc7b5bd..9338d0b 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -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 } diff --git a/cmd/daemon/record.go b/cmd/daemon/record.go index 38dce4b..e9acfcc 100644 --- a/cmd/daemon/record.go +++ b/cmd/daemon/record.go @@ -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) } diff --git a/cmd/daemon/run-server.go b/cmd/daemon/run-server.go index 9ad1310..f161672 100644 --- a/cmd/daemon/run-server.go +++ b/cmd/daemon/run-server.go @@ -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) } diff --git a/cmd/daemon/session-init.go b/cmd/daemon/session-init.go index da1e236..2015fb0 100644 --- a/cmd/daemon/session-init.go +++ b/cmd/daemon/session-init.go @@ -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") }() } diff --git a/cmd/daemon/status.go b/cmd/daemon/status.go index 599b8dd..3c6b416 100644 --- a/cmd/daemon/status.go +++ b/cmd/daemon/status.go @@ -2,16 +2,19 @@ package main import ( "encoding/json" - "log" "net/http" - "strconv" - "github.com/curusarn/resh/pkg/httpclient" - "github.com/curusarn/resh/pkg/msg" + "github.com/curusarn/resh/internal/msg" + "go.uber.org/zap" ) -func statusHandler(w http.ResponseWriter, r *http.Request) { - log.Println("/status START") +type statusHandler struct { + sugar *zap.SugaredLogger +} + +func (h *statusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + sugar := h.sugar.With(zap.String("endpoint", "/status")) + sugar.Debugw("Handling request ...") resp := msg.StatusResponse{ Status: true, Version: version, @@ -19,23 +22,12 @@ func statusHandler(w http.ResponseWriter, r *http.Request) { } jsn, err := json.Marshal(&resp) if err != nil { - log.Println("Encoding error:", err) - log.Println("Response:", resp) + sugar.Errorw("Error when marshaling", + "error", err, + "response", resp, + ) return } w.Write(jsn) - log.Println("/status END") -} - -func isDaemonRunning(port int) (bool, error) { - url := "http://localhost:" + strconv.Itoa(port) + "/status" - client := httpclient.New() - resp, err := client.Get(url) - if err != nil { - log.Printf("Error while checking daemon status - "+ - "it's probably not running: %v\n", err) - return false, err - } - defer resp.Body.Close() - return true, nil + sugar.Infow("Request handled") } diff --git a/cmd/postcollect/main.go b/cmd/postcollect/main.go index b5cb6b0..87f25b7 100644 --- a/cmd/postcollect/main.go +++ b/cmd/postcollect/main.go @@ -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") } diff --git a/cmd/sanitize/main.go b/cmd/sanitize/main.go deleted file mode 100644 index c4fd60a..0000000 --- a/cmd/sanitize/main.go +++ /dev/null @@ -1,523 +0,0 @@ -package main - -import ( - "bufio" - "crypto/sha256" - "encoding/binary" - "encoding/hex" - "encoding/json" - "errors" - "flag" - "fmt" - "log" - "math" - "net/url" - "os" - "os/user" - "path" - "path/filepath" - "strconv" - "strings" - "unicode" - - "github.com/coreos/go-semver/semver" - "github.com/curusarn/resh/pkg/records" - giturls "github.com/whilp/git-urls" -) - -// version from git set during build -var version string - -// commit from git set during build -var commit string - -func main() { - usr, _ := user.Current() - dir := usr.HomeDir - historyPath := filepath.Join(dir, ".resh_history.json") - // outputPath := filepath.Join(dir, "resh_history_sanitized.json") - sanitizerDataPath := filepath.Join(dir, ".resh", "sanitizer_data") - - showVersion := flag.Bool("version", false, "Show version and exit") - showRevision := flag.Bool("revision", false, "Show git revision and exit") - trimHashes := flag.Int("trim-hashes", 12, "Trim hashes to N characters, '0' turns off trimming") - inputPath := flag.String("input", historyPath, "Input file") - outputPath := flag.String("output", "", "Output file (default: use stdout)") - - flag.Parse() - - if *showVersion == true { - fmt.Println(version) - os.Exit(0) - } - if *showRevision == true { - fmt.Println(commit) - os.Exit(0) - } - sanitizer := sanitizer{hashLength: *trimHashes} - err := sanitizer.init(sanitizerDataPath) - if err != nil { - log.Fatal("Sanitizer init() error:", err) - } - - inputFile, err := os.Open(*inputPath) - if err != nil { - log.Fatal("Open() resh history file error:", err) - } - defer inputFile.Close() - - var writer *bufio.Writer - if *outputPath == "" { - writer = bufio.NewWriter(os.Stdout) - } else { - outputFile, err := os.Create(*outputPath) - if err != nil { - log.Fatal("Create() output file error:", err) - } - defer outputFile.Close() - writer = bufio.NewWriter(outputFile) - } - defer writer.Flush() - - scanner := bufio.NewScanner(inputFile) - for scanner.Scan() { - record := records.Record{} - fallbackRecord := records.FallbackRecord{} - line := scanner.Text() - err = json.Unmarshal([]byte(line), &record) - if err != nil { - err = json.Unmarshal([]byte(line), &fallbackRecord) - if err != nil { - log.Println("Line:", line) - log.Fatal("Decoding error:", err) - } - record = records.Convert(&fallbackRecord) - } - err = sanitizer.sanitizeRecord(&record) - if err != nil { - log.Println("Line:", line) - log.Fatal("Sanitization error:", err) - } - outLine, err := json.Marshal(&record) - if err != nil { - log.Println("Line:", line) - log.Fatal("Encoding error:", err) - } - // fmt.Println(string(outLine)) - n, err := writer.WriteString(string(outLine) + "\n") - if err != nil { - log.Fatal(err) - } - if n == 0 { - log.Fatal("Nothing was written", n) - } - } -} - -type sanitizer struct { - hashLength int - whitelist map[string]bool -} - -func (s *sanitizer) init(dataPath string) error { - globalData := path.Join(dataPath, "whitelist.txt") - s.whitelist = loadData(globalData) - return nil -} - -func loadData(fname string) map[string]bool { - file, err := os.Open(fname) - if err != nil { - log.Fatal("Open() file error:", err) - } - defer file.Close() - - scanner := bufio.NewScanner(file) - data := make(map[string]bool) - for scanner.Scan() { - line := scanner.Text() - data[line] = true - } - return data -} - -func (s *sanitizer) sanitizeRecord(record *records.Record) error { - // hash directories of the paths - record.Pwd = s.sanitizePath(record.Pwd) - record.RealPwd = s.sanitizePath(record.RealPwd) - record.PwdAfter = s.sanitizePath(record.PwdAfter) - record.RealPwdAfter = s.sanitizePath(record.RealPwdAfter) - record.GitDir = s.sanitizePath(record.GitDir) - record.GitDirAfter = s.sanitizePath(record.GitDirAfter) - record.GitRealDir = s.sanitizePath(record.GitRealDir) - record.GitRealDirAfter = s.sanitizePath(record.GitRealDirAfter) - record.Home = s.sanitizePath(record.Home) - record.ShellEnv = s.sanitizePath(record.ShellEnv) - - // hash the most sensitive info, do not tokenize - record.Host = s.hashToken(record.Host) - record.Login = s.hashToken(record.Login) - record.MachineID = s.hashToken(record.MachineID) - - var err error - // this changes git url a bit but I'm still happy with the result - // e.g. "git@github.com:curusarn/resh" becomes "ssh://git@github.com/3385162f14d7/5a7b2909005c" - // notice the "ssh://" prefix - record.GitOriginRemote, err = s.sanitizeGitURL(record.GitOriginRemote) - if err != nil { - log.Println("Error while snitizing GitOriginRemote url", record.GitOriginRemote, ":", err) - return err - } - record.GitOriginRemoteAfter, err = s.sanitizeGitURL(record.GitOriginRemoteAfter) - if err != nil { - log.Println("Error while snitizing GitOriginRemoteAfter url", record.GitOriginRemoteAfter, ":", err) - return err - } - - // sanitization destroys original CmdLine length -> save it - record.CmdLength = len(record.CmdLine) - - record.CmdLine, err = s.sanitizeCmdLine(record.CmdLine) - if err != nil { - log.Fatal("Cmd:", record.CmdLine, "; sanitization error:", err) - } - record.RecallLastCmdLine, err = s.sanitizeCmdLine(record.RecallLastCmdLine) - if err != nil { - log.Fatal("RecallLastCmdLine:", record.RecallLastCmdLine, "; sanitization error:", err) - } - - if len(record.RecallActionsRaw) > 0 { - record.RecallActionsRaw, err = s.sanitizeRecallActions(record.RecallActionsRaw, record.ReshVersion) - if err != nil { - log.Println("RecallActionsRaw:", record.RecallActionsRaw, "; sanitization error:", err) - } - } - // add a flag to signify that the record has been sanitized - record.Sanitized = true - return nil -} - -func fixSeparator(str string) string { - if len(str) > 0 && str[0] == ';' { - return "|||" + str[1:] - } - return str -} - -func minIndex(str string, substrs []string) (idx, substrIdx int) { - minMatch := math.MaxInt32 - for i, sep := range substrs { - match := strings.Index(str, sep) - if match != -1 && match < minMatch { - minMatch = match - substrIdx = i - } - } - idx = minMatch - return -} - -// sanitizes the recall actions by replacing the recall prefix with it's length -func (s *sanitizer) sanitizeRecallActions(str string, reshVersion string) (string, error) { - if len(str) == 0 { - return "", nil - } - var separators []string - seps := []string{"|||"} - refVersion, err := semver.NewVersion("2.5.14") - if err != nil { - return str, fmt.Errorf("sanitizeRecallActions: semver error: %s", err.Error()) - } - if len(reshVersion) == 0 { - return str, errors.New("sanitizeRecallActions: record.ReshVersion is an empty string") - } - if reshVersion == "dev" { - reshVersion = "0.0.0" - } - if reshVersion[0] == 'v' { - reshVersion = reshVersion[1:] - } - recordVersion, err := semver.NewVersion(reshVersion) - if err != nil { - return str, fmt.Errorf("sanitizeRecallActions: semver error: %s; version string: %s", err.Error(), reshVersion) - } - if recordVersion.LessThan(*refVersion) { - seps = append(seps, ";") - } - - actions := []string{"arrow_up", "arrow_down", "control_R"} - for _, sep := range seps { - for _, action := range actions { - separators = append(separators, sep+action+":") - } - } - /* - - find any of {|||,;}{arrow_up,arrow_down,control_R}: in the recallActions (on the lowest index) - - use found substring to parse out the next prefix - - sanitize prefix - - add fixed substring and sanitized prefix to output - */ - doBreak := false - sanStr := "" - idx := 0 - var currSeparator string - tokenLen, sepIdx := minIndex(str, separators) - if tokenLen != 0 { - return str, errors.New("sanitizeReacallActions: unexpected string before first action/separator") - } - currSeparator = separators[sepIdx] - idx += len(currSeparator) - for !doBreak { - tokenLen, sepIdx := minIndex(str[idx:], separators) - if tokenLen > len(str[idx:]) { - tokenLen = len(str[idx:]) - doBreak = true - } - // token := str[idx : idx+tokenLen] - sanStr += fixSeparator(currSeparator) + strconv.Itoa(tokenLen) - currSeparator = separators[sepIdx] - idx += tokenLen + len(currSeparator) - } - return sanStr, nil -} - -func (s *sanitizer) sanitizeCmdLine(cmdLine string) (string, error) { - const optionEndingChars = "\"$'\\#[]!><|;{}()*,?~&=`:@^/+%." // all bash control characters, '=', ... - const optionAllowedChars = "-_" // characters commonly found inside of options - sanCmdLine := "" - buff := "" - - // simple options shouldn't be sanitized - // 1) whitespace 2) "-" or "--" 3) letters, digits, "-", "_" 4) ending whitespace or any of "=;)" - var optionDetected bool - - prevR3 := ' ' - prevR2 := ' ' - prevR := ' ' - for _, r := range cmdLine { - switch optionDetected { - case true: - if unicode.IsSpace(r) || strings.ContainsRune(optionEndingChars, r) { - // whitespace or option ends the option - // => add option unsanitized - optionDetected = false - if len(buff) > 0 { - sanCmdLine += buff - buff = "" - } - sanCmdLine += string(r) - } else if unicode.IsLetter(r) == false && unicode.IsDigit(r) == false && - strings.ContainsRune(optionAllowedChars, r) == false { - // r is not any of allowed chars for an option: letter, digit, "-" or "_" - // => sanitize - if len(buff) > 0 { - sanToken, err := s.sanitizeCmdToken(buff) - if err != nil { - log.Println("WARN: got error while sanitizing cmdLine:", cmdLine) - // return cmdLine, err - } - sanCmdLine += sanToken - buff = "" - } - sanCmdLine += string(r) - } else { - buff += string(r) - } - case false: - // split command on all non-letter and non-digit characters - if unicode.IsLetter(r) == false && unicode.IsDigit(r) == false { - // split token - if len(buff) > 0 { - sanToken, err := s.sanitizeCmdToken(buff) - if err != nil { - log.Println("WARN: got error while sanitizing cmdLine:", cmdLine) - // return cmdLine, err - } - sanCmdLine += sanToken - buff = "" - } - sanCmdLine += string(r) - } else { - if (unicode.IsSpace(prevR2) && prevR == '-') || - (unicode.IsSpace(prevR3) && prevR2 == '-' && prevR == '-') { - optionDetected = true - } - buff += string(r) - } - } - prevR3 = prevR2 - prevR2 = prevR - prevR = r - } - if len(buff) <= 0 { - // nothing in the buffer => work is done - return sanCmdLine, nil - } - if optionDetected { - // option detected => dont sanitize - sanCmdLine += buff - return sanCmdLine, nil - } - // sanitize - sanToken, err := s.sanitizeCmdToken(buff) - if err != nil { - log.Println("WARN: got error while sanitizing cmdLine:", cmdLine) - // return cmdLine, err - } - sanCmdLine += sanToken - return sanCmdLine, nil -} - -func (s *sanitizer) sanitizeGitURL(rawURL string) (string, error) { - if len(rawURL) <= 0 { - return rawURL, nil - } - parsedURL, err := giturls.Parse(rawURL) - if err != nil { - return rawURL, err - } - return s.sanitizeParsedURL(parsedURL) -} - -func (s *sanitizer) sanitizeURL(rawURL string) (string, error) { - if len(rawURL) <= 0 { - return rawURL, nil - } - parsedURL, err := url.Parse(rawURL) - if err != nil { - return rawURL, err - } - return s.sanitizeParsedURL(parsedURL) -} - -func (s *sanitizer) sanitizeParsedURL(parsedURL *url.URL) (string, error) { - parsedURL.Opaque = s.sanitizeToken(parsedURL.Opaque) - - userinfo := parsedURL.User.Username() // only get username => password won't even make it to the sanitized data - if len(userinfo) > 0 { - parsedURL.User = url.User(s.sanitizeToken(userinfo)) - } else { - // we need to do this because `gitUrls.Parse()` sets `User` to `url.User("")` instead of `nil` - parsedURL.User = nil - } - var err error - parsedURL.Host, err = s.sanitizeTwoPartToken(parsedURL.Host, ":") - if err != nil { - return parsedURL.String(), err - } - parsedURL.Path = s.sanitizePath(parsedURL.Path) - // ForceQuery bool - parsedURL.RawQuery = s.sanitizeToken(parsedURL.RawQuery) - parsedURL.Fragment = s.sanitizeToken(parsedURL.Fragment) - - return parsedURL.String(), nil -} - -func (s *sanitizer) sanitizePath(path string) string { - var sanPath string - for _, token := range strings.Split(path, "/") { - if s.whitelist[token] != true { - token = s.hashToken(token) - } - sanPath += token + "/" - } - if len(sanPath) > 0 { - sanPath = sanPath[:len(sanPath)-1] - } - return sanPath -} - -func (s *sanitizer) sanitizeTwoPartToken(token string, delimeter string) (string, error) { - tokenParts := strings.Split(token, delimeter) - if len(tokenParts) <= 1 { - return s.sanitizeToken(token), nil - } - if len(tokenParts) == 2 { - return s.sanitizeToken(tokenParts[0]) + delimeter + s.sanitizeToken(tokenParts[1]), nil - } - return token, errors.New("Token has more than two parts") -} - -func (s *sanitizer) sanitizeCmdToken(token string) (string, error) { - // there shouldn't be tokens with letters or digits mixed together with symbols - if len(token) <= 1 { - // NOTE: do not sanitize single letter tokens - return token, nil - } - if s.isInWhitelist(token) == true { - return token, nil - } - - isLettersOrDigits := true - // isDigits := true - isOtherCharacters := true - for _, r := range token { - if unicode.IsDigit(r) == false && unicode.IsLetter(r) == false { - isLettersOrDigits = false - // isDigits = false - } - // if unicode.IsDigit(r) == false { - // isDigits = false - // } - if unicode.IsDigit(r) || unicode.IsLetter(r) { - isOtherCharacters = false - } - } - // NOTE: I decided that I don't want a special sanitization for numbers - // if isDigits { - // return s.hashNumericToken(token), nil - // } - if isLettersOrDigits { - return s.hashToken(token), nil - } - if isOtherCharacters { - return token, nil - } - log.Println("WARN: cmd token is made of mix of letters or digits and other characters; token:", token) - // return token, errors.New("cmd token is made of mix of letters or digits and other characters") - return s.hashToken(token), errors.New("cmd token is made of mix of letters or digits and other characters") -} - -func (s *sanitizer) sanitizeToken(token string) string { - if len(token) <= 1 { - // NOTE: do not sanitize single letter tokens - return token - } - if s.isInWhitelist(token) { - return token - } - return s.hashToken(token) -} - -func (s *sanitizer) hashToken(token string) string { - if len(token) <= 0 { - return token - } - // hash with sha256 - sum := sha256.Sum256([]byte(token)) - return s.trimHash(hex.EncodeToString(sum[:])) -} - -func (s *sanitizer) hashNumericToken(token string) string { - if len(token) <= 0 { - return token - } - sum := sha256.Sum256([]byte(token)) - sumInt := int(binary.LittleEndian.Uint64(sum[:])) - if sumInt < 0 { - return strconv.Itoa(sumInt * -1) - } - return s.trimHash(strconv.Itoa(sumInt)) -} - -func (s *sanitizer) trimHash(hash string) string { - length := s.hashLength - if length <= 0 || len(hash) < length { - length = len(hash) - } - return hash[:length] -} - -func (s *sanitizer) isInWhitelist(token string) bool { - return s.whitelist[strings.ToLower(token)] == true -} diff --git a/cmd/session-init/main.go b/cmd/session-init/main.go index 75f5298..0205dec 100644 --- a/cmd/session-init/main.go +++ b/cmd/session-init/main.go @@ -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") } diff --git a/conf/config.toml b/conf/config.toml index 9db8b94..52adf66 100644 --- a/conf/config.toml +++ b/conf/config.toml @@ -1,5 +1,5 @@ port = 2627 sesswatchPeriodSeconds = 120 sesshistInitHistorySize = 1000 -debug = false bindControlR = true +logVerbosity = info diff --git a/go.mod b/go.mod index ce2450a..4c3db28 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index b44a8eb..c1e937d 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/cfg/cfg.go b/internal/cfg/cfg.go new file mode 100644 index 0000000..4587bf1 --- /dev/null +++ b/internal/cfg/cfg.go @@ -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 +} diff --git a/pkg/collect/collect.go b/internal/collect/collect.go similarity index 59% rename from pkg/collect/collect.go rename to internal/collect/collect.go index b1c2fb6..179859a 100644 --- a/pkg/collect/collect.go +++ b/internal/collect/collect.go @@ -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 diff --git a/pkg/histcli/histcli.go b/internal/histcli/histcli.go similarity index 92% rename from pkg/histcli/histcli.go rename to internal/histcli/histcli.go index f9b6611..9bedf61 100644 --- a/pkg/histcli/histcli.go +++ b/internal/histcli/histcli.go @@ -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 diff --git a/pkg/histfile/histfile.go b/internal/histfile/histfile.go similarity index 57% rename from pkg/histfile/histfile.go rename to internal/histfile/histfile.go index 17e7ffb..9d7cce0 100644 --- a/pkg/histfile/histfile.go +++ b/internal/histfile/histfile.go @@ -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{} diff --git a/pkg/histlist/histlist.go b/internal/histlist/histlist.go similarity index 62% rename from pkg/histlist/histlist.go rename to internal/histlist/histlist.go index a3f4334..7c80c7f 100644 --- a/pkg/histlist/histlist.go +++ b/internal/histlist/histlist.go @@ -1,9 +1,11 @@ package histlist -import "log" +import "go.uber.org/zap" // Histlist is a deduplicated list of cmdLines type Histlist struct { + // TODO: I'm not excited about logger being passed here + sugar *zap.SugaredLogger // list of commands lines (deduplicated) List []string // lookup: cmdLine -> last index @@ -11,13 +13,16 @@ type Histlist struct { } // New Histlist -func New() Histlist { - return Histlist{LastIndex: make(map[string]int)} +func New(sugar *zap.SugaredLogger) Histlist { + return Histlist{ + sugar: sugar.With("component", "histlist"), + LastIndex: make(map[string]int), + } } // Copy Histlist func Copy(hl Histlist) Histlist { - newHl := New() + newHl := New(hl.sugar) // copy list newHl.List = make([]string, len(hl.List)) copy(newHl.List, hl.List) @@ -36,7 +41,10 @@ func (h *Histlist) AddCmdLine(cmdLine string) { if found { // remove duplicate if cmdLine != h.List[idx] { - log.Println("histlist ERROR: Adding cmdLine:", cmdLine, " != LastIndex[cmdLine]:", h.List[idx]) + h.sugar.DPanicw("Index key is different than actual cmd line in the list", + "indexKeyCmdLine", cmdLine, + "actualCmdLine", h.List[idx], + ) } h.List = append(h.List[:idx], h.List[idx+1:]...) // idx++ @@ -44,7 +52,10 @@ func (h *Histlist) AddCmdLine(cmdLine string) { cmdLn := h.List[idx] h.LastIndex[cmdLn]-- if idx != h.LastIndex[cmdLn] { - log.Println("histlist ERROR: Shifting LastIndex idx:", idx, " != LastIndex[cmdLn]:", h.LastIndex[cmdLn]) + h.sugar.DPanicw("Index position is different than actual position of the cmd line", + "actualPosition", idx, + "indexedPosition", h.LastIndex[cmdLn], + ) } idx++ } @@ -53,7 +64,10 @@ func (h *Histlist) AddCmdLine(cmdLine string) { h.LastIndex[cmdLine] = len(h.List) // append new cmdline h.List = append(h.List, cmdLine) - // log.Println("histlist: Added cmdLine:", cmdLine, "; history length:", lenBefore, "->", len(h.List)) + h.sugar.Debugw("Added cmdLine", + "cmdLine", cmdLine, + "historyLength", len(h.List), + ) } // AddHistlist contents of another histlist to this histlist diff --git a/pkg/httpclient/httpclient.go b/internal/httpclient/httpclient.go similarity index 100% rename from pkg/httpclient/httpclient.go rename to internal/httpclient/httpclient.go diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..b031b77 --- /dev/null +++ b/internal/logger/logger.go @@ -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 +} diff --git a/pkg/msg/msg.go b/internal/msg/msg.go similarity index 92% rename from pkg/msg/msg.go rename to internal/msg/msg.go index 3c85987..cf8e3ea 100644 --- a/pkg/msg/msg.go +++ b/internal/msg/msg.go @@ -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 { diff --git a/internal/output/output.go b/internal/output/output.go new file mode 100644 index 0000000..769f099 --- /dev/null +++ b/internal/output/output.go @@ -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)) +} diff --git a/pkg/records/records.go b/internal/records/records.go similarity index 88% rename from pkg/records/records.go rename to internal/records/records.go index ef0f561..b1f9a85 100644 --- a/pkg/records/records.go +++ b/internal/records/records.go @@ -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() diff --git a/pkg/records/records_test.go b/internal/records/records_test.go similarity index 96% rename from pkg/records/records_test.go rename to internal/records/records_test.go index 5ef3c55..35c6920 100644 --- a/pkg/records/records_test.go +++ b/internal/records/records_test.go @@ -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) } diff --git a/pkg/records/testdata/resh_history.json b/internal/records/testdata/resh_history.json similarity index 100% rename from pkg/records/testdata/resh_history.json rename to internal/records/testdata/resh_history.json diff --git a/pkg/searchapp/highlight.go b/internal/searchapp/highlight.go similarity index 100% rename from pkg/searchapp/highlight.go rename to internal/searchapp/highlight.go diff --git a/pkg/searchapp/item.go b/internal/searchapp/item.go similarity index 99% rename from pkg/searchapp/item.go rename to internal/searchapp/item.go index 6908c25..c015428 100644 --- a/pkg/searchapp/item.go +++ b/internal/searchapp/item.go @@ -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) diff --git a/pkg/searchapp/item_test.go b/internal/searchapp/item_test.go similarity index 100% rename from pkg/searchapp/item_test.go rename to internal/searchapp/item_test.go diff --git a/pkg/searchapp/query.go b/internal/searchapp/query.go similarity index 80% rename from pkg/searchapp/query.go rename to internal/searchapp/query.go index 2b8227d..fc2870a 100644 --- a/pkg/searchapp/query.go +++ b/internal/searchapp/query.go @@ -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 { diff --git a/internal/searchapp/test.go b/internal/searchapp/test.go new file mode 100644 index 0000000..36d55e4 --- /dev/null +++ b/internal/searchapp/test.go @@ -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} +} diff --git a/pkg/searchapp/time.go b/internal/searchapp/time.go similarity index 100% rename from pkg/searchapp/time.go rename to internal/searchapp/time.go diff --git a/pkg/sess/sess.go b/internal/sess/sess.go similarity index 100% rename from pkg/sess/sess.go rename to internal/sess/sess.go diff --git a/pkg/sesswatch/sesswatch.go b/internal/sesswatch/sesswatch.go similarity index 55% rename from pkg/sesswatch/sesswatch.go rename to internal/sesswatch/sesswatch.go index 3d786c5..e5a55ec 100644 --- a/pkg/sesswatch/sesswatch.go +++ b/internal/sesswatch/sesswatch.go @@ -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 } diff --git a/internal/signalhandler/signalhander.go b/internal/signalhandler/signalhander.go new file mode 100644 index 0000000..b8188d6 --- /dev/null +++ b/internal/signalhandler/signalhander.go @@ -0,0 +1,74 @@ +package signalhandler + +import ( + "context" + "net/http" + "os" + "os/signal" + "strconv" + "syscall" + "time" + + "go.uber.org/zap" +) + +func sendSignals(sugar *zap.SugaredLogger, sig os.Signal, subscribers []chan os.Signal, done chan string) { + for _, sub := range subscribers { + sub <- sig + } + sugar.Warnw("Sent shutdown signals to components") + chanCount := len(subscribers) + start := time.Now() + delay := time.Millisecond * 100 + timeout := time.Millisecond * 2000 + + for { + select { + case _ = <-done: + chanCount-- + if chanCount == 0 { + sugar.Warnw("All components shut down successfully") + return + } + default: + time.Sleep(delay) + } + if time.Since(start) > timeout { + sugar.Errorw("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, + ) + } +} diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go deleted file mode 100644 index 06cc44d..0000000 --- a/pkg/cfg/cfg.go +++ /dev/null @@ -1,12 +0,0 @@ -package cfg - -// Config struct -type Config struct { - Port int - SesswatchPeriodSeconds uint - SesshistInitHistorySize int - Debug bool - BindArrowKeysBash bool - BindArrowKeysZsh bool - BindControlR bool -} diff --git a/pkg/searchapp/test.go b/pkg/searchapp/test.go deleted file mode 100644 index 30b850f..0000000 --- a/pkg/searchapp/test.go +++ /dev/null @@ -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} -} diff --git a/pkg/signalhandler/signalhander.go b/pkg/signalhandler/signalhander.go deleted file mode 100644 index 5d2233e..0000000 --- a/pkg/signalhandler/signalhander.go +++ /dev/null @@ -1,65 +0,0 @@ -package signalhandler - -import ( - "context" - "log" - "net/http" - "os" - "os/signal" - "strconv" - "syscall" - "time" -) - -func sendSignals(sig os.Signal, subscribers []chan os.Signal, done chan string) { - for _, sub := range subscribers { - sub <- sig - } - chanCount := len(subscribers) - start := time.Now() - delay := time.Millisecond * 100 - timeout := time.Millisecond * 2000 - - for { - select { - case _ = <-done: - chanCount-- - if chanCount == 0 { - log.Println("signalhandler: All components shut down successfully") - return - } - default: - time.Sleep(delay) - } - if time.Since(start) > timeout { - log.Println("signalhandler: Timouted while waiting for proper shutdown - " + strconv.Itoa(chanCount) + " boxes are up after " + timeout.String()) - return - } - } -} - -// Run catches and handles signals -func Run(subscribers []chan os.Signal, done chan string, server *http.Server) { - signals := make(chan os.Signal, 1) - - signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) - - var sig os.Signal - for { - sig := <-signals - log.Printf("signalhandler: Got signal '%s'\n", sig.String()) - if sig == syscall.SIGTERM { - // Shutdown daemon on SIGTERM - break - } - log.Printf("signalhandler: Ignoring signal '%s'. Send SIGTERM to trigger shutdown.\n", sig.String()) - } - - log.Println("signalhandler: Sending shutdown signals to components") - sendSignals(sig, subscribers, done) - - log.Println("signalhandler: Shutting down the server") - if err := server.Shutdown(context.Background()); err != nil { - log.Printf("HTTP server Shutdown: %v", err) - } -} diff --git a/scripts/util.sh b/scripts/util.sh index 4746eb0..96a643a 100644 --- a/scripts/util.sh +++ b/scripts/util.sh @@ -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 }