mirror of https://github.com/curusarn/resh
parent
e06fe26375
commit
f5349fc45b
@ -1,66 +0,0 @@ |
|||||||
package cmd |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
"io/ioutil" |
|
||||||
"os" |
|
||||||
"path/filepath" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/cmd/control/status" |
|
||||||
"github.com/spf13/cobra" |
|
||||||
) |
|
||||||
|
|
||||||
var debugCmd = &cobra.Command{ |
|
||||||
Use: "debug", |
|
||||||
Short: "debug utils for resh", |
|
||||||
Long: "Reloads resh rc files. Shows logs and output from last runs of resh", |
|
||||||
} |
|
||||||
|
|
||||||
var debugReloadCmd = &cobra.Command{ |
|
||||||
Use: "reload", |
|
||||||
Short: "reload resh rc files", |
|
||||||
Long: "Reload resh rc files", |
|
||||||
Run: func(cmd *cobra.Command, args []string) { |
|
||||||
exitCode = status.ReloadRcFiles |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
var debugInspectCmd = &cobra.Command{ |
|
||||||
Use: "inspect", |
|
||||||
Short: "inspect session history", |
|
||||||
Run: func(cmd *cobra.Command, args []string) { |
|
||||||
exitCode = status.InspectSessionHistory |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
var debugOutputCmd = &cobra.Command{ |
|
||||||
Use: "output", |
|
||||||
Short: "shows output from last runs of resh", |
|
||||||
Long: "Shows output from last runs of resh", |
|
||||||
Run: func(cmd *cobra.Command, args []string) { |
|
||||||
files := []string{ |
|
||||||
"daemon_last_run_out.txt", |
|
||||||
"collect_last_run_out.txt", |
|
||||||
"postcollect_last_run_out.txt", |
|
||||||
"session_init_last_run_out.txt", |
|
||||||
"cli_last_run_out.txt", |
|
||||||
} |
|
||||||
dir := os.Getenv("__RESH_XDG_CACHE_HOME") |
|
||||||
for _, fpath := range files { |
|
||||||
fpath := filepath.Join(dir, fpath) |
|
||||||
debugReadFile(fpath) |
|
||||||
} |
|
||||||
exitCode = status.Success |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
func debugReadFile(path string) { |
|
||||||
fmt.Println("============================================================") |
|
||||||
fmt.Println(" filepath:", path) |
|
||||||
fmt.Println("============================================================") |
|
||||||
dat, err := ioutil.ReadFile(path) |
|
||||||
if err != nil { |
|
||||||
fmt.Println("ERROR while reading file:", err) |
|
||||||
} |
|
||||||
fmt.Println(string(dat)) |
|
||||||
} |
|
||||||
@ -1,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 |
|
||||||
) |
|
||||||
@ -1,28 +0,0 @@ |
|||||||
package main |
|
||||||
|
|
||||||
import ( |
|
||||||
"io/ioutil" |
|
||||||
"log" |
|
||||||
"os/exec" |
|
||||||
"strconv" |
|
||||||
"strings" |
|
||||||
) |
|
||||||
|
|
||||||
func killDaemon(pidfile string) error { |
|
||||||
dat, err := ioutil.ReadFile(pidfile) |
|
||||||
if err != nil { |
|
||||||
log.Println("Reading pid file failed", err) |
|
||||||
} |
|
||||||
log.Print(string(dat)) |
|
||||||
pid, err := strconv.Atoi(strings.TrimSuffix(string(dat), "\n")) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("Pidfile contents are malformed", err) |
|
||||||
} |
|
||||||
cmd := exec.Command("kill", "-s", "sigint", strconv.Itoa(pid)) |
|
||||||
err = cmd.Run() |
|
||||||
if err != nil { |
|
||||||
log.Printf("Command finished with error: %v", err) |
|
||||||
return err |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,87 +1,141 @@ |
|||||||
package main |
package main |
||||||
|
|
||||||
import ( |
import ( |
||||||
//"flag"
|
"fmt" |
||||||
|
|
||||||
"io/ioutil" |
"io/ioutil" |
||||||
"log" |
|
||||||
"os" |
"os" |
||||||
"os/user" |
"os/exec" |
||||||
"path/filepath" |
"path/filepath" |
||||||
"strconv" |
"strconv" |
||||||
|
"strings" |
||||||
|
|
||||||
"github.com/BurntSushi/toml" |
"github.com/curusarn/resh/internal/cfg" |
||||||
"github.com/curusarn/resh/pkg/cfg" |
"github.com/curusarn/resh/internal/httpclient" |
||||||
|
"github.com/curusarn/resh/internal/logger" |
||||||
|
"go.uber.org/zap" |
||||||
) |
) |
||||||
|
|
||||||
// version from git set during build
|
// info passed during build
|
||||||
var version string |
var version string |
||||||
|
|
||||||
// commit from git set during build
|
|
||||||
var commit string |
var commit string |
||||||
|
var developement bool |
||||||
// Debug switch
|
|
||||||
var Debug = false |
|
||||||
|
|
||||||
func main() { |
func main() { |
||||||
log.Println("Daemon starting... \n" + |
config, errCfg := cfg.New() |
||||||
"version: " + version + |
logger, _ := logger.New("daemon", config.LogLevel, developement) |
||||||
" commit: " + commit) |
defer logger.Sync() // flushes buffer, if any
|
||||||
usr, _ := user.Current() |
if errCfg != nil { |
||||||
dir := usr.HomeDir |
logger.Error("Error while getting configuration", zap.Error(errCfg)) |
||||||
pidfilePath := filepath.Join(dir, ".resh/resh.pid") |
|
||||||
configPath := filepath.Join(dir, ".config/resh.toml") |
|
||||||
reshHistoryPath := filepath.Join(dir, ".resh_history.json") |
|
||||||
bashHistoryPath := filepath.Join(dir, ".bash_history") |
|
||||||
zshHistoryPath := filepath.Join(dir, ".zsh_history") |
|
||||||
logPath := filepath.Join(dir, ".resh/daemon.log") |
|
||||||
|
|
||||||
f, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) |
|
||||||
if err != nil { |
|
||||||
log.Fatalf("Error opening file: %v\n", err) |
|
||||||
} |
} |
||||||
defer f.Close() |
sugar := logger.Sugar() |
||||||
|
d := daemon{sugar: sugar} |
||||||
|
sugar.Infow("Deamon starting ...", |
||||||
|
"version", version, |
||||||
|
"commit", commit, |
||||||
|
) |
||||||
|
|
||||||
log.SetOutput(f) |
// xdgCacheHome := d.getEnvOrPanic("__RESH_XDG_CACHE_HOME")
|
||||||
log.SetPrefix(strconv.Itoa(os.Getpid()) + " | ") |
// xdgDataHome := d.getEnvOrPanic("__RESH_XDG_DATA_HOME")
|
||||||
|
|
||||||
var config cfg.Config |
// TODO: rethink PID file and logs location
|
||||||
if _, err := toml.DecodeFile(configPath, &config); err != nil { |
homeDir, err := os.UserHomeDir() |
||||||
log.Printf("Error reading config: %v\n", err) |
if err != nil { |
||||||
return |
sugar.Fatalw("Could not get user home dir", zap.Error(err)) |
||||||
} |
|
||||||
if config.Debug { |
|
||||||
Debug = true |
|
||||||
log.SetFlags(log.LstdFlags | log.Lmicroseconds) |
|
||||||
} |
} |
||||||
|
PIDFile := filepath.Join(homeDir, ".resh/resh.pid") |
||||||
|
reshHistoryPath := filepath.Join(homeDir, ".resh_history.json") |
||||||
|
bashHistoryPath := filepath.Join(homeDir, ".bash_history") |
||||||
|
zshHistoryPath := filepath.Join(homeDir, ".zsh_history") |
||||||
|
|
||||||
|
sugar = sugar.With(zap.Int("daemonPID", os.Getpid())) |
||||||
|
|
||||||
res, err := isDaemonRunning(config.Port) |
res, err := d.isDaemonRunning(config.Port) |
||||||
if err != nil { |
if err != nil { |
||||||
log.Printf("Error while checking if the daemon is runnnig"+ |
sugar.Errorw("Error while checking daemon status - "+ |
||||||
" - it's probably not running: %v\n", err) |
"it's probably not running", "error", err) |
||||||
} |
} |
||||||
if res { |
if res { |
||||||
log.Println("Daemon is already running - exiting!") |
sugar.Errorw("Daemon is already running - exiting!") |
||||||
return |
return |
||||||
} |
} |
||||||
_, err = os.Stat(pidfilePath) |
_, err = os.Stat(PIDFile) |
||||||
if err == nil { |
if err == nil { |
||||||
log.Println("Pidfile exists") |
sugar.Warn("Pidfile exists") |
||||||
// kill daemon
|
// kill daemon
|
||||||
err = killDaemon(pidfilePath) |
err = d.killDaemon(PIDFile) |
||||||
|
if err != nil { |
||||||
|
sugar.Errorw("Could not kill daemon", |
||||||
|
"error", err, |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
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 { |
if err != nil { |
||||||
log.Printf("Error while killing daemon: %v\n", err) |
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) |
||||||
} |
} |
||||||
err = ioutil.WriteFile(pidfilePath, []byte(strconv.Itoa(os.Getpid())), 0644) |
d.sugar.Infow("Succesfully read PID file", "contents", string(dat)) |
||||||
|
pid, err := strconv.Atoi(strings.TrimSuffix(string(dat), "\n")) |
||||||
if err != nil { |
if err != nil { |
||||||
log.Fatalf("Could not create pidfile: %v\n", err) |
return fmt.Errorf("could not parse PID file contents: %w", err) |
||||||
} |
} |
||||||
runServer(config, reshHistoryPath, bashHistoryPath, zshHistoryPath) |
d.sugar.Infow("Successfully parsed PID", "PID", pid) |
||||||
log.Println("main: Removing pidfile ...") |
cmd := exec.Command("kill", "-s", "sigint", strconv.Itoa(pid)) |
||||||
err = os.Remove(pidfilePath) |
err = cmd.Run() |
||||||
if err != nil { |
if err != nil { |
||||||
log.Printf("Could not delete pidfile: %v\n", err) |
return fmt.Errorf("kill command finished with error: %w", err) |
||||||
} |
} |
||||||
log.Println("main: Shutdown - bye") |
return nil |
||||||
} |
} |
||||||
|
|||||||
@ -1,523 +0,0 @@ |
|||||||
package main |
|
||||||
|
|
||||||
import ( |
|
||||||
"bufio" |
|
||||||
"crypto/sha256" |
|
||||||
"encoding/binary" |
|
||||||
"encoding/hex" |
|
||||||
"encoding/json" |
|
||||||
"errors" |
|
||||||
"flag" |
|
||||||
"fmt" |
|
||||||
"log" |
|
||||||
"math" |
|
||||||
"net/url" |
|
||||||
"os" |
|
||||||
"os/user" |
|
||||||
"path" |
|
||||||
"path/filepath" |
|
||||||
"strconv" |
|
||||||
"strings" |
|
||||||
"unicode" |
|
||||||
|
|
||||||
"github.com/coreos/go-semver/semver" |
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
giturls "github.com/whilp/git-urls" |
|
||||||
) |
|
||||||
|
|
||||||
// version from git set during build
|
|
||||||
var version string |
|
||||||
|
|
||||||
// commit from git set during build
|
|
||||||
var commit string |
|
||||||
|
|
||||||
func main() { |
|
||||||
usr, _ := user.Current() |
|
||||||
dir := usr.HomeDir |
|
||||||
historyPath := filepath.Join(dir, ".resh_history.json") |
|
||||||
// outputPath := filepath.Join(dir, "resh_history_sanitized.json")
|
|
||||||
sanitizerDataPath := filepath.Join(dir, ".resh", "sanitizer_data") |
|
||||||
|
|
||||||
showVersion := flag.Bool("version", false, "Show version and exit") |
|
||||||
showRevision := flag.Bool("revision", false, "Show git revision and exit") |
|
||||||
trimHashes := flag.Int("trim-hashes", 12, "Trim hashes to N characters, '0' turns off trimming") |
|
||||||
inputPath := flag.String("input", historyPath, "Input file") |
|
||||||
outputPath := flag.String("output", "", "Output file (default: use stdout)") |
|
||||||
|
|
||||||
flag.Parse() |
|
||||||
|
|
||||||
if *showVersion == true { |
|
||||||
fmt.Println(version) |
|
||||||
os.Exit(0) |
|
||||||
} |
|
||||||
if *showRevision == true { |
|
||||||
fmt.Println(commit) |
|
||||||
os.Exit(0) |
|
||||||
} |
|
||||||
sanitizer := sanitizer{hashLength: *trimHashes} |
|
||||||
err := sanitizer.init(sanitizerDataPath) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("Sanitizer init() error:", err) |
|
||||||
} |
|
||||||
|
|
||||||
inputFile, err := os.Open(*inputPath) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("Open() resh history file error:", err) |
|
||||||
} |
|
||||||
defer inputFile.Close() |
|
||||||
|
|
||||||
var writer *bufio.Writer |
|
||||||
if *outputPath == "" { |
|
||||||
writer = bufio.NewWriter(os.Stdout) |
|
||||||
} else { |
|
||||||
outputFile, err := os.Create(*outputPath) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("Create() output file error:", err) |
|
||||||
} |
|
||||||
defer outputFile.Close() |
|
||||||
writer = bufio.NewWriter(outputFile) |
|
||||||
} |
|
||||||
defer writer.Flush() |
|
||||||
|
|
||||||
scanner := bufio.NewScanner(inputFile) |
|
||||||
for scanner.Scan() { |
|
||||||
record := records.Record{} |
|
||||||
fallbackRecord := records.FallbackRecord{} |
|
||||||
line := scanner.Text() |
|
||||||
err = json.Unmarshal([]byte(line), &record) |
|
||||||
if err != nil { |
|
||||||
err = json.Unmarshal([]byte(line), &fallbackRecord) |
|
||||||
if err != nil { |
|
||||||
log.Println("Line:", line) |
|
||||||
log.Fatal("Decoding error:", err) |
|
||||||
} |
|
||||||
record = records.Convert(&fallbackRecord) |
|
||||||
} |
|
||||||
err = sanitizer.sanitizeRecord(&record) |
|
||||||
if err != nil { |
|
||||||
log.Println("Line:", line) |
|
||||||
log.Fatal("Sanitization error:", err) |
|
||||||
} |
|
||||||
outLine, err := json.Marshal(&record) |
|
||||||
if err != nil { |
|
||||||
log.Println("Line:", line) |
|
||||||
log.Fatal("Encoding error:", err) |
|
||||||
} |
|
||||||
// fmt.Println(string(outLine))
|
|
||||||
n, err := writer.WriteString(string(outLine) + "\n") |
|
||||||
if err != nil { |
|
||||||
log.Fatal(err) |
|
||||||
} |
|
||||||
if n == 0 { |
|
||||||
log.Fatal("Nothing was written", n) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
type sanitizer struct { |
|
||||||
hashLength int |
|
||||||
whitelist map[string]bool |
|
||||||
} |
|
||||||
|
|
||||||
func (s *sanitizer) init(dataPath string) error { |
|
||||||
globalData := path.Join(dataPath, "whitelist.txt") |
|
||||||
s.whitelist = loadData(globalData) |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func loadData(fname string) map[string]bool { |
|
||||||
file, err := os.Open(fname) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("Open() file error:", err) |
|
||||||
} |
|
||||||
defer file.Close() |
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file) |
|
||||||
data := make(map[string]bool) |
|
||||||
for scanner.Scan() { |
|
||||||
line := scanner.Text() |
|
||||||
data[line] = true |
|
||||||
} |
|
||||||
return data |
|
||||||
} |
|
||||||
|
|
||||||
func (s *sanitizer) sanitizeRecord(record *records.Record) error { |
|
||||||
// hash directories of the paths
|
|
||||||
record.Pwd = s.sanitizePath(record.Pwd) |
|
||||||
record.RealPwd = s.sanitizePath(record.RealPwd) |
|
||||||
record.PwdAfter = s.sanitizePath(record.PwdAfter) |
|
||||||
record.RealPwdAfter = s.sanitizePath(record.RealPwdAfter) |
|
||||||
record.GitDir = s.sanitizePath(record.GitDir) |
|
||||||
record.GitDirAfter = s.sanitizePath(record.GitDirAfter) |
|
||||||
record.GitRealDir = s.sanitizePath(record.GitRealDir) |
|
||||||
record.GitRealDirAfter = s.sanitizePath(record.GitRealDirAfter) |
|
||||||
record.Home = s.sanitizePath(record.Home) |
|
||||||
record.ShellEnv = s.sanitizePath(record.ShellEnv) |
|
||||||
|
|
||||||
// hash the most sensitive info, do not tokenize
|
|
||||||
record.Host = s.hashToken(record.Host) |
|
||||||
record.Login = s.hashToken(record.Login) |
|
||||||
record.MachineID = s.hashToken(record.MachineID) |
|
||||||
|
|
||||||
var err error |
|
||||||
// this changes git url a bit but I'm still happy with the result
|
|
||||||
// e.g. "git@github.com:curusarn/resh" becomes "ssh://git@github.com/3385162f14d7/5a7b2909005c"
|
|
||||||
// notice the "ssh://" prefix
|
|
||||||
record.GitOriginRemote, err = s.sanitizeGitURL(record.GitOriginRemote) |
|
||||||
if err != nil { |
|
||||||
log.Println("Error while snitizing GitOriginRemote url", record.GitOriginRemote, ":", err) |
|
||||||
return err |
|
||||||
} |
|
||||||
record.GitOriginRemoteAfter, err = s.sanitizeGitURL(record.GitOriginRemoteAfter) |
|
||||||
if err != nil { |
|
||||||
log.Println("Error while snitizing GitOriginRemoteAfter url", record.GitOriginRemoteAfter, ":", err) |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// sanitization destroys original CmdLine length -> save it
|
|
||||||
record.CmdLength = len(record.CmdLine) |
|
||||||
|
|
||||||
record.CmdLine, err = s.sanitizeCmdLine(record.CmdLine) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("Cmd:", record.CmdLine, "; sanitization error:", err) |
|
||||||
} |
|
||||||
record.RecallLastCmdLine, err = s.sanitizeCmdLine(record.RecallLastCmdLine) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("RecallLastCmdLine:", record.RecallLastCmdLine, "; sanitization error:", err) |
|
||||||
} |
|
||||||
|
|
||||||
if len(record.RecallActionsRaw) > 0 { |
|
||||||
record.RecallActionsRaw, err = s.sanitizeRecallActions(record.RecallActionsRaw, record.ReshVersion) |
|
||||||
if err != nil { |
|
||||||
log.Println("RecallActionsRaw:", record.RecallActionsRaw, "; sanitization error:", err) |
|
||||||
} |
|
||||||
} |
|
||||||
// add a flag to signify that the record has been sanitized
|
|
||||||
record.Sanitized = true |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func fixSeparator(str string) string { |
|
||||||
if len(str) > 0 && str[0] == ';' { |
|
||||||
return "|||" + str[1:] |
|
||||||
} |
|
||||||
return str |
|
||||||
} |
|
||||||
|
|
||||||
func minIndex(str string, substrs []string) (idx, substrIdx int) { |
|
||||||
minMatch := math.MaxInt32 |
|
||||||
for i, sep := range substrs { |
|
||||||
match := strings.Index(str, sep) |
|
||||||
if match != -1 && match < minMatch { |
|
||||||
minMatch = match |
|
||||||
substrIdx = i |
|
||||||
} |
|
||||||
} |
|
||||||
idx = minMatch |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// sanitizes the recall actions by replacing the recall prefix with it's length
|
|
||||||
func (s *sanitizer) sanitizeRecallActions(str string, reshVersion string) (string, error) { |
|
||||||
if len(str) == 0 { |
|
||||||
return "", nil |
|
||||||
} |
|
||||||
var separators []string |
|
||||||
seps := []string{"|||"} |
|
||||||
refVersion, err := semver.NewVersion("2.5.14") |
|
||||||
if err != nil { |
|
||||||
return str, fmt.Errorf("sanitizeRecallActions: semver error: %s", err.Error()) |
|
||||||
} |
|
||||||
if len(reshVersion) == 0 { |
|
||||||
return str, errors.New("sanitizeRecallActions: record.ReshVersion is an empty string") |
|
||||||
} |
|
||||||
if reshVersion == "dev" { |
|
||||||
reshVersion = "0.0.0" |
|
||||||
} |
|
||||||
if reshVersion[0] == 'v' { |
|
||||||
reshVersion = reshVersion[1:] |
|
||||||
} |
|
||||||
recordVersion, err := semver.NewVersion(reshVersion) |
|
||||||
if err != nil { |
|
||||||
return str, fmt.Errorf("sanitizeRecallActions: semver error: %s; version string: %s", err.Error(), reshVersion) |
|
||||||
} |
|
||||||
if recordVersion.LessThan(*refVersion) { |
|
||||||
seps = append(seps, ";") |
|
||||||
} |
|
||||||
|
|
||||||
actions := []string{"arrow_up", "arrow_down", "control_R"} |
|
||||||
for _, sep := range seps { |
|
||||||
for _, action := range actions { |
|
||||||
separators = append(separators, sep+action+":") |
|
||||||
} |
|
||||||
} |
|
||||||
/* |
|
||||||
- find any of {|||,;}{arrow_up,arrow_down,control_R}: in the recallActions (on the lowest index) |
|
||||||
- use found substring to parse out the next prefix |
|
||||||
- sanitize prefix |
|
||||||
- add fixed substring and sanitized prefix to output |
|
||||||
*/ |
|
||||||
doBreak := false |
|
||||||
sanStr := "" |
|
||||||
idx := 0 |
|
||||||
var currSeparator string |
|
||||||
tokenLen, sepIdx := minIndex(str, separators) |
|
||||||
if tokenLen != 0 { |
|
||||||
return str, errors.New("sanitizeReacallActions: unexpected string before first action/separator") |
|
||||||
} |
|
||||||
currSeparator = separators[sepIdx] |
|
||||||
idx += len(currSeparator) |
|
||||||
for !doBreak { |
|
||||||
tokenLen, sepIdx := minIndex(str[idx:], separators) |
|
||||||
if tokenLen > len(str[idx:]) { |
|
||||||
tokenLen = len(str[idx:]) |
|
||||||
doBreak = true |
|
||||||
} |
|
||||||
// token := str[idx : idx+tokenLen]
|
|
||||||
sanStr += fixSeparator(currSeparator) + strconv.Itoa(tokenLen) |
|
||||||
currSeparator = separators[sepIdx] |
|
||||||
idx += tokenLen + len(currSeparator) |
|
||||||
} |
|
||||||
return sanStr, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (s *sanitizer) sanitizeCmdLine(cmdLine string) (string, error) { |
|
||||||
const optionEndingChars = "\"$'\\#[]!><|;{}()*,?~&=`:@^/+%." // all bash control characters, '=', ...
|
|
||||||
const optionAllowedChars = "-_" // characters commonly found inside of options
|
|
||||||
sanCmdLine := "" |
|
||||||
buff := "" |
|
||||||
|
|
||||||
// simple options shouldn't be sanitized
|
|
||||||
// 1) whitespace 2) "-" or "--" 3) letters, digits, "-", "_" 4) ending whitespace or any of "=;)"
|
|
||||||
var optionDetected bool |
|
||||||
|
|
||||||
prevR3 := ' ' |
|
||||||
prevR2 := ' ' |
|
||||||
prevR := ' ' |
|
||||||
for _, r := range cmdLine { |
|
||||||
switch optionDetected { |
|
||||||
case true: |
|
||||||
if unicode.IsSpace(r) || strings.ContainsRune(optionEndingChars, r) { |
|
||||||
// whitespace or option ends the option
|
|
||||||
// => add option unsanitized
|
|
||||||
optionDetected = false |
|
||||||
if len(buff) > 0 { |
|
||||||
sanCmdLine += buff |
|
||||||
buff = "" |
|
||||||
} |
|
||||||
sanCmdLine += string(r) |
|
||||||
} else if unicode.IsLetter(r) == false && unicode.IsDigit(r) == false && |
|
||||||
strings.ContainsRune(optionAllowedChars, r) == false { |
|
||||||
// r is not any of allowed chars for an option: letter, digit, "-" or "_"
|
|
||||||
// => sanitize
|
|
||||||
if len(buff) > 0 { |
|
||||||
sanToken, err := s.sanitizeCmdToken(buff) |
|
||||||
if err != nil { |
|
||||||
log.Println("WARN: got error while sanitizing cmdLine:", cmdLine) |
|
||||||
// return cmdLine, err
|
|
||||||
} |
|
||||||
sanCmdLine += sanToken |
|
||||||
buff = "" |
|
||||||
} |
|
||||||
sanCmdLine += string(r) |
|
||||||
} else { |
|
||||||
buff += string(r) |
|
||||||
} |
|
||||||
case false: |
|
||||||
// split command on all non-letter and non-digit characters
|
|
||||||
if unicode.IsLetter(r) == false && unicode.IsDigit(r) == false { |
|
||||||
// split token
|
|
||||||
if len(buff) > 0 { |
|
||||||
sanToken, err := s.sanitizeCmdToken(buff) |
|
||||||
if err != nil { |
|
||||||
log.Println("WARN: got error while sanitizing cmdLine:", cmdLine) |
|
||||||
// return cmdLine, err
|
|
||||||
} |
|
||||||
sanCmdLine += sanToken |
|
||||||
buff = "" |
|
||||||
} |
|
||||||
sanCmdLine += string(r) |
|
||||||
} else { |
|
||||||
if (unicode.IsSpace(prevR2) && prevR == '-') || |
|
||||||
(unicode.IsSpace(prevR3) && prevR2 == '-' && prevR == '-') { |
|
||||||
optionDetected = true |
|
||||||
} |
|
||||||
buff += string(r) |
|
||||||
} |
|
||||||
} |
|
||||||
prevR3 = prevR2 |
|
||||||
prevR2 = prevR |
|
||||||
prevR = r |
|
||||||
} |
|
||||||
if len(buff) <= 0 { |
|
||||||
// nothing in the buffer => work is done
|
|
||||||
return sanCmdLine, nil |
|
||||||
} |
|
||||||
if optionDetected { |
|
||||||
// option detected => dont sanitize
|
|
||||||
sanCmdLine += buff |
|
||||||
return sanCmdLine, nil |
|
||||||
} |
|
||||||
// sanitize
|
|
||||||
sanToken, err := s.sanitizeCmdToken(buff) |
|
||||||
if err != nil { |
|
||||||
log.Println("WARN: got error while sanitizing cmdLine:", cmdLine) |
|
||||||
// return cmdLine, err
|
|
||||||
} |
|
||||||
sanCmdLine += sanToken |
|
||||||
return sanCmdLine, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (s *sanitizer) sanitizeGitURL(rawURL string) (string, error) { |
|
||||||
if len(rawURL) <= 0 { |
|
||||||
return rawURL, nil |
|
||||||
} |
|
||||||
parsedURL, err := giturls.Parse(rawURL) |
|
||||||
if err != nil { |
|
||||||
return rawURL, err |
|
||||||
} |
|
||||||
return s.sanitizeParsedURL(parsedURL) |
|
||||||
} |
|
||||||
|
|
||||||
func (s *sanitizer) sanitizeURL(rawURL string) (string, error) { |
|
||||||
if len(rawURL) <= 0 { |
|
||||||
return rawURL, nil |
|
||||||
} |
|
||||||
parsedURL, err := url.Parse(rawURL) |
|
||||||
if err != nil { |
|
||||||
return rawURL, err |
|
||||||
} |
|
||||||
return s.sanitizeParsedURL(parsedURL) |
|
||||||
} |
|
||||||
|
|
||||||
func (s *sanitizer) sanitizeParsedURL(parsedURL *url.URL) (string, error) { |
|
||||||
parsedURL.Opaque = s.sanitizeToken(parsedURL.Opaque) |
|
||||||
|
|
||||||
userinfo := parsedURL.User.Username() // only get username => password won't even make it to the sanitized data
|
|
||||||
if len(userinfo) > 0 { |
|
||||||
parsedURL.User = url.User(s.sanitizeToken(userinfo)) |
|
||||||
} else { |
|
||||||
// we need to do this because `gitUrls.Parse()` sets `User` to `url.User("")` instead of `nil`
|
|
||||||
parsedURL.User = nil |
|
||||||
} |
|
||||||
var err error |
|
||||||
parsedURL.Host, err = s.sanitizeTwoPartToken(parsedURL.Host, ":") |
|
||||||
if err != nil { |
|
||||||
return parsedURL.String(), err |
|
||||||
} |
|
||||||
parsedURL.Path = s.sanitizePath(parsedURL.Path) |
|
||||||
// ForceQuery bool
|
|
||||||
parsedURL.RawQuery = s.sanitizeToken(parsedURL.RawQuery) |
|
||||||
parsedURL.Fragment = s.sanitizeToken(parsedURL.Fragment) |
|
||||||
|
|
||||||
return parsedURL.String(), nil |
|
||||||
} |
|
||||||
|
|
||||||
func (s *sanitizer) sanitizePath(path string) string { |
|
||||||
var sanPath string |
|
||||||
for _, token := range strings.Split(path, "/") { |
|
||||||
if s.whitelist[token] != true { |
|
||||||
token = s.hashToken(token) |
|
||||||
} |
|
||||||
sanPath += token + "/" |
|
||||||
} |
|
||||||
if len(sanPath) > 0 { |
|
||||||
sanPath = sanPath[:len(sanPath)-1] |
|
||||||
} |
|
||||||
return sanPath |
|
||||||
} |
|
||||||
|
|
||||||
func (s *sanitizer) sanitizeTwoPartToken(token string, delimeter string) (string, error) { |
|
||||||
tokenParts := strings.Split(token, delimeter) |
|
||||||
if len(tokenParts) <= 1 { |
|
||||||
return s.sanitizeToken(token), nil |
|
||||||
} |
|
||||||
if len(tokenParts) == 2 { |
|
||||||
return s.sanitizeToken(tokenParts[0]) + delimeter + s.sanitizeToken(tokenParts[1]), nil |
|
||||||
} |
|
||||||
return token, errors.New("Token has more than two parts") |
|
||||||
} |
|
||||||
|
|
||||||
func (s *sanitizer) sanitizeCmdToken(token string) (string, error) { |
|
||||||
// there shouldn't be tokens with letters or digits mixed together with symbols
|
|
||||||
if len(token) <= 1 { |
|
||||||
// NOTE: do not sanitize single letter tokens
|
|
||||||
return token, nil |
|
||||||
} |
|
||||||
if s.isInWhitelist(token) == true { |
|
||||||
return token, nil |
|
||||||
} |
|
||||||
|
|
||||||
isLettersOrDigits := true |
|
||||||
// isDigits := true
|
|
||||||
isOtherCharacters := true |
|
||||||
for _, r := range token { |
|
||||||
if unicode.IsDigit(r) == false && unicode.IsLetter(r) == false { |
|
||||||
isLettersOrDigits = false |
|
||||||
// isDigits = false
|
|
||||||
} |
|
||||||
// if unicode.IsDigit(r) == false {
|
|
||||||
// isDigits = false
|
|
||||||
// }
|
|
||||||
if unicode.IsDigit(r) || unicode.IsLetter(r) { |
|
||||||
isOtherCharacters = false |
|
||||||
} |
|
||||||
} |
|
||||||
// NOTE: I decided that I don't want a special sanitization for numbers
|
|
||||||
// if isDigits {
|
|
||||||
// return s.hashNumericToken(token), nil
|
|
||||||
// }
|
|
||||||
if isLettersOrDigits { |
|
||||||
return s.hashToken(token), nil |
|
||||||
} |
|
||||||
if isOtherCharacters { |
|
||||||
return token, nil |
|
||||||
} |
|
||||||
log.Println("WARN: cmd token is made of mix of letters or digits and other characters; token:", token) |
|
||||||
// return token, errors.New("cmd token is made of mix of letters or digits and other characters")
|
|
||||||
return s.hashToken(token), errors.New("cmd token is made of mix of letters or digits and other characters") |
|
||||||
} |
|
||||||
|
|
||||||
func (s *sanitizer) sanitizeToken(token string) string { |
|
||||||
if len(token) <= 1 { |
|
||||||
// NOTE: do not sanitize single letter tokens
|
|
||||||
return token |
|
||||||
} |
|
||||||
if s.isInWhitelist(token) { |
|
||||||
return token |
|
||||||
} |
|
||||||
return s.hashToken(token) |
|
||||||
} |
|
||||||
|
|
||||||
func (s *sanitizer) hashToken(token string) string { |
|
||||||
if len(token) <= 0 { |
|
||||||
return token |
|
||||||
} |
|
||||||
// hash with sha256
|
|
||||||
sum := sha256.Sum256([]byte(token)) |
|
||||||
return s.trimHash(hex.EncodeToString(sum[:])) |
|
||||||
} |
|
||||||
|
|
||||||
func (s *sanitizer) hashNumericToken(token string) string { |
|
||||||
if len(token) <= 0 { |
|
||||||
return token |
|
||||||
} |
|
||||||
sum := sha256.Sum256([]byte(token)) |
|
||||||
sumInt := int(binary.LittleEndian.Uint64(sum[:])) |
|
||||||
if sumInt < 0 { |
|
||||||
return strconv.Itoa(sumInt * -1) |
|
||||||
} |
|
||||||
return s.trimHash(strconv.Itoa(sumInt)) |
|
||||||
} |
|
||||||
|
|
||||||
func (s *sanitizer) trimHash(hash string) string { |
|
||||||
length := s.hashLength |
|
||||||
if length <= 0 || len(hash) < length { |
|
||||||
length = len(hash) |
|
||||||
} |
|
||||||
return hash[:length] |
|
||||||
} |
|
||||||
|
|
||||||
func (s *sanitizer) isInWhitelist(token string) bool { |
|
||||||
return s.whitelist[strings.ToLower(token)] == true |
|
||||||
} |
|
||||||
@ -1,5 +1,5 @@ |
|||||||
port = 2627 |
port = 2627 |
||||||
sesswatchPeriodSeconds = 120 |
sesswatchPeriodSeconds = 120 |
||||||
sesshistInitHistorySize = 1000 |
sesshistInitHistorySize = 1000 |
||||||
debug = false |
|
||||||
bindControlR = true |
bindControlR = true |
||||||
|
logVerbosity = info |
||||||
|
|||||||
@ -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 |
||||||
|
} |
||||||
@ -1,7 +1,7 @@ |
|||||||
package histcli |
package histcli |
||||||
|
|
||||||
import ( |
import ( |
||||||
"github.com/curusarn/resh/pkg/records" |
"github.com/curusarn/resh/internal/records" |
||||||
) |
) |
||||||
|
|
||||||
// Histcli is a dump of history preprocessed for resh cli purposes
|
// Histcli is a dump of history preprocessed for resh cli purposes
|
||||||
@ -0,0 +1,28 @@ |
|||||||
|
package logger |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
|
||||||
|
"go.uber.org/zap" |
||||||
|
"go.uber.org/zap/zapcore" |
||||||
|
) |
||||||
|
|
||||||
|
func New(executable string, level zapcore.Level, developement bool) (*zap.Logger, error) { |
||||||
|
// TODO: consider getting log path from config ?
|
||||||
|
homeDir, err := os.UserHomeDir() |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("error while getting home dir: %w", err) |
||||||
|
} |
||||||
|
logPath := filepath.Join(homeDir, ".resh/log.json") |
||||||
|
loggerConfig := zap.NewProductionConfig() |
||||||
|
loggerConfig.OutputPaths = []string{logPath} |
||||||
|
loggerConfig.Level.SetLevel(level) |
||||||
|
loggerConfig.Development = developement // DPanic panics in developement
|
||||||
|
logger, err := loggerConfig.Build() |
||||||
|
if err != nil { |
||||||
|
return logger, fmt.Errorf("error while creating logger: %w", err) |
||||||
|
} |
||||||
|
return logger.With(zap.String("executable", executable)), err |
||||||
|
} |
||||||
@ -1,6 +1,6 @@ |
|||||||
package msg |
package msg |
||||||
|
|
||||||
import "github.com/curusarn/resh/pkg/records" |
import "github.com/curusarn/resh/internal/records" |
||||||
|
|
||||||
// CliMsg struct
|
// CliMsg struct
|
||||||
type CliMsg struct { |
type CliMsg struct { |
||||||
@ -0,0 +1,69 @@ |
|||||||
|
package output |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
|
||||||
|
"go.uber.org/zap" |
||||||
|
) |
||||||
|
|
||||||
|
// Output wrapper for writting to logger and stdout/stderr at the same time
|
||||||
|
// useful for errors that should be presented to the user
|
||||||
|
type Output struct { |
||||||
|
Logger *zap.Logger |
||||||
|
ErrPrefix string |
||||||
|
} |
||||||
|
|
||||||
|
func New(logger *zap.Logger, prefix string) *Output { |
||||||
|
return &Output{ |
||||||
|
Logger: logger, |
||||||
|
ErrPrefix: prefix, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (f *Output) Info(msg string) { |
||||||
|
fmt.Fprintf(os.Stdout, msg) |
||||||
|
f.Logger.Info(msg) |
||||||
|
} |
||||||
|
|
||||||
|
func (f *Output) Error(msg string, err error) { |
||||||
|
fmt.Fprintf(os.Stderr, "%s: %s: %v", f.ErrPrefix, msg, err) |
||||||
|
f.Logger.Error(msg, zap.Error(err)) |
||||||
|
} |
||||||
|
|
||||||
|
func (f *Output) Fatal(msg string, err error) { |
||||||
|
fmt.Fprintf(os.Stderr, "%s: %s: %v", f.ErrPrefix, msg, err) |
||||||
|
f.Logger.Fatal(msg, zap.Error(err)) |
||||||
|
} |
||||||
|
|
||||||
|
var msgDeamonNotRunning = `Resh-daemon didn't respond - it's probably not running. |
||||||
|
|
||||||
|
-> Try restarting this terminal window to bring resh-daemon back up |
||||||
|
-> If the problem persists you can check resh-daemon logs: ~/.resh/log.json |
||||||
|
-> You can create an issue at: https://github.com/curusarn/resh/issues
|
||||||
|
` |
||||||
|
var msgVersionMismatch = `This terminal session was started with different resh version than is installed now. |
||||||
|
It looks like you updated resh and didn't restart this terminal. |
||||||
|
|
||||||
|
-> Restart this terminal window to fix that |
||||||
|
` |
||||||
|
|
||||||
|
func (f *Output) ErrorDaemonNotRunning(err error) { |
||||||
|
fmt.Fprintf(os.Stderr, "%s: %s", f.ErrPrefix, msgDeamonNotRunning) |
||||||
|
f.Logger.Error("Daemon is not running", zap.Error(err)) |
||||||
|
} |
||||||
|
|
||||||
|
func (f *Output) FatalDaemonNotRunning(err error) { |
||||||
|
fmt.Fprintf(os.Stderr, "%s: %s", f.ErrPrefix, msgDeamonNotRunning) |
||||||
|
f.Logger.Fatal("Daemon is not running", zap.Error(err)) |
||||||
|
} |
||||||
|
|
||||||
|
func (f *Output) ErrorVersionMismatch(err error) { |
||||||
|
fmt.Fprintf(os.Stderr, "%s: %s", f.ErrPrefix, msgVersionMismatch) |
||||||
|
f.Logger.Fatal("Version mismatch", zap.Error(err)) |
||||||
|
} |
||||||
|
|
||||||
|
func (f *Output) FatalVersionMismatch(err error) { |
||||||
|
fmt.Fprintf(os.Stderr, "%s: %s", f.ErrPrefix, msgVersionMismatch) |
||||||
|
f.Logger.Fatal("Version mismatch", zap.Error(err)) |
||||||
|
} |
||||||
@ -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} |
||||||
|
} |
||||||
@ -0,0 +1,74 @@ |
|||||||
|
package signalhandler |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"net/http" |
||||||
|
"os" |
||||||
|
"os/signal" |
||||||
|
"strconv" |
||||||
|
"syscall" |
||||||
|
"time" |
||||||
|
|
||||||
|
"go.uber.org/zap" |
||||||
|
) |
||||||
|
|
||||||
|
func sendSignals(sugar *zap.SugaredLogger, sig os.Signal, subscribers []chan os.Signal, done chan string) { |
||||||
|
for _, sub := range subscribers { |
||||||
|
sub <- sig |
||||||
|
} |
||||||
|
sugar.Warnw("Sent shutdown signals to components") |
||||||
|
chanCount := len(subscribers) |
||||||
|
start := time.Now() |
||||||
|
delay := time.Millisecond * 100 |
||||||
|
timeout := time.Millisecond * 2000 |
||||||
|
|
||||||
|
for { |
||||||
|
select { |
||||||
|
case _ = <-done: |
||||||
|
chanCount-- |
||||||
|
if chanCount == 0 { |
||||||
|
sugar.Warnw("All components shut down successfully") |
||||||
|
return |
||||||
|
} |
||||||
|
default: |
||||||
|
time.Sleep(delay) |
||||||
|
} |
||||||
|
if time.Since(start) > timeout { |
||||||
|
sugar.Errorw("Timouted while waiting for proper shutdown", |
||||||
|
"componentsStillUp", strconv.Itoa(chanCount), |
||||||
|
"timeout", timeout.String(), |
||||||
|
) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Run catches and handles signals
|
||||||
|
func Run(sugar *zap.SugaredLogger, subscribers []chan os.Signal, done chan string, server *http.Server) { |
||||||
|
sugar = sugar.With("module", "signalhandler") |
||||||
|
signals := make(chan os.Signal, 1) |
||||||
|
|
||||||
|
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) |
||||||
|
|
||||||
|
var sig os.Signal |
||||||
|
for { |
||||||
|
sig := <-signals |
||||||
|
sugarSig := sugar.With("signal", sig.String()) |
||||||
|
sugarSig.Infow("Got signal") |
||||||
|
if sig == syscall.SIGTERM { |
||||||
|
// Shutdown daemon on SIGTERM
|
||||||
|
break |
||||||
|
} |
||||||
|
sugarSig.Warnw("Ignoring signal. Send SIGTERM to trigger shutdown.") |
||||||
|
} |
||||||
|
|
||||||
|
sugar.Infow("Sending shutdown signals to components ...") |
||||||
|
sendSignals(sugar, sig, subscribers, done) |
||||||
|
|
||||||
|
sugar.Infow("Shutting down the server ...") |
||||||
|
if err := server.Shutdown(context.Background()); err != nil { |
||||||
|
sugar.Errorw("Error while shuting down HTTP server", |
||||||
|
"error", err, |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
@ -1,12 +0,0 @@ |
|||||||
package cfg |
|
||||||
|
|
||||||
// Config struct
|
|
||||||
type Config struct { |
|
||||||
Port int |
|
||||||
SesswatchPeriodSeconds uint |
|
||||||
SesshistInitHistorySize int |
|
||||||
Debug bool |
|
||||||
BindArrowKeysBash bool |
|
||||||
BindArrowKeysZsh bool |
|
||||||
BindControlR bool |
|
||||||
} |
|
||||||
@ -1,21 +0,0 @@ |
|||||||
package searchapp |
|
||||||
|
|
||||||
import ( |
|
||||||
"github.com/curusarn/resh/pkg/histcli" |
|
||||||
"github.com/curusarn/resh/pkg/msg" |
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
) |
|
||||||
|
|
||||||
// LoadHistoryFromFile ...
|
|
||||||
func LoadHistoryFromFile(historyPath string, numLines int) msg.CliResponse { |
|
||||||
recs := records.LoadFromFile(historyPath) |
|
||||||
if numLines != 0 && numLines < len(recs) { |
|
||||||
recs = recs[:numLines] |
|
||||||
} |
|
||||||
cliRecords := histcli.New() |
|
||||||
for i := len(recs) - 1; i >= 0; i-- { |
|
||||||
rec := recs[i] |
|
||||||
cliRecords.AddRecord(rec) |
|
||||||
} |
|
||||||
return msg.CliResponse{CliRecords: cliRecords.List} |
|
||||||
} |
|
||||||
@ -1,65 +0,0 @@ |
|||||||
package signalhandler |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"log" |
|
||||||
"net/http" |
|
||||||
"os" |
|
||||||
"os/signal" |
|
||||||
"strconv" |
|
||||||
"syscall" |
|
||||||
"time" |
|
||||||
) |
|
||||||
|
|
||||||
func sendSignals(sig os.Signal, subscribers []chan os.Signal, done chan string) { |
|
||||||
for _, sub := range subscribers { |
|
||||||
sub <- sig |
|
||||||
} |
|
||||||
chanCount := len(subscribers) |
|
||||||
start := time.Now() |
|
||||||
delay := time.Millisecond * 100 |
|
||||||
timeout := time.Millisecond * 2000 |
|
||||||
|
|
||||||
for { |
|
||||||
select { |
|
||||||
case _ = <-done: |
|
||||||
chanCount-- |
|
||||||
if chanCount == 0 { |
|
||||||
log.Println("signalhandler: All components shut down successfully") |
|
||||||
return |
|
||||||
} |
|
||||||
default: |
|
||||||
time.Sleep(delay) |
|
||||||
} |
|
||||||
if time.Since(start) > timeout { |
|
||||||
log.Println("signalhandler: Timouted while waiting for proper shutdown - " + strconv.Itoa(chanCount) + " boxes are up after " + timeout.String()) |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Run catches and handles signals
|
|
||||||
func Run(subscribers []chan os.Signal, done chan string, server *http.Server) { |
|
||||||
signals := make(chan os.Signal, 1) |
|
||||||
|
|
||||||
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) |
|
||||||
|
|
||||||
var sig os.Signal |
|
||||||
for { |
|
||||||
sig := <-signals |
|
||||||
log.Printf("signalhandler: Got signal '%s'\n", sig.String()) |
|
||||||
if sig == syscall.SIGTERM { |
|
||||||
// Shutdown daemon on SIGTERM
|
|
||||||
break |
|
||||||
} |
|
||||||
log.Printf("signalhandler: Ignoring signal '%s'. Send SIGTERM to trigger shutdown.\n", sig.String()) |
|
||||||
} |
|
||||||
|
|
||||||
log.Println("signalhandler: Sending shutdown signals to components") |
|
||||||
sendSignals(sig, subscribers, done) |
|
||||||
|
|
||||||
log.Println("signalhandler: Shutting down the server") |
|
||||||
if err := server.Shutdown(context.Background()); err != nil { |
|
||||||
log.Printf("HTTP server Shutdown: %v", err) |
|
||||||
} |
|
||||||
} |
|
||||||
Loading…
Reference in new issue