mirror of https://github.com/curusarn/resh
Merge 414a42f767 into 7ae8e34ce8
commit
f7eba8a67d
@ -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,80 +0,0 @@ |
|||||||
package cmd |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
"os" |
|
||||||
"os/user" |
|
||||||
"path/filepath" |
|
||||||
|
|
||||||
"github.com/BurntSushi/toml" |
|
||||||
"github.com/curusarn/resh/cmd/control/status" |
|
||||||
"github.com/curusarn/resh/pkg/cfg" |
|
||||||
"github.com/spf13/cobra" |
|
||||||
) |
|
||||||
|
|
||||||
// Enable commands
|
|
||||||
|
|
||||||
var enableCmd = &cobra.Command{ |
|
||||||
Use: "enable", |
|
||||||
Short: "enable RESH features (bindings)", |
|
||||||
} |
|
||||||
|
|
||||||
var enableControlRBindingCmd = &cobra.Command{ |
|
||||||
Use: "ctrl_r_binding", |
|
||||||
Short: "enable RESH-CLI binding for Ctrl+R", |
|
||||||
Run: func(cmd *cobra.Command, args []string) { |
|
||||||
exitCode = enableDisableControlRBindingGlobally(true) |
|
||||||
if exitCode == status.Success { |
|
||||||
exitCode = status.EnableControlRBinding |
|
||||||
} |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
// Disable commands
|
|
||||||
|
|
||||||
var disableCmd = &cobra.Command{ |
|
||||||
Use: "disable", |
|
||||||
Short: "disable RESH features (bindings)", |
|
||||||
} |
|
||||||
|
|
||||||
var disableControlRBindingCmd = &cobra.Command{ |
|
||||||
Use: "ctrl_r_binding", |
|
||||||
Short: "disable RESH-CLI binding for Ctrl+R", |
|
||||||
Run: func(cmd *cobra.Command, args []string) { |
|
||||||
exitCode = enableDisableControlRBindingGlobally(false) |
|
||||||
if exitCode == status.Success { |
|
||||||
exitCode = status.DisableControlRBinding |
|
||||||
} |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
func enableDisableControlRBindingGlobally(value bool) status.Code { |
|
||||||
usr, _ := user.Current() |
|
||||||
dir := usr.HomeDir |
|
||||||
configPath := filepath.Join(dir, ".config/resh.toml") |
|
||||||
var config cfg.Config |
|
||||||
if _, err := toml.DecodeFile(configPath, &config); err != nil { |
|
||||||
fmt.Println("Error reading config", err) |
|
||||||
return status.Fail |
|
||||||
} |
|
||||||
if config.BindControlR != value { |
|
||||||
config.BindControlR = value |
|
||||||
|
|
||||||
f, err := os.Create(configPath) |
|
||||||
if err != nil { |
|
||||||
fmt.Println("Error: Failed to create/open file:", configPath, "; error:", err) |
|
||||||
return status.Fail |
|
||||||
} |
|
||||||
defer f.Close() |
|
||||||
if err := toml.NewEncoder(f).Encode(config); err != nil { |
|
||||||
fmt.Println("Error: Failed to encode and write the config values to hdd. error:", err) |
|
||||||
return status.Fail |
|
||||||
} |
|
||||||
} |
|
||||||
if value { |
|
||||||
fmt.Println("RESH SEARCH app Ctrl+R binding: ENABLED") |
|
||||||
} else { |
|
||||||
fmt.Println("RESH SEARCH app Ctrl+R binding: DISABLED") |
|
||||||
} |
|
||||||
return status.Success |
|
||||||
} |
|
||||||
@ -1,54 +0,0 @@ |
|||||||
package cmd |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
"os" |
|
||||||
"os/exec" |
|
||||||
"os/user" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/cmd/control/status" |
|
||||||
"github.com/spf13/cobra" |
|
||||||
) |
|
||||||
|
|
||||||
var sanitizeCmd = &cobra.Command{ |
|
||||||
Use: "sanitize", |
|
||||||
Short: "produce a sanitized version of your RESH history", |
|
||||||
Run: func(cmd *cobra.Command, args []string) { |
|
||||||
exitCode = status.Success |
|
||||||
usr, _ := user.Current() |
|
||||||
dir := usr.HomeDir |
|
||||||
|
|
||||||
fmt.Println() |
|
||||||
fmt.Println(" HOW IT WORKS") |
|
||||||
fmt.Println(" In sanitized history, all sensitive information is replaced with its SHA256 hashes.") |
|
||||||
fmt.Println() |
|
||||||
fmt.Println("Creating sanitized history files ...") |
|
||||||
fmt.Println(" * ~/resh_history_sanitized.json (full lengh hashes)") |
|
||||||
execCmd := exec.Command("resh-sanitize", "-trim-hashes", "0", "--output", dir+"/resh_history_sanitized.json") |
|
||||||
execCmd.Stdout = os.Stdout |
|
||||||
execCmd.Stderr = os.Stderr |
|
||||||
err := execCmd.Run() |
|
||||||
if err != nil { |
|
||||||
exitCode = status.Fail |
|
||||||
} |
|
||||||
|
|
||||||
fmt.Println(" * ~/resh_history_sanitized_trim12.json (12 char hashes)") |
|
||||||
execCmd = exec.Command("resh-sanitize", "-trim-hashes", "12", "--output", dir+"/resh_history_sanitized_trim12.json") |
|
||||||
execCmd.Stdout = os.Stdout |
|
||||||
execCmd.Stderr = os.Stderr |
|
||||||
err = execCmd.Run() |
|
||||||
if err != nil { |
|
||||||
exitCode = status.Fail |
|
||||||
} |
|
||||||
fmt.Println() |
|
||||||
fmt.Println("Please direct all questions and/or issues to: https://github.com/curusarn/resh/issues") |
|
||||||
fmt.Println() |
|
||||||
fmt.Println("Please look at the resulting sanitized history using commands below.") |
|
||||||
fmt.Println(" * Pretty print JSON") |
|
||||||
fmt.Println(" cat ~/resh_history_sanitized_trim12.json | jq") |
|
||||||
fmt.Println() |
|
||||||
fmt.Println(" * Only show commands, don't show metadata") |
|
||||||
fmt.Println(" cat ~/resh_history_sanitized_trim12.json | jq '.[\"cmdLine\"]'") |
|
||||||
fmt.Println() |
|
||||||
}, |
|
||||||
} |
|
||||||
@ -1,72 +0,0 @@ |
|||||||
package cmd |
|
||||||
|
|
||||||
import ( |
|
||||||
"encoding/json" |
|
||||||
"fmt" |
|
||||||
"io/ioutil" |
|
||||||
"log" |
|
||||||
"net/http" |
|
||||||
"os" |
|
||||||
"strconv" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/cmd/control/status" |
|
||||||
"github.com/curusarn/resh/pkg/msg" |
|
||||||
"github.com/spf13/cobra" |
|
||||||
) |
|
||||||
|
|
||||||
var statusCmd = &cobra.Command{ |
|
||||||
Use: "status", |
|
||||||
Short: "show RESH status", |
|
||||||
Run: func(cmd *cobra.Command, args []string) { |
|
||||||
fmt.Println("resh " + version) |
|
||||||
fmt.Println() |
|
||||||
fmt.Println("Resh versions ...") |
|
||||||
fmt.Println(" * installed: " + version + " (" + commit + ")") |
|
||||||
versionEnv, found := os.LookupEnv("__RESH_VERSION") |
|
||||||
if found == false { |
|
||||||
versionEnv = "UNKNOWN!" |
|
||||||
} |
|
||||||
commitEnv, found := os.LookupEnv("__RESH_REVISION") |
|
||||||
if found == false { |
|
||||||
commitEnv = "unknown" |
|
||||||
} |
|
||||||
fmt.Println(" * this shell session: " + versionEnv + " (" + commitEnv + ")") |
|
||||||
|
|
||||||
resp, err := getDaemonStatus(config.Port) |
|
||||||
if err != nil { |
|
||||||
fmt.Println(" * RESH-DAEMON IS NOT RUNNING") |
|
||||||
fmt.Println(" * Please REPORT this here: https://github.com/curusarn/resh/issues") |
|
||||||
fmt.Println(" * Please RESTART this terminal window") |
|
||||||
exitCode = status.Fail |
|
||||||
return |
|
||||||
} |
|
||||||
fmt.Println(" * daemon: " + resp.Version + " (" + resp.Commit + ")") |
|
||||||
|
|
||||||
if version != resp.Version || version != versionEnv { |
|
||||||
fmt.Println(" * THERE IS A MISMATCH BETWEEN VERSIONS!") |
|
||||||
fmt.Println(" * Please REPORT this here: https://github.com/curusarn/resh/issues") |
|
||||||
fmt.Println(" * Please RESTART this terminal window") |
|
||||||
} |
|
||||||
|
|
||||||
exitCode = status.ReshStatus |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
func getDaemonStatus(port int) (msg.StatusResponse, error) { |
|
||||||
mess := msg.StatusResponse{} |
|
||||||
url := "http://localhost:" + strconv.Itoa(port) + "/status" |
|
||||||
resp, err := http.Get(url) |
|
||||||
if err != nil { |
|
||||||
return mess, err |
|
||||||
} |
|
||||||
defer resp.Body.Close() |
|
||||||
jsn, err := ioutil.ReadAll(resp.Body) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("Error while reading 'daemon /status' response:", err) |
|
||||||
} |
|
||||||
err = json.Unmarshal(jsn, &mess) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("Error while decoding 'daemon /status' response:", err) |
|
||||||
} |
|
||||||
return mess, nil |
|
||||||
} |
|
||||||
@ -0,0 +1,81 @@ |
|||||||
|
package cmd |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"net/http" |
||||||
|
"os" |
||||||
|
"strconv" |
||||||
|
|
||||||
|
"github.com/curusarn/resh/internal/msg" |
||||||
|
"github.com/spf13/cobra" |
||||||
|
) |
||||||
|
|
||||||
|
var versionCmd = &cobra.Command{ |
||||||
|
Use: "version", |
||||||
|
Short: "show RESH version", |
||||||
|
Run: func(cmd *cobra.Command, args []string) { |
||||||
|
printVersion("Installed", version, commit) |
||||||
|
|
||||||
|
versionEnv := getEnvVarWithDefault("__RESH_VERSION", "<unknown>") |
||||||
|
commitEnv := getEnvVarWithDefault("__RESH_REVISION", "<unknown>") |
||||||
|
printVersion("This terminal session", versionEnv, commitEnv) |
||||||
|
|
||||||
|
// TODO: use output.Output.Error... for these
|
||||||
|
resp, err := getDaemonStatus(config.Port) |
||||||
|
if err != nil { |
||||||
|
fmt.Fprintf(os.Stderr, "\nERROR: Resh-daemon didn't respond - it's probably not running.\n\n") |
||||||
|
fmt.Fprintf(os.Stderr, "-> Try restarting this terminal window to bring resh-daemon back up.\n") |
||||||
|
fmt.Fprintf(os.Stderr, "-> If the problem persists you can check resh-daemon logs: ~/.resh/daemon.log\n") |
||||||
|
fmt.Fprintf(os.Stderr, "-> You can file an issue at: https://github.com/curusarn/resh/issues\n") |
||||||
|
return |
||||||
|
} |
||||||
|
printVersion("Currently running daemon", resp.Version, resp.Commit) |
||||||
|
|
||||||
|
if version != resp.Version { |
||||||
|
fmt.Fprintf(os.Stderr, "\nWARN: Resh-daemon is running in different version than is installed now - it looks like something went wrong during resh update.\n\n") |
||||||
|
fmt.Fprintf(os.Stderr, "-> Kill resh-daemon and then launch a new terminal window to fix that.\n") |
||||||
|
fmt.Fprintf(os.Stderr, " $ pkill resh-daemon\n") |
||||||
|
fmt.Fprintf(os.Stderr, "-> You can file an issue at: https://github.com/curusarn/resh/issues\n") |
||||||
|
return |
||||||
|
} |
||||||
|
if version != versionEnv { |
||||||
|
fmt.Fprintf(os.Stderr, "\nWARN: 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.\n\n") |
||||||
|
fmt.Fprintf(os.Stderr, "-> Restart this terminal window to fix that.\n") |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
func printVersion(title, version, commit string) { |
||||||
|
fmt.Printf("%s: %s (commit: %s)\n", title, version, commit) |
||||||
|
} |
||||||
|
|
||||||
|
func getEnvVarWithDefault(varName, defaultValue string) string { |
||||||
|
val, found := os.LookupEnv(varName) |
||||||
|
if !found { |
||||||
|
return defaultValue |
||||||
|
} |
||||||
|
return val |
||||||
|
} |
||||||
|
|
||||||
|
func getDaemonStatus(port int) (msg.StatusResponse, error) { |
||||||
|
mess := msg.StatusResponse{} |
||||||
|
url := "http://localhost:" + strconv.Itoa(port) + "/status" |
||||||
|
resp, err := http.Get(url) |
||||||
|
if err != nil { |
||||||
|
return mess, err |
||||||
|
} |
||||||
|
defer resp.Body.Close() |
||||||
|
jsn, err := io.ReadAll(resp.Body) |
||||||
|
if err != nil { |
||||||
|
out.Fatal("Error while reading 'daemon /status' response", err) |
||||||
|
} |
||||||
|
err = json.Unmarshal(jsn, &mess) |
||||||
|
if err != nil { |
||||||
|
out.Fatal("Error while decoding 'daemon /status' response", err) |
||||||
|
} |
||||||
|
return mess, nil |
||||||
|
} |
||||||
@ -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, err := logger.New("daemon", config.LogLevel, developement) |
||||||
" 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 { |
if err != nil { |
||||||
log.Fatalf("Error opening file: %v\n", err) |
fmt.Printf("Error while creating logger: %v", err) |
||||||
} |
} |
||||||
defer f.Close() |
defer logger.Sync() // flushes buffer, if any
|
||||||
|
if errCfg != nil { |
||||||
log.SetOutput(f) |
logger.Error("Error while getting configuration", zap.Error(errCfg)) |
||||||
log.SetPrefix(strconv.Itoa(os.Getpid()) + " | ") |
|
||||||
|
|
||||||
var config cfg.Config |
|
||||||
if _, err := toml.DecodeFile(configPath, &config); err != nil { |
|
||||||
log.Printf("Error reading config: %v\n", err) |
|
||||||
return |
|
||||||
} |
} |
||||||
if config.Debug { |
sugar := logger.Sugar() |
||||||
Debug = true |
d := daemon{sugar: sugar} |
||||||
log.SetFlags(log.LstdFlags | log.Lmicroseconds) |
sugar.Infow("Deamon starting ...", |
||||||
|
"version", version, |
||||||
|
"commit", commit, |
||||||
|
) |
||||||
|
|
||||||
|
// 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 { |
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 { |
if err != nil { |
||||||
log.Printf("Error while killing daemon: %v\n", err) |
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) |
||||||
} |
} |
||||||
err = ioutil.WriteFile(pidfilePath, []byte(strconv.Itoa(os.Getpid())), 0644) |
d.sugar.Infow("Successfully read PID file", "contents", string(dat)) |
||||||
|
pid, err := strconv.Atoi(strings.TrimSuffix(string(dat), "\n")) |
||||||
if err != nil { |
if err != nil { |
||||||
log.Fatalf("Could not create pidfile: %v\n", err) |
return fmt.Errorf("could not parse PID file contents: %w", err) |
||||||
} |
} |
||||||
runServer(config, reshHistoryPath, bashHistoryPath, zshHistoryPath) |
d.sugar.Infow("Successfully parsed PID", "PID", pid) |
||||||
log.Println("main: Removing pidfile ...") |
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,109 +0,0 @@ |
|||||||
package main |
|
||||||
|
|
||||||
import ( |
|
||||||
"encoding/json" |
|
||||||
"io/ioutil" |
|
||||||
"log" |
|
||||||
"net/http" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/collect" |
|
||||||
"github.com/curusarn/resh/pkg/msg" |
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
"github.com/curusarn/resh/pkg/sesshist" |
|
||||||
) |
|
||||||
|
|
||||||
type recallHandler struct { |
|
||||||
sesshistDispatch *sesshist.Dispatch |
|
||||||
} |
|
||||||
|
|
||||||
func (h *recallHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
|
||||||
if Debug { |
|
||||||
log.Println("/recall START") |
|
||||||
log.Println("/recall reading body ...") |
|
||||||
} |
|
||||||
jsn, err := ioutil.ReadAll(r.Body) |
|
||||||
if err != nil { |
|
||||||
log.Println("Error reading the body", err) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
rec := records.SlimRecord{} |
|
||||||
if Debug { |
|
||||||
log.Println("/recall unmarshaling record ...") |
|
||||||
} |
|
||||||
err = json.Unmarshal(jsn, &rec) |
|
||||||
if err != nil { |
|
||||||
log.Println("Decoding error:", err) |
|
||||||
log.Println("Payload:", jsn) |
|
||||||
return |
|
||||||
} |
|
||||||
if Debug { |
|
||||||
log.Println("/recall recalling ...") |
|
||||||
} |
|
||||||
found := true |
|
||||||
cmd, err := h.sesshistDispatch.Recall(rec.SessionID, rec.RecallHistno, rec.RecallPrefix) |
|
||||||
if err != nil { |
|
||||||
log.Println("/recall - sess id:", rec.SessionID, " - histno:", rec.RecallHistno, " -> ERROR") |
|
||||||
log.Println("Recall error:", err) |
|
||||||
found = false |
|
||||||
cmd = "" |
|
||||||
} |
|
||||||
resp := collect.SingleResponse{CmdLine: cmd, Found: found} |
|
||||||
if Debug { |
|
||||||
log.Println("/recall marshaling response ...") |
|
||||||
} |
|
||||||
jsn, err = json.Marshal(&resp) |
|
||||||
if err != nil { |
|
||||||
log.Println("Encoding error:", err) |
|
||||||
log.Println("Response:", resp) |
|
||||||
return |
|
||||||
} |
|
||||||
if Debug { |
|
||||||
log.Println(string(jsn)) |
|
||||||
log.Println("/recall writing response ...") |
|
||||||
} |
|
||||||
w.Write(jsn) |
|
||||||
log.Println("/recall END - sess id:", rec.SessionID, " - histno:", rec.RecallHistno, " -> ", cmd, " (found:", found, ")") |
|
||||||
} |
|
||||||
|
|
||||||
type inspectHandler struct { |
|
||||||
sesshistDispatch *sesshist.Dispatch |
|
||||||
} |
|
||||||
|
|
||||||
func (h *inspectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
|
||||||
log.Println("/inspect START") |
|
||||||
log.Println("/inspect reading body ...") |
|
||||||
jsn, err := ioutil.ReadAll(r.Body) |
|
||||||
if err != nil { |
|
||||||
log.Println("Error reading the body", err) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
mess := msg.InspectMsg{} |
|
||||||
log.Println("/inspect unmarshaling record ...") |
|
||||||
err = json.Unmarshal(jsn, &mess) |
|
||||||
if err != nil { |
|
||||||
log.Println("Decoding error:", err) |
|
||||||
log.Println("Payload:", jsn) |
|
||||||
return |
|
||||||
} |
|
||||||
log.Println("/inspect recalling ...") |
|
||||||
cmds, err := h.sesshistDispatch.Inspect(mess.SessionID, int(mess.Count)) |
|
||||||
if err != nil { |
|
||||||
log.Println("/inspect - sess id:", mess.SessionID, " - count:", mess.Count, " -> ERROR") |
|
||||||
log.Println("Inspect error:", err) |
|
||||||
return |
|
||||||
} |
|
||||||
resp := msg.MultiResponse{CmdLines: cmds} |
|
||||||
log.Println("/inspect marshaling response ...") |
|
||||||
jsn, err = json.Marshal(&resp) |
|
||||||
if err != nil { |
|
||||||
log.Println("Encoding error:", err) |
|
||||||
log.Println("Response:", resp) |
|
||||||
return |
|
||||||
} |
|
||||||
// log.Println(string(jsn))
|
|
||||||
log.Println("/inspect writing response ...") |
|
||||||
w.Write(jsn) |
|
||||||
log.Println("/inspect END - sess id:", mess.SessionID, " - count:", mess.Count) |
|
||||||
} |
|
||||||
@ -1,152 +0,0 @@ |
|||||||
package main |
|
||||||
|
|
||||||
import ( |
|
||||||
"flag" |
|
||||||
"fmt" |
|
||||||
"log" |
|
||||||
"os" |
|
||||||
"os/user" |
|
||||||
"path/filepath" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/histanal" |
|
||||||
"github.com/curusarn/resh/pkg/strat" |
|
||||||
) |
|
||||||
|
|
||||||
// version from git set during build
|
|
||||||
var version string |
|
||||||
|
|
||||||
// commit from git set during build
|
|
||||||
var commit string |
|
||||||
|
|
||||||
func main() { |
|
||||||
const maxCandidates = 50 |
|
||||||
|
|
||||||
usr, _ := user.Current() |
|
||||||
dir := usr.HomeDir |
|
||||||
historyPath := filepath.Join(dir, ".resh_history.json") |
|
||||||
historyPathBatchMode := filepath.Join(dir, "resh_history.json") |
|
||||||
sanitizedHistoryPath := filepath.Join(dir, "resh_history_sanitized.json") |
|
||||||
// tmpPath := "/tmp/resh-evaluate-tmp.json"
|
|
||||||
|
|
||||||
showVersion := flag.Bool("version", false, "Show version and exit") |
|
||||||
showRevision := flag.Bool("revision", false, "Show git revision and exit") |
|
||||||
input := flag.String("input", "", |
|
||||||
"Input file (default: "+historyPath+" OR "+sanitizedHistoryPath+ |
|
||||||
" depending on --sanitized-input option)") |
|
||||||
// outputDir := flag.String("output", "/tmp/resh-evaluate", "Output directory")
|
|
||||||
sanitizedInput := flag.Bool("sanitized-input", false, |
|
||||||
"Handle input as sanitized (also changes default value for input argument)") |
|
||||||
plottingScript := flag.String("plotting-script", "resh-evaluate-plot.py", "Script to use for plotting") |
|
||||||
inputDataRoot := flag.String("input-data-root", "", |
|
||||||
"Input data root, enables batch mode, looks for files matching --input option") |
|
||||||
slow := flag.Bool("slow", false, |
|
||||||
"Enables strategies that takes a long time (e.g. markov chain strategies).") |
|
||||||
skipFailedCmds := flag.Bool("skip-failed-cmds", false, |
|
||||||
"Skips records with non-zero exit status.") |
|
||||||
debugRecords := flag.Float64("debug", 0, "Debug records - percentage of records that should be debugged.") |
|
||||||
|
|
||||||
flag.Parse() |
|
||||||
|
|
||||||
// handle show{Version,Revision} options
|
|
||||||
if *showVersion == true { |
|
||||||
fmt.Println(version) |
|
||||||
os.Exit(0) |
|
||||||
} |
|
||||||
if *showRevision == true { |
|
||||||
fmt.Println(commit) |
|
||||||
os.Exit(0) |
|
||||||
} |
|
||||||
|
|
||||||
// handle batch mode
|
|
||||||
batchMode := false |
|
||||||
if *inputDataRoot != "" { |
|
||||||
batchMode = true |
|
||||||
} |
|
||||||
// set default input
|
|
||||||
if *input == "" { |
|
||||||
if *sanitizedInput { |
|
||||||
*input = sanitizedHistoryPath |
|
||||||
} else if batchMode { |
|
||||||
*input = historyPathBatchMode |
|
||||||
} else { |
|
||||||
*input = historyPath |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
var evaluator histanal.HistEval |
|
||||||
if batchMode { |
|
||||||
evaluator = histanal.NewHistEvalBatchMode(*input, *inputDataRoot, maxCandidates, *skipFailedCmds, *debugRecords, *sanitizedInput) |
|
||||||
} else { |
|
||||||
evaluator = histanal.NewHistEval(*input, maxCandidates, *skipFailedCmds, *debugRecords, *sanitizedInput) |
|
||||||
} |
|
||||||
|
|
||||||
var simpleStrategies []strat.ISimpleStrategy |
|
||||||
var strategies []strat.IStrategy |
|
||||||
|
|
||||||
// dummy := strategyDummy{}
|
|
||||||
// simpleStrategies = append(simpleStrategies, &dummy)
|
|
||||||
|
|
||||||
simpleStrategies = append(simpleStrategies, &strat.Recent{}) |
|
||||||
|
|
||||||
// frequent := strategyFrequent{}
|
|
||||||
// frequent.init()
|
|
||||||
// simpleStrategies = append(simpleStrategies, &frequent)
|
|
||||||
|
|
||||||
// random := strategyRandom{candidatesSize: maxCandidates}
|
|
||||||
// random.init()
|
|
||||||
// simpleStrategies = append(simpleStrategies, &random)
|
|
||||||
|
|
||||||
directory := strat.DirectorySensitive{} |
|
||||||
directory.Init() |
|
||||||
simpleStrategies = append(simpleStrategies, &directory) |
|
||||||
|
|
||||||
// dynamicDistG := strat.DynamicRecordDistance{
|
|
||||||
// MaxDepth: 3000,
|
|
||||||
// DistParams: records.DistParams{Pwd: 10, RealPwd: 10, SessionID: 1, Time: 1, Git: 10},
|
|
||||||
// Label: "10*pwd,10*realpwd,session,time,10*git",
|
|
||||||
// }
|
|
||||||
// dynamicDistG.Init()
|
|
||||||
// strategies = append(strategies, &dynamicDistG)
|
|
||||||
|
|
||||||
// NOTE: this is the decent one !!!
|
|
||||||
// distanceStaticBest := strat.RecordDistance{
|
|
||||||
// MaxDepth: 3000,
|
|
||||||
// DistParams: records.DistParams{Pwd: 10, RealPwd: 10, SessionID: 1, Time: 1},
|
|
||||||
// Label: "10*pwd,10*realpwd,session,time",
|
|
||||||
// }
|
|
||||||
// strategies = append(strategies, &distanceStaticBest)
|
|
||||||
|
|
||||||
recentBash := strat.RecentBash{} |
|
||||||
recentBash.Init() |
|
||||||
strategies = append(strategies, &recentBash) |
|
||||||
|
|
||||||
if *slow { |
|
||||||
|
|
||||||
markovCmd := strat.MarkovChainCmd{Order: 1} |
|
||||||
markovCmd.Init() |
|
||||||
|
|
||||||
markovCmd2 := strat.MarkovChainCmd{Order: 2} |
|
||||||
markovCmd2.Init() |
|
||||||
|
|
||||||
markov := strat.MarkovChain{Order: 1} |
|
||||||
markov.Init() |
|
||||||
|
|
||||||
markov2 := strat.MarkovChain{Order: 2} |
|
||||||
markov2.Init() |
|
||||||
|
|
||||||
simpleStrategies = append(simpleStrategies, &markovCmd2, &markovCmd, &markov2, &markov) |
|
||||||
} |
|
||||||
|
|
||||||
for _, strategy := range simpleStrategies { |
|
||||||
strategies = append(strategies, strat.NewSimpleStrategyWrapper(strategy)) |
|
||||||
} |
|
||||||
|
|
||||||
for _, strat := range strategies { |
|
||||||
err := evaluator.Evaluate(strat) |
|
||||||
if err != nil { |
|
||||||
log.Println("Evaluator evaluate() error:", err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
evaluator.CalculateStatsAndPlot(*plottingScript) |
|
||||||
} |
|
||||||
@ -1,7 +0,0 @@ |
|||||||
package main |
|
||||||
|
|
||||||
import "fmt" |
|
||||||
|
|
||||||
func main() { |
|
||||||
fmt.Println("Hell world") |
|
||||||
} |
|
||||||
@ -1,87 +0,0 @@ |
|||||||
package main |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"encoding/json" |
|
||||||
"flag" |
|
||||||
"fmt" |
|
||||||
"io/ioutil" |
|
||||||
"log" |
|
||||||
"net/http" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/BurntSushi/toml" |
|
||||||
"github.com/curusarn/resh/pkg/cfg" |
|
||||||
"github.com/curusarn/resh/pkg/msg" |
|
||||||
|
|
||||||
"os/user" |
|
||||||
"path/filepath" |
|
||||||
"strconv" |
|
||||||
) |
|
||||||
|
|
||||||
// version from git set during build
|
|
||||||
var version string |
|
||||||
|
|
||||||
// commit from git set during build
|
|
||||||
var commit string |
|
||||||
|
|
||||||
func main() { |
|
||||||
usr, _ := user.Current() |
|
||||||
dir := usr.HomeDir |
|
||||||
configPath := filepath.Join(dir, "/.config/resh.toml") |
|
||||||
|
|
||||||
var config cfg.Config |
|
||||||
if _, err := toml.DecodeFile(configPath, &config); err != nil { |
|
||||||
log.Fatal("Error reading config:", err) |
|
||||||
} |
|
||||||
|
|
||||||
sessionID := flag.String("sessionID", "", "resh generated session id") |
|
||||||
count := flag.Uint("count", 10, "Number of cmdLines to return") |
|
||||||
flag.Parse() |
|
||||||
|
|
||||||
if *sessionID == "" { |
|
||||||
fmt.Println("Error: you need to specify sessionId") |
|
||||||
} |
|
||||||
|
|
||||||
m := msg.InspectMsg{SessionID: *sessionID, Count: *count} |
|
||||||
resp := SendInspectMsg(m, strconv.Itoa(config.Port)) |
|
||||||
for _, cmdLine := range resp.CmdLines { |
|
||||||
fmt.Println("`" + cmdLine + "'") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// SendInspectMsg to daemon
|
|
||||||
func SendInspectMsg(m msg.InspectMsg, port string) msg.MultiResponse { |
|
||||||
recJSON, err := json.Marshal(m) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("send err 1", err) |
|
||||||
} |
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", "http://localhost:"+port+"/inspect", |
|
||||||
bytes.NewBuffer(recJSON)) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("send err 2", err) |
|
||||||
} |
|
||||||
req.Header.Set("Content-Type", "application/json") |
|
||||||
|
|
||||||
client := http.Client{ |
|
||||||
Timeout: 3 * time.Second, |
|
||||||
} |
|
||||||
resp, err := client.Do(req) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("resh-daemon is not running - try restarting this terminal") |
|
||||||
} |
|
||||||
|
|
||||||
defer resp.Body.Close() |
|
||||||
body, err := ioutil.ReadAll(resp.Body) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("read response error") |
|
||||||
} |
|
||||||
// log.Println(string(body))
|
|
||||||
response := msg.MultiResponse{} |
|
||||||
err = json.Unmarshal(body, &response) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("unmarshal resp error: ", err) |
|
||||||
} |
|
||||||
return response |
|
||||||
} |
|
||||||
@ -0,0 +1,14 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
func backup() { |
||||||
|
panic("Backup not implemented yet!") |
||||||
|
// Backup ~/.resh
|
||||||
|
// Backup xdg_data/resh/history.reshjson
|
||||||
|
// TODO: figure out history file localtions when using history sync
|
||||||
|
} |
||||||
|
|
||||||
|
func rollback() { |
||||||
|
panic("Rollback not implemented yet!") |
||||||
|
// Rollback ~/.resh
|
||||||
|
// Rollback history
|
||||||
|
} |
||||||
@ -0,0 +1,50 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
) |
||||||
|
|
||||||
|
// info passed during build
|
||||||
|
var version string |
||||||
|
var commit string |
||||||
|
var developement bool |
||||||
|
|
||||||
|
func main() { |
||||||
|
if len(os.Args) < 2 { |
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: Not eonugh arguments\n") |
||||||
|
printUsage(os.Stderr) |
||||||
|
} |
||||||
|
command := os.Args[1] |
||||||
|
switch command { |
||||||
|
case "backup": |
||||||
|
backup() |
||||||
|
case "rollback": |
||||||
|
rollback() |
||||||
|
case "migrate-config": |
||||||
|
migrateConfig() |
||||||
|
case "migrate-history": |
||||||
|
migrateHistory() |
||||||
|
case "help": |
||||||
|
printUsage(os.Stdout) |
||||||
|
default: |
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: Unknown command: %s\n", command) |
||||||
|
printUsage(os.Stderr) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func printUsage(f *os.File) { |
||||||
|
usage := ` |
||||||
|
USAGE: ./install-utils COMMAND |
||||||
|
Utils used during RESH instalation. |
||||||
|
|
||||||
|
COMMANDS: |
||||||
|
backup backup resh installation and data |
||||||
|
rollback restore resh installation and data from backup |
||||||
|
migrate-config update config to reflect updates |
||||||
|
migrate-history update history to reflect updates |
||||||
|
help show this help |
||||||
|
|
||||||
|
` |
||||||
|
fmt.Fprintf(f, usage) |
||||||
|
} |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
|
||||||
|
"github.com/curusarn/resh/internal/cfg" |
||||||
|
) |
||||||
|
|
||||||
|
func migrateConfig() { |
||||||
|
err := cfg.Touch() |
||||||
|
if err != nil { |
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: Failed to touch config file: %v\n", err) |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
changes, err := cfg.Migrate() |
||||||
|
if err != nil { |
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: Failed to update config file: %v\n", err) |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
if changes { |
||||||
|
fmt.Printf("RESH config file format has changed since last update - your config was updated to reflect the changes.\n") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func migrateHistory() { |
||||||
|
// homeDir, err := os.UserHomeDir()
|
||||||
|
// if err != nil {
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
// TODO: Find history in:
|
||||||
|
// - .resh/history.json (copy) - message user to delete the file once they confirm the new setup works
|
||||||
|
// - .resh_history.json (copy) - message user to delete the file once they confirm the new setup works
|
||||||
|
// - xdg_data/resh/history.reshjson
|
||||||
|
|
||||||
|
// Read xdg_data/resh/history.reshjson
|
||||||
|
// Write xdg_data/resh/history.reshjson
|
||||||
|
// the old format can be found in the backup dir
|
||||||
|
} |
||||||
@ -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 @@ |
|||||||
port = 2627 |
configVersion = "v1" |
||||||
sesswatchPeriodSeconds = 120 |
|
||||||
sesshistInitHistorySize = 1000 |
|
||||||
debug = false |
|
||||||
bindControlR = true |
|
||||||
|
|||||||
@ -1,7 +0,0 @@ |
|||||||
# copyright information |
|
||||||
|
|
||||||
Whitelist contains content from variety of sources. |
|
||||||
|
|
||||||
Part of the whitelist (`./whitelist.txt`) is made of copyrighted content from [FileInfo.com](https://fileinfo.com/filetypes/common). |
|
||||||
|
|
||||||
This content was used with permission from FileInfo.com. |
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,227 @@ |
|||||||
|
package cfg |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
"path" |
||||||
|
|
||||||
|
"github.com/BurntSushi/toml" |
||||||
|
"go.uber.org/zap" |
||||||
|
"go.uber.org/zap/zapcore" |
||||||
|
) |
||||||
|
|
||||||
|
// configFile used to parse the config file
|
||||||
|
type configFile struct { |
||||||
|
// ConfigVersion - never remove this
|
||||||
|
ConfigVersion *string |
||||||
|
|
||||||
|
// added in legacy
|
||||||
|
Port *int |
||||||
|
SesswatchPeriodSeconds *uint |
||||||
|
SesshistInitHistorySize *int |
||||||
|
BindControlR *bool |
||||||
|
Debug *bool |
||||||
|
|
||||||
|
// added in v1
|
||||||
|
LogLevel *string |
||||||
|
|
||||||
|
// added in legacy
|
||||||
|
// deprecated in v1
|
||||||
|
BindArrowKeysBash *bool |
||||||
|
BindArrowKeysZsh *bool |
||||||
|
|
||||||
|
SyncConnectorAddress *string |
||||||
|
SyncConnectorAuthToken *string |
||||||
|
SyncConnectorPullPeriodSeconds *int |
||||||
|
SyncConnectorSendPeriodSeconds *int |
||||||
|
} |
||||||
|
|
||||||
|
// Config returned by this package to be used in the rest of the project
|
||||||
|
type Config struct { |
||||||
|
// Port used by daemon and rest of the components to communicate
|
||||||
|
// Make sure to restart the daemon when you change it
|
||||||
|
Port int |
||||||
|
|
||||||
|
// BindControlR causes CTRL+R to launch the search app
|
||||||
|
BindControlR bool |
||||||
|
// LogLevel used to filter logs
|
||||||
|
LogLevel zapcore.Level |
||||||
|
|
||||||
|
// Debug mode for search app
|
||||||
|
Debug bool |
||||||
|
// SessionWatchPeriodSeconds is how often should daemon check if terminal
|
||||||
|
// sessions are still alive
|
||||||
|
// There is not much need to adjust the value both memory overhead of watched sessions
|
||||||
|
// and the CPU overhead of checking them are relatively low
|
||||||
|
SessionWatchPeriodSeconds uint |
||||||
|
// ReshHistoryMinSize is how large resh history needs to be for
|
||||||
|
// daemon to ignore standard shell history files
|
||||||
|
// Ignoring standard shell history gives us more consistent experience,
|
||||||
|
// but you can increase this to something large to see standard shell history in RESH search
|
||||||
|
ReshHistoryMinSize int |
||||||
|
|
||||||
|
// SyncConnectorAddress used by the daemon to connect to the Sync Connector
|
||||||
|
// examples:
|
||||||
|
// - http://localhost:1234
|
||||||
|
// - http://localhost:1234
|
||||||
|
// - http://192.168.1.1:1324
|
||||||
|
// - https://domain.tld
|
||||||
|
// - https://domain.tld/resh
|
||||||
|
SyncConnectorAddress *string |
||||||
|
|
||||||
|
// SyncConnectorAuthToken used by the daemon to authenticate with the Sync Connector
|
||||||
|
SyncConnectorAuthToken string |
||||||
|
|
||||||
|
// SyncConnectorPullPeriodSeconds how often should Resh daemon download history from Sync Connector
|
||||||
|
SyncConnectorPullPeriodSeconds int |
||||||
|
|
||||||
|
// SyncConnectorSendPeriodSeconds how often should Resh daemon send history to the Sync Connector
|
||||||
|
SyncConnectorSendPeriodSeconds int |
||||||
|
} |
||||||
|
|
||||||
|
// defaults for config
|
||||||
|
var defaults = Config{ |
||||||
|
Port: 2627, |
||||||
|
LogLevel: zap.InfoLevel, |
||||||
|
BindControlR: true, |
||||||
|
|
||||||
|
Debug: false, |
||||||
|
SessionWatchPeriodSeconds: 600, |
||||||
|
ReshHistoryMinSize: 1000, |
||||||
|
|
||||||
|
SyncConnectorPullPeriodSeconds: 30, |
||||||
|
SyncConnectorSendPeriodSeconds: 30, |
||||||
|
} |
||||||
|
|
||||||
|
const headerComment = `## |
||||||
|
###################### |
||||||
|
## RESH config (v1) ## |
||||||
|
###################### |
||||||
|
## Here you can find info about RESH configuration options. |
||||||
|
## You can uncomment the options and custimize them. |
||||||
|
|
||||||
|
## Required. |
||||||
|
## The config format can change in future versions. |
||||||
|
## ConfigVersion helps us seemlessly upgrade to the new formats. |
||||||
|
# ConfigVersion = "v1" |
||||||
|
|
||||||
|
## Port used by RESH daemon and rest of the components to communicate. |
||||||
|
## Make sure to restart the daemon (pkill resh-daemon) when you change it. |
||||||
|
# Port = 2627 |
||||||
|
|
||||||
|
## Controls how much and how detailed logs all RESH components produce. |
||||||
|
## Use "debug" for full logs when you encounter an issue |
||||||
|
## Options: "debug", "info", "warn", "error", "fatal" |
||||||
|
# LogLevel = "info" |
||||||
|
|
||||||
|
## When BindControlR is "true" RESH search app is bound to CTRL+R on terminal startuA |
||||||
|
# BindControlR = true |
||||||
|
|
||||||
|
## When Debug is "true" the RESH search app runs in debug mode. |
||||||
|
## This is useful for development. |
||||||
|
# Debug = false |
||||||
|
|
||||||
|
## Daemon keeps track of running terminal sessions. |
||||||
|
## SessionWatchPeriodSeconds controls how often daemon checks if the sessions are still alive. |
||||||
|
## You shouldn't need to adjust this. |
||||||
|
# SessionWatchPeriodSeconds = 600 |
||||||
|
|
||||||
|
## When RESH is first installed there is no RESH history so there is nothing to search. |
||||||
|
## As a temporary woraround, RESH daemon parses bash/zsh shell history and searches it. |
||||||
|
## Once RESH history is big enough RESH stops using bash/zsh history. |
||||||
|
## ReshHistoryMinSize controls how big RESH history needs to be before this happens. |
||||||
|
## You can increase this this to e.g. 10000 to get RESH to use bash/zsh history longer. |
||||||
|
# ReshHistoryMinSize = 1000 |
||||||
|
|
||||||
|
` |
||||||
|
|
||||||
|
// TODO: Add description for the new options.
|
||||||
|
|
||||||
|
func getConfigPath() (string, error) { |
||||||
|
fname := "resh.toml" |
||||||
|
xdgDir, found := os.LookupEnv("XDG_CONFIG_HOME") |
||||||
|
if found { |
||||||
|
return path.Join(xdgDir, fname), nil |
||||||
|
} |
||||||
|
homeDir, err := os.UserHomeDir() |
||||||
|
if err != nil { |
||||||
|
return "", fmt.Errorf("could not get user home dir: %w", err) |
||||||
|
} |
||||||
|
return path.Join(homeDir, ".config", fname), nil |
||||||
|
} |
||||||
|
|
||||||
|
func readConfig(path string) (*configFile, error) { |
||||||
|
var config configFile |
||||||
|
if _, err := toml.DecodeFile(path, &config); err != nil { |
||||||
|
return &config, fmt.Errorf("could not decode config: %w", err) |
||||||
|
} |
||||||
|
return &config, nil |
||||||
|
} |
||||||
|
|
||||||
|
func getConfig() (*configFile, error) { |
||||||
|
path, err := getConfigPath() |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("could not get config file path: %w", err) |
||||||
|
} |
||||||
|
return readConfig(path) |
||||||
|
} |
||||||
|
|
||||||
|
// returned config is always usable, returned errors are informative
|
||||||
|
func processAndFillDefaults(configF *configFile) (Config, error) { |
||||||
|
config := defaults |
||||||
|
|
||||||
|
if configF.Port != nil { |
||||||
|
config.Port = *configF.Port |
||||||
|
} |
||||||
|
if configF.SesswatchPeriodSeconds != nil { |
||||||
|
config.SessionWatchPeriodSeconds = *configF.SesswatchPeriodSeconds |
||||||
|
} |
||||||
|
if configF.SesshistInitHistorySize != nil { |
||||||
|
config.ReshHistoryMinSize = *configF.SesshistInitHistorySize |
||||||
|
} |
||||||
|
|
||||||
|
var err error |
||||||
|
if configF.LogLevel != nil { |
||||||
|
logLevel, err := zapcore.ParseLevel(*configF.LogLevel) |
||||||
|
if err != nil { |
||||||
|
err = fmt.Errorf("could not parse log level: %w", err) |
||||||
|
} else { |
||||||
|
config.LogLevel = logLevel |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if configF.BindControlR != nil { |
||||||
|
config.BindControlR = *configF.BindControlR |
||||||
|
} |
||||||
|
|
||||||
|
config.SyncConnectorAddress = configF.SyncConnectorAddress |
||||||
|
|
||||||
|
if configF.SyncConnectorAuthToken != nil { |
||||||
|
config.SyncConnectorAuthToken = *configF.SyncConnectorAuthToken |
||||||
|
} |
||||||
|
|
||||||
|
if configF.SyncConnectorPullPeriodSeconds != nil { |
||||||
|
config.SyncConnectorPullPeriodSeconds = *configF.SyncConnectorPullPeriodSeconds |
||||||
|
} |
||||||
|
|
||||||
|
if configF.SyncConnectorSendPeriodSeconds != nil { |
||||||
|
config.SyncConnectorSendPeriodSeconds = *configF.SyncConnectorSendPeriodSeconds |
||||||
|
} |
||||||
|
|
||||||
|
return config, err |
||||||
|
} |
||||||
|
|
||||||
|
// New returns a config file
|
||||||
|
// returned config is always usable, returned errors are informative
|
||||||
|
func New() (Config, error) { |
||||||
|
configF, err := getConfig() |
||||||
|
if err != nil { |
||||||
|
return defaults, fmt.Errorf("using default config because of error while getting/reading config: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
config, err := processAndFillDefaults(configF) |
||||||
|
if err != nil { |
||||||
|
return config, fmt.Errorf("errors while processing config: %w", err) |
||||||
|
} |
||||||
|
return config, nil |
||||||
|
} |
||||||
@ -0,0 +1,117 @@ |
|||||||
|
package cfg |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
|
||||||
|
"github.com/BurntSushi/toml" |
||||||
|
) |
||||||
|
|
||||||
|
// Touch config file
|
||||||
|
func Touch() error { |
||||||
|
path, err := getConfigPath() |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("could not get config file path: %w", err) |
||||||
|
} |
||||||
|
file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("could not open/create config file: %w", err) |
||||||
|
} |
||||||
|
err = file.Close() |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("could not close config file: %w", err) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Migrate old config versions to current config version
|
||||||
|
// returns true if any changes were made to the config
|
||||||
|
func Migrate() (bool, error) { |
||||||
|
path, err := getConfigPath() |
||||||
|
if err != nil { |
||||||
|
return false, fmt.Errorf("could not get config file path: %w", err) |
||||||
|
} |
||||||
|
configF, err := readConfig(path) |
||||||
|
if err != nil { |
||||||
|
return false, fmt.Errorf("could not read config: %w", err) |
||||||
|
} |
||||||
|
const current = "v1" |
||||||
|
if configF.ConfigVersion != nil && *configF.ConfigVersion == current { |
||||||
|
return false, nil |
||||||
|
} |
||||||
|
|
||||||
|
if configF.ConfigVersion == nil { |
||||||
|
configF, err = legacyToV1(configF) |
||||||
|
if err != nil { |
||||||
|
return true, fmt.Errorf("error converting config from version 'legacy' to 'v1': %w", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if *configF.ConfigVersion != current { |
||||||
|
return false, fmt.Errorf("unrecognized config version: '%s'", *configF.ConfigVersion) |
||||||
|
} |
||||||
|
err = writeConfig(configF, path) |
||||||
|
if err != nil { |
||||||
|
return true, fmt.Errorf("could not write migrated config: %w", err) |
||||||
|
} |
||||||
|
return true, nil |
||||||
|
} |
||||||
|
|
||||||
|
// writeConfig should only be used when migrating config to new version
|
||||||
|
// writing the config file discards all comments in the config file (limitation of TOML library)
|
||||||
|
// to make up for lost comments we add header comment to the start of the file
|
||||||
|
func writeConfig(config *configFile, path string) error { |
||||||
|
file, err := os.OpenFile(path, os.O_RDWR|os.O_TRUNC, 0666) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("could not open config for writing: %w", err) |
||||||
|
} |
||||||
|
defer file.Close() |
||||||
|
_, err = file.WriteString(headerComment) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("could not write config header: %w", err) |
||||||
|
} |
||||||
|
err = toml.NewEncoder(file).Encode(config) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("could not encode config: %w", err) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func legacyToV1(config *configFile) (*configFile, error) { |
||||||
|
if config.ConfigVersion != nil { |
||||||
|
return nil, fmt.Errorf("config version is not 'legacy': '%s'", *config.ConfigVersion) |
||||||
|
} |
||||||
|
version := "v1" |
||||||
|
newConf := configFile{ |
||||||
|
ConfigVersion: &version, |
||||||
|
} |
||||||
|
// Remove defaults
|
||||||
|
if config.Port != nil && *config.Port != 2627 { |
||||||
|
newConf.Port = config.Port |
||||||
|
} |
||||||
|
if config.SesswatchPeriodSeconds != nil && *config.SesswatchPeriodSeconds != 120 { |
||||||
|
newConf.SesswatchPeriodSeconds = config.SesswatchPeriodSeconds |
||||||
|
} |
||||||
|
if config.SesshistInitHistorySize != nil && *config.SesshistInitHistorySize != 1000 { |
||||||
|
newConf.SesshistInitHistorySize = config.SesshistInitHistorySize |
||||||
|
} |
||||||
|
if config.BindControlR != nil && *config.BindControlR != true { |
||||||
|
newConf.BindControlR = config.BindControlR |
||||||
|
} |
||||||
|
if config.Debug != nil && *config.Debug != false { |
||||||
|
newConf.Debug = config.Debug |
||||||
|
} |
||||||
|
return &newConf, nil |
||||||
|
} |
||||||
|
|
||||||
|
// func v1ToV2(config *configFile) (*configFile, error) {
|
||||||
|
// if *config.ConfigVersion != "v1" {
|
||||||
|
// return nil, fmt.Errorf("config version is not 'legacy': '%s'", *config.ConfigVersion)
|
||||||
|
// }
|
||||||
|
// version := "v2"
|
||||||
|
// newConf := configFile{
|
||||||
|
// ConfigVersion: &version,
|
||||||
|
// // Here goes all config fields - no need to prune defaults like we do for legacy
|
||||||
|
// }
|
||||||
|
// return &newConf, nil
|
||||||
|
// }
|
||||||
@ -0,0 +1,116 @@ |
|||||||
|
package collect |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/json" |
||||||
|
"io/ioutil" |
||||||
|
"net/http" |
||||||
|
"path/filepath" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/curusarn/resh/internal/output" |
||||||
|
"github.com/curusarn/resh/internal/recordint" |
||||||
|
"go.uber.org/zap" |
||||||
|
) |
||||||
|
|
||||||
|
// SendRecord to daemon
|
||||||
|
func SendRecord(out *output.Output, r recordint.Collect, port, path string) { |
||||||
|
out.Logger.Debug("Sending record ...", |
||||||
|
zap.String("cmdLine", r.Rec.CmdLine), |
||||||
|
zap.String("sessionID", r.SessionID), |
||||||
|
) |
||||||
|
recJSON, err := json.Marshal(r) |
||||||
|
if err != nil { |
||||||
|
out.Fatal("Error while encoding record", err) |
||||||
|
} |
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", "http://localhost:"+port+path, |
||||||
|
bytes.NewBuffer(recJSON)) |
||||||
|
if err != nil { |
||||||
|
out.Fatal("Error while sending record", err) |
||||||
|
} |
||||||
|
req.Header.Set("Content-Type", "application/json") |
||||||
|
|
||||||
|
client := http.Client{ |
||||||
|
Timeout: 1 * time.Second, |
||||||
|
} |
||||||
|
_, err = client.Do(req) |
||||||
|
if err != nil { |
||||||
|
out.FatalDaemonNotRunning(err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// SendSessionInit to daemon
|
||||||
|
func SendSessionInit(out *output.Output, r recordint.SessionInit, port string) { |
||||||
|
out.Logger.Debug("Sending session init ...", |
||||||
|
zap.String("sessionID", r.SessionID), |
||||||
|
zap.Int("sessionPID", r.SessionPID), |
||||||
|
) |
||||||
|
recJSON, err := json.Marshal(r) |
||||||
|
if err != nil { |
||||||
|
out.Fatal("Error while encoding record", err) |
||||||
|
} |
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", "http://localhost:"+port+"/session_init", |
||||||
|
bytes.NewBuffer(recJSON)) |
||||||
|
if err != nil { |
||||||
|
out.Fatal("Error while sending record", err) |
||||||
|
} |
||||||
|
req.Header.Set("Content-Type", "application/json") |
||||||
|
|
||||||
|
client := http.Client{ |
||||||
|
Timeout: 1 * time.Second, |
||||||
|
} |
||||||
|
_, err = client.Do(req) |
||||||
|
if err != nil { |
||||||
|
out.FatalDaemonNotRunning(err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// ReadFileContent and return it as a string
|
||||||
|
func ReadFileContent(logger *zap.Logger, path string) string { |
||||||
|
dat, err := ioutil.ReadFile(path) |
||||||
|
if err != nil { |
||||||
|
logger.Error("Error reading file", |
||||||
|
zap.String("filePath", path), |
||||||
|
zap.Error(err), |
||||||
|
) |
||||||
|
return "" |
||||||
|
} |
||||||
|
return strings.TrimSuffix(string(dat), "\n") |
||||||
|
} |
||||||
|
|
||||||
|
// GetGitDirs based on result of git "cdup" command
|
||||||
|
func GetGitDirs(logger *zap.Logger, cdup string, exitCode int, pwd string) (string, string) { |
||||||
|
if exitCode != 0 { |
||||||
|
return "", "" |
||||||
|
} |
||||||
|
abspath := filepath.Clean(filepath.Join(pwd, cdup)) |
||||||
|
realpath, err := filepath.EvalSymlinks(abspath) |
||||||
|
if err != nil { |
||||||
|
logger.Error("Error while handling git dir paths", zap.Error(err)) |
||||||
|
return "", "" |
||||||
|
} |
||||||
|
return abspath, realpath |
||||||
|
} |
||||||
|
|
||||||
|
// GetTimezoneOffsetInSeconds based on zone returned by date command
|
||||||
|
func GetTimezoneOffsetInSeconds(logger *zap.Logger, zone string) float64 { |
||||||
|
// date +%z -> "+0200"
|
||||||
|
hoursStr := zone[:3] |
||||||
|
minsStr := zone[3:] |
||||||
|
hours, err := strconv.Atoi(hoursStr) |
||||||
|
if err != nil { |
||||||
|
logger.Error("Error while parsing hours in timezone offset", zap.Error(err)) |
||||||
|
return -1 |
||||||
|
} |
||||||
|
mins, err := strconv.Atoi(minsStr) |
||||||
|
if err != nil { |
||||||
|
logger.Error("Errot while parsing minutes in timezone offset:", zap.Error(err)) |
||||||
|
return -1 |
||||||
|
} |
||||||
|
secs := ((hours * 60) + mins) * 60 |
||||||
|
return float64(secs) |
||||||
|
} |
||||||
@ -0,0 +1,71 @@ |
|||||||
|
package datadir |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
"path" |
||||||
|
) |
||||||
|
|
||||||
|
// You should not need this caching
|
||||||
|
// It messes with proper dependency injection
|
||||||
|
// Find another way
|
||||||
|
|
||||||
|
// type dirCache struct {
|
||||||
|
// dir string
|
||||||
|
// err error
|
||||||
|
//
|
||||||
|
// cached bool
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var cache dirCache
|
||||||
|
//
|
||||||
|
// func getPathNoCache() (string, error) {
|
||||||
|
// reshDir := "resh"
|
||||||
|
// xdgDir, found := os.LookupEnv("XDG_DATA_HOME")
|
||||||
|
// if found {
|
||||||
|
// return path.Join(xdgDir, reshDir), nil
|
||||||
|
// }
|
||||||
|
// homeDir, err := os.UserHomeDir()
|
||||||
|
// if err != nil {
|
||||||
|
// return "", fmt.Errorf("error while getting home dir: %w", err)
|
||||||
|
// }
|
||||||
|
// return path.Join(homeDir, ".local/share/", reshDir), nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func GetPath() (string, error) {
|
||||||
|
// if !cache.cached {
|
||||||
|
// dir, err := getPathNoCache()
|
||||||
|
// cache = dirCache{
|
||||||
|
// dir: dir,
|
||||||
|
// err: err,
|
||||||
|
// cached: true,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return cache.dir, cache.err
|
||||||
|
// }
|
||||||
|
|
||||||
|
func GetPath() (string, error) { |
||||||
|
reshDir := "resh" |
||||||
|
xdgDir, found := os.LookupEnv("XDG_DATA_HOME") |
||||||
|
if found { |
||||||
|
return path.Join(xdgDir, reshDir), nil |
||||||
|
} |
||||||
|
homeDir, err := os.UserHomeDir() |
||||||
|
if err != nil { |
||||||
|
return "", fmt.Errorf("error while getting home dir: %w", err) |
||||||
|
} |
||||||
|
return path.Join(homeDir, ".local/share/", reshDir), nil |
||||||
|
} |
||||||
|
|
||||||
|
func MakePath() (string, error) { |
||||||
|
path, err := GetPath() |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
err = os.MkdirAll(path, 0755) |
||||||
|
// skip "exists" error
|
||||||
|
if err != nil && !os.IsExist(err) { |
||||||
|
return "", fmt.Errorf("error while creating directories: %w", err) |
||||||
|
} |
||||||
|
return path, nil |
||||||
|
} |
||||||
@ -0,0 +1,49 @@ |
|||||||
|
package device |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
"path" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
func GetID(dataDir string) (string, error) { |
||||||
|
fname := "device-id" |
||||||
|
dat, err := os.ReadFile(path.Join(dataDir, fname)) |
||||||
|
if err != nil { |
||||||
|
return "", fmt.Errorf("could not read file with device-id: %w", err) |
||||||
|
} |
||||||
|
id := strings.TrimRight(string(dat), "\n") |
||||||
|
return id, nil |
||||||
|
} |
||||||
|
|
||||||
|
func GetName(dataDir string) (string, error) { |
||||||
|
fname := "device-name" |
||||||
|
dat, err := os.ReadFile(path.Join(dataDir, fname)) |
||||||
|
if err != nil { |
||||||
|
return "", fmt.Errorf("could not read file with device-name: %w", err) |
||||||
|
} |
||||||
|
name := strings.TrimRight(string(dat), "\n") |
||||||
|
return name, nil |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: implement, possibly with a better name
|
||||||
|
// func CheckID(dataDir string) (string, error) {
|
||||||
|
// fname := "device-id"
|
||||||
|
// dat, err := os.ReadFile(path.Join(dataDir, fname))
|
||||||
|
// if err != nil {
|
||||||
|
// return "", fmt.Errorf("could not read file with device-id: %w", err)
|
||||||
|
// }
|
||||||
|
// id := strings.TrimRight(string(dat), "\n")
|
||||||
|
// return id, nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func CheckName(dataDir string) (string, error) {
|
||||||
|
// fname := "device-id"
|
||||||
|
// dat, err := os.ReadFile(path.Join(dataDir, fname))
|
||||||
|
// if err != nil {
|
||||||
|
// return "", fmt.Errorf("could not read file with device-id: %w", err)
|
||||||
|
// }
|
||||||
|
// id := strings.TrimRight(string(dat), "\n")
|
||||||
|
// return id, nil
|
||||||
|
// }
|
||||||
@ -0,0 +1,86 @@ |
|||||||
|
package histcli |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/curusarn/resh/internal/recordint" |
||||||
|
"github.com/curusarn/resh/record" |
||||||
|
"go.uber.org/zap" |
||||||
|
"sync" |
||||||
|
) |
||||||
|
|
||||||
|
// Histcli is a dump of history preprocessed for resh cli purposes
|
||||||
|
type Histcli struct { |
||||||
|
// list of records
|
||||||
|
list []recordint.SearchApp |
||||||
|
// TODO It is not optimal to keep both raw and list but is necessary for syncConnector now
|
||||||
|
raw []record.V1 |
||||||
|
knownIds map[string]struct{} |
||||||
|
lock sync.RWMutex |
||||||
|
sugar *zap.SugaredLogger |
||||||
|
latest map[string]float64 |
||||||
|
} |
||||||
|
|
||||||
|
// New Histcli
|
||||||
|
func New(sugar *zap.SugaredLogger) *Histcli { |
||||||
|
return &Histcli{ |
||||||
|
sugar: sugar.With(zap.String("component", "histCli")), |
||||||
|
knownIds: map[string]struct{}{}, |
||||||
|
latest: map[string]float64{}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// AddRecord to the histcli
|
||||||
|
func (h *Histcli) AddRecord(rec *recordint.Indexed) { |
||||||
|
cli := recordint.NewSearchApp(rec) |
||||||
|
h.lock.Lock() |
||||||
|
defer h.lock.Unlock() |
||||||
|
|
||||||
|
if _, ok := h.knownIds[rec.Rec.RecordID]; !ok { |
||||||
|
h.knownIds[rec.Rec.RecordID] = struct{}{} |
||||||
|
h.list = append(h.list, cli) |
||||||
|
h.raw = append(h.raw, rec.Rec) |
||||||
|
h.updateLatestPerDevice(cli) |
||||||
|
} else { |
||||||
|
h.sugar.Debugw("Record is already present", "id", rec.Rec.RecordID) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// AddCmdLine to the histcli
|
||||||
|
func (h *Histcli) AddCmdLine(cmdline string) { |
||||||
|
cli := recordint.NewSearchAppFromCmdLine(cmdline) |
||||||
|
h.lock.Lock() |
||||||
|
defer h.lock.Unlock() |
||||||
|
|
||||||
|
h.list = append(h.list, cli) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *Histcli) Dump() []recordint.SearchApp { |
||||||
|
h.lock.RLock() |
||||||
|
defer h.lock.RUnlock() |
||||||
|
|
||||||
|
return h.list |
||||||
|
} |
||||||
|
|
||||||
|
func (h *Histcli) DumpRaw() []record.V1 { |
||||||
|
h.lock.RLock() |
||||||
|
defer h.lock.RUnlock() |
||||||
|
|
||||||
|
return h.raw |
||||||
|
} |
||||||
|
|
||||||
|
// updateLatestPerDevice should be called only with write lock because it does not lock on its own.
|
||||||
|
func (h *Histcli) updateLatestPerDevice(rec recordint.SearchApp) { |
||||||
|
if l, ok := h.latest[rec.DeviceID]; ok { |
||||||
|
if rec.Time > l { |
||||||
|
h.latest[rec.DeviceID] = rec.Time |
||||||
|
} |
||||||
|
} else { |
||||||
|
h.latest[rec.DeviceID] = rec.Time |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (h *Histcli) LatestRecordsPerDevice() map[string]float64 { |
||||||
|
h.lock.RLock() |
||||||
|
defer h.lock.RUnlock() |
||||||
|
|
||||||
|
return h.latest |
||||||
|
} |
||||||
@ -0,0 +1,277 @@ |
|||||||
|
package histfile |
||||||
|
|
||||||
|
import ( |
||||||
|
"math" |
||||||
|
"os" |
||||||
|
"strconv" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"github.com/curusarn/resh/internal/histcli" |
||||||
|
"github.com/curusarn/resh/internal/histlist" |
||||||
|
"github.com/curusarn/resh/internal/recio" |
||||||
|
"github.com/curusarn/resh/internal/recordint" |
||||||
|
"github.com/curusarn/resh/internal/records" |
||||||
|
"github.com/curusarn/resh/internal/recutil" |
||||||
|
"github.com/curusarn/resh/record" |
||||||
|
"go.uber.org/zap" |
||||||
|
) |
||||||
|
|
||||||
|
// TODO: get rid of histfile - use histio instead
|
||||||
|
// Histfile writes records to histfile
|
||||||
|
type Histfile struct { |
||||||
|
sugar *zap.SugaredLogger |
||||||
|
|
||||||
|
sessionsMutex sync.Mutex |
||||||
|
sessions map[string]recordint.Collect |
||||||
|
historyPath string |
||||||
|
|
||||||
|
// NOTE: we have separate histories which only differ if there was not enough resh_history
|
||||||
|
// resh_history itself is common for both bash and zsh
|
||||||
|
bashCmdLines histlist.Histlist |
||||||
|
zshCmdLines histlist.Histlist |
||||||
|
|
||||||
|
cliRecords *histcli.Histcli |
||||||
|
|
||||||
|
rio *recio.RecIO |
||||||
|
} |
||||||
|
|
||||||
|
// New creates new histfile and runs its gorutines
|
||||||
|
func New(sugar *zap.SugaredLogger, input chan recordint.Collect, sessionsToDrop chan string, |
||||||
|
reshHistoryPath string, bashHistoryPath string, zshHistoryPath string, |
||||||
|
maxInitHistSize int, minInitHistSizeKB int, |
||||||
|
signals chan os.Signal, shutdownDone chan string, histCli *histcli.Histcli) *Histfile { |
||||||
|
|
||||||
|
rio := recio.New(sugar.With("module", "histfile")) |
||||||
|
hf := Histfile{ |
||||||
|
sugar: sugar.With("module", "histfile"), |
||||||
|
sessions: map[string]recordint.Collect{}, |
||||||
|
historyPath: reshHistoryPath, |
||||||
|
bashCmdLines: histlist.New(sugar), |
||||||
|
zshCmdLines: histlist.New(sugar), |
||||||
|
cliRecords: histCli, |
||||||
|
rio: &rio, |
||||||
|
} |
||||||
|
go hf.loadHistory(bashHistoryPath, zshHistoryPath, maxInitHistSize, minInitHistSizeKB) |
||||||
|
go hf.writer(input, signals, shutdownDone) |
||||||
|
go hf.sessionGC(sessionsToDrop) |
||||||
|
return &hf |
||||||
|
} |
||||||
|
|
||||||
|
// load records from resh history, reverse, enrich and save
|
||||||
|
func (h *Histfile) loadCliRecords(recs []recordint.Indexed) { |
||||||
|
for _, cmdline := range h.bashCmdLines.List { |
||||||
|
h.cliRecords.AddCmdLine(cmdline) |
||||||
|
} |
||||||
|
for _, cmdline := range h.zshCmdLines.List { |
||||||
|
h.cliRecords.AddCmdLine(cmdline) |
||||||
|
} |
||||||
|
for i := len(recs) - 1; i >= 0; i-- { |
||||||
|
rec := recs[i] |
||||||
|
h.cliRecords.AddRecord(&rec) |
||||||
|
} |
||||||
|
h.sugar.Infow("Resh history loaded", |
||||||
|
"historyRecordsCount", len(h.cliRecords.Dump()), |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
// loadsHistory from resh_history and if there is not enough of it also load native shell histories
|
||||||
|
func (h *Histfile) loadHistory(bashHistoryPath, zshHistoryPath string, maxInitHistSize, minInitHistSizeKB int) { |
||||||
|
h.sugar.Infow("Checking if resh_history is large enough ...") |
||||||
|
fi, err := os.Stat(h.historyPath) |
||||||
|
var size int |
||||||
|
if err != nil { |
||||||
|
h.sugar.Errorw("Failed to stat resh_history file", "error", err) |
||||||
|
} else { |
||||||
|
size = int(fi.Size()) |
||||||
|
} |
||||||
|
useNativeHistories := false |
||||||
|
if size/1024 < minInitHistSizeKB { |
||||||
|
useNativeHistories = true |
||||||
|
h.sugar.Warnw("Resh_history is too small - loading native bash and zsh history ...") |
||||||
|
h.bashCmdLines = records.LoadCmdLinesFromBashFile(h.sugar, bashHistoryPath) |
||||||
|
h.sugar.Infow("Bash history loaded", "cmdLineCount", len(h.bashCmdLines.List)) |
||||||
|
h.zshCmdLines = records.LoadCmdLinesFromZshFile(h.sugar, zshHistoryPath) |
||||||
|
h.sugar.Infow("Zsh history loaded", "cmdLineCount", len(h.zshCmdLines.List)) |
||||||
|
// no maxInitHistSize when using native histories
|
||||||
|
maxInitHistSize = math.MaxInt32 |
||||||
|
} |
||||||
|
h.sugar.Debugw("Loading resh history from file ...", |
||||||
|
"historyFile", h.historyPath, |
||||||
|
) |
||||||
|
history, err := h.rio.ReadAndFixFile(h.historyPath, 3) |
||||||
|
if err != nil { |
||||||
|
h.sugar.Panicf("Failed to read file: %w", err) |
||||||
|
} |
||||||
|
h.sugar.Infow("Resh history loaded from file", |
||||||
|
"historyFile", h.historyPath, |
||||||
|
"recordCount", len(history), |
||||||
|
) |
||||||
|
go h.loadCliRecords(history) |
||||||
|
// NOTE: keeping this weird interface for now because we might use it in the future
|
||||||
|
// when we only load bash or zsh history
|
||||||
|
reshCmdLines := loadCmdLines(h.sugar, history) |
||||||
|
h.sugar.Infow("Resh history loaded and processed", |
||||||
|
"recordCount", len(reshCmdLines.List), |
||||||
|
) |
||||||
|
if useNativeHistories == false { |
||||||
|
h.bashCmdLines = reshCmdLines |
||||||
|
h.zshCmdLines = histlist.Copy(reshCmdLines) |
||||||
|
return |
||||||
|
} |
||||||
|
h.bashCmdLines.AddHistlist(reshCmdLines) |
||||||
|
h.sugar.Infow("Processed bash history and resh history together", "cmdLinecount", len(h.bashCmdLines.List)) |
||||||
|
h.zshCmdLines.AddHistlist(reshCmdLines) |
||||||
|
h.sugar.Infow("Processed zsh history and resh history together", "cmdLineCount", len(h.zshCmdLines.List)) |
||||||
|
} |
||||||
|
|
||||||
|
// sessionGC reads sessionIDs from channel and deletes them from histfile struct
|
||||||
|
func (h *Histfile) sessionGC(sessionsToDrop chan string) { |
||||||
|
for { |
||||||
|
func() { |
||||||
|
session := <-sessionsToDrop |
||||||
|
sugar := h.sugar.With("sessionID", session) |
||||||
|
sugar.Debugw("Got session to drop") |
||||||
|
h.sessionsMutex.Lock() |
||||||
|
defer h.sessionsMutex.Unlock() |
||||||
|
if part1, found := h.sessions[session]; found == true { |
||||||
|
sugar.Infow("Dropping session") |
||||||
|
delete(h.sessions, session) |
||||||
|
go h.rio.AppendToFile(h.historyPath, []record.V1{part1.Rec}) |
||||||
|
} else { |
||||||
|
sugar.Infow("No hanging parts for session - nothing to drop") |
||||||
|
} |
||||||
|
}() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// writer reads records from channel, merges them and writes them to file
|
||||||
|
func (h *Histfile) writer(collect chan recordint.Collect, signals chan os.Signal, shutdownDone chan string) { |
||||||
|
for { |
||||||
|
func() { |
||||||
|
select { |
||||||
|
case rec := <-collect: |
||||||
|
part := "2" |
||||||
|
if rec.Rec.PartOne { |
||||||
|
part = "1" |
||||||
|
} |
||||||
|
sugar := h.sugar.With( |
||||||
|
"recordCmdLine", rec.Rec.CmdLine, |
||||||
|
"recordPart", part, |
||||||
|
"recordShell", rec.Shell, |
||||||
|
) |
||||||
|
sugar.Debugw("Got record") |
||||||
|
h.sessionsMutex.Lock() |
||||||
|
defer h.sessionsMutex.Unlock() |
||||||
|
|
||||||
|
// allows nested sessions to merge records properly
|
||||||
|
mergeID := rec.SessionID + "_" + strconv.Itoa(rec.Shlvl) |
||||||
|
sugar = sugar.With("mergeID", mergeID) |
||||||
|
if rec.Rec.PartOne { |
||||||
|
if _, found := h.sessions[mergeID]; found { |
||||||
|
msg := "Got another first part of the records before merging the previous one - overwriting!" |
||||||
|
if rec.Shell == "zsh" { |
||||||
|
sugar.Warnw(msg) |
||||||
|
} else { |
||||||
|
sugar.Infow(msg + " Unfortunately this is normal in bash, it can't be prevented.") |
||||||
|
} |
||||||
|
} |
||||||
|
h.sessions[mergeID] = rec |
||||||
|
} else { |
||||||
|
if part1, found := h.sessions[mergeID]; found == false { |
||||||
|
sugar.Warnw("Got second part of record and nothing to merge it with - ignoring!") |
||||||
|
} else { |
||||||
|
delete(h.sessions, mergeID) |
||||||
|
go h.mergeAndWriteRecord(sugar, part1, rec) |
||||||
|
} |
||||||
|
} |
||||||
|
case sig := <-signals: |
||||||
|
sugar := h.sugar.With( |
||||||
|
"signal", sig.String(), |
||||||
|
) |
||||||
|
sugar.Infow("Got signal") |
||||||
|
h.sessionsMutex.Lock() |
||||||
|
defer h.sessionsMutex.Unlock() |
||||||
|
sugar.Debugw("Unlocked mutex") |
||||||
|
|
||||||
|
for sessID, rec := range h.sessions { |
||||||
|
sugar.Warnw("Writing incomplete record for session", |
||||||
|
"sessionID", sessID, |
||||||
|
) |
||||||
|
h.writeRecord(sugar, rec.Rec) |
||||||
|
} |
||||||
|
sugar.Debugw("Shutdown successful") |
||||||
|
shutdownDone <- "histfile" |
||||||
|
return |
||||||
|
} |
||||||
|
}() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (h *Histfile) writeRecord(sugar *zap.SugaredLogger, rec record.V1) { |
||||||
|
h.rio.AppendToFile(h.historyPath, []record.V1{rec}) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *Histfile) mergeAndWriteRecord(sugar *zap.SugaredLogger, part1 recordint.Collect, part2 recordint.Collect) { |
||||||
|
rec, err := recutil.Merge(&part1, &part2) |
||||||
|
if err != nil { |
||||||
|
sugar.Errorw("Error while merging records", "error", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
cmdLine := rec.CmdLine |
||||||
|
h.bashCmdLines.AddCmdLine(cmdLine) |
||||||
|
h.zshCmdLines.AddCmdLine(cmdLine) |
||||||
|
h.cliRecords.AddRecord(&recordint.Indexed{ |
||||||
|
// TODO: is this what we want?
|
||||||
|
Rec: rec, |
||||||
|
}) |
||||||
|
|
||||||
|
h.rio.AppendToFile(h.historyPath, []record.V1{rec}) |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: use errors in RecIO
|
||||||
|
// func writeRecord(sugar *zap.SugaredLogger, rec record.V1, outputPath string) {
|
||||||
|
// recJSON, err := json.Marshal(rec)
|
||||||
|
// if err != nil {
|
||||||
|
// sugar.Errorw("Marshalling error", "error", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// f, err := os.OpenFile(outputPath,
|
||||||
|
// os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
// if err != nil {
|
||||||
|
// sugar.Errorw("Could not open file", "error", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// defer f.Close()
|
||||||
|
// _, err = f.Write(append(recJSON, []byte("\n")...))
|
||||||
|
// if err != nil {
|
||||||
|
// sugar.Errorw("Error while writing record",
|
||||||
|
// "recordRaw", rec,
|
||||||
|
// "error", err,
|
||||||
|
// )
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
func loadCmdLines(sugar *zap.SugaredLogger, recs []recordint.Indexed) histlist.Histlist { |
||||||
|
hl := histlist.New(sugar) |
||||||
|
// go from bottom and deduplicate
|
||||||
|
var cmdLines []string |
||||||
|
cmdLinesSet := map[string]bool{} |
||||||
|
for i := len(recs) - 1; i >= 0; i-- { |
||||||
|
cmdLine := recs[i].Rec.CmdLine |
||||||
|
if cmdLinesSet[cmdLine] { |
||||||
|
continue |
||||||
|
} |
||||||
|
cmdLinesSet[cmdLine] = true |
||||||
|
cmdLines = append([]string{cmdLine}, cmdLines...) |
||||||
|
// if len(cmdLines) > limit {
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
} |
||||||
|
// add everything to histlist
|
||||||
|
for _, cmdLine := range cmdLines { |
||||||
|
hl.AddCmdLine(cmdLine) |
||||||
|
} |
||||||
|
return hl |
||||||
|
} |
||||||
@ -0,0 +1,56 @@ |
|||||||
|
package histio |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"github.com/curusarn/resh/internal/recio" |
||||||
|
"github.com/curusarn/resh/internal/recordint" |
||||||
|
"go.uber.org/zap" |
||||||
|
) |
||||||
|
|
||||||
|
type histfile struct { |
||||||
|
sugar *zap.SugaredLogger |
||||||
|
// deviceID string
|
||||||
|
path string |
||||||
|
|
||||||
|
mu sync.RWMutex |
||||||
|
data []recordint.Indexed |
||||||
|
fileinfo os.FileInfo |
||||||
|
} |
||||||
|
|
||||||
|
func newHistfile(sugar *zap.SugaredLogger, path string) *histfile { |
||||||
|
return &histfile{ |
||||||
|
sugar: sugar.With( |
||||||
|
// FIXME: drop V1 once original histfile is gone
|
||||||
|
"component", "histfileV1", |
||||||
|
"path", path, |
||||||
|
), |
||||||
|
// deviceID: deviceID,
|
||||||
|
path: path, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (h *histfile) updateFromFile() error { |
||||||
|
rio := recio.New(h.sugar) |
||||||
|
// TODO: decide and handle errors
|
||||||
|
newData, _, err := rio.ReadFile(h.path) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("could not read history file: %w", err) |
||||||
|
} |
||||||
|
h.mu.Lock() |
||||||
|
defer h.mu.Unlock() |
||||||
|
h.data = newData |
||||||
|
h.updateFileInfo() |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (h *histfile) updateFileInfo() error { |
||||||
|
info, err := os.Stat(h.path) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("history file not found: %w", err) |
||||||
|
} |
||||||
|
h.fileinfo = info |
||||||
|
return nil |
||||||
|
} |
||||||
@ -0,0 +1,44 @@ |
|||||||
|
package histio |
||||||
|
|
||||||
|
import ( |
||||||
|
"path" |
||||||
|
|
||||||
|
"github.com/curusarn/resh/internal/recordint" |
||||||
|
"github.com/curusarn/resh/record" |
||||||
|
"go.uber.org/zap" |
||||||
|
) |
||||||
|
|
||||||
|
type Histio struct { |
||||||
|
sugar *zap.SugaredLogger |
||||||
|
histDir string |
||||||
|
|
||||||
|
thisDeviceID string |
||||||
|
thisHistory *histfile |
||||||
|
// TODO: remote histories
|
||||||
|
// moreHistories map[string]*histfile
|
||||||
|
|
||||||
|
recordsToAppend chan record.V1 |
||||||
|
recordsToFlag chan recordint.Flag |
||||||
|
} |
||||||
|
|
||||||
|
func New(sugar *zap.SugaredLogger, dataDir, deviceID string) *Histio { |
||||||
|
sugarHistio := sugar.With(zap.String("component", "histio")) |
||||||
|
histDir := path.Join(dataDir, "history") |
||||||
|
currPath := path.Join(histDir, deviceID) |
||||||
|
// TODO: file extension for the history, yes or no? (<id>.reshjson vs. <id>)
|
||||||
|
|
||||||
|
// TODO: discover other history files, exclude current
|
||||||
|
|
||||||
|
return &Histio{ |
||||||
|
sugar: sugarHistio, |
||||||
|
histDir: histDir, |
||||||
|
|
||||||
|
thisDeviceID: deviceID, |
||||||
|
thisHistory: newHistfile(sugar, currPath), |
||||||
|
// moreHistories: ...
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (h *Histio) Append(r *record.V1) { |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,27 @@ |
|||||||
|
package logger |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"path/filepath" |
||||||
|
|
||||||
|
"github.com/curusarn/resh/internal/datadir" |
||||||
|
"go.uber.org/zap" |
||||||
|
"go.uber.org/zap/zapcore" |
||||||
|
) |
||||||
|
|
||||||
|
func New(executable string, level zapcore.Level, developement bool) (*zap.Logger, error) { |
||||||
|
dataDir, err := datadir.GetPath() |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("error while getting resh data dir: %w", err) |
||||||
|
} |
||||||
|
logPath := filepath.Join(dataDir, "log.json") |
||||||
|
loggerConfig := zap.NewProductionConfig() |
||||||
|
loggerConfig.OutputPaths = []string{logPath} |
||||||
|
loggerConfig.Level.SetLevel(level) |
||||||
|
loggerConfig.Development = 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 |
||||||
|
} |
||||||
@ -0,0 +1,21 @@ |
|||||||
|
package msg |
||||||
|
|
||||||
|
import "github.com/curusarn/resh/internal/recordint" |
||||||
|
|
||||||
|
// CliMsg struct
|
||||||
|
type CliMsg struct { |
||||||
|
SessionID string |
||||||
|
PWD string |
||||||
|
} |
||||||
|
|
||||||
|
// CliResponse struct
|
||||||
|
type CliResponse struct { |
||||||
|
Records []recordint.SearchApp |
||||||
|
} |
||||||
|
|
||||||
|
// StatusResponse struct
|
||||||
|
type StatusResponse struct { |
||||||
|
Status bool `json:"status"` |
||||||
|
Version string `json:"version"` |
||||||
|
Commit string `json:"commit"` |
||||||
|
} |
||||||
@ -0,0 +1,77 @@ |
|||||||
|
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: ~/.local/share/resh/log.json (or ~/$XDG_DATA_HOME/resh/log.json) |
||||||
|
-> You can create an issue at: https://github.com/curusarn/resh/issues
|
||||||
|
|
||||||
|
` |
||||||
|
var 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(installedVer, terminalVer string) { |
||||||
|
fmt.Fprintf(os.Stderr, "%s: %s\n\n(installed version: %s, this terminal version: %s)", |
||||||
|
f.ErrPrefix, msgVersionMismatch, installedVer, terminalVer) |
||||||
|
f.Logger.Fatal("Version mismatch", |
||||||
|
zap.String("installed", installedVer), |
||||||
|
zap.String("terminal", terminalVer)) |
||||||
|
} |
||||||
|
|
||||||
|
func (f *Output) FatalVersionMismatch(installedVer, terminalVer string) { |
||||||
|
fmt.Fprintf(os.Stderr, "%s: %s\n(installed version: %s, this terminal version: %s)\n", |
||||||
|
f.ErrPrefix, msgVersionMismatch, installedVer, terminalVer) |
||||||
|
f.Logger.Fatal("Version mismatch", |
||||||
|
zap.String("installed", installedVer), |
||||||
|
zap.String("terminal", terminalVer)) |
||||||
|
} |
||||||
@ -0,0 +1,37 @@ |
|||||||
|
package recconv |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
|
||||||
|
"github.com/curusarn/resh/record" |
||||||
|
) |
||||||
|
|
||||||
|
func LegacyToV1(r *record.Legacy) *record.V1 { |
||||||
|
return &record.V1{ |
||||||
|
// FIXME: fill in all the fields
|
||||||
|
|
||||||
|
// Flags: 0,
|
||||||
|
|
||||||
|
CmdLine: r.CmdLine, |
||||||
|
ExitCode: r.ExitCode, |
||||||
|
|
||||||
|
DeviceID: r.ReshUUID, |
||||||
|
SessionID: r.SessionID, |
||||||
|
RecordID: r.RecordID, |
||||||
|
|
||||||
|
Home: r.Home, |
||||||
|
Pwd: r.Pwd, |
||||||
|
RealPwd: r.RealPwd, |
||||||
|
|
||||||
|
// Logname: r.Login,
|
||||||
|
Device: r.Host, |
||||||
|
|
||||||
|
GitOriginRemote: r.GitOriginRemote, |
||||||
|
|
||||||
|
Time: fmt.Sprintf("%.4f", r.RealtimeBefore), |
||||||
|
Duration: fmt.Sprintf("%.4f", r.RealtimeDuration), |
||||||
|
|
||||||
|
PartOne: r.PartOne, |
||||||
|
PartsNotMerged: !r.PartsMerged, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,158 @@ |
|||||||
|
package recio |
||||||
|
|
||||||
|
import ( |
||||||
|
"bufio" |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"os" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/curusarn/resh/internal/recconv" |
||||||
|
"github.com/curusarn/resh/internal/recordint" |
||||||
|
"github.com/curusarn/resh/record" |
||||||
|
"go.uber.org/zap" |
||||||
|
) |
||||||
|
|
||||||
|
func (r *RecIO) ReadAndFixFile(fpath string, maxErrors int) ([]recordint.Indexed, error) { |
||||||
|
recs, numErrs, err := r.ReadFile(fpath) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if numErrs > maxErrors { |
||||||
|
return nil, fmt.Errorf("encountered too many decoding errors") |
||||||
|
} |
||||||
|
if numErrs == 0 { |
||||||
|
return recs, nil |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: check there error messages
|
||||||
|
r.sugar.Warnw("Some history records could not be decoded - fixing resh history file by dropping them", |
||||||
|
"corruptedRecords", numErrs, |
||||||
|
) |
||||||
|
fpathBak := fpath + ".bak" |
||||||
|
r.sugar.Infow("Backing up current corrupted history file", |
||||||
|
"backupFilename", fpathBak, |
||||||
|
) |
||||||
|
// TODO: maybe use upstram copy function
|
||||||
|
err = copyFile(fpath, fpathBak) |
||||||
|
if err != nil { |
||||||
|
r.sugar.Errorw("Failed to create a backup history file - aborting fixing history file", |
||||||
|
"backupFilename", fpathBak, |
||||||
|
zap.Error(err), |
||||||
|
) |
||||||
|
return recs, nil |
||||||
|
} |
||||||
|
r.sugar.Info("Writing resh history file without errors ...") |
||||||
|
var recsV1 []record.V1 |
||||||
|
for _, rec := range recs { |
||||||
|
recsV1 = append(recsV1, rec.Rec) |
||||||
|
} |
||||||
|
err = r.OverwriteFile(fpath, recsV1) |
||||||
|
if err != nil { |
||||||
|
r.sugar.Errorw("Failed write fixed history file - aborting fixing history file", |
||||||
|
"filename", fpath, |
||||||
|
zap.Error(err), |
||||||
|
) |
||||||
|
} |
||||||
|
return recs, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RecIO) ReadFile(fpath string) ([]recordint.Indexed, int, error) { |
||||||
|
var recs []recordint.Indexed |
||||||
|
file, err := os.Open(fpath) |
||||||
|
if err != nil { |
||||||
|
return nil, 0, fmt.Errorf("failed to open history file: %w", err) |
||||||
|
} |
||||||
|
defer file.Close() |
||||||
|
|
||||||
|
reader := bufio.NewReader(file) |
||||||
|
numErrs := 0 |
||||||
|
var idx int |
||||||
|
for { |
||||||
|
var line string |
||||||
|
line, err = reader.ReadString('\n') |
||||||
|
if err != nil { |
||||||
|
break |
||||||
|
} |
||||||
|
idx++ |
||||||
|
rec, err := r.decodeLine(line) |
||||||
|
if err != nil { |
||||||
|
numErrs++ |
||||||
|
continue |
||||||
|
} |
||||||
|
recidx := recordint.Indexed{ |
||||||
|
Rec: *rec, |
||||||
|
// TODO: Is line index actually enough?
|
||||||
|
// Don't we want to count bytes because we will scan by number of bytes?
|
||||||
|
// hint: https://benjamincongdon.me/blog/2018/04/10/Counting-Scanned-Bytes-in-Go/
|
||||||
|
Idx: idx, |
||||||
|
} |
||||||
|
recs = append(recs, recidx) |
||||||
|
} |
||||||
|
if err != io.EOF { |
||||||
|
r.sugar.Error("Error while loading file", zap.Error(err)) |
||||||
|
} |
||||||
|
r.sugar.Infow("Loaded resh history records", |
||||||
|
"recordCount", len(recs), |
||||||
|
) |
||||||
|
return recs, numErrs, nil |
||||||
|
} |
||||||
|
|
||||||
|
func copyFile(source, dest string) error { |
||||||
|
from, err := os.Open(source) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer from.Close() |
||||||
|
|
||||||
|
// This is equivalnet to: os.OpenFile(dest, os.O_RDWR|os.O_CREATE, 0666)
|
||||||
|
to, err := os.Create(dest) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer to.Close() |
||||||
|
|
||||||
|
_, err = io.Copy(to, from) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RecIO) decodeLine(line string) (*record.V1, error) { |
||||||
|
idx := strings.Index(line, "{") |
||||||
|
if idx == -1 { |
||||||
|
return nil, fmt.Errorf("no openning brace found") |
||||||
|
} |
||||||
|
schema := line[:idx] |
||||||
|
jsn := line[idx:] |
||||||
|
switch schema { |
||||||
|
case "v1": |
||||||
|
var rec record.V1 |
||||||
|
err := decodeAnyRecord(jsn, &rec) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return &rec, nil |
||||||
|
case "": |
||||||
|
var rec record.Legacy |
||||||
|
err := decodeAnyRecord(jsn, &rec) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return recconv.LegacyToV1(&rec), nil |
||||||
|
default: |
||||||
|
return nil, fmt.Errorf("unknown record schema/type '%s'", schema) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: find out if we are loosing performance because of the use of interface{}
|
||||||
|
|
||||||
|
func decodeAnyRecord(jsn string, rec interface{}) error { |
||||||
|
err := json.Unmarshal([]byte(jsn), &rec) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("failed to decode json: %w", err) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
@ -0,0 +1,13 @@ |
|||||||
|
package recio |
||||||
|
|
||||||
|
import ( |
||||||
|
"go.uber.org/zap" |
||||||
|
) |
||||||
|
|
||||||
|
type RecIO struct { |
||||||
|
sugar *zap.SugaredLogger |
||||||
|
} |
||||||
|
|
||||||
|
func New(sugar *zap.SugaredLogger) RecIO { |
||||||
|
return RecIO{sugar: sugar} |
||||||
|
} |
||||||
@ -0,0 +1,62 @@ |
|||||||
|
package recio |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
|
||||||
|
"github.com/curusarn/resh/internal/recordint" |
||||||
|
"github.com/curusarn/resh/record" |
||||||
|
) |
||||||
|
|
||||||
|
// TODO: better errors
|
||||||
|
func (r *RecIO) OverwriteFile(fpath string, recs []record.V1) error { |
||||||
|
file, err := os.Create(fpath) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer file.Close() |
||||||
|
return writeRecords(file, recs) |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: better errors
|
||||||
|
func (r *RecIO) AppendToFile(fpath string, recs []record.V1) error { |
||||||
|
file, err := os.OpenFile(fpath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer file.Close() |
||||||
|
return writeRecords(file, recs) |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: better errors
|
||||||
|
func (r *RecIO) EditRecordFlagsInFile(fpath string, idx int, rec recordint.Flag) error { |
||||||
|
// FIXME: implement
|
||||||
|
// open file "not as append"
|
||||||
|
// scan to the correct line
|
||||||
|
r.sugar.Error("not implemented yet (FIXME)") |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func writeRecords(file *os.File, recs []record.V1) error { |
||||||
|
for _, rec := range recs { |
||||||
|
jsn, err := encodeV1Record(rec) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
_, err = file.Write(jsn) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func encodeV1Record(rec record.V1) ([]byte, error) { |
||||||
|
version := []byte("v1") |
||||||
|
jsn, err := json.Marshal(rec) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("failed to encode json: %w", err) |
||||||
|
} |
||||||
|
return append(append(version, jsn...), []byte("\n")...), nil |
||||||
|
} |
||||||
@ -0,0 +1,34 @@ |
|||||||
|
package recordint |
||||||
|
|
||||||
|
import "github.com/curusarn/resh/record" |
||||||
|
|
||||||
|
type Collect struct { |
||||||
|
// record merging
|
||||||
|
SessionID string |
||||||
|
Shlvl int |
||||||
|
// session watching
|
||||||
|
SessionPID int |
||||||
|
Shell string |
||||||
|
|
||||||
|
Rec record.V1 |
||||||
|
} |
||||||
|
|
||||||
|
type Postcollect struct { |
||||||
|
// record merging
|
||||||
|
SessionID string |
||||||
|
Shlvl int |
||||||
|
// session watching
|
||||||
|
SessionPID int |
||||||
|
|
||||||
|
RecordID string |
||||||
|
ExitCode int |
||||||
|
Duration float64 |
||||||
|
} |
||||||
|
|
||||||
|
type SessionInit struct { |
||||||
|
// record merging
|
||||||
|
SessionID string |
||||||
|
Shlvl int |
||||||
|
// session watching
|
||||||
|
SessionPID int |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
package recordint |
||||||
|
|
||||||
|
type Flag struct { |
||||||
|
deviceID string |
||||||
|
recordID string |
||||||
|
|
||||||
|
flagDeleted bool |
||||||
|
flagFavourite bool |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
package recordint |
||||||
|
|
||||||
|
import "github.com/curusarn/resh/record" |
||||||
|
|
||||||
|
// Indexed record allows us to find records in history file in order to edit them
|
||||||
|
type Indexed struct { |
||||||
|
Rec record.V1 |
||||||
|
Idx int |
||||||
|
} |
||||||
@ -0,0 +1,2 @@ |
|||||||
|
// Package recordint provides internal record types that are passed between resh components
|
||||||
|
package recordint |
||||||
@ -0,0 +1,77 @@ |
|||||||
|
package recordint |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/url" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
|
||||||
|
giturls "github.com/whilp/git-urls" |
||||||
|
) |
||||||
|
|
||||||
|
// SearchApp record used for sending records to RESH-CLI
|
||||||
|
type SearchApp struct { |
||||||
|
IsRaw bool |
||||||
|
SessionID string |
||||||
|
DeviceID string |
||||||
|
|
||||||
|
CmdLine string |
||||||
|
Host string |
||||||
|
Pwd string |
||||||
|
Home string // helps us to collapse /home/user to tilde
|
||||||
|
GitOriginRemote string |
||||||
|
ExitCode int |
||||||
|
|
||||||
|
Time float64 |
||||||
|
|
||||||
|
// file index
|
||||||
|
Idx int |
||||||
|
} |
||||||
|
|
||||||
|
// NewCliRecordFromCmdLine
|
||||||
|
func NewSearchAppFromCmdLine(cmdLine string) SearchApp { |
||||||
|
return SearchApp{ |
||||||
|
IsRaw: true, |
||||||
|
CmdLine: cmdLine, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// NewCliRecord from EnrichedRecord
|
||||||
|
func NewSearchApp(r *Indexed) SearchApp { |
||||||
|
// TODO: we used to validate records with recutil.Validate()
|
||||||
|
// TODO: handle this error
|
||||||
|
time, _ := strconv.ParseFloat(r.Rec.Time, 64) |
||||||
|
return SearchApp{ |
||||||
|
IsRaw: false, |
||||||
|
SessionID: r.Rec.SessionID, |
||||||
|
DeviceID: r.Rec.DeviceID, |
||||||
|
CmdLine: r.Rec.CmdLine, |
||||||
|
Host: r.Rec.Device, |
||||||
|
Pwd: r.Rec.Pwd, |
||||||
|
Home: r.Rec.Home, |
||||||
|
// TODO: is this the right place to normalize the git remote
|
||||||
|
GitOriginRemote: normalizeGitRemote(r.Rec.GitOriginRemote), |
||||||
|
ExitCode: r.Rec.ExitCode, |
||||||
|
Time: time, |
||||||
|
|
||||||
|
Idx: r.Idx, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: maybe move this to a more appropriate place
|
||||||
|
// normalizeGitRemote helper
|
||||||
|
func normalizeGitRemote(gitRemote string) string { |
||||||
|
if strings.HasSuffix(gitRemote, ".git") { |
||||||
|
gitRemote = gitRemote[:len(gitRemote)-4] |
||||||
|
} |
||||||
|
parsedURL, err := giturls.Parse(gitRemote) |
||||||
|
if err != nil { |
||||||
|
// TODO: log this error
|
||||||
|
return gitRemote |
||||||
|
} |
||||||
|
if parsedURL.User == nil || parsedURL.User.Username() == "" { |
||||||
|
parsedURL.User = url.User("git") |
||||||
|
} |
||||||
|
// TODO: figure out what scheme we want
|
||||||
|
parsedURL.Scheme = "git+ssh" |
||||||
|
return parsedURL.String() |
||||||
|
} |
||||||
@ -0,0 +1,79 @@ |
|||||||
|
package records |
||||||
|
|
||||||
|
import ( |
||||||
|
"bufio" |
||||||
|
"os" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/curusarn/resh/internal/histlist" |
||||||
|
"go.uber.org/zap" |
||||||
|
) |
||||||
|
|
||||||
|
// LoadCmdLinesFromZshFile loads cmdlines from zsh history file
|
||||||
|
func LoadCmdLinesFromZshFile(sugar *zap.SugaredLogger, fname string) histlist.Histlist { |
||||||
|
hl := histlist.New(sugar) |
||||||
|
file, err := os.Open(fname) |
||||||
|
if err != nil { |
||||||
|
sugar.Error("Failed to open zsh history file - skipping reading zsh history", zap.Error(err)) |
||||||
|
return hl |
||||||
|
} |
||||||
|
defer file.Close() |
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file) |
||||||
|
for scanner.Scan() { |
||||||
|
line := scanner.Text() |
||||||
|
// trim newline
|
||||||
|
line = strings.TrimRight(line, "\n") |
||||||
|
var cmd string |
||||||
|
// zsh format EXTENDED_HISTORY
|
||||||
|
// : 1576270617:0;make install
|
||||||
|
// zsh format no EXTENDED_HISTORY
|
||||||
|
// make install
|
||||||
|
if len(line) == 0 { |
||||||
|
// skip empty
|
||||||
|
continue |
||||||
|
} |
||||||
|
if strings.Contains(line, ":") && strings.Contains(line, ";") && |
||||||
|
len(strings.Split(line, ":")) >= 3 && len(strings.Split(line, ";")) >= 2 { |
||||||
|
// contains at least 2x ':' and 1x ';' => assume EXTENDED_HISTORY
|
||||||
|
cmd = strings.Split(line, ";")[1] |
||||||
|
} else { |
||||||
|
cmd = line |
||||||
|
} |
||||||
|
hl.AddCmdLine(cmd) |
||||||
|
} |
||||||
|
return hl |
||||||
|
} |
||||||
|
|
||||||
|
// LoadCmdLinesFromBashFile loads cmdlines from bash history file
|
||||||
|
func LoadCmdLinesFromBashFile(sugar *zap.SugaredLogger, fname string) histlist.Histlist { |
||||||
|
hl := histlist.New(sugar) |
||||||
|
file, err := os.Open(fname) |
||||||
|
if err != nil { |
||||||
|
sugar.Error("Failed to open bash history file - skipping reading bash history", zap.Error(err)) |
||||||
|
return hl |
||||||
|
} |
||||||
|
defer file.Close() |
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file) |
||||||
|
for scanner.Scan() { |
||||||
|
line := scanner.Text() |
||||||
|
// trim newline
|
||||||
|
line = strings.TrimRight(line, "\n") |
||||||
|
// trim spaces from left
|
||||||
|
line = strings.TrimLeft(line, " ") |
||||||
|
// bash format (two lines)
|
||||||
|
// #1576199174
|
||||||
|
// make install
|
||||||
|
if strings.HasPrefix(line, "#") { |
||||||
|
// is either timestamp or comment => skip
|
||||||
|
continue |
||||||
|
} |
||||||
|
if len(line) == 0 { |
||||||
|
// skip empty
|
||||||
|
continue |
||||||
|
} |
||||||
|
hl.AddCmdLine(line) |
||||||
|
} |
||||||
|
return hl |
||||||
|
} |
||||||
@ -0,0 +1,51 @@ |
|||||||
|
package recutil |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
|
||||||
|
"github.com/curusarn/resh/internal/recordint" |
||||||
|
"github.com/curusarn/resh/record" |
||||||
|
) |
||||||
|
|
||||||
|
// TODO: reintroduce validation
|
||||||
|
// Validate returns error if the record is invalid
|
||||||
|
// func Validate(r *record.V1) error {
|
||||||
|
// if r.CmdLine == "" {
|
||||||
|
// return errors.New("There is no CmdLine")
|
||||||
|
// }
|
||||||
|
// if r.Time == 0 {
|
||||||
|
// return errors.New("There is no Time")
|
||||||
|
// }
|
||||||
|
// if r.RealPwd == "" {
|
||||||
|
// return errors.New("There is no Real Pwd")
|
||||||
|
// }
|
||||||
|
// if r.Pwd == "" {
|
||||||
|
// return errors.New("There is no Pwd")
|
||||||
|
// }
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// TODO: maybe more to a more appropriate place
|
||||||
|
// TODO: cleanup the interface - stop modifying the part1 and returning a ew record at the same time
|
||||||
|
// Merge two records (part1 - collect + part2 - postcollect)
|
||||||
|
func Merge(r1 *recordint.Collect, r2 *recordint.Collect) (record.V1, error) { |
||||||
|
if r1.SessionID != r2.SessionID { |
||||||
|
return record.V1{}, errors.New("Records to merge are not from the same sesion - r1:" + r1.SessionID + " r2:" + r2.SessionID) |
||||||
|
} |
||||||
|
if r1.Rec.RecordID != r2.Rec.RecordID { |
||||||
|
return record.V1{}, errors.New("Records to merge do not have the same ID - r1:" + r1.Rec.RecordID + " r2:" + r2.Rec.RecordID) |
||||||
|
} |
||||||
|
|
||||||
|
r := recordint.Collect{ |
||||||
|
SessionID: r1.SessionID, |
||||||
|
Shlvl: r1.Shlvl, |
||||||
|
SessionPID: r1.SessionPID, |
||||||
|
|
||||||
|
Rec: r1.Rec, |
||||||
|
} |
||||||
|
r.Rec.ExitCode = r2.Rec.ExitCode |
||||||
|
r.Rec.Duration = r2.Rec.Duration |
||||||
|
r.Rec.PartOne = false |
||||||
|
r.Rec.PartsNotMerged = false |
||||||
|
return r.Rec, nil |
||||||
|
} |
||||||
@ -0,0 +1,26 @@ |
|||||||
|
package searchapp |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/curusarn/resh/internal/histcli" |
||||||
|
"github.com/curusarn/resh/internal/msg" |
||||||
|
"github.com/curusarn/resh/internal/recio" |
||||||
|
"go.uber.org/zap" |
||||||
|
) |
||||||
|
|
||||||
|
// LoadHistoryFromFile ...
|
||||||
|
func LoadHistoryFromFile(sugar *zap.SugaredLogger, historyPath string, numLines int) msg.CliResponse { |
||||||
|
rio := recio.New(sugar) |
||||||
|
recs, _, err := rio.ReadFile(historyPath) |
||||||
|
if err != nil { |
||||||
|
sugar.Panicf("failed to read hisotry file: %w", err) |
||||||
|
} |
||||||
|
if numLines != 0 && numLines < len(recs) { |
||||||
|
recs = recs[:numLines] |
||||||
|
} |
||||||
|
cliRecords := histcli.New(sugar) |
||||||
|
for i := len(recs) - 1; i >= 0; i-- { |
||||||
|
rec := recs[i] |
||||||
|
cliRecords.AddRecord(&rec) |
||||||
|
} |
||||||
|
return msg.CliResponse{Records: cliRecords.Dump()} |
||||||
|
} |
||||||
@ -0,0 +1,96 @@ |
|||||||
|
package sesswatch |
||||||
|
|
||||||
|
import ( |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/curusarn/resh/internal/recordint" |
||||||
|
"github.com/mitchellh/go-ps" |
||||||
|
"go.uber.org/zap" |
||||||
|
) |
||||||
|
|
||||||
|
type sesswatch struct { |
||||||
|
sugar *zap.SugaredLogger |
||||||
|
|
||||||
|
sessionsToDrop []chan string |
||||||
|
sleepSeconds uint |
||||||
|
|
||||||
|
watchedSessions map[string]bool |
||||||
|
mutex sync.Mutex |
||||||
|
} |
||||||
|
|
||||||
|
// Go runs the session watcher - watches sessions and sends
|
||||||
|
func Go(sugar *zap.SugaredLogger, |
||||||
|
sessionsToWatch chan recordint.SessionInit, sessionsToWatchRecords chan recordint.Collect, |
||||||
|
sessionsToDrop []chan string, sleepSeconds uint) { |
||||||
|
|
||||||
|
sw := sesswatch{ |
||||||
|
sugar: sugar.With("module", "sesswatch"), |
||||||
|
sessionsToDrop: sessionsToDrop, |
||||||
|
sleepSeconds: sleepSeconds, |
||||||
|
watchedSessions: map[string]bool{}, |
||||||
|
} |
||||||
|
go sw.waiter(sessionsToWatch, sessionsToWatchRecords) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *sesswatch) waiter(sessionsToWatch chan recordint.SessionInit, sessionsToWatchRecords chan recordint.Collect) { |
||||||
|
for { |
||||||
|
func() { |
||||||
|
select { |
||||||
|
case rec := <-sessionsToWatch: |
||||||
|
// normal way to start watching a session
|
||||||
|
id := rec.SessionID |
||||||
|
pid := rec.SessionPID |
||||||
|
sugar := s.sugar.With( |
||||||
|
"sessionID", rec.SessionID, |
||||||
|
"sessionPID", rec.SessionPID, |
||||||
|
) |
||||||
|
s.mutex.Lock() |
||||||
|
defer s.mutex.Unlock() |
||||||
|
if s.watchedSessions[id] == false { |
||||||
|
sugar.Infow("Starting watching new session") |
||||||
|
s.watchedSessions[id] = true |
||||||
|
go s.watcher(sugar, id, pid) |
||||||
|
} |
||||||
|
case rec := <-sessionsToWatchRecords: |
||||||
|
// additional safety - watch sessions that were never properly initialized
|
||||||
|
id := rec.SessionID |
||||||
|
pid := rec.SessionPID |
||||||
|
sugar := s.sugar.With( |
||||||
|
"sessionID", rec.SessionID, |
||||||
|
"sessionPID", rec.SessionPID, |
||||||
|
) |
||||||
|
s.mutex.Lock() |
||||||
|
defer s.mutex.Unlock() |
||||||
|
if s.watchedSessions[id] == false { |
||||||
|
sugar.Warnw("Starting watching new session based on '/record'") |
||||||
|
s.watchedSessions[id] = true |
||||||
|
go s.watcher(sugar, id, pid) |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *sesswatch) watcher(sugar *zap.SugaredLogger, sessionID string, sessionPID int) { |
||||||
|
for { |
||||||
|
time.Sleep(time.Duration(s.sleepSeconds) * time.Second) |
||||||
|
proc, err := ps.FindProcess(sessionPID) |
||||||
|
if err != nil { |
||||||
|
sugar.Errorw("Error while finding process", "error", err) |
||||||
|
} else if proc == nil { |
||||||
|
sugar.Infow("Dropping session") |
||||||
|
func() { |
||||||
|
s.mutex.Lock() |
||||||
|
defer s.mutex.Unlock() |
||||||
|
s.watchedSessions[sessionID] = false |
||||||
|
}() |
||||||
|
for _, ch := range s.sessionsToDrop { |
||||||
|
sugar.Debugw("Sending 'drop session' message ...") |
||||||
|
ch <- sessionID |
||||||
|
sugar.Debugw("Sending 'drop session' message DONE") |
||||||
|
} |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,74 @@ |
|||||||
|
package signalhandler |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"net/http" |
||||||
|
"os" |
||||||
|
"os/signal" |
||||||
|
"strconv" |
||||||
|
"syscall" |
||||||
|
"time" |
||||||
|
|
||||||
|
"go.uber.org/zap" |
||||||
|
) |
||||||
|
|
||||||
|
func sendSignals(sugar *zap.SugaredLogger, sig os.Signal, subscribers []chan os.Signal, done chan string) { |
||||||
|
for _, sub := range subscribers { |
||||||
|
sub <- sig |
||||||
|
} |
||||||
|
sugar.Warnw("Sent shutdown signals to components") |
||||||
|
chanCount := len(subscribers) |
||||||
|
start := time.Now() |
||||||
|
delay := time.Millisecond * 100 |
||||||
|
timeout := time.Millisecond * 2000 |
||||||
|
|
||||||
|
for { |
||||||
|
select { |
||||||
|
case _ = <-done: |
||||||
|
chanCount-- |
||||||
|
if chanCount == 0 { |
||||||
|
sugar.Warnw("All components shut down successfully") |
||||||
|
return |
||||||
|
} |
||||||
|
default: |
||||||
|
time.Sleep(delay) |
||||||
|
} |
||||||
|
if time.Since(start) > timeout { |
||||||
|
sugar.Errorw("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, |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,118 @@ |
|||||||
|
package syncconnector |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"github.com/curusarn/resh/record" |
||||||
|
"io" |
||||||
|
"net/http" |
||||||
|
"strconv" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
func (sc SyncConnector) getLatestRecord(machineId *string) (map[string]string, error) { |
||||||
|
return map[string]string{}, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (sc SyncConnector) downloadRecords(lastRecords map[string]float64) ([]record.V1, error) { |
||||||
|
var records []record.V1 |
||||||
|
|
||||||
|
client := http.Client{ |
||||||
|
Timeout: 3 * time.Second, |
||||||
|
} |
||||||
|
|
||||||
|
latestRes := map[string]string{} |
||||||
|
for device, t := range lastRecords { |
||||||
|
sc.sugar.Debugf("Latest for %s is %f", device, t) |
||||||
|
latestRes[device] = fmt.Sprintf("%.4f", t) |
||||||
|
} |
||||||
|
|
||||||
|
latestJson, err := json.Marshal(latestRes) |
||||||
|
if err != nil { |
||||||
|
sc.sugar.Errorw("converting latest to JSON failed", "err", err) |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
reqBody := bytes.NewBuffer(latestJson) |
||||||
|
|
||||||
|
address := sc.getAddressWithPath(historyEndpoint) |
||||||
|
resp, err := client.Post(address, "application/json", reqBody) |
||||||
|
if err != nil { |
||||||
|
sc.sugar.Errorw("history request failed", "address", address, "err", err) |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
defer func(Body io.ReadCloser) { |
||||||
|
err := Body.Close() |
||||||
|
if err != nil { |
||||||
|
sc.sugar.Errorw("reader close failed", "err", err) |
||||||
|
} |
||||||
|
}(resp.Body) |
||||||
|
body, err := io.ReadAll(resp.Body) |
||||||
|
if err != nil { |
||||||
|
sc.sugar.Warnw("reading response body failed", "err", err) |
||||||
|
} |
||||||
|
|
||||||
|
err = json.Unmarshal(body, &records) |
||||||
|
if err != nil { |
||||||
|
sc.sugar.Errorw("Unmarshalling failed", "err", err) |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return records, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (sc SyncConnector) latest() (map[string]float64, error) { |
||||||
|
var knownDevices []string |
||||||
|
for deviceId, _ := range sc.history.LatestRecordsPerDevice() { |
||||||
|
knownDevices = append(knownDevices, deviceId) |
||||||
|
} |
||||||
|
|
||||||
|
client := http.Client{ |
||||||
|
Timeout: 3 * time.Second, |
||||||
|
} |
||||||
|
|
||||||
|
knownJson, err := json.Marshal(knownDevices) |
||||||
|
if err != nil { |
||||||
|
sc.sugar.Errorw("converting latest to JSON failed", "err", err) |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
reqBody := bytes.NewBuffer(knownJson) |
||||||
|
|
||||||
|
address := sc.getAddressWithPath(latestEndpoint) |
||||||
|
resp, err := client.Post(address, "application/json", reqBody) |
||||||
|
if err != nil { |
||||||
|
sc.sugar.Errorw("latest request failed", "address", address, "err", err) |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
defer func(Body io.ReadCloser) { |
||||||
|
err := Body.Close() |
||||||
|
if err != nil { |
||||||
|
sc.sugar.Errorw("reader close failed", "err", err) |
||||||
|
} |
||||||
|
}(resp.Body) |
||||||
|
body, err := io.ReadAll(resp.Body) |
||||||
|
if err != nil { |
||||||
|
sc.sugar.Warnw("reading response body failed", "err", err) |
||||||
|
} |
||||||
|
|
||||||
|
latest := map[string]string{} |
||||||
|
|
||||||
|
err = json.Unmarshal(body, &latest) |
||||||
|
if err != nil { |
||||||
|
sc.sugar.Errorw("Unmarshalling failed", "err", err) |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
l := make(map[string]float64, len(latest)) |
||||||
|
for deviceId, ts := range latest { |
||||||
|
t, err := strconv.ParseFloat(ts, 64) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
l[deviceId] = t |
||||||
|
} |
||||||
|
|
||||||
|
return l, nil |
||||||
|
} |
||||||
@ -0,0 +1,79 @@ |
|||||||
|
package syncconnector |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/curusarn/resh/internal/histcli" |
||||||
|
"github.com/curusarn/resh/internal/recordint" |
||||||
|
"go.uber.org/zap" |
||||||
|
"net/url" |
||||||
|
"path" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
const storeEndpoint = "/store" |
||||||
|
const historyEndpoint = "/history" |
||||||
|
const latestEndpoint = "/latest" |
||||||
|
|
||||||
|
type SyncConnector struct { |
||||||
|
sugar *zap.SugaredLogger |
||||||
|
|
||||||
|
address *url.URL |
||||||
|
authToken string |
||||||
|
|
||||||
|
history *histcli.Histcli |
||||||
|
} |
||||||
|
|
||||||
|
func New(sugar *zap.SugaredLogger, address string, authToken string, pullPeriodSeconds int, sendPeriodSeconds int, history *histcli.Histcli) (*SyncConnector, error) { |
||||||
|
parsedAddress, err := url.Parse(address) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
sc := &SyncConnector{ |
||||||
|
sugar: sugar.With(zap.String("component", "syncConnector")), |
||||||
|
authToken: authToken, |
||||||
|
address: parsedAddress, |
||||||
|
history: history, |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: propagate signals
|
||||||
|
go func(sc *SyncConnector) { |
||||||
|
for _ = range time.Tick(time.Second * time.Duration(pullPeriodSeconds)) { |
||||||
|
sc.sugar.Debug("checking remote for new records") |
||||||
|
|
||||||
|
recs, err := sc.downloadRecords(sc.history.LatestRecordsPerDevice()) |
||||||
|
if err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
sc.sugar.Debugf("Got %d records", len(recs)) |
||||||
|
|
||||||
|
for _, rec := range recs { |
||||||
|
sc.history.AddRecord(&recordint.Indexed{ |
||||||
|
Rec: rec, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
}(sc) |
||||||
|
|
||||||
|
go func(sc *SyncConnector) { |
||||||
|
// wait to properly load all the records
|
||||||
|
time.Sleep(time.Second * time.Duration(sendPeriodSeconds)) |
||||||
|
for _ = range time.Tick(time.Second * time.Duration(sendPeriodSeconds)) { |
||||||
|
sc.sugar.Debug("syncing local records to the remote") |
||||||
|
|
||||||
|
err := sc.write() |
||||||
|
if err != nil { |
||||||
|
sc.sugar.Warnw("sending records to the remote failed", "err", err) |
||||||
|
} |
||||||
|
} |
||||||
|
}(sc) |
||||||
|
|
||||||
|
return sc, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (sc SyncConnector) getAddressWithPath(endpoint string) string { |
||||||
|
address := *sc.address |
||||||
|
address.Path = path.Join(address.Path, endpoint) |
||||||
|
return address.String() |
||||||
|
} |
||||||
@ -0,0 +1,77 @@ |
|||||||
|
package syncconnector |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/json" |
||||||
|
"github.com/curusarn/resh/record" |
||||||
|
"io" |
||||||
|
"net/http" |
||||||
|
"strconv" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
func (sc SyncConnector) write() error { |
||||||
|
latestRemote, err := sc.latest() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
latestLocal := sc.history.LatestRecordsPerDevice() |
||||||
|
remoteIsOlder := false |
||||||
|
for deviceId, lastLocal := range latestLocal { |
||||||
|
if lastRemote, ok := latestRemote[deviceId]; !ok { |
||||||
|
// Unknown deviceId on the remote - add records have to be sent
|
||||||
|
remoteIsOlder = true |
||||||
|
break |
||||||
|
} else if lastLocal > lastRemote { |
||||||
|
remoteIsOlder = true |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
if !remoteIsOlder { |
||||||
|
sc.sugar.Debug("No need to sync remote, there are no newer local records") |
||||||
|
return nil |
||||||
|
} |
||||||
|
var toSend []record.V1 |
||||||
|
for _, r := range sc.history.DumpRaw() { |
||||||
|
t, err := strconv.ParseFloat(r.Time, 64) |
||||||
|
if err != nil { |
||||||
|
sc.sugar.Warnw("Invalid time for record - skipping", "time", r.Time) |
||||||
|
continue |
||||||
|
} |
||||||
|
l, ok := latestRemote[r.DeviceID] |
||||||
|
if ok && l >= t { |
||||||
|
continue |
||||||
|
} |
||||||
|
sc.sugar.Infow("record is newer", "new", t, "old", l, "id", r.RecordID, "deviceid", r.DeviceID) |
||||||
|
toSend = append(toSend, r) |
||||||
|
} |
||||||
|
|
||||||
|
client := http.Client{ |
||||||
|
Timeout: 3 * time.Second, |
||||||
|
} |
||||||
|
|
||||||
|
toSendJson, err := json.Marshal(toSend) |
||||||
|
if err != nil { |
||||||
|
sc.sugar.Errorw("converting toSend to JSON failed", "err", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
reqBody := bytes.NewBuffer(toSendJson) |
||||||
|
|
||||||
|
address := sc.getAddressWithPath(storeEndpoint) |
||||||
|
resp, err := client.Post(address, "application/json", reqBody) |
||||||
|
if err != nil { |
||||||
|
sc.sugar.Errorw("store request failed", "address", address, "err", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
defer func(Body io.ReadCloser) { |
||||||
|
err := Body.Close() |
||||||
|
if err != nil { |
||||||
|
sc.sugar.Errorw("reader close failed", "err", err) |
||||||
|
} |
||||||
|
}(resp.Body) |
||||||
|
|
||||||
|
sc.sugar.Debugw("store call", "status", resp.Status) |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
@ -1,12 +0,0 @@ |
|||||||
package cfg |
|
||||||
|
|
||||||
// Config struct
|
|
||||||
type Config struct { |
|
||||||
Port int |
|
||||||
SesswatchPeriodSeconds uint |
|
||||||
SesshistInitHistorySize int |
|
||||||
Debug bool |
|
||||||
BindArrowKeysBash bool |
|
||||||
BindArrowKeysZsh bool |
|
||||||
BindControlR bool |
|
||||||
} |
|
||||||
@ -1,120 +0,0 @@ |
|||||||
package collect |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"encoding/json" |
|
||||||
"io/ioutil" |
|
||||||
"log" |
|
||||||
"net/http" |
|
||||||
"path/filepath" |
|
||||||
"strconv" |
|
||||||
"strings" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/httpclient" |
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
) |
|
||||||
|
|
||||||
// SingleResponse json struct
|
|
||||||
type SingleResponse struct { |
|
||||||
Found bool `json:"found"` |
|
||||||
CmdLine string `json:"cmdline"` |
|
||||||
} |
|
||||||
|
|
||||||
// SendRecallRequest to daemon
|
|
||||||
func SendRecallRequest(r records.SlimRecord, port string) (string, bool) { |
|
||||||
recJSON, err := json.Marshal(r) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("send err 1", err) |
|
||||||
} |
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", "http://localhost:"+port+"/recall", |
|
||||||
bytes.NewBuffer(recJSON)) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("send err 2", err) |
|
||||||
} |
|
||||||
req.Header.Set("Content-Type", "application/json") |
|
||||||
|
|
||||||
client := httpclient.New() |
|
||||||
resp, err := client.Do(req) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("resh-daemon is not running - try restarting this terminal") |
|
||||||
} |
|
||||||
|
|
||||||
defer resp.Body.Close() |
|
||||||
body, err := ioutil.ReadAll(resp.Body) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("read response error") |
|
||||||
} |
|
||||||
log.Println(string(body)) |
|
||||||
response := SingleResponse{} |
|
||||||
err = json.Unmarshal(body, &response) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("unmarshal resp error: ", err) |
|
||||||
} |
|
||||||
log.Println(response) |
|
||||||
return response.CmdLine, response.Found |
|
||||||
} |
|
||||||
|
|
||||||
// SendRecord to daemon
|
|
||||||
func SendRecord(r records.Record, port, path string) { |
|
||||||
recJSON, err := json.Marshal(r) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("send err 1", err) |
|
||||||
} |
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", "http://localhost:"+port+path, |
|
||||||
bytes.NewBuffer(recJSON)) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("send err 2", err) |
|
||||||
} |
|
||||||
req.Header.Set("Content-Type", "application/json") |
|
||||||
|
|
||||||
client := httpclient.New() |
|
||||||
_, err = client.Do(req) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("resh-daemon is not running - try restarting this terminal") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// ReadFileContent and return it as a string
|
|
||||||
func ReadFileContent(path string) string { |
|
||||||
dat, err := ioutil.ReadFile(path) |
|
||||||
if err != nil { |
|
||||||
return "" |
|
||||||
//log.Fatal("failed to open " + path)
|
|
||||||
} |
|
||||||
return strings.TrimSuffix(string(dat), "\n") |
|
||||||
} |
|
||||||
|
|
||||||
// GetGitDirs based on result of git "cdup" command
|
|
||||||
func GetGitDirs(cdup string, exitCode int, pwd string) (string, string) { |
|
||||||
if exitCode != 0 { |
|
||||||
return "", "" |
|
||||||
} |
|
||||||
abspath := filepath.Clean(filepath.Join(pwd, cdup)) |
|
||||||
realpath, err := filepath.EvalSymlinks(abspath) |
|
||||||
if err != nil { |
|
||||||
log.Println("err while handling git dir paths:", err) |
|
||||||
return "", "" |
|
||||||
} |
|
||||||
return abspath, realpath |
|
||||||
} |
|
||||||
|
|
||||||
// GetTimezoneOffsetInSeconds based on zone returned by date command
|
|
||||||
func GetTimezoneOffsetInSeconds(zone string) float64 { |
|
||||||
// date +%z -> "+0200"
|
|
||||||
hoursStr := zone[:3] |
|
||||||
minsStr := zone[3:] |
|
||||||
hours, err := strconv.Atoi(hoursStr) |
|
||||||
if err != nil { |
|
||||||
log.Println("err while parsing hours in timezone offset:", err) |
|
||||||
return -1 |
|
||||||
} |
|
||||||
mins, err := strconv.Atoi(minsStr) |
|
||||||
if err != nil { |
|
||||||
log.Println("err while parsing mins in timezone offset:", err) |
|
||||||
return -1 |
|
||||||
} |
|
||||||
secs := ((hours * 60) + mins) * 60 |
|
||||||
return float64(secs) |
|
||||||
} |
|
||||||
@ -1,246 +0,0 @@ |
|||||||
package histanal |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"encoding/json" |
|
||||||
"fmt" |
|
||||||
"log" |
|
||||||
"math/rand" |
|
||||||
"os" |
|
||||||
"os/exec" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
"github.com/curusarn/resh/pkg/strat" |
|
||||||
"github.com/jpillora/longestcommon" |
|
||||||
|
|
||||||
"github.com/schollz/progressbar" |
|
||||||
) |
|
||||||
|
|
||||||
type matchJSON struct { |
|
||||||
Match bool |
|
||||||
Distance int |
|
||||||
CharsRecalled int |
|
||||||
} |
|
||||||
|
|
||||||
type multiMatchItemJSON struct { |
|
||||||
Distance int |
|
||||||
CharsRecalled int |
|
||||||
} |
|
||||||
|
|
||||||
type multiMatchJSON struct { |
|
||||||
Match bool |
|
||||||
Entries []multiMatchItemJSON |
|
||||||
} |
|
||||||
|
|
||||||
type strategyJSON struct { |
|
||||||
Title string |
|
||||||
Description string |
|
||||||
Matches []matchJSON |
|
||||||
PrefixMatches []multiMatchJSON |
|
||||||
} |
|
||||||
|
|
||||||
// HistEval evaluates history
|
|
||||||
type HistEval struct { |
|
||||||
HistLoad |
|
||||||
BatchMode bool |
|
||||||
maxCandidates int |
|
||||||
Strategies []strategyJSON |
|
||||||
} |
|
||||||
|
|
||||||
// NewHistEval constructs new HistEval
|
|
||||||
func NewHistEval(inputPath string, |
|
||||||
maxCandidates int, skipFailedCmds bool, |
|
||||||
debugRecords float64, sanitizedInput bool) HistEval { |
|
||||||
|
|
||||||
e := HistEval{ |
|
||||||
HistLoad: HistLoad{ |
|
||||||
skipFailedCmds: skipFailedCmds, |
|
||||||
debugRecords: debugRecords, |
|
||||||
sanitizedInput: sanitizedInput, |
|
||||||
}, |
|
||||||
maxCandidates: maxCandidates, |
|
||||||
BatchMode: false, |
|
||||||
} |
|
||||||
records := e.loadHistoryRecords(inputPath) |
|
||||||
device := deviceRecords{Records: records} |
|
||||||
user := userRecords{} |
|
||||||
user.Devices = append(user.Devices, device) |
|
||||||
e.UsersRecords = append(e.UsersRecords, user) |
|
||||||
e.preprocessRecords() |
|
||||||
return e |
|
||||||
} |
|
||||||
|
|
||||||
// NewHistEvalBatchMode constructs new HistEval in batch mode
|
|
||||||
func NewHistEvalBatchMode(input string, inputDataRoot string, |
|
||||||
maxCandidates int, skipFailedCmds bool, |
|
||||||
debugRecords float64, sanitizedInput bool) HistEval { |
|
||||||
|
|
||||||
e := HistEval{ |
|
||||||
HistLoad: HistLoad{ |
|
||||||
skipFailedCmds: skipFailedCmds, |
|
||||||
debugRecords: debugRecords, |
|
||||||
sanitizedInput: sanitizedInput, |
|
||||||
}, |
|
||||||
maxCandidates: maxCandidates, |
|
||||||
BatchMode: false, |
|
||||||
} |
|
||||||
e.UsersRecords = e.loadHistoryRecordsBatchMode(input, inputDataRoot) |
|
||||||
e.preprocessRecords() |
|
||||||
return e |
|
||||||
} |
|
||||||
|
|
||||||
func (e *HistEval) preprocessDeviceRecords(device deviceRecords) deviceRecords { |
|
||||||
sessionIDs := map[string]uint64{} |
|
||||||
var nextID uint64 |
|
||||||
nextID = 1 // start with 1 because 0 won't get saved to json
|
|
||||||
for k, record := range device.Records { |
|
||||||
id, found := sessionIDs[record.SessionID] |
|
||||||
if found == false { |
|
||||||
id = nextID |
|
||||||
sessionIDs[record.SessionID] = id |
|
||||||
nextID++ |
|
||||||
} |
|
||||||
device.Records[k].SeqSessionID = id |
|
||||||
// assert
|
|
||||||
if record.Sanitized != e.sanitizedInput { |
|
||||||
if e.sanitizedInput { |
|
||||||
log.Fatal("ASSERT failed: '--sanitized-input' is present but data is not sanitized") |
|
||||||
} |
|
||||||
log.Fatal("ASSERT failed: data is sanitized but '--sanitized-input' is not present") |
|
||||||
} |
|
||||||
device.Records[k].SeqSessionID = id |
|
||||||
if e.debugRecords > 0 && rand.Float64() < e.debugRecords { |
|
||||||
device.Records[k].DebugThisRecord = true |
|
||||||
} |
|
||||||
} |
|
||||||
// sort.SliceStable(device.Records, func(x, y int) bool {
|
|
||||||
// if device.Records[x].SeqSessionID == device.Records[y].SeqSessionID {
|
|
||||||
// return device.Records[x].RealtimeAfterLocal < device.Records[y].RealtimeAfterLocal
|
|
||||||
// }
|
|
||||||
// return device.Records[x].SeqSessionID < device.Records[y].SeqSessionID
|
|
||||||
// })
|
|
||||||
|
|
||||||
// iterate from back and mark last record of each session
|
|
||||||
sessionIDSet := map[string]bool{} |
|
||||||
for i := len(device.Records) - 1; i >= 0; i-- { |
|
||||||
var record *records.EnrichedRecord |
|
||||||
record = &device.Records[i] |
|
||||||
if sessionIDSet[record.SessionID] { |
|
||||||
continue |
|
||||||
} |
|
||||||
sessionIDSet[record.SessionID] = true |
|
||||||
record.LastRecordOfSession = true |
|
||||||
} |
|
||||||
return device |
|
||||||
} |
|
||||||
|
|
||||||
// enrich records and add sequential session ID
|
|
||||||
func (e *HistEval) preprocessRecords() { |
|
||||||
for i := range e.UsersRecords { |
|
||||||
for j := range e.UsersRecords[i].Devices { |
|
||||||
e.UsersRecords[i].Devices[j] = e.preprocessDeviceRecords(e.UsersRecords[i].Devices[j]) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Evaluate a given strategy
|
|
||||||
func (e *HistEval) Evaluate(strategy strat.IStrategy) error { |
|
||||||
title, description := strategy.GetTitleAndDescription() |
|
||||||
log.Println("Evaluating strategy:", title, "-", description) |
|
||||||
strategyData := strategyJSON{Title: title, Description: description} |
|
||||||
for i := range e.UsersRecords { |
|
||||||
for j := range e.UsersRecords[i].Devices { |
|
||||||
bar := progressbar.New(len(e.UsersRecords[i].Devices[j].Records)) |
|
||||||
var prevRecord records.EnrichedRecord |
|
||||||
for _, record := range e.UsersRecords[i].Devices[j].Records { |
|
||||||
if e.skipFailedCmds && record.ExitCode != 0 { |
|
||||||
continue |
|
||||||
} |
|
||||||
candidates := strategy.GetCandidates(records.Stripped(record)) |
|
||||||
if record.DebugThisRecord { |
|
||||||
log.Println() |
|
||||||
log.Println("===================================================") |
|
||||||
log.Println("STRATEGY:", title, "-", description) |
|
||||||
log.Println("===================================================") |
|
||||||
log.Println("Previous record:") |
|
||||||
if prevRecord.RealtimeBefore == 0 { |
|
||||||
log.Println("== NIL") |
|
||||||
} else { |
|
||||||
rec, _ := prevRecord.ToString() |
|
||||||
log.Println(rec) |
|
||||||
} |
|
||||||
log.Println("---------------------------------------------------") |
|
||||||
log.Println("Recommendations for:") |
|
||||||
rec, _ := record.ToString() |
|
||||||
log.Println(rec) |
|
||||||
log.Println("---------------------------------------------------") |
|
||||||
for i, candidate := range candidates { |
|
||||||
if i > 10 { |
|
||||||
break |
|
||||||
} |
|
||||||
log.Println(string(candidate)) |
|
||||||
} |
|
||||||
log.Println("===================================================") |
|
||||||
} |
|
||||||
|
|
||||||
matchFound := false |
|
||||||
longestPrefixMatchLength := 0 |
|
||||||
multiMatch := multiMatchJSON{} |
|
||||||
for i, candidate := range candidates { |
|
||||||
// make an option (--calculate-total) to turn this on/off ?
|
|
||||||
// if i >= e.maxCandidates {
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
commonPrefixLength := len(longestcommon.Prefix([]string{candidate, record.CmdLine})) |
|
||||||
if commonPrefixLength > longestPrefixMatchLength { |
|
||||||
longestPrefixMatchLength = commonPrefixLength |
|
||||||
prefixMatch := multiMatchItemJSON{Distance: i + 1, CharsRecalled: commonPrefixLength} |
|
||||||
multiMatch.Match = true |
|
||||||
multiMatch.Entries = append(multiMatch.Entries, prefixMatch) |
|
||||||
} |
|
||||||
if candidate == record.CmdLine { |
|
||||||
match := matchJSON{Match: true, Distance: i + 1, CharsRecalled: record.CmdLength} |
|
||||||
matchFound = true |
|
||||||
strategyData.Matches = append(strategyData.Matches, match) |
|
||||||
strategyData.PrefixMatches = append(strategyData.PrefixMatches, multiMatch) |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
if matchFound == false { |
|
||||||
strategyData.Matches = append(strategyData.Matches, matchJSON{}) |
|
||||||
strategyData.PrefixMatches = append(strategyData.PrefixMatches, multiMatch) |
|
||||||
} |
|
||||||
err := strategy.AddHistoryRecord(&record) |
|
||||||
if err != nil { |
|
||||||
log.Println("Error while evauating", err) |
|
||||||
return err |
|
||||||
} |
|
||||||
bar.Add(1) |
|
||||||
prevRecord = record |
|
||||||
} |
|
||||||
strategy.ResetHistory() |
|
||||||
fmt.Println() |
|
||||||
} |
|
||||||
} |
|
||||||
e.Strategies = append(e.Strategies, strategyData) |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// CalculateStatsAndPlot results
|
|
||||||
func (e *HistEval) CalculateStatsAndPlot(scriptName string) { |
|
||||||
evalJSON, err := json.Marshal(e) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("json marshal error", err) |
|
||||||
} |
|
||||||
buffer := bytes.Buffer{} |
|
||||||
buffer.Write(evalJSON) |
|
||||||
// run python script to stat and plot/
|
|
||||||
cmd := exec.Command(scriptName) |
|
||||||
cmd.Stdout = os.Stdout |
|
||||||
cmd.Stderr = os.Stderr |
|
||||||
cmd.Stdin = &buffer |
|
||||||
err = cmd.Run() |
|
||||||
if err != nil { |
|
||||||
log.Printf("Command finished with error: %v", err) |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,180 +0,0 @@ |
|||||||
package histanal |
|
||||||
|
|
||||||
import ( |
|
||||||
"bufio" |
|
||||||
"encoding/json" |
|
||||||
"fmt" |
|
||||||
"io/ioutil" |
|
||||||
"log" |
|
||||||
"math/rand" |
|
||||||
"os" |
|
||||||
"path/filepath" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
) |
|
||||||
|
|
||||||
type deviceRecords struct { |
|
||||||
Name string |
|
||||||
Records []records.EnrichedRecord |
|
||||||
} |
|
||||||
|
|
||||||
type userRecords struct { |
|
||||||
Name string |
|
||||||
Devices []deviceRecords |
|
||||||
} |
|
||||||
|
|
||||||
// HistLoad loads history
|
|
||||||
type HistLoad struct { |
|
||||||
UsersRecords []userRecords |
|
||||||
skipFailedCmds bool |
|
||||||
sanitizedInput bool |
|
||||||
debugRecords float64 |
|
||||||
} |
|
||||||
|
|
||||||
func (e *HistLoad) preprocessDeviceRecords(device deviceRecords) deviceRecords { |
|
||||||
sessionIDs := map[string]uint64{} |
|
||||||
var nextID uint64 |
|
||||||
nextID = 1 // start with 1 because 0 won't get saved to json
|
|
||||||
for k, record := range device.Records { |
|
||||||
id, found := sessionIDs[record.SessionID] |
|
||||||
if found == false { |
|
||||||
id = nextID |
|
||||||
sessionIDs[record.SessionID] = id |
|
||||||
nextID++ |
|
||||||
} |
|
||||||
device.Records[k].SeqSessionID = id |
|
||||||
// assert
|
|
||||||
if record.Sanitized != e.sanitizedInput { |
|
||||||
if e.sanitizedInput { |
|
||||||
log.Fatal("ASSERT failed: '--sanitized-input' is present but data is not sanitized") |
|
||||||
} |
|
||||||
log.Fatal("ASSERT failed: data is sanitized but '--sanitized-input' is not present") |
|
||||||
} |
|
||||||
device.Records[k].SeqSessionID = id |
|
||||||
if e.debugRecords > 0 && rand.Float64() < e.debugRecords { |
|
||||||
device.Records[k].DebugThisRecord = true |
|
||||||
} |
|
||||||
} |
|
||||||
// sort.SliceStable(device.Records, func(x, y int) bool {
|
|
||||||
// if device.Records[x].SeqSessionID == device.Records[y].SeqSessionID {
|
|
||||||
// return device.Records[x].RealtimeAfterLocal < device.Records[y].RealtimeAfterLocal
|
|
||||||
// }
|
|
||||||
// return device.Records[x].SeqSessionID < device.Records[y].SeqSessionID
|
|
||||||
// })
|
|
||||||
|
|
||||||
// iterate from back and mark last record of each session
|
|
||||||
sessionIDSet := map[string]bool{} |
|
||||||
for i := len(device.Records) - 1; i >= 0; i-- { |
|
||||||
var record *records.EnrichedRecord |
|
||||||
record = &device.Records[i] |
|
||||||
if sessionIDSet[record.SessionID] { |
|
||||||
continue |
|
||||||
} |
|
||||||
sessionIDSet[record.SessionID] = true |
|
||||||
record.LastRecordOfSession = true |
|
||||||
} |
|
||||||
return device |
|
||||||
} |
|
||||||
|
|
||||||
// enrich records and add sequential session ID
|
|
||||||
func (e *HistLoad) preprocessRecords() { |
|
||||||
for i := range e.UsersRecords { |
|
||||||
for j := range e.UsersRecords[i].Devices { |
|
||||||
e.UsersRecords[i].Devices[j] = e.preprocessDeviceRecords(e.UsersRecords[i].Devices[j]) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (e *HistLoad) loadHistoryRecordsBatchMode(fname string, dataRootPath string) []userRecords { |
|
||||||
var records []userRecords |
|
||||||
info, err := os.Stat(dataRootPath) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("Error: Directory", dataRootPath, "does not exist - exiting! (", err, ")") |
|
||||||
} |
|
||||||
if info.IsDir() == false { |
|
||||||
log.Fatal("Error:", dataRootPath, "is not a directory - exiting!") |
|
||||||
} |
|
||||||
users, err := ioutil.ReadDir(dataRootPath) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("Could not read directory:", dataRootPath) |
|
||||||
} |
|
||||||
fmt.Println("Listing users in <", dataRootPath, ">...") |
|
||||||
for _, user := range users { |
|
||||||
userRecords := userRecords{Name: user.Name()} |
|
||||||
userFullPath := filepath.Join(dataRootPath, user.Name()) |
|
||||||
if user.IsDir() == false { |
|
||||||
log.Println("Warn: Unexpected file (not a directory) <", userFullPath, "> - skipping.") |
|
||||||
continue |
|
||||||
} |
|
||||||
fmt.Println() |
|
||||||
fmt.Printf("*- %s\n", user.Name()) |
|
||||||
devices, err := ioutil.ReadDir(userFullPath) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("Could not read directory:", userFullPath) |
|
||||||
} |
|
||||||
for _, device := range devices { |
|
||||||
deviceRecords := deviceRecords{Name: device.Name()} |
|
||||||
deviceFullPath := filepath.Join(userFullPath, device.Name()) |
|
||||||
if device.IsDir() == false { |
|
||||||
log.Println("Warn: Unexpected file (not a directory) <", deviceFullPath, "> - skipping.") |
|
||||||
continue |
|
||||||
} |
|
||||||
fmt.Printf(" \\- %s\n", device.Name()) |
|
||||||
files, err := ioutil.ReadDir(deviceFullPath) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("Could not read directory:", deviceFullPath) |
|
||||||
} |
|
||||||
for _, file := range files { |
|
||||||
fileFullPath := filepath.Join(deviceFullPath, file.Name()) |
|
||||||
if file.Name() == fname { |
|
||||||
fmt.Printf(" \\- %s - loading ...", file.Name()) |
|
||||||
// load the data
|
|
||||||
deviceRecords.Records = e.loadHistoryRecords(fileFullPath) |
|
||||||
fmt.Println(" OK ✓") |
|
||||||
} else { |
|
||||||
fmt.Printf(" \\- %s - skipped\n", file.Name()) |
|
||||||
} |
|
||||||
} |
|
||||||
userRecords.Devices = append(userRecords.Devices, deviceRecords) |
|
||||||
} |
|
||||||
records = append(records, userRecords) |
|
||||||
} |
|
||||||
return records |
|
||||||
} |
|
||||||
|
|
||||||
func (e *HistLoad) loadHistoryRecords(fname string) []records.EnrichedRecord { |
|
||||||
file, err := os.Open(fname) |
|
||||||
if err != nil { |
|
||||||
log.Fatal("Open() resh history file error:", err) |
|
||||||
} |
|
||||||
defer file.Close() |
|
||||||
|
|
||||||
var recs []records.EnrichedRecord |
|
||||||
scanner := bufio.NewScanner(file) |
|
||||||
for scanner.Scan() { |
|
||||||
record := records.Record{} |
|
||||||
fallbackRecord := records.FallbackRecord{} |
|
||||||
line := scanner.Text() |
|
||||||
err = json.Unmarshal([]byte(line), &record) |
|
||||||
if err != nil { |
|
||||||
err = json.Unmarshal([]byte(line), &fallbackRecord) |
|
||||||
if err != nil { |
|
||||||
log.Println("Line:", line) |
|
||||||
log.Fatal("Decoding error:", err) |
|
||||||
} |
|
||||||
record = records.Convert(&fallbackRecord) |
|
||||||
} |
|
||||||
if e.sanitizedInput == false { |
|
||||||
if record.CmdLength != 0 { |
|
||||||
log.Fatal("Assert failed - 'cmdLength' is set in raw data. Maybe you want to use '--sanitized-input' option?") |
|
||||||
} |
|
||||||
record.CmdLength = len(record.CmdLine) |
|
||||||
} else if record.CmdLength == 0 { |
|
||||||
log.Fatal("Assert failed - 'cmdLength' is unset in the data. This should not happen.") |
|
||||||
} |
|
||||||
if !e.skipFailedCmds || record.ExitCode == 0 { |
|
||||||
recs = append(recs, records.Enriched(record)) |
|
||||||
} |
|
||||||
} |
|
||||||
return recs |
|
||||||
} |
|
||||||
@ -1,31 +0,0 @@ |
|||||||
package histcli |
|
||||||
|
|
||||||
import ( |
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
) |
|
||||||
|
|
||||||
// Histcli is a dump of history preprocessed for resh cli purposes
|
|
||||||
type Histcli struct { |
|
||||||
// list of records
|
|
||||||
List []records.CliRecord |
|
||||||
} |
|
||||||
|
|
||||||
// New Histcli
|
|
||||||
func New() Histcli { |
|
||||||
return Histcli{} |
|
||||||
} |
|
||||||
|
|
||||||
// AddRecord to the histcli
|
|
||||||
func (h *Histcli) AddRecord(record records.Record) { |
|
||||||
enriched := records.Enriched(record) |
|
||||||
cli := records.NewCliRecord(enriched) |
|
||||||
|
|
||||||
h.List = append(h.List, cli) |
|
||||||
} |
|
||||||
|
|
||||||
// AddCmdLine to the histcli
|
|
||||||
func (h *Histcli) AddCmdLine(cmdline string) { |
|
||||||
cli := records.NewCliRecordFromCmdLine(cmdline) |
|
||||||
|
|
||||||
h.List = append(h.List, cli) |
|
||||||
} |
|
||||||
@ -1,262 +0,0 @@ |
|||||||
package histfile |
|
||||||
|
|
||||||
import ( |
|
||||||
"encoding/json" |
|
||||||
"log" |
|
||||||
"math" |
|
||||||
"os" |
|
||||||
"strconv" |
|
||||||
"sync" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/histcli" |
|
||||||
"github.com/curusarn/resh/pkg/histlist" |
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
) |
|
||||||
|
|
||||||
// Histfile writes records to histfile
|
|
||||||
type Histfile struct { |
|
||||||
sessionsMutex sync.Mutex |
|
||||||
sessions map[string]records.Record |
|
||||||
historyPath string |
|
||||||
|
|
||||||
recentMutex sync.Mutex |
|
||||||
recentRecords []records.Record |
|
||||||
|
|
||||||
// NOTE: we have separate histories which only differ if there was not enough resh_history
|
|
||||||
// resh_history itself is common for both bash and zsh
|
|
||||||
bashCmdLines histlist.Histlist |
|
||||||
zshCmdLines histlist.Histlist |
|
||||||
|
|
||||||
cliRecords histcli.Histcli |
|
||||||
} |
|
||||||
|
|
||||||
// New creates new histfile and runs its gorutines
|
|
||||||
func New(input chan records.Record, sessionsToDrop chan string, |
|
||||||
reshHistoryPath string, bashHistoryPath string, zshHistoryPath string, |
|
||||||
maxInitHistSize int, minInitHistSizeKB int, |
|
||||||
signals chan os.Signal, shutdownDone chan string) *Histfile { |
|
||||||
|
|
||||||
hf := Histfile{ |
|
||||||
sessions: map[string]records.Record{}, |
|
||||||
historyPath: reshHistoryPath, |
|
||||||
bashCmdLines: histlist.New(), |
|
||||||
zshCmdLines: histlist.New(), |
|
||||||
cliRecords: histcli.New(), |
|
||||||
} |
|
||||||
go hf.loadHistory(bashHistoryPath, zshHistoryPath, maxInitHistSize, minInitHistSizeKB) |
|
||||||
go hf.writer(input, signals, shutdownDone) |
|
||||||
go hf.sessionGC(sessionsToDrop) |
|
||||||
return &hf |
|
||||||
} |
|
||||||
|
|
||||||
// load records from resh history, reverse, enrich and save
|
|
||||||
func (h *Histfile) loadCliRecords(recs []records.Record) { |
|
||||||
for _, cmdline := range h.bashCmdLines.List { |
|
||||||
h.cliRecords.AddCmdLine(cmdline) |
|
||||||
} |
|
||||||
for _, cmdline := range h.zshCmdLines.List { |
|
||||||
h.cliRecords.AddCmdLine(cmdline) |
|
||||||
} |
|
||||||
for i := len(recs) - 1; i >= 0; i-- { |
|
||||||
rec := recs[i] |
|
||||||
h.cliRecords.AddRecord(rec) |
|
||||||
} |
|
||||||
log.Println("histfile: resh history loaded - history records count:", len(h.cliRecords.List)) |
|
||||||
} |
|
||||||
|
|
||||||
// loadsHistory from resh_history and if there is not enough of it also load native shell histories
|
|
||||||
func (h *Histfile) loadHistory(bashHistoryPath, zshHistoryPath string, maxInitHistSize, minInitHistSizeKB int) { |
|
||||||
h.recentMutex.Lock() |
|
||||||
defer h.recentMutex.Unlock() |
|
||||||
log.Println("histfile: Checking if resh_history is large enough ...") |
|
||||||
fi, err := os.Stat(h.historyPath) |
|
||||||
var size int |
|
||||||
if err != nil { |
|
||||||
log.Println("histfile ERROR: failed to stat resh_history file:", err) |
|
||||||
} else { |
|
||||||
size = int(fi.Size()) |
|
||||||
} |
|
||||||
useNativeHistories := false |
|
||||||
if size/1024 < minInitHistSizeKB { |
|
||||||
useNativeHistories = true |
|
||||||
log.Println("histfile WARN: resh_history is too small - loading native bash and zsh history ...") |
|
||||||
h.bashCmdLines = records.LoadCmdLinesFromBashFile(bashHistoryPath) |
|
||||||
log.Println("histfile: bash history loaded - cmdLine count:", len(h.bashCmdLines.List)) |
|
||||||
h.zshCmdLines = records.LoadCmdLinesFromZshFile(zshHistoryPath) |
|
||||||
log.Println("histfile: zsh history loaded - cmdLine count:", len(h.zshCmdLines.List)) |
|
||||||
// no maxInitHistSize when using native histories
|
|
||||||
maxInitHistSize = math.MaxInt32 |
|
||||||
} |
|
||||||
log.Println("histfile: Loading resh history from file ...") |
|
||||||
history := records.LoadFromFile(h.historyPath, math.MaxInt32) |
|
||||||
log.Println("histfile: resh history loaded from file - count:", len(history)) |
|
||||||
go h.loadCliRecords(history) |
|
||||||
// NOTE: keeping this weird interface for now because we might use it in the future
|
|
||||||
// when we only load bash or zsh history
|
|
||||||
reshCmdLines := loadCmdLines(history) |
|
||||||
log.Println("histfile: resh history loaded - cmdLine count:", len(reshCmdLines.List)) |
|
||||||
if useNativeHistories == false { |
|
||||||
h.bashCmdLines = reshCmdLines |
|
||||||
h.zshCmdLines = histlist.Copy(reshCmdLines) |
|
||||||
return |
|
||||||
} |
|
||||||
h.bashCmdLines.AddHistlist(reshCmdLines) |
|
||||||
log.Println("histfile: bash history + resh history - cmdLine count:", len(h.bashCmdLines.List)) |
|
||||||
h.zshCmdLines.AddHistlist(reshCmdLines) |
|
||||||
log.Println("histfile: zsh history + resh history - cmdLine count:", len(h.zshCmdLines.List)) |
|
||||||
} |
|
||||||
|
|
||||||
// sessionGC reads sessionIDs from channel and deletes them from histfile struct
|
|
||||||
func (h *Histfile) sessionGC(sessionsToDrop chan string) { |
|
||||||
for { |
|
||||||
func() { |
|
||||||
session := <-sessionsToDrop |
|
||||||
log.Println("histfile: got session to drop", session) |
|
||||||
h.sessionsMutex.Lock() |
|
||||||
defer h.sessionsMutex.Unlock() |
|
||||||
if part1, found := h.sessions[session]; found == true { |
|
||||||
log.Println("histfile: Dropping session:", session) |
|
||||||
delete(h.sessions, session) |
|
||||||
go writeRecord(part1, h.historyPath) |
|
||||||
} else { |
|
||||||
log.Println("histfile: No hanging parts for session:", session) |
|
||||||
} |
|
||||||
}() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// writer reads records from channel, merges them and writes them to file
|
|
||||||
func (h *Histfile) writer(input chan records.Record, signals chan os.Signal, shutdownDone chan string) { |
|
||||||
for { |
|
||||||
func() { |
|
||||||
select { |
|
||||||
case record := <-input: |
|
||||||
h.sessionsMutex.Lock() |
|
||||||
defer h.sessionsMutex.Unlock() |
|
||||||
|
|
||||||
// allows nested sessions to merge records properly
|
|
||||||
mergeID := record.SessionID + "_" + strconv.Itoa(record.Shlvl) |
|
||||||
if record.PartOne { |
|
||||||
if _, found := h.sessions[mergeID]; found { |
|
||||||
log.Println("histfile WARN: Got another first part of the records before merging the previous one - overwriting! " + |
|
||||||
"(this happens in bash because bash-preexec runs when it's not supposed to)") |
|
||||||
} |
|
||||||
h.sessions[mergeID] = record |
|
||||||
} else { |
|
||||||
if part1, found := h.sessions[mergeID]; found == false { |
|
||||||
log.Println("histfile ERROR: Got second part of records and nothing to merge it with - ignoring! (mergeID:", mergeID, ")") |
|
||||||
} else { |
|
||||||
delete(h.sessions, mergeID) |
|
||||||
go h.mergeAndWriteRecord(part1, record) |
|
||||||
} |
|
||||||
} |
|
||||||
case sig := <-signals: |
|
||||||
log.Println("histfile: Got signal " + sig.String()) |
|
||||||
h.sessionsMutex.Lock() |
|
||||||
defer h.sessionsMutex.Unlock() |
|
||||||
log.Println("histfile DEBUG: Unlocked mutex") |
|
||||||
|
|
||||||
for sessID, record := range h.sessions { |
|
||||||
log.Printf("histfile WARN: Writing incomplete record for session: %v\n", sessID) |
|
||||||
h.writeRecord(record) |
|
||||||
} |
|
||||||
log.Println("histfile DEBUG: Shutdown success") |
|
||||||
shutdownDone <- "histfile" |
|
||||||
return |
|
||||||
} |
|
||||||
}() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (h *Histfile) writeRecord(part1 records.Record) { |
|
||||||
writeRecord(part1, h.historyPath) |
|
||||||
} |
|
||||||
|
|
||||||
func (h *Histfile) mergeAndWriteRecord(part1, part2 records.Record) { |
|
||||||
err := part1.Merge(part2) |
|
||||||
if err != nil { |
|
||||||
log.Println("Error while merging", err) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
func() { |
|
||||||
h.recentMutex.Lock() |
|
||||||
defer h.recentMutex.Unlock() |
|
||||||
h.recentRecords = append(h.recentRecords, part1) |
|
||||||
cmdLine := part1.CmdLine |
|
||||||
h.bashCmdLines.AddCmdLine(cmdLine) |
|
||||||
h.zshCmdLines.AddCmdLine(cmdLine) |
|
||||||
h.cliRecords.AddRecord(part1) |
|
||||||
}() |
|
||||||
|
|
||||||
writeRecord(part1, h.historyPath) |
|
||||||
} |
|
||||||
|
|
||||||
func writeRecord(rec records.Record, outputPath string) { |
|
||||||
recJSON, err := json.Marshal(rec) |
|
||||||
if err != nil { |
|
||||||
log.Println("Marshalling error", err) |
|
||||||
return |
|
||||||
} |
|
||||||
f, err := os.OpenFile(outputPath, |
|
||||||
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) |
|
||||||
if err != nil { |
|
||||||
log.Println("Could not open file", err) |
|
||||||
return |
|
||||||
} |
|
||||||
defer f.Close() |
|
||||||
_, err = f.Write(append(recJSON, []byte("\n")...)) |
|
||||||
if err != nil { |
|
||||||
log.Printf("Error while writing: %v, %s\n", rec, err) |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// GetRecentCmdLines returns recent cmdLines
|
|
||||||
func (h *Histfile) GetRecentCmdLines(shell string, limit int) histlist.Histlist { |
|
||||||
// NOTE: limit does nothing atm
|
|
||||||
h.recentMutex.Lock() |
|
||||||
defer h.recentMutex.Unlock() |
|
||||||
log.Println("histfile: History requested ...") |
|
||||||
var hl histlist.Histlist |
|
||||||
if shell == "bash" { |
|
||||||
hl = histlist.Copy(h.bashCmdLines) |
|
||||||
log.Println("histfile: history copied (bash) - cmdLine count:", len(hl.List)) |
|
||||||
return hl |
|
||||||
} |
|
||||||
if shell != "zsh" { |
|
||||||
log.Println("histfile ERROR: Unknown shell: ", shell) |
|
||||||
} |
|
||||||
hl = histlist.Copy(h.zshCmdLines) |
|
||||||
log.Println("histfile: history copied (zsh) - cmdLine count:", len(hl.List)) |
|
||||||
return hl |
|
||||||
} |
|
||||||
|
|
||||||
// DumpCliRecords returns enriched records
|
|
||||||
func (h *Histfile) DumpCliRecords() histcli.Histcli { |
|
||||||
// don't forget locks in the future
|
|
||||||
return h.cliRecords |
|
||||||
} |
|
||||||
|
|
||||||
func loadCmdLines(recs []records.Record) histlist.Histlist { |
|
||||||
hl := histlist.New() |
|
||||||
// go from bottom and deduplicate
|
|
||||||
var cmdLines []string |
|
||||||
cmdLinesSet := map[string]bool{} |
|
||||||
for i := len(recs) - 1; i >= 0; i-- { |
|
||||||
cmdLine := recs[i].CmdLine |
|
||||||
if cmdLinesSet[cmdLine] { |
|
||||||
continue |
|
||||||
} |
|
||||||
cmdLinesSet[cmdLine] = true |
|
||||||
cmdLines = append([]string{cmdLine}, cmdLines...) |
|
||||||
// if len(cmdLines) > limit {
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
} |
|
||||||
// add everything to histlist
|
|
||||||
for _, cmdLine := range cmdLines { |
|
||||||
hl.AddCmdLine(cmdLine) |
|
||||||
} |
|
||||||
return hl |
|
||||||
} |
|
||||||
@ -1,32 +0,0 @@ |
|||||||
package msg |
|
||||||
|
|
||||||
import "github.com/curusarn/resh/pkg/records" |
|
||||||
|
|
||||||
// CliMsg struct
|
|
||||||
type CliMsg struct { |
|
||||||
SessionID string `json:"sessionID"` |
|
||||||
PWD string `json:"pwd"` |
|
||||||
} |
|
||||||
|
|
||||||
// CliResponse struct
|
|
||||||
type CliResponse struct { |
|
||||||
CliRecords []records.CliRecord `json:"cliRecords"` |
|
||||||
} |
|
||||||
|
|
||||||
// InspectMsg struct
|
|
||||||
type InspectMsg struct { |
|
||||||
SessionID string `json:"sessionId"` |
|
||||||
Count uint `json:"count"` |
|
||||||
} |
|
||||||
|
|
||||||
// MultiResponse struct
|
|
||||||
type MultiResponse struct { |
|
||||||
CmdLines []string `json:"cmdlines"` |
|
||||||
} |
|
||||||
|
|
||||||
// StatusResponse struct
|
|
||||||
type StatusResponse struct { |
|
||||||
Status bool `json:"status"` |
|
||||||
Version string `json:"version"` |
|
||||||
Commit string `json:"commit"` |
|
||||||
} |
|
||||||
@ -1,689 +0,0 @@ |
|||||||
package records |
|
||||||
|
|
||||||
import ( |
|
||||||
"bufio" |
|
||||||
"encoding/json" |
|
||||||
"errors" |
|
||||||
"io" |
|
||||||
"log" |
|
||||||
"math" |
|
||||||
"os" |
|
||||||
"strconv" |
|
||||||
"strings" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/histlist" |
|
||||||
"github.com/mattn/go-shellwords" |
|
||||||
) |
|
||||||
|
|
||||||
// BaseRecord - common base for Record and FallbackRecord
|
|
||||||
type BaseRecord struct { |
|
||||||
// core
|
|
||||||
CmdLine string `json:"cmdLine"` |
|
||||||
ExitCode int `json:"exitCode"` |
|
||||||
Shell string `json:"shell"` |
|
||||||
Uname string `json:"uname"` |
|
||||||
SessionID string `json:"sessionId"` |
|
||||||
RecordID string `json:"recordId"` |
|
||||||
|
|
||||||
// posix
|
|
||||||
Home string `json:"home"` |
|
||||||
Lang string `json:"lang"` |
|
||||||
LcAll string `json:"lcAll"` |
|
||||||
Login string `json:"login"` |
|
||||||
//Path string `json:"path"`
|
|
||||||
Pwd string `json:"pwd"` |
|
||||||
PwdAfter string `json:"pwdAfter"` |
|
||||||
ShellEnv string `json:"shellEnv"` |
|
||||||
Term string `json:"term"` |
|
||||||
|
|
||||||
// non-posix"`
|
|
||||||
RealPwd string `json:"realPwd"` |
|
||||||
RealPwdAfter string `json:"realPwdAfter"` |
|
||||||
Pid int `json:"pid"` |
|
||||||
SessionPID int `json:"sessionPid"` |
|
||||||
Host string `json:"host"` |
|
||||||
Hosttype string `json:"hosttype"` |
|
||||||
Ostype string `json:"ostype"` |
|
||||||
Machtype string `json:"machtype"` |
|
||||||
Shlvl int `json:"shlvl"` |
|
||||||
|
|
||||||
// before after
|
|
||||||
TimezoneBefore string `json:"timezoneBefore"` |
|
||||||
TimezoneAfter string `json:"timezoneAfter"` |
|
||||||
|
|
||||||
RealtimeBefore float64 `json:"realtimeBefore"` |
|
||||||
RealtimeAfter float64 `json:"realtimeAfter"` |
|
||||||
RealtimeBeforeLocal float64 `json:"realtimeBeforeLocal"` |
|
||||||
RealtimeAfterLocal float64 `json:"realtimeAfterLocal"` |
|
||||||
|
|
||||||
RealtimeDuration float64 `json:"realtimeDuration"` |
|
||||||
RealtimeSinceSessionStart float64 `json:"realtimeSinceSessionStart"` |
|
||||||
RealtimeSinceBoot float64 `json:"realtimeSinceBoot"` |
|
||||||
//Logs []string `json: "logs"`
|
|
||||||
|
|
||||||
GitDir string `json:"gitDir"` |
|
||||||
GitRealDir string `json:"gitRealDir"` |
|
||||||
GitOriginRemote string `json:"gitOriginRemote"` |
|
||||||
GitDirAfter string `json:"gitDirAfter"` |
|
||||||
GitRealDirAfter string `json:"gitRealDirAfter"` |
|
||||||
GitOriginRemoteAfter string `json:"gitOriginRemoteAfter"` |
|
||||||
MachineID string `json:"machineId"` |
|
||||||
|
|
||||||
OsReleaseID string `json:"osReleaseId"` |
|
||||||
OsReleaseVersionID string `json:"osReleaseVersionId"` |
|
||||||
OsReleaseIDLike string `json:"osReleaseIdLike"` |
|
||||||
OsReleaseName string `json:"osReleaseName"` |
|
||||||
OsReleasePrettyName string `json:"osReleasePrettyName"` |
|
||||||
|
|
||||||
ReshUUID string `json:"reshUuid"` |
|
||||||
ReshVersion string `json:"reshVersion"` |
|
||||||
ReshRevision string `json:"reshRevision"` |
|
||||||
|
|
||||||
// records come in two parts (collect and postcollect)
|
|
||||||
PartOne bool `json:"partOne,omitempty"` // false => part two
|
|
||||||
PartsMerged bool `json:"partsMerged"` |
|
||||||
// special flag -> not an actual record but an session end
|
|
||||||
SessionExit bool `json:"sessionExit,omitempty"` |
|
||||||
|
|
||||||
// recall metadata
|
|
||||||
Recalled bool `json:"recalled"` |
|
||||||
RecallHistno int `json:"recallHistno,omitempty"` |
|
||||||
RecallStrategy string `json:"recallStrategy,omitempty"` |
|
||||||
RecallActionsRaw string `json:"recallActionsRaw,omitempty"` |
|
||||||
RecallActions []string `json:"recallActions,omitempty"` |
|
||||||
RecallLastCmdLine string `json:"recallLastCmdLine"` |
|
||||||
|
|
||||||
// recall command
|
|
||||||
RecallPrefix string `json:"recallPrefix,omitempty"` |
|
||||||
|
|
||||||
// added by sanitizatizer
|
|
||||||
Sanitized bool `json:"sanitized,omitempty"` |
|
||||||
CmdLength int `json:"cmdLength,omitempty"` |
|
||||||
} |
|
||||||
|
|
||||||
// Record representing single executed command with its metadata
|
|
||||||
type Record struct { |
|
||||||
BaseRecord |
|
||||||
|
|
||||||
Cols string `json:"cols"` |
|
||||||
Lines string `json:"lines"` |
|
||||||
} |
|
||||||
|
|
||||||
// EnrichedRecord - record enriched with additional data
|
|
||||||
type EnrichedRecord struct { |
|
||||||
Record |
|
||||||
|
|
||||||
// enriching fields - added "later"
|
|
||||||
Command string `json:"command"` |
|
||||||
FirstWord string `json:"firstWord"` |
|
||||||
Invalid bool `json:"invalid"` |
|
||||||
SeqSessionID uint64 `json:"seqSessionId"` |
|
||||||
LastRecordOfSession bool `json:"lastRecordOfSession"` |
|
||||||
DebugThisRecord bool `json:"debugThisRecord"` |
|
||||||
Errors []string `json:"errors"` |
|
||||||
// SeqSessionID uint64 `json:"seqSessionId,omitempty"`
|
|
||||||
} |
|
||||||
|
|
||||||
// FallbackRecord when record is too old and can't be parsed into regular Record
|
|
||||||
type FallbackRecord struct { |
|
||||||
BaseRecord |
|
||||||
// older version of the record where cols and lines are int
|
|
||||||
|
|
||||||
Cols int `json:"cols"` // notice the int type
|
|
||||||
Lines int `json:"lines"` // notice the int type
|
|
||||||
} |
|
||||||
|
|
||||||
// SlimRecord used for recalling because unmarshalling record w/ 50+ fields is too slow
|
|
||||||
type SlimRecord struct { |
|
||||||
SessionID string `json:"sessionId"` |
|
||||||
RecallHistno int `json:"recallHistno,omitempty"` |
|
||||||
RecallPrefix string `json:"recallPrefix,omitempty"` |
|
||||||
|
|
||||||
// extra recall - we might use these in the future
|
|
||||||
// Pwd string `json:"pwd"`
|
|
||||||
// RealPwd string `json:"realPwd"`
|
|
||||||
// GitDir string `json:"gitDir"`
|
|
||||||
// GitRealDir string `json:"gitRealDir"`
|
|
||||||
// GitOriginRemote string `json:"gitOriginRemote"`
|
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
// CliRecord used for sending records to RESH-CLI
|
|
||||||
type CliRecord struct { |
|
||||||
IsRaw bool `json:"isRaw"` |
|
||||||
SessionID string `json:"sessionId"` |
|
||||||
|
|
||||||
CmdLine string `json:"cmdLine"` |
|
||||||
Host string `json:"host"` |
|
||||||
Pwd string `json:"pwd"` |
|
||||||
Home string `json:"home"` // helps us to collapse /home/user to tilde
|
|
||||||
GitOriginRemote string `json:"gitOriginRemote"` |
|
||||||
ExitCode int `json:"exitCode"` |
|
||||||
|
|
||||||
RealtimeBefore float64 `json:"realtimeBefore"` |
|
||||||
// RealtimeAfter float64 `json:"realtimeAfter"`
|
|
||||||
// RealtimeDuration float64 `json:"realtimeDuration"`
|
|
||||||
} |
|
||||||
|
|
||||||
// NewCliRecordFromCmdLine from EnrichedRecord
|
|
||||||
func NewCliRecordFromCmdLine(cmdLine string) CliRecord { |
|
||||||
return CliRecord{ |
|
||||||
IsRaw: true, |
|
||||||
CmdLine: cmdLine, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// NewCliRecord from EnrichedRecord
|
|
||||||
func NewCliRecord(r EnrichedRecord) CliRecord { |
|
||||||
return CliRecord{ |
|
||||||
IsRaw: false, |
|
||||||
SessionID: r.SessionID, |
|
||||||
CmdLine: r.CmdLine, |
|
||||||
Host: r.Host, |
|
||||||
Pwd: r.Pwd, |
|
||||||
Home: r.Home, |
|
||||||
GitOriginRemote: r.GitOriginRemote, |
|
||||||
ExitCode: r.ExitCode, |
|
||||||
RealtimeBefore: r.RealtimeBefore, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Convert from FallbackRecord to Record
|
|
||||||
func Convert(r *FallbackRecord) Record { |
|
||||||
return Record{ |
|
||||||
BaseRecord: r.BaseRecord, |
|
||||||
// these two lines are the only reason we are doing this
|
|
||||||
Cols: strconv.Itoa(r.Cols), |
|
||||||
Lines: strconv.Itoa(r.Lines), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// ToString - returns record the json
|
|
||||||
func (r EnrichedRecord) ToString() (string, error) { |
|
||||||
jsonRec, err := json.Marshal(r) |
|
||||||
if err != nil { |
|
||||||
return "marshalling error", err |
|
||||||
} |
|
||||||
return string(jsonRec), nil |
|
||||||
} |
|
||||||
|
|
||||||
// Enriched - returnd enriched record
|
|
||||||
func Enriched(r Record) EnrichedRecord { |
|
||||||
record := EnrichedRecord{Record: r} |
|
||||||
// normlize git remote
|
|
||||||
record.GitOriginRemote = NormalizeGitRemote(record.GitOriginRemote) |
|
||||||
record.GitOriginRemoteAfter = NormalizeGitRemote(record.GitOriginRemoteAfter) |
|
||||||
// Get command/first word from commandline
|
|
||||||
var err error |
|
||||||
err = r.Validate() |
|
||||||
if err != nil { |
|
||||||
record.Errors = append(record.Errors, "Validate error:"+err.Error()) |
|
||||||
// rec, _ := record.ToString()
|
|
||||||
// log.Println("Invalid command:", rec)
|
|
||||||
record.Invalid = true |
|
||||||
} |
|
||||||
record.Command, record.FirstWord, err = GetCommandAndFirstWord(r.CmdLine) |
|
||||||
if err != nil { |
|
||||||
record.Errors = append(record.Errors, "GetCommandAndFirstWord error:"+err.Error()) |
|
||||||
// rec, _ := record.ToString()
|
|
||||||
// log.Println("Invalid command:", rec)
|
|
||||||
record.Invalid = true // should this be really invalid ?
|
|
||||||
} |
|
||||||
return record |
|
||||||
} |
|
||||||
|
|
||||||
// Merge two records (part1 - collect + part2 - postcollect)
|
|
||||||
func (r *Record) Merge(r2 Record) error { |
|
||||||
if r.PartOne == false || r2.PartOne { |
|
||||||
return errors.New("Expected part1 and part2 of the same record - usage: part1.Merge(part2)") |
|
||||||
} |
|
||||||
if r.SessionID != r2.SessionID { |
|
||||||
return errors.New("Records to merge are not from the same sesion - r1:" + r.SessionID + " r2:" + r2.SessionID) |
|
||||||
} |
|
||||||
if r.CmdLine != r2.CmdLine { |
|
||||||
return errors.New("Records to merge are not parts of the same records - r1:" + r.CmdLine + " r2:" + r2.CmdLine) |
|
||||||
} |
|
||||||
if r.RecordID != r2.RecordID { |
|
||||||
return errors.New("Records to merge do not have the same ID - r1:" + r.RecordID + " r2:" + r2.RecordID) |
|
||||||
} |
|
||||||
// r.RealtimeBefore != r2.RealtimeBefore - can't be used because of bash-preexec runs when it's not supposed to
|
|
||||||
r.ExitCode = r2.ExitCode |
|
||||||
r.PwdAfter = r2.PwdAfter |
|
||||||
r.RealPwdAfter = r2.RealPwdAfter |
|
||||||
r.GitDirAfter = r2.GitDirAfter |
|
||||||
r.GitRealDirAfter = r2.GitRealDirAfter |
|
||||||
r.RealtimeAfter = r2.RealtimeAfter |
|
||||||
r.GitOriginRemoteAfter = r2.GitOriginRemoteAfter |
|
||||||
r.TimezoneAfter = r2.TimezoneAfter |
|
||||||
r.RealtimeAfterLocal = r2.RealtimeAfterLocal |
|
||||||
r.RealtimeDuration = r2.RealtimeDuration |
|
||||||
|
|
||||||
r.PartsMerged = true |
|
||||||
r.PartOne = false |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// Validate - returns error if the record is invalid
|
|
||||||
func (r *Record) Validate() error { |
|
||||||
if r.CmdLine == "" { |
|
||||||
return errors.New("There is no CmdLine") |
|
||||||
} |
|
||||||
if r.RealtimeBefore == 0 || r.RealtimeAfter == 0 { |
|
||||||
return errors.New("There is no Time") |
|
||||||
} |
|
||||||
if r.RealtimeBeforeLocal == 0 || r.RealtimeAfterLocal == 0 { |
|
||||||
return errors.New("There is no Local Time") |
|
||||||
} |
|
||||||
if r.RealPwd == "" || r.RealPwdAfter == "" { |
|
||||||
return errors.New("There is no Real Pwd") |
|
||||||
} |
|
||||||
if r.Pwd == "" || r.PwdAfter == "" { |
|
||||||
return errors.New("There is no Pwd") |
|
||||||
} |
|
||||||
|
|
||||||
// TimezoneBefore
|
|
||||||
// TimezoneAfter
|
|
||||||
|
|
||||||
// RealtimeDuration
|
|
||||||
// RealtimeSinceSessionStart - TODO: add later
|
|
||||||
// RealtimeSinceBoot - TODO: add later
|
|
||||||
|
|
||||||
// device extras
|
|
||||||
// Host
|
|
||||||
// Hosttype
|
|
||||||
// Ostype
|
|
||||||
// Machtype
|
|
||||||
// OsReleaseID
|
|
||||||
// OsReleaseVersionID
|
|
||||||
// OsReleaseIDLike
|
|
||||||
// OsReleaseName
|
|
||||||
// OsReleasePrettyName
|
|
||||||
|
|
||||||
// session extras
|
|
||||||
// Term
|
|
||||||
// Shlvl
|
|
||||||
|
|
||||||
// static info
|
|
||||||
// Lang
|
|
||||||
// LcAll
|
|
||||||
|
|
||||||
// meta
|
|
||||||
// ReshUUID
|
|
||||||
// ReshVersion
|
|
||||||
// ReshRevision
|
|
||||||
|
|
||||||
// added by sanitizatizer
|
|
||||||
// Sanitized
|
|
||||||
// CmdLength
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// SetCmdLine sets cmdLine and related members
|
|
||||||
func (r *EnrichedRecord) SetCmdLine(cmdLine string) { |
|
||||||
r.CmdLine = cmdLine |
|
||||||
r.CmdLength = len(cmdLine) |
|
||||||
r.ExitCode = 0 |
|
||||||
var err error |
|
||||||
r.Command, r.FirstWord, err = GetCommandAndFirstWord(cmdLine) |
|
||||||
if err != nil { |
|
||||||
r.Errors = append(r.Errors, "GetCommandAndFirstWord error:"+err.Error()) |
|
||||||
// log.Println("Invalid command:", r.CmdLine)
|
|
||||||
r.Invalid = true |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Stripped returns record stripped of all info that is not available during prediction
|
|
||||||
func Stripped(r EnrichedRecord) EnrichedRecord { |
|
||||||
// clear the cmd itself
|
|
||||||
r.SetCmdLine("") |
|
||||||
// replace after info with before info
|
|
||||||
r.PwdAfter = r.Pwd |
|
||||||
r.RealPwdAfter = r.RealPwd |
|
||||||
r.TimezoneAfter = r.TimezoneBefore |
|
||||||
r.RealtimeAfter = r.RealtimeBefore |
|
||||||
r.RealtimeAfterLocal = r.RealtimeBeforeLocal |
|
||||||
// clear some more stuff
|
|
||||||
r.RealtimeDuration = 0 |
|
||||||
r.LastRecordOfSession = false |
|
||||||
return r |
|
||||||
} |
|
||||||
|
|
||||||
// GetCommandAndFirstWord func
|
|
||||||
func GetCommandAndFirstWord(cmdLine string) (string, string, error) { |
|
||||||
args, err := shellwords.Parse(cmdLine) |
|
||||||
if err != nil { |
|
||||||
// log.Println("shellwords Error:", err, " (cmdLine: <", cmdLine, "> )")
|
|
||||||
return "", "", err |
|
||||||
} |
|
||||||
if len(args) == 0 { |
|
||||||
return "", "", nil |
|
||||||
} |
|
||||||
i := 0 |
|
||||||
for true { |
|
||||||
// commands in shell sometimes look like this `variable=something command argument otherArgument --option`
|
|
||||||
// to get the command we skip over tokens that contain '='
|
|
||||||
if strings.ContainsRune(args[i], '=') && len(args) > i+1 { |
|
||||||
i++ |
|
||||||
continue |
|
||||||
} |
|
||||||
return args[i], args[0], nil |
|
||||||
} |
|
||||||
log.Fatal("GetCommandAndFirstWord error: this should not happen!") |
|
||||||
return "ERROR", "ERROR", errors.New("this should not happen - contact developer ;)") |
|
||||||
} |
|
||||||
|
|
||||||
// NormalizeGitRemote func
|
|
||||||
func NormalizeGitRemote(gitRemote string) string { |
|
||||||
if strings.HasSuffix(gitRemote, ".git") { |
|
||||||
return gitRemote[:len(gitRemote)-4] |
|
||||||
} |
|
||||||
return gitRemote |
|
||||||
} |
|
||||||
|
|
||||||
// DistParams is used to supply params to Enrichedrecords.DistanceTo()
|
|
||||||
type DistParams struct { |
|
||||||
ExitCode float64 |
|
||||||
MachineID float64 |
|
||||||
SessionID float64 |
|
||||||
Login float64 |
|
||||||
Shell float64 |
|
||||||
Pwd float64 |
|
||||||
RealPwd float64 |
|
||||||
Git float64 |
|
||||||
Time float64 |
|
||||||
} |
|
||||||
|
|
||||||
// DistanceTo another record
|
|
||||||
func (r *EnrichedRecord) DistanceTo(r2 EnrichedRecord, p DistParams) float64 { |
|
||||||
var dist float64 |
|
||||||
dist = 0 |
|
||||||
|
|
||||||
// lev distance or something? TODO later
|
|
||||||
// CmdLine
|
|
||||||
|
|
||||||
// exit code
|
|
||||||
if r.ExitCode != r2.ExitCode { |
|
||||||
if r.ExitCode == 0 || r2.ExitCode == 0 { |
|
||||||
// one success + one error -> 1
|
|
||||||
dist += 1 * p.ExitCode |
|
||||||
} else { |
|
||||||
// two different errors
|
|
||||||
dist += 0.5 * p.ExitCode |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// machine/device
|
|
||||||
if r.MachineID != r2.MachineID { |
|
||||||
dist += 1 * p.MachineID |
|
||||||
} |
|
||||||
// Uname
|
|
||||||
|
|
||||||
// session
|
|
||||||
if r.SessionID != r2.SessionID { |
|
||||||
dist += 1 * p.SessionID |
|
||||||
} |
|
||||||
// Pid - add because of nested shells?
|
|
||||||
// SessionPid
|
|
||||||
|
|
||||||
// user
|
|
||||||
if r.Login != r2.Login { |
|
||||||
dist += 1 * p.Login |
|
||||||
} |
|
||||||
// Home
|
|
||||||
|
|
||||||
// shell
|
|
||||||
if r.Shell != r2.Shell { |
|
||||||
dist += 1 * p.Shell |
|
||||||
} |
|
||||||
// ShellEnv
|
|
||||||
|
|
||||||
// pwd
|
|
||||||
if r.Pwd != r2.Pwd { |
|
||||||
// TODO: compare using hierarchy
|
|
||||||
// TODO: make more important
|
|
||||||
dist += 1 * p.Pwd |
|
||||||
} |
|
||||||
if r.RealPwd != r2.RealPwd { |
|
||||||
// TODO: -||-
|
|
||||||
dist += 1 * p.RealPwd |
|
||||||
} |
|
||||||
// PwdAfter
|
|
||||||
// RealPwdAfter
|
|
||||||
|
|
||||||
// git
|
|
||||||
if r.GitDir != r2.GitDir { |
|
||||||
dist += 1 * p.Git |
|
||||||
} |
|
||||||
if r.GitRealDir != r2.GitRealDir { |
|
||||||
dist += 1 * p.Git |
|
||||||
} |
|
||||||
if r.GitOriginRemote != r2.GitOriginRemote { |
|
||||||
dist += 1 * p.Git |
|
||||||
} |
|
||||||
|
|
||||||
// time
|
|
||||||
// this can actually get negative for differences of less than one second which is fine
|
|
||||||
// distance grows by 1 with every order
|
|
||||||
distTime := math.Log10(math.Abs(r.RealtimeBefore-r2.RealtimeBefore)) * p.Time |
|
||||||
if math.IsNaN(distTime) == false && math.IsInf(distTime, 0) == false { |
|
||||||
dist += distTime |
|
||||||
} |
|
||||||
// RealtimeBeforeLocal
|
|
||||||
// RealtimeAfter
|
|
||||||
// RealtimeAfterLocal
|
|
||||||
|
|
||||||
// TimezoneBefore
|
|
||||||
// TimezoneAfter
|
|
||||||
|
|
||||||
// RealtimeDuration
|
|
||||||
// RealtimeSinceSessionStart - TODO: add later
|
|
||||||
// RealtimeSinceBoot - TODO: add later
|
|
||||||
|
|
||||||
// device extras
|
|
||||||
// Host
|
|
||||||
// Hosttype
|
|
||||||
// Ostype
|
|
||||||
// Machtype
|
|
||||||
// OsReleaseID
|
|
||||||
// OsReleaseVersionID
|
|
||||||
// OsReleaseIDLike
|
|
||||||
// OsReleaseName
|
|
||||||
// OsReleasePrettyName
|
|
||||||
|
|
||||||
// session extras
|
|
||||||
// Term
|
|
||||||
// Shlvl
|
|
||||||
|
|
||||||
// static info
|
|
||||||
// Lang
|
|
||||||
// LcAll
|
|
||||||
|
|
||||||
// meta
|
|
||||||
// ReshUUID
|
|
||||||
// ReshVersion
|
|
||||||
// ReshRevision
|
|
||||||
|
|
||||||
// added by sanitizatizer
|
|
||||||
// Sanitized
|
|
||||||
// CmdLength
|
|
||||||
|
|
||||||
return dist |
|
||||||
} |
|
||||||
|
|
||||||
// LoadFromFile loads records from 'fname' file
|
|
||||||
func LoadFromFile(fname string, limit int) []Record { |
|
||||||
const allowedErrors = 1 |
|
||||||
var encounteredErrors int |
|
||||||
// NOTE: limit does nothing atm
|
|
||||||
var recs []Record |
|
||||||
file, err := os.Open(fname) |
|
||||||
if err != nil { |
|
||||||
log.Println("Open() resh history file error:", err) |
|
||||||
log.Println("WARN: Skipping reading resh history!") |
|
||||||
return recs |
|
||||||
} |
|
||||||
defer file.Close() |
|
||||||
|
|
||||||
reader := bufio.NewReader(file) |
|
||||||
var i int |
|
||||||
var firstErrLine int |
|
||||||
for { |
|
||||||
line, err := reader.ReadString('\n') |
|
||||||
if err != nil { |
|
||||||
break |
|
||||||
} |
|
||||||
i++ |
|
||||||
record := Record{} |
|
||||||
fallbackRecord := FallbackRecord{} |
|
||||||
err = json.Unmarshal([]byte(line), &record) |
|
||||||
if err != nil { |
|
||||||
err = json.Unmarshal([]byte(line), &fallbackRecord) |
|
||||||
if err != nil { |
|
||||||
if encounteredErrors == 0 { |
|
||||||
firstErrLine = i |
|
||||||
} |
|
||||||
encounteredErrors++ |
|
||||||
log.Println("Line:", line) |
|
||||||
log.Println("Decoding error:", err) |
|
||||||
if encounteredErrors > allowedErrors { |
|
||||||
log.Fatalf("Fatal: Encountered more than %d decoding errors (%d)", allowedErrors, encounteredErrors) |
|
||||||
} |
|
||||||
} |
|
||||||
record = Convert(&fallbackRecord) |
|
||||||
} |
|
||||||
recs = append(recs, record) |
|
||||||
} |
|
||||||
// log.Println("records: done loading file:", err)
|
|
||||||
if err != io.EOF { |
|
||||||
log.Println("records: error while loading file:", err) |
|
||||||
} |
|
||||||
// log.Println("records: Loaded lines - count:", i)
|
|
||||||
if encounteredErrors > 0 { |
|
||||||
// fix errors in the history file
|
|
||||||
log.Printf("There were %d decoding errors, the first error happend on line %d/%d", encounteredErrors, firstErrLine, i) |
|
||||||
log.Println("Backing up current history file ...") |
|
||||||
err := copyFile(fname, fname+".bak") |
|
||||||
if err != nil { |
|
||||||
log.Fatalln("Failed to backup history file with decode errors") |
|
||||||
} |
|
||||||
log.Println("Writing out a history file without errors ...") |
|
||||||
err = writeHistory(fname, recs) |
|
||||||
if err != nil { |
|
||||||
log.Fatalln("Fatal: Failed write out new history") |
|
||||||
} |
|
||||||
} |
|
||||||
log.Println("records: Loaded records - count:", len(recs)) |
|
||||||
return recs |
|
||||||
} |
|
||||||
|
|
||||||
func copyFile(source, dest string) error { |
|
||||||
from, err := os.Open(source) |
|
||||||
if err != nil { |
|
||||||
// log.Println("Open() resh history file error:", err)
|
|
||||||
return err |
|
||||||
} |
|
||||||
defer from.Close() |
|
||||||
|
|
||||||
// to, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE, 0666)
|
|
||||||
to, err := os.Create(dest) |
|
||||||
if err != nil { |
|
||||||
// log.Println("Create() resh history backup error:", err)
|
|
||||||
return err |
|
||||||
} |
|
||||||
defer to.Close() |
|
||||||
|
|
||||||
_, err = io.Copy(to, from) |
|
||||||
if err != nil { |
|
||||||
// log.Println("Copy() resh history to backup error:", err)
|
|
||||||
return err |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func writeHistory(fname string, history []Record) error { |
|
||||||
file, err := os.Create(fname) |
|
||||||
if err != nil { |
|
||||||
// log.Println("Create() resh history error:", err)
|
|
||||||
return err |
|
||||||
} |
|
||||||
defer file.Close() |
|
||||||
for _, rec := range history { |
|
||||||
jsn, err := json.Marshal(rec) |
|
||||||
if err != nil { |
|
||||||
log.Fatalln("Encode error!") |
|
||||||
} |
|
||||||
file.Write(append(jsn, []byte("\n")...)) |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// LoadCmdLinesFromZshFile loads cmdlines from zsh history file
|
|
||||||
func LoadCmdLinesFromZshFile(fname string) histlist.Histlist { |
|
||||||
hl := histlist.New() |
|
||||||
file, err := os.Open(fname) |
|
||||||
if err != nil { |
|
||||||
log.Println("Open() zsh history file error:", err) |
|
||||||
log.Println("WARN: Skipping reading zsh history!") |
|
||||||
return hl |
|
||||||
} |
|
||||||
defer file.Close() |
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file) |
|
||||||
for scanner.Scan() { |
|
||||||
line := scanner.Text() |
|
||||||
// trim newline
|
|
||||||
line = strings.TrimRight(line, "\n") |
|
||||||
var cmd string |
|
||||||
// zsh format EXTENDED_HISTORY
|
|
||||||
// : 1576270617:0;make install
|
|
||||||
// zsh format no EXTENDED_HISTORY
|
|
||||||
// make install
|
|
||||||
if len(line) == 0 { |
|
||||||
// skip empty
|
|
||||||
continue |
|
||||||
} |
|
||||||
if strings.Contains(line, ":") && strings.Contains(line, ";") && |
|
||||||
len(strings.Split(line, ":")) >= 3 && len(strings.Split(line, ";")) >= 2 { |
|
||||||
// contains at least 2x ':' and 1x ';' => assume EXTENDED_HISTORY
|
|
||||||
cmd = strings.Split(line, ";")[1] |
|
||||||
} else { |
|
||||||
cmd = line |
|
||||||
} |
|
||||||
hl.AddCmdLine(cmd) |
|
||||||
} |
|
||||||
return hl |
|
||||||
} |
|
||||||
|
|
||||||
// LoadCmdLinesFromBashFile loads cmdlines from bash history file
|
|
||||||
func LoadCmdLinesFromBashFile(fname string) histlist.Histlist { |
|
||||||
hl := histlist.New() |
|
||||||
file, err := os.Open(fname) |
|
||||||
if err != nil { |
|
||||||
log.Println("Open() bash history file error:", err) |
|
||||||
log.Println("WARN: Skipping reading bash history!") |
|
||||||
return hl |
|
||||||
} |
|
||||||
defer file.Close() |
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file) |
|
||||||
for scanner.Scan() { |
|
||||||
line := scanner.Text() |
|
||||||
// trim newline
|
|
||||||
line = strings.TrimRight(line, "\n") |
|
||||||
// trim spaces from left
|
|
||||||
line = strings.TrimLeft(line, " ") |
|
||||||
// bash format (two lines)
|
|
||||||
// #1576199174
|
|
||||||
// make install
|
|
||||||
if strings.HasPrefix(line, "#") { |
|
||||||
// is either timestamp or comment => skip
|
|
||||||
continue |
|
||||||
} |
|
||||||
if len(line) == 0 { |
|
||||||
// skip empty
|
|
||||||
continue |
|
||||||
} |
|
||||||
hl.AddCmdLine(line) |
|
||||||
} |
|
||||||
return hl |
|
||||||
} |
|
||||||
@ -1,152 +0,0 @@ |
|||||||
package records |
|
||||||
|
|
||||||
import ( |
|
||||||
"bufio" |
|
||||||
"encoding/json" |
|
||||||
"log" |
|
||||||
"os" |
|
||||||
"testing" |
|
||||||
) |
|
||||||
|
|
||||||
func GetTestRecords() []Record { |
|
||||||
file, err := os.Open("testdata/resh_history.json") |
|
||||||
if err != nil { |
|
||||||
log.Fatal("Open() resh history file error:", err) |
|
||||||
} |
|
||||||
defer file.Close() |
|
||||||
|
|
||||||
var recs []Record |
|
||||||
scanner := bufio.NewScanner(file) |
|
||||||
for scanner.Scan() { |
|
||||||
record := Record{} |
|
||||||
line := scanner.Text() |
|
||||||
err = json.Unmarshal([]byte(line), &record) |
|
||||||
if err != nil { |
|
||||||
log.Println("Line:", line) |
|
||||||
log.Fatal("Decoding error:", err) |
|
||||||
} |
|
||||||
recs = append(recs, record) |
|
||||||
} |
|
||||||
return recs |
|
||||||
} |
|
||||||
|
|
||||||
func GetTestEnrichedRecords() []EnrichedRecord { |
|
||||||
var recs []EnrichedRecord |
|
||||||
for _, rec := range GetTestRecords() { |
|
||||||
recs = append(recs, Enriched(rec)) |
|
||||||
} |
|
||||||
return recs |
|
||||||
} |
|
||||||
|
|
||||||
func TestToString(t *testing.T) { |
|
||||||
for _, rec := range GetTestEnrichedRecords() { |
|
||||||
_, err := rec.ToString() |
|
||||||
if err != nil { |
|
||||||
t.Error("ToString() failed") |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestEnriched(t *testing.T) { |
|
||||||
record := Record{BaseRecord: BaseRecord{CmdLine: "cmd arg1 arg2"}} |
|
||||||
enriched := Enriched(record) |
|
||||||
if enriched.FirstWord != "cmd" || enriched.Command != "cmd" { |
|
||||||
t.Error("Enriched() returned reocord w/ wrong Command OR FirstWord") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestValidate(t *testing.T) { |
|
||||||
record := EnrichedRecord{} |
|
||||||
if record.Validate() == nil { |
|
||||||
t.Error("Validate() didn't return an error for invalid record") |
|
||||||
} |
|
||||||
record.CmdLine = "cmd arg" |
|
||||||
record.FirstWord = "cmd" |
|
||||||
record.Command = "cmd" |
|
||||||
time := 1234.5678 |
|
||||||
record.RealtimeBefore = time |
|
||||||
record.RealtimeAfter = time |
|
||||||
record.RealtimeBeforeLocal = time |
|
||||||
record.RealtimeAfterLocal = time |
|
||||||
pwd := "/pwd" |
|
||||||
record.Pwd = pwd |
|
||||||
record.PwdAfter = pwd |
|
||||||
record.RealPwd = pwd |
|
||||||
record.RealPwdAfter = pwd |
|
||||||
if record.Validate() != nil { |
|
||||||
t.Error("Validate() returned an error for a valid record") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestSetCmdLine(t *testing.T) { |
|
||||||
record := EnrichedRecord{} |
|
||||||
cmdline := "cmd arg1 arg2" |
|
||||||
record.SetCmdLine(cmdline) |
|
||||||
if record.CmdLine != cmdline || record.Command != "cmd" || record.FirstWord != "cmd" { |
|
||||||
t.Error() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestStripped(t *testing.T) { |
|
||||||
for _, rec := range GetTestEnrichedRecords() { |
|
||||||
stripped := Stripped(rec) |
|
||||||
|
|
||||||
// there should be no cmdline
|
|
||||||
if stripped.CmdLine != "" || |
|
||||||
stripped.FirstWord != "" || |
|
||||||
stripped.Command != "" { |
|
||||||
t.Error("Stripped() returned record w/ info about CmdLine, Command OR FirstWord") |
|
||||||
} |
|
||||||
// *after* fields should be overwritten by *before* fields
|
|
||||||
if stripped.PwdAfter != stripped.Pwd || |
|
||||||
stripped.RealPwdAfter != stripped.RealPwd || |
|
||||||
stripped.TimezoneAfter != stripped.TimezoneBefore || |
|
||||||
stripped.RealtimeAfter != stripped.RealtimeBefore || |
|
||||||
stripped.RealtimeAfterLocal != stripped.RealtimeBeforeLocal { |
|
||||||
t.Error("Stripped() returned record w/ different *after* and *before* values - *after* fields should be overwritten by *before* fields") |
|
||||||
} |
|
||||||
// there should be no information about duration and session end
|
|
||||||
if stripped.RealtimeDuration != 0 || |
|
||||||
stripped.LastRecordOfSession != false { |
|
||||||
t.Error("Stripped() returned record with too much information") |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestGetCommandAndFirstWord(t *testing.T) { |
|
||||||
cmd, stWord, err := GetCommandAndFirstWord("cmd arg1 arg2") |
|
||||||
if err != nil || cmd != "cmd" || stWord != "cmd" { |
|
||||||
t.Error("GetCommandAndFirstWord() returned wrong Command OR FirstWord") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestDistanceTo(t *testing.T) { |
|
||||||
paramsFull := DistParams{ |
|
||||||
ExitCode: 1, |
|
||||||
MachineID: 1, |
|
||||||
SessionID: 1, |
|
||||||
Login: 1, |
|
||||||
Shell: 1, |
|
||||||
Pwd: 1, |
|
||||||
RealPwd: 1, |
|
||||||
Git: 1, |
|
||||||
Time: 1, |
|
||||||
} |
|
||||||
paramsZero := DistParams{} |
|
||||||
var prevRec EnrichedRecord |
|
||||||
for _, rec := range GetTestEnrichedRecords() { |
|
||||||
dist := rec.DistanceTo(rec, paramsFull) |
|
||||||
if dist != 0 { |
|
||||||
t.Error("DistanceTo() itself should be always 0") |
|
||||||
} |
|
||||||
dist = rec.DistanceTo(prevRec, paramsFull) |
|
||||||
if dist == 0 { |
|
||||||
t.Error("DistanceTo() between two test records shouldn't be 0") |
|
||||||
} |
|
||||||
dist = rec.DistanceTo(prevRec, paramsZero) |
|
||||||
if dist != 0 { |
|
||||||
t.Error("DistanceTo() should be 0 when DistParams is all zeros") |
|
||||||
} |
|
||||||
prevRec = rec |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,27 +0,0 @@ |
|||||||
{"cmdLine":"ls","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"d5c0fe70-c80b-4715-87cb-f8d8d5b4c673","cols":"80","lines":"24","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon","pwdAfter":"/home/simon","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon","realPwdAfter":"/home/simon","pid":14560,"sessionPid":14560,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1566762905.173595,"realtimeAfter":1566762905.1894295,"realtimeBeforeLocal":1566770105.173595,"realtimeAfterLocal":1566770105.1894295,"realtimeDuration":0.015834569931030273,"realtimeSinceSessionStart":1.7122540473937988,"realtimeSinceBoot":20766.542254047396,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"} |
|
||||||
{"cmdLine":"find . -name applications","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"c5251955-3a64-4353-952e-08d62a898694","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon","pwdAfter":"/home/simon","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon","realPwdAfter":"/home/simon","pid":3109,"sessionPid":3109,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567420001.2531302,"realtimeAfter":1567420002.4311218,"realtimeBeforeLocal":1567427201.2531302,"realtimeAfterLocal":1567427202.4311218,"realtimeDuration":1.1779916286468506,"realtimeSinceSessionStart":957.4848053455353,"realtimeSinceBoot":2336.594805345535,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"} |
|
||||||
{"cmdLine":"desktop-file-validate curusarn.sync-clipboards.desktop ","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"c5251955-3a64-4353-952e-08d62a898694","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/.local/share/applications","pwdAfter":"/home/simon/.local/share/applications","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/.local/share/applications","realPwdAfter":"/home/simon/.local/share/applications","pid":3109,"sessionPid":3109,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567421748.2965438,"realtimeAfter":1567421748.3068867,"realtimeBeforeLocal":1567428948.2965438,"realtimeAfterLocal":1567428948.3068867,"realtimeDuration":0.010342836380004883,"realtimeSinceSessionStart":2704.528218984604,"realtimeSinceBoot":4083.6382189846036,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"} |
|
||||||
{"cmdLine":"cat /tmp/extensions | grep '.'","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"f044cdbf-fd51-4c37-8528-dcd98fc7b6d9","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon","pwdAfter":"/home/simon","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon","realPwdAfter":"/home/simon","pid":6887,"sessionPid":6887,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567461416.6871984,"realtimeAfter":1567461416.7336714,"realtimeBeforeLocal":1567468616.6871984,"realtimeAfterLocal":1567468616.7336714,"realtimeDuration":0.046473026275634766,"realtimeSinceSessionStart":21.45597553253174,"realtimeSinceBoot":43752.03597553253,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"} |
|
||||||
{"cmdLine":"cd git/resh/","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"f044cdbf-fd51-4c37-8528-dcd98fc7b6d9","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon","realPwdAfter":"/home/simon/git/resh","pid":6887,"sessionPid":6887,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567461667.8806899,"realtimeAfter":1567461667.8949044,"realtimeBeforeLocal":1567468867.8806899,"realtimeAfterLocal":1567468867.8949044,"realtimeDuration":0.014214515686035156,"realtimeSinceSessionStart":272.64946699142456,"realtimeSinceBoot":44003.229466991426,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"} |
|
||||||
{"cmdLine":"git s","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"f044cdbf-fd51-4c37-8528-dcd98fc7b6d9","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":6887,"sessionPid":6887,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567461707.6467602,"realtimeAfter":1567461707.7177293,"realtimeBeforeLocal":1567468907.6467602,"realtimeAfterLocal":1567468907.7177293,"realtimeDuration":0.0709691047668457,"realtimeSinceSessionStart":312.4155373573303,"realtimeSinceBoot":44042.99553735733,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"} |
|
||||||
{"cmdLine":"cat /tmp/extensions | grep '^\\.' | cut -f1 |tr '[:upper:]' '[:lower:]' ","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"f044cdbf-fd51-4c37-8528-dcd98fc7b6d9","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":6887,"sessionPid":6887,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567461722.813049,"realtimeAfter":1567461722.8280325,"realtimeBeforeLocal":1567468922.813049,"realtimeAfterLocal":1567468922.8280325,"realtimeDuration":0.014983415603637695,"realtimeSinceSessionStart":327.581826210022,"realtimeSinceBoot":44058.161826210024,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"} |
|
||||||
{"cmdLine":"tig","exitCode":127,"shell":"bash","uname":"Linux","sessionId":"f044cdbf-fd51-4c37-8528-dcd98fc7b6d9","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":6887,"sessionPid":6887,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567461906.3896828,"realtimeAfter":1567461906.4084594,"realtimeBeforeLocal":1567469106.3896828,"realtimeAfterLocal":1567469106.4084594,"realtimeDuration":0.018776655197143555,"realtimeSinceSessionStart":511.1584599018097,"realtimeSinceBoot":44241.73845990181,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"752acb916f2a"} |
|
||||||
{"cmdLine":"resh-sanitize-history | jq","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"a3318c80-3521-4b22-aa64-ea0f6c641410","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon","pwdAfter":"/home/simon","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon","realPwdAfter":"/home/simon","pid":14601,"sessionPid":14601,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567547116.2430356,"realtimeAfter":1567547116.7547352,"realtimeBeforeLocal":1567554316.2430356,"realtimeAfterLocal":1567554316.7547352,"realtimeDuration":0.5116996765136719,"realtimeSinceSessionStart":15.841878414154053,"realtimeSinceBoot":30527.201878414155,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} |
|
||||||
{"cmdLine":"sudo pacman -S ansible","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"64154f2d-a4bc-4463-a690-520080b61ead","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/kristin","pwdAfter":"/home/simon/git/kristin","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/kristin","realPwdAfter":"/home/simon/git/kristin","pid":5663,"sessionPid":5663,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567609042.0166302,"realtimeAfter":1567609076.9726007,"realtimeBeforeLocal":1567616242.0166302,"realtimeAfterLocal":1567616276.9726007,"realtimeDuration":34.95597052574158,"realtimeSinceSessionStart":1617.0794131755829,"realtimeSinceBoot":6120.029413175583,"gitDir":"/home/simon/git/kristin","gitRealDir":"/home/simon/git/kristin","gitOriginRemote":"git@gitlab.com:sucvut/kristin.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} |
|
||||||
{"cmdLine":"vagrant up","exitCode":1,"shell":"bash","uname":"Linux","sessionId":"64154f2d-a4bc-4463-a690-520080b61ead","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/kristin","pwdAfter":"/home/simon/git/kristin","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/kristin","realPwdAfter":"/home/simon/git/kristin","pid":5663,"sessionPid":5663,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567609090.7359188,"realtimeAfter":1567609098.3125577,"realtimeBeforeLocal":1567616290.7359188,"realtimeAfterLocal":1567616298.3125577,"realtimeDuration":7.57663893699646,"realtimeSinceSessionStart":1665.798701763153,"realtimeSinceBoot":6168.748701763153,"gitDir":"/home/simon/git/kristin","gitRealDir":"/home/simon/git/kristin","gitOriginRemote":"git@gitlab.com:sucvut/kristin.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} |
|
||||||
{"cmdLine":"sudo modprobe vboxnetflt","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"64154f2d-a4bc-4463-a690-520080b61ead","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/kristin","pwdAfter":"/home/simon/git/kristin","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/kristin","realPwdAfter":"/home/simon/git/kristin","pid":5663,"sessionPid":5663,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567609143.2847652,"realtimeAfter":1567609143.3116078,"realtimeBeforeLocal":1567616343.2847652,"realtimeAfterLocal":1567616343.3116078,"realtimeDuration":0.026842594146728516,"realtimeSinceSessionStart":1718.3475482463837,"realtimeSinceBoot":6221.2975482463835,"gitDir":"/home/simon/git/kristin","gitRealDir":"/home/simon/git/kristin","gitOriginRemote":"git@gitlab.com:sucvut/kristin.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} |
|
||||||
{"cmdLine":"echo $RANDOM","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"8ddacadc-6e73-483c-b347-4e18df204466","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon","pwdAfter":"/home/simon","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon","realPwdAfter":"/home/simon","pid":31387,"sessionPid":31387,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567727039.6540458,"realtimeAfter":1567727039.6629689,"realtimeBeforeLocal":1567734239.6540458,"realtimeAfterLocal":1567734239.6629689,"realtimeDuration":0.008923053741455078,"realtimeSinceSessionStart":1470.7667458057404,"realtimeSinceBoot":18495.01674580574,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} |
|
||||||
{"cmdLine":"make resh-evaluate ","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567977478.9672194,"realtimeAfter":1567977479.5449634,"realtimeBeforeLocal":1567984678.9672194,"realtimeAfterLocal":1567984679.5449634,"realtimeDuration":0.5777440071105957,"realtimeSinceSessionStart":5738.577540636063,"realtimeSinceBoot":20980.42754063606,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} |
|
||||||
{"cmdLine":"cat ~/.resh_history.json | grep \"./resh-eval\" | jq","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"105","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1567986105.3988302,"realtimeAfter":1567986105.4809113,"realtimeBeforeLocal":1567993305.3988302,"realtimeAfterLocal":1567993305.4809113,"realtimeDuration":0.08208107948303223,"realtimeSinceSessionStart":14365.00915145874,"realtimeSinceBoot":29606.85915145874,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} |
|
||||||
{"cmdLine":"git c \"add sanitized flag to record, add Enrich() to record\"","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1568063976.9103937,"realtimeAfter":1568063976.9326868,"realtimeBeforeLocal":1568071176.9103937,"realtimeAfterLocal":1568071176.9326868,"realtimeDuration":0.0222930908203125,"realtimeSinceSessionStart":92236.52071499825,"realtimeSinceBoot":107478.37071499825,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} |
|
||||||
{"cmdLine":"git s","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1568063978.2340608,"realtimeAfter":1568063978.252463,"realtimeBeforeLocal":1568071178.2340608,"realtimeAfterLocal":1568071178.252463,"realtimeDuration":0.0184023380279541,"realtimeSinceSessionStart":92237.84438204765,"realtimeSinceBoot":107479.69438204766,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} |
|
||||||
{"cmdLine":"git a evaluate/results.go ","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1568063989.0446353,"realtimeAfter":1568063989.2452207,"realtimeBeforeLocal":1568071189.0446353,"realtimeAfterLocal":1568071189.2452207,"realtimeDuration":0.20058536529541016,"realtimeSinceSessionStart":92248.65495657921,"realtimeSinceBoot":107490.50495657921,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} |
|
||||||
{"cmdLine":"sudo pacman -S python-pip","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1568072068.3557143,"realtimeAfter":1568072070.7509863,"realtimeBeforeLocal":1568079268.3557143,"realtimeAfterLocal":1568079270.7509863,"realtimeDuration":2.3952720165252686,"realtimeSinceSessionStart":100327.96603560448,"realtimeSinceBoot":115569.81603560448,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} |
|
||||||
{"cmdLine":"pip3 install matplotlib","exitCode":1,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1568072088.5575967,"realtimeAfter":1568072094.372314,"realtimeBeforeLocal":1568079288.5575967,"realtimeAfterLocal":1568079294.372314,"realtimeDuration":5.8147172927856445,"realtimeSinceSessionStart":100348.16791796684,"realtimeSinceBoot":115590.01791796685,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} |
|
||||||
{"cmdLine":"sudo pip3 install matplotlib","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1568072106.138616,"realtimeAfter":1568072115.1124601,"realtimeBeforeLocal":1568079306.138616,"realtimeAfterLocal":1568079315.1124601,"realtimeDuration":8.973844051361084,"realtimeSinceSessionStart":100365.7489373684,"realtimeSinceBoot":115607.5989373684,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} |
|
||||||
{"cmdLine":"./resh-evaluate --plotting-script evaluate/resh-evaluate-plot.py --input ~/git/resh_private/history_data/simon/dell/resh_history.json ","exitCode":130,"shell":"bash","uname":"Linux","sessionId":"93998b68-ec48-4e48-9e4a-b37b39f5439e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":9463,"sessionPid":9463,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1568076266.9364285,"realtimeAfter":1568076288.1131275,"realtimeBeforeLocal":1568083466.9364285,"realtimeAfterLocal":1568083488.1131275,"realtimeDuration":21.176698923110962,"realtimeSinceSessionStart":104526.54674983025,"realtimeSinceBoot":119768.39674983025,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.1","reshRevision":"737bc0a4df38","cmdLength":0} |
|
||||||
{"cmdLine":"git c \"Add a bunch of useless comments to make linter happy\"","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"04050353-a97d-4435-9248-f47dd08b2f2a","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":14702,"sessionPid":14702,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1569456045.8763022,"realtimeAfter":1569456045.9030173,"realtimeBeforeLocal":1569463245.8763022,"realtimeAfterLocal":1569463245.9030173,"realtimeDuration":0.02671504020690918,"realtimeSinceSessionStart":2289.789242744446,"realtimeSinceBoot":143217.91924274445,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.3","reshRevision":"188d8b420493","sanitized":false} |
|
||||||
{"cmdLine":"fuck","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"a4aadf03-610d-4731-ba94-5b7ce21e7bb9","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":3413,"sessionPid":3413,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1569687682.4250975,"realtimeAfter":1569687682.5877323,"realtimeBeforeLocal":1569694882.4250975,"realtimeAfterLocal":1569694882.5877323,"realtimeDuration":0.16263484954833984,"realtimeSinceSessionStart":264603.49496507645,"realtimeSinceBoot":374854.48496507644,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.3","reshRevision":"188d8b420493","sanitized":false} |
|
||||||
{"cmdLine":"code .","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"87c7ab14-ae51-408d-adbc-fc4f9d28de6e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":31947,"sessionPid":31947,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1569709366.523767,"realtimeAfter":1569709367.516908,"realtimeBeforeLocal":1569716566.523767,"realtimeAfterLocal":1569716567.516908,"realtimeDuration":0.9931409358978271,"realtimeSinceSessionStart":23846.908839941025,"realtimeSinceBoot":396539.888839941,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.3","reshRevision":"188d8b420493","sanitized":false} |
|
||||||
{"cmdLine":"make test","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"87c7ab14-ae51-408d-adbc-fc4f9d28de6e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon/git/resh","pwdAfter":"/home/simon/git/resh","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon/git/resh","realPwdAfter":"/home/simon/git/resh","pid":31947,"sessionPid":31947,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1569709371.89966,"realtimeAfter":1569709377.430194,"realtimeBeforeLocal":1569716571.89966,"realtimeAfterLocal":1569716577.430194,"realtimeDuration":5.530533790588379,"realtimeSinceSessionStart":23852.284733057022,"realtimeSinceBoot":396545.264733057,"gitDir":"/home/simon/git/resh","gitRealDir":"/home/simon/git/resh","gitOriginRemote":"git@github.com:curusarn/resh.git","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.3","reshRevision":"188d8b420493","sanitized":false} |
|
||||||
{"cmdLine":"mkdir ~/git/resh/testdata","exitCode":0,"shell":"bash","uname":"Linux","sessionId":"71529b60-2e7b-4d5b-8dc1-6d0740b58e9e","cols":"211","lines":"56","home":"/home/simon","lang":"en_US.UTF-8","lcAll":"","login":"simon","pwd":"/home/simon","pwdAfter":"/home/simon","shellEnv":"/bin/bash","term":"xterm-256color","realPwd":"/home/simon","realPwdAfter":"/home/simon","pid":21224,"sessionPid":21224,"host":"simon-pc","hosttype":"x86_64","ostype":"linux-gnu","machtype":"x86_64-pc-linux-gnu","shlvl":1,"timezoneBefore":"+0200","timezoneAfter":"+0200","realtimeBefore":1569709838.4642656,"realtimeAfter":1569709838.4718792,"realtimeBeforeLocal":1569717038.4642656,"realtimeAfterLocal":1569717038.4718792,"realtimeDuration":0.007613658905029297,"realtimeSinceSessionStart":9.437154054641724,"realtimeSinceBoot":397011.02715405467,"gitDir":"","gitRealDir":"","gitOriginRemote":"","machineId":"c70365240bc647f09e2490722cc8186b","osReleaseId":"manjaro","osReleaseVersionId":"","osReleaseIdLike":"arch","osReleaseName":"Manjaro Linux","osReleasePrettyName":"Manjaro Linux","reshUuid":"","reshVersion":"1.1.3","reshRevision":"188d8b420493","sanitized":false} |
|
||||||
@ -1,23 +0,0 @@ |
|||||||
package searchapp |
|
||||||
|
|
||||||
import ( |
|
||||||
"math" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/histcli" |
|
||||||
"github.com/curusarn/resh/pkg/msg" |
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
) |
|
||||||
|
|
||||||
// LoadHistoryFromFile ...
|
|
||||||
func LoadHistoryFromFile(historyPath string, numLines int) msg.CliResponse { |
|
||||||
recs := records.LoadFromFile(historyPath, math.MaxInt32) |
|
||||||
if numLines != 0 && numLines < len(recs) { |
|
||||||
recs = recs[:numLines] |
|
||||||
} |
|
||||||
cliRecords := histcli.New() |
|
||||||
for i := len(recs) - 1; i >= 0; i-- { |
|
||||||
rec := recs[i] |
|
||||||
cliRecords.AddRecord(rec) |
|
||||||
} |
|
||||||
return msg.CliResponse{CliRecords: cliRecords.List} |
|
||||||
} |
|
||||||
@ -1,243 +0,0 @@ |
|||||||
package sesshist |
|
||||||
|
|
||||||
import ( |
|
||||||
"errors" |
|
||||||
"log" |
|
||||||
"strconv" |
|
||||||
"strings" |
|
||||||
"sync" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/histfile" |
|
||||||
"github.com/curusarn/resh/pkg/histlist" |
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
) |
|
||||||
|
|
||||||
// Dispatch Recall() calls to an apropriate session history (sesshist)
|
|
||||||
type Dispatch struct { |
|
||||||
sessions map[string]*sesshist |
|
||||||
mutex sync.RWMutex |
|
||||||
|
|
||||||
history *histfile.Histfile |
|
||||||
historyInitSize int |
|
||||||
} |
|
||||||
|
|
||||||
// NewDispatch creates a new sesshist.Dispatch and starts necessary gorutines
|
|
||||||
func NewDispatch(sessionsToInit chan records.Record, sessionsToDrop chan string, |
|
||||||
recordsToAdd chan records.Record, history *histfile.Histfile, historyInitSize int) *Dispatch { |
|
||||||
|
|
||||||
s := Dispatch{ |
|
||||||
sessions: map[string]*sesshist{}, |
|
||||||
history: history, |
|
||||||
historyInitSize: historyInitSize, |
|
||||||
} |
|
||||||
go s.sessionInitializer(sessionsToInit) |
|
||||||
go s.sessionDropper(sessionsToDrop) |
|
||||||
go s.recordAdder(recordsToAdd) |
|
||||||
return &s |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Dispatch) sessionInitializer(sessionsToInit chan records.Record) { |
|
||||||
for { |
|
||||||
record := <-sessionsToInit |
|
||||||
log.Println("sesshist: got session to init - " + record.SessionID) |
|
||||||
s.initSession(record.SessionID, record.Shell) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Dispatch) sessionDropper(sessionsToDrop chan string) { |
|
||||||
for { |
|
||||||
sessionID := <-sessionsToDrop |
|
||||||
log.Println("sesshist: got session to drop - " + sessionID) |
|
||||||
s.dropSession(sessionID) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Dispatch) recordAdder(recordsToAdd chan records.Record) { |
|
||||||
for { |
|
||||||
record := <-recordsToAdd |
|
||||||
if record.PartOne { |
|
||||||
log.Println("sesshist: got record to add - " + record.CmdLine) |
|
||||||
s.addRecentRecord(record.SessionID, record) |
|
||||||
} else { |
|
||||||
// this inits session on RESH update
|
|
||||||
s.checkSession(record.SessionID, record.Shell) |
|
||||||
} |
|
||||||
// TODO: we will need to handle part2 as well eventually
|
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Dispatch) checkSession(sessionID, shell string) { |
|
||||||
s.mutex.RLock() |
|
||||||
_, found := s.sessions[sessionID] |
|
||||||
s.mutex.RUnlock() |
|
||||||
if found == false { |
|
||||||
err := s.initSession(sessionID, shell) |
|
||||||
if err != nil { |
|
||||||
log.Println("sesshist: Error while checking session:", err) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// InitSession struct
|
|
||||||
func (s *Dispatch) initSession(sessionID, shell string) error { |
|
||||||
log.Println("sesshist: initializing session - " + sessionID) |
|
||||||
s.mutex.RLock() |
|
||||||
_, found := s.sessions[sessionID] |
|
||||||
s.mutex.RUnlock() |
|
||||||
|
|
||||||
if found == true { |
|
||||||
return errors.New("sesshist ERROR: Can't INIT already existing session " + sessionID) |
|
||||||
} |
|
||||||
|
|
||||||
log.Println("sesshist: loading history to populate session - " + sessionID) |
|
||||||
historyCmdLines := s.history.GetRecentCmdLines(shell, s.historyInitSize) |
|
||||||
|
|
||||||
s.mutex.Lock() |
|
||||||
defer s.mutex.Unlock() |
|
||||||
// init sesshist and populate it with history loaded from file
|
|
||||||
s.sessions[sessionID] = &sesshist{ |
|
||||||
recentCmdLines: historyCmdLines, |
|
||||||
} |
|
||||||
log.Println("sesshist: session init done - " + sessionID) |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// DropSession struct
|
|
||||||
func (s *Dispatch) dropSession(sessionID string) error { |
|
||||||
s.mutex.RLock() |
|
||||||
_, found := s.sessions[sessionID] |
|
||||||
s.mutex.RUnlock() |
|
||||||
|
|
||||||
if found == false { |
|
||||||
return errors.New("sesshist ERROR: Can't DROP not existing session " + sessionID) |
|
||||||
} |
|
||||||
|
|
||||||
s.mutex.Lock() |
|
||||||
defer s.mutex.Unlock() |
|
||||||
delete(s.sessions, sessionID) |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// AddRecent record to session
|
|
||||||
func (s *Dispatch) addRecentRecord(sessionID string, record records.Record) error { |
|
||||||
log.Println("sesshist: Adding a record, RLocking main lock ...") |
|
||||||
s.mutex.RLock() |
|
||||||
log.Println("sesshist: Getting a session ...") |
|
||||||
session, found := s.sessions[sessionID] |
|
||||||
log.Println("sesshist: RUnlocking main lock ...") |
|
||||||
s.mutex.RUnlock() |
|
||||||
|
|
||||||
if found == false { |
|
||||||
log.Println("sesshist ERROR: addRecentRecord(): No session history for SessionID " + sessionID + " - creating session history.") |
|
||||||
s.initSession(sessionID, record.Shell) |
|
||||||
return s.addRecentRecord(sessionID, record) |
|
||||||
} |
|
||||||
log.Println("sesshist: RLocking session lock (w/ defer) ...") |
|
||||||
session.mutex.Lock() |
|
||||||
defer session.mutex.Unlock() |
|
||||||
session.recentRecords = append(session.recentRecords, record) |
|
||||||
session.recentCmdLines.AddCmdLine(record.CmdLine) |
|
||||||
log.Println("sesshist: record:", record.CmdLine, "; added to session:", sessionID, |
|
||||||
"; session len:", len(session.recentCmdLines.List), "; session len (records):", len(session.recentRecords)) |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// Recall command from recent session history
|
|
||||||
func (s *Dispatch) Recall(sessionID string, histno int, prefix string) (string, error) { |
|
||||||
log.Println("sesshist - recall: RLocking main lock ...") |
|
||||||
s.mutex.RLock() |
|
||||||
log.Println("sesshist - recall: Getting session history struct ...") |
|
||||||
session, found := s.sessions[sessionID] |
|
||||||
s.mutex.RUnlock() |
|
||||||
|
|
||||||
if found == false { |
|
||||||
// TODO: propagate actual shell here so we can use it
|
|
||||||
go s.initSession(sessionID, "bash") |
|
||||||
return "", errors.New("sesshist ERROR: No session history for SessionID " + sessionID + " - creating one ...") |
|
||||||
} |
|
||||||
log.Println("sesshist - recall: Locking session lock ...") |
|
||||||
session.mutex.Lock() |
|
||||||
defer session.mutex.Unlock() |
|
||||||
if prefix == "" { |
|
||||||
log.Println("sesshist - recall: Getting records by histno ...") |
|
||||||
return session.getRecordByHistno(histno) |
|
||||||
} |
|
||||||
log.Println("sesshist - recall: Searching for records by prefix ...") |
|
||||||
return session.searchRecordByPrefix(prefix, histno) |
|
||||||
} |
|
||||||
|
|
||||||
// Inspect commands in recent session history
|
|
||||||
func (s *Dispatch) Inspect(sessionID string, count int) ([]string, error) { |
|
||||||
prefix := "" |
|
||||||
log.Println("sesshist - inspect: RLocking main lock ...") |
|
||||||
s.mutex.RLock() |
|
||||||
log.Println("sesshist - inspect: Getting session history struct ...") |
|
||||||
session, found := s.sessions[sessionID] |
|
||||||
s.mutex.RUnlock() |
|
||||||
|
|
||||||
if found == false { |
|
||||||
// go s.initSession(sessionID)
|
|
||||||
return nil, errors.New("sesshist ERROR: No session history for SessionID " + sessionID + " - should we create one?") |
|
||||||
} |
|
||||||
log.Println("sesshist - inspect: Locking session lock ...") |
|
||||||
session.mutex.Lock() |
|
||||||
defer session.mutex.Unlock() |
|
||||||
if prefix == "" { |
|
||||||
log.Println("sesshist - inspect: Getting records by histno ...") |
|
||||||
idx := len(session.recentCmdLines.List) - count |
|
||||||
if idx < 0 { |
|
||||||
idx = 0 |
|
||||||
} |
|
||||||
return session.recentCmdLines.List[idx:], nil |
|
||||||
} |
|
||||||
log.Println("sesshist - inspect: Searching for records by prefix ... ERROR - Not implemented") |
|
||||||
return nil, errors.New("sesshist ERROR: Inspect - Searching for records by prefix Not implemented yet") |
|
||||||
} |
|
||||||
|
|
||||||
type sesshist struct { |
|
||||||
mutex sync.Mutex |
|
||||||
recentRecords []records.Record |
|
||||||
recentCmdLines histlist.Histlist |
|
||||||
} |
|
||||||
|
|
||||||
func (s *sesshist) getRecordByHistno(histno int) (string, error) { |
|
||||||
// addRecords() appends records to the end of the slice
|
|
||||||
// -> this func handles the indexing
|
|
||||||
if histno == 0 { |
|
||||||
return "", errors.New("sesshist ERROR: 'histno == 0' is not a record from history") |
|
||||||
} |
|
||||||
if histno < 0 { |
|
||||||
return "", errors.New("sesshist ERROR: 'histno < 0' is a command from future (not supperted yet)") |
|
||||||
} |
|
||||||
index := len(s.recentCmdLines.List) - histno |
|
||||||
if index < 0 { |
|
||||||
return "", errors.New("sesshist ERROR: 'histno > number of commands in the session' (" + strconv.Itoa(len(s.recentCmdLines.List)) + ")") |
|
||||||
} |
|
||||||
return s.recentCmdLines.List[index], nil |
|
||||||
} |
|
||||||
|
|
||||||
func (s *sesshist) searchRecordByPrefix(prefix string, histno int) (string, error) { |
|
||||||
if histno == 0 { |
|
||||||
return "", errors.New("sesshist ERROR: 'histno == 0' is not a record from history") |
|
||||||
} |
|
||||||
if histno < 0 { |
|
||||||
return "", errors.New("sesshist ERROR: 'histno < 0' is a command from future (not supperted yet)") |
|
||||||
} |
|
||||||
index := len(s.recentCmdLines.List) - histno |
|
||||||
if index < 0 { |
|
||||||
return "", errors.New("sesshist ERROR: 'histno > number of commands in the session' (" + strconv.Itoa(len(s.recentCmdLines.List)) + ")") |
|
||||||
} |
|
||||||
cmdLines := []string{} |
|
||||||
for i := len(s.recentCmdLines.List) - 1; i >= 0; i-- { |
|
||||||
if strings.HasPrefix(s.recentCmdLines.List[i], prefix) { |
|
||||||
cmdLines = append(cmdLines, s.recentCmdLines.List[i]) |
|
||||||
if len(cmdLines) >= histno { |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
if len(cmdLines) < histno { |
|
||||||
return "", errors.New("sesshist ERROR: 'histno > number of commands matching with given prefix' (" + strconv.Itoa(len(cmdLines)) + ")") |
|
||||||
} |
|
||||||
return cmdLines[histno-1], nil |
|
||||||
} |
|
||||||
@ -1,78 +0,0 @@ |
|||||||
package sesswatch |
|
||||||
|
|
||||||
import ( |
|
||||||
"log" |
|
||||||
"sync" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
"github.com/mitchellh/go-ps" |
|
||||||
) |
|
||||||
|
|
||||||
type sesswatch struct { |
|
||||||
sessionsToDrop []chan string |
|
||||||
sleepSeconds uint |
|
||||||
|
|
||||||
watchedSessions map[string]bool |
|
||||||
mutex sync.Mutex |
|
||||||
} |
|
||||||
|
|
||||||
// Go runs the session watcher - watches sessions and sends
|
|
||||||
func Go(sessionsToWatch chan records.Record, sessionsToWatchRecords chan records.Record, sessionsToDrop []chan string, sleepSeconds uint) { |
|
||||||
sw := sesswatch{sessionsToDrop: sessionsToDrop, sleepSeconds: sleepSeconds, watchedSessions: map[string]bool{}} |
|
||||||
go sw.waiter(sessionsToWatch, sessionsToWatchRecords) |
|
||||||
} |
|
||||||
|
|
||||||
func (s *sesswatch) waiter(sessionsToWatch chan records.Record, sessionsToWatchRecords chan records.Record) { |
|
||||||
for { |
|
||||||
func() { |
|
||||||
select { |
|
||||||
case record := <-sessionsToWatch: |
|
||||||
// normal way to start watching a session
|
|
||||||
id := record.SessionID |
|
||||||
pid := record.SessionPID |
|
||||||
s.mutex.Lock() |
|
||||||
defer s.mutex.Unlock() |
|
||||||
if s.watchedSessions[id] == false { |
|
||||||
log.Println("sesswatch: start watching NEW session ~ pid:", id, "~", pid) |
|
||||||
s.watchedSessions[id] = true |
|
||||||
go s.watcher(id, pid) |
|
||||||
} |
|
||||||
case record := <-sessionsToWatchRecords: |
|
||||||
// additional safety - watch sessions that were never properly initialized
|
|
||||||
id := record.SessionID |
|
||||||
pid := record.SessionPID |
|
||||||
s.mutex.Lock() |
|
||||||
defer s.mutex.Unlock() |
|
||||||
if s.watchedSessions[id] == false { |
|
||||||
log.Println("sesswatch WARN: start watching NEW session (based on /record) ~ pid:", id, "~", pid) |
|
||||||
s.watchedSessions[id] = true |
|
||||||
go s.watcher(id, pid) |
|
||||||
} |
|
||||||
} |
|
||||||
}() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (s *sesswatch) watcher(sessionID string, sessionPID int) { |
|
||||||
for { |
|
||||||
time.Sleep(time.Duration(s.sleepSeconds) * time.Second) |
|
||||||
proc, err := ps.FindProcess(sessionPID) |
|
||||||
if err != nil { |
|
||||||
log.Println("sesswatch ERROR: error while finding process:", sessionPID) |
|
||||||
} else if proc == nil { |
|
||||||
log.Println("sesswatch: Dropping session ~ pid:", sessionID, "~", sessionPID) |
|
||||||
func() { |
|
||||||
s.mutex.Lock() |
|
||||||
defer s.mutex.Unlock() |
|
||||||
s.watchedSessions[sessionID] = false |
|
||||||
}() |
|
||||||
for _, ch := range s.sessionsToDrop { |
|
||||||
log.Println("sesswatch: sending 'drop session' message ...") |
|
||||||
ch <- sessionID |
|
||||||
log.Println("sesswatch: sending 'drop session' message DONE") |
|
||||||
} |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,65 +0,0 @@ |
|||||||
package signalhandler |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"log" |
|
||||||
"net/http" |
|
||||||
"os" |
|
||||||
"os/signal" |
|
||||||
"strconv" |
|
||||||
"syscall" |
|
||||||
"time" |
|
||||||
) |
|
||||||
|
|
||||||
func sendSignals(sig os.Signal, subscribers []chan os.Signal, done chan string) { |
|
||||||
for _, sub := range subscribers { |
|
||||||
sub <- sig |
|
||||||
} |
|
||||||
chanCount := len(subscribers) |
|
||||||
start := time.Now() |
|
||||||
delay := time.Millisecond * 100 |
|
||||||
timeout := time.Millisecond * 2000 |
|
||||||
|
|
||||||
for { |
|
||||||
select { |
|
||||||
case _ = <-done: |
|
||||||
chanCount-- |
|
||||||
if chanCount == 0 { |
|
||||||
log.Println("signalhandler: All components shut down successfully") |
|
||||||
return |
|
||||||
} |
|
||||||
default: |
|
||||||
time.Sleep(delay) |
|
||||||
} |
|
||||||
if time.Since(start) > timeout { |
|
||||||
log.Println("signalhandler: Timouted while waiting for proper shutdown - " + strconv.Itoa(chanCount) + " boxes are up after " + timeout.String()) |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Run catches and handles signals
|
|
||||||
func Run(subscribers []chan os.Signal, done chan string, server *http.Server) { |
|
||||||
signals := make(chan os.Signal, 1) |
|
||||||
|
|
||||||
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) |
|
||||||
|
|
||||||
var sig os.Signal |
|
||||||
for { |
|
||||||
sig := <-signals |
|
||||||
log.Printf("signalhandler: Got signal '%s'\n", sig.String()) |
|
||||||
if sig == syscall.SIGTERM { |
|
||||||
// Shutdown daemon on SIGTERM
|
|
||||||
break |
|
||||||
} |
|
||||||
log.Printf("signalhandler: Ignoring signal '%s'. Send SIGTERM to trigger shutdown.\n", sig.String()) |
|
||||||
} |
|
||||||
|
|
||||||
log.Println("signalhandler: Sending shutdown signals to components") |
|
||||||
sendSignals(sig, subscribers, done) |
|
||||||
|
|
||||||
log.Println("signalhandler: Shutting down the server") |
|
||||||
if err := server.Shutdown(context.Background()); err != nil { |
|
||||||
log.Printf("HTTP server Shutdown: %v", err) |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,47 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import "github.com/curusarn/resh/pkg/records" |
|
||||||
|
|
||||||
// DirectorySensitive prediction/recommendation strategy
|
|
||||||
type DirectorySensitive struct { |
|
||||||
history map[string][]string |
|
||||||
lastPwd string |
|
||||||
} |
|
||||||
|
|
||||||
// Init see name
|
|
||||||
func (s *DirectorySensitive) Init() { |
|
||||||
s.history = map[string][]string{} |
|
||||||
} |
|
||||||
|
|
||||||
// GetTitleAndDescription see name
|
|
||||||
func (s *DirectorySensitive) GetTitleAndDescription() (string, string) { |
|
||||||
return "directory sensitive (recent)", "Use recent commands executed is the same directory" |
|
||||||
} |
|
||||||
|
|
||||||
// GetCandidates see name
|
|
||||||
func (s *DirectorySensitive) GetCandidates() []string { |
|
||||||
return s.history[s.lastPwd] |
|
||||||
} |
|
||||||
|
|
||||||
// AddHistoryRecord see name
|
|
||||||
func (s *DirectorySensitive) AddHistoryRecord(record *records.EnrichedRecord) error { |
|
||||||
// work on history for PWD
|
|
||||||
pwd := record.Pwd |
|
||||||
// remove previous occurance of record
|
|
||||||
for i, cmd := range s.history[pwd] { |
|
||||||
if cmd == record.CmdLine { |
|
||||||
s.history[pwd] = append(s.history[pwd][:i], s.history[pwd][i+1:]...) |
|
||||||
} |
|
||||||
} |
|
||||||
// append new record
|
|
||||||
s.history[pwd] = append([]string{record.CmdLine}, s.history[pwd]...) |
|
||||||
s.lastPwd = record.PwdAfter |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ResetHistory see name
|
|
||||||
func (s *DirectorySensitive) ResetHistory() error { |
|
||||||
s.Init() |
|
||||||
s.history = map[string][]string{} |
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,29 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import "github.com/curusarn/resh/pkg/records" |
|
||||||
|
|
||||||
// Dummy prediction/recommendation strategy
|
|
||||||
type Dummy struct { |
|
||||||
history []string |
|
||||||
} |
|
||||||
|
|
||||||
// GetTitleAndDescription see name
|
|
||||||
func (s *Dummy) GetTitleAndDescription() (string, string) { |
|
||||||
return "dummy", "Return empty candidate list" |
|
||||||
} |
|
||||||
|
|
||||||
// GetCandidates see name
|
|
||||||
func (s *Dummy) GetCandidates() []string { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// AddHistoryRecord see name
|
|
||||||
func (s *Dummy) AddHistoryRecord(record *records.EnrichedRecord) error { |
|
||||||
s.history = append(s.history, record.CmdLine) |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ResetHistory see name
|
|
||||||
func (s *Dummy) ResetHistory() error { |
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,91 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import ( |
|
||||||
"math" |
|
||||||
"sort" |
|
||||||
"strconv" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
) |
|
||||||
|
|
||||||
// DynamicRecordDistance prediction/recommendation strategy
|
|
||||||
type DynamicRecordDistance struct { |
|
||||||
history []records.EnrichedRecord |
|
||||||
DistParams records.DistParams |
|
||||||
pwdHistogram map[string]int |
|
||||||
realPwdHistogram map[string]int |
|
||||||
gitOriginHistogram map[string]int |
|
||||||
MaxDepth int |
|
||||||
Label string |
|
||||||
} |
|
||||||
|
|
||||||
type strDynDistEntry struct { |
|
||||||
cmdLine string |
|
||||||
distance float64 |
|
||||||
} |
|
||||||
|
|
||||||
// Init see name
|
|
||||||
func (s *DynamicRecordDistance) Init() { |
|
||||||
s.history = nil |
|
||||||
s.pwdHistogram = map[string]int{} |
|
||||||
s.realPwdHistogram = map[string]int{} |
|
||||||
s.gitOriginHistogram = map[string]int{} |
|
||||||
} |
|
||||||
|
|
||||||
// GetTitleAndDescription see name
|
|
||||||
func (s *DynamicRecordDistance) GetTitleAndDescription() (string, string) { |
|
||||||
return "dynamic record distance (depth:" + strconv.Itoa(s.MaxDepth) + ";" + s.Label + ")", "Use TF-IDF record distance to recommend commands" |
|
||||||
} |
|
||||||
|
|
||||||
func (s *DynamicRecordDistance) idf(count int) float64 { |
|
||||||
return math.Log(float64(len(s.history)) / float64(count)) |
|
||||||
} |
|
||||||
|
|
||||||
// GetCandidates see name
|
|
||||||
func (s *DynamicRecordDistance) GetCandidates(strippedRecord records.EnrichedRecord) []string { |
|
||||||
if len(s.history) == 0 { |
|
||||||
return nil |
|
||||||
} |
|
||||||
var mapItems []strDynDistEntry |
|
||||||
for i, record := range s.history { |
|
||||||
if s.MaxDepth != 0 && i > s.MaxDepth { |
|
||||||
break |
|
||||||
} |
|
||||||
distParams := records.DistParams{ |
|
||||||
Pwd: s.DistParams.Pwd * s.idf(s.pwdHistogram[strippedRecord.PwdAfter]), |
|
||||||
RealPwd: s.DistParams.RealPwd * s.idf(s.realPwdHistogram[strippedRecord.RealPwdAfter]), |
|
||||||
Git: s.DistParams.Git * s.idf(s.gitOriginHistogram[strippedRecord.GitOriginRemote]), |
|
||||||
Time: s.DistParams.Time, |
|
||||||
SessionID: s.DistParams.SessionID, |
|
||||||
} |
|
||||||
distance := record.DistanceTo(strippedRecord, distParams) |
|
||||||
mapItems = append(mapItems, strDynDistEntry{record.CmdLine, distance}) |
|
||||||
} |
|
||||||
sort.SliceStable(mapItems, func(i int, j int) bool { return mapItems[i].distance < mapItems[j].distance }) |
|
||||||
var hist []string |
|
||||||
histSet := map[string]bool{} |
|
||||||
for _, item := range mapItems { |
|
||||||
if histSet[item.cmdLine] { |
|
||||||
continue |
|
||||||
} |
|
||||||
histSet[item.cmdLine] = true |
|
||||||
hist = append(hist, item.cmdLine) |
|
||||||
} |
|
||||||
return hist |
|
||||||
} |
|
||||||
|
|
||||||
// AddHistoryRecord see name
|
|
||||||
func (s *DynamicRecordDistance) AddHistoryRecord(record *records.EnrichedRecord) error { |
|
||||||
// append record to front
|
|
||||||
s.history = append([]records.EnrichedRecord{*record}, s.history...) |
|
||||||
s.pwdHistogram[record.Pwd]++ |
|
||||||
s.realPwdHistogram[record.RealPwd]++ |
|
||||||
s.gitOriginHistogram[record.GitOriginRemote]++ |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ResetHistory see name
|
|
||||||
func (s *DynamicRecordDistance) ResetHistory() error { |
|
||||||
s.Init() |
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,53 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import ( |
|
||||||
"sort" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
) |
|
||||||
|
|
||||||
// Frequent prediction/recommendation strategy
|
|
||||||
type Frequent struct { |
|
||||||
history map[string]int |
|
||||||
} |
|
||||||
|
|
||||||
type strFrqEntry struct { |
|
||||||
cmdLine string |
|
||||||
count int |
|
||||||
} |
|
||||||
|
|
||||||
// Init see name
|
|
||||||
func (s *Frequent) Init() { |
|
||||||
s.history = map[string]int{} |
|
||||||
} |
|
||||||
|
|
||||||
// GetTitleAndDescription see name
|
|
||||||
func (s *Frequent) GetTitleAndDescription() (string, string) { |
|
||||||
return "frequent", "Use frequent commands" |
|
||||||
} |
|
||||||
|
|
||||||
// GetCandidates see name
|
|
||||||
func (s *Frequent) GetCandidates() []string { |
|
||||||
var mapItems []strFrqEntry |
|
||||||
for cmdLine, count := range s.history { |
|
||||||
mapItems = append(mapItems, strFrqEntry{cmdLine, count}) |
|
||||||
} |
|
||||||
sort.Slice(mapItems, func(i int, j int) bool { return mapItems[i].count > mapItems[j].count }) |
|
||||||
var hist []string |
|
||||||
for _, item := range mapItems { |
|
||||||
hist = append(hist, item.cmdLine) |
|
||||||
} |
|
||||||
return hist |
|
||||||
} |
|
||||||
|
|
||||||
// AddHistoryRecord see name
|
|
||||||
func (s *Frequent) AddHistoryRecord(record *records.EnrichedRecord) error { |
|
||||||
s.history[record.CmdLine]++ |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ResetHistory see name
|
|
||||||
func (s *Frequent) ResetHistory() error { |
|
||||||
s.Init() |
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,97 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import ( |
|
||||||
"sort" |
|
||||||
"strconv" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
"github.com/mb-14/gomarkov" |
|
||||||
) |
|
||||||
|
|
||||||
// MarkovChainCmd prediction/recommendation strategy
|
|
||||||
type MarkovChainCmd struct { |
|
||||||
Order int |
|
||||||
history []strMarkCmdHistoryEntry |
|
||||||
historyCmds []string |
|
||||||
} |
|
||||||
|
|
||||||
type strMarkCmdHistoryEntry struct { |
|
||||||
cmd string |
|
||||||
cmdLine string |
|
||||||
} |
|
||||||
|
|
||||||
type strMarkCmdEntry struct { |
|
||||||
cmd string |
|
||||||
transProb float64 |
|
||||||
} |
|
||||||
|
|
||||||
// Init see name
|
|
||||||
func (s *MarkovChainCmd) Init() { |
|
||||||
s.history = nil |
|
||||||
s.historyCmds = nil |
|
||||||
} |
|
||||||
|
|
||||||
// GetTitleAndDescription see name
|
|
||||||
func (s *MarkovChainCmd) GetTitleAndDescription() (string, string) { |
|
||||||
return "command-based markov chain (order " + strconv.Itoa(s.Order) + ")", "Use command-based markov chain to recommend commands" |
|
||||||
} |
|
||||||
|
|
||||||
// GetCandidates see name
|
|
||||||
func (s *MarkovChainCmd) GetCandidates() []string { |
|
||||||
if len(s.history) < s.Order { |
|
||||||
var hist []string |
|
||||||
for _, item := range s.history { |
|
||||||
hist = append(hist, item.cmdLine) |
|
||||||
} |
|
||||||
return hist |
|
||||||
} |
|
||||||
chain := gomarkov.NewChain(s.Order) |
|
||||||
|
|
||||||
chain.Add(s.historyCmds) |
|
||||||
|
|
||||||
cmdsSet := map[string]bool{} |
|
||||||
var entries []strMarkCmdEntry |
|
||||||
for _, cmd := range s.historyCmds { |
|
||||||
if cmdsSet[cmd] { |
|
||||||
continue |
|
||||||
} |
|
||||||
cmdsSet[cmd] = true |
|
||||||
prob, _ := chain.TransitionProbability(cmd, s.historyCmds[len(s.historyCmds)-s.Order:]) |
|
||||||
entries = append(entries, strMarkCmdEntry{cmd: cmd, transProb: prob}) |
|
||||||
} |
|
||||||
sort.Slice(entries, func(i int, j int) bool { return entries[i].transProb > entries[j].transProb }) |
|
||||||
var hist []string |
|
||||||
histSet := map[string]bool{} |
|
||||||
for i := len(s.history) - 1; i >= 0; i-- { |
|
||||||
if histSet[s.history[i].cmdLine] { |
|
||||||
continue |
|
||||||
} |
|
||||||
histSet[s.history[i].cmdLine] = true |
|
||||||
if s.history[i].cmd == entries[0].cmd { |
|
||||||
hist = append(hist, s.history[i].cmdLine) |
|
||||||
} |
|
||||||
} |
|
||||||
// log.Println("################")
|
|
||||||
// log.Println(s.history[len(s.history)-s.order:])
|
|
||||||
// log.Println(" -> ")
|
|
||||||
// x := math.Min(float64(len(hist)), 3)
|
|
||||||
// log.Println(entries[:int(x)])
|
|
||||||
// x = math.Min(float64(len(hist)), 5)
|
|
||||||
// log.Println(hist[:int(x)])
|
|
||||||
// log.Println("################")
|
|
||||||
return hist |
|
||||||
} |
|
||||||
|
|
||||||
// AddHistoryRecord see name
|
|
||||||
func (s *MarkovChainCmd) AddHistoryRecord(record *records.EnrichedRecord) error { |
|
||||||
s.history = append(s.history, strMarkCmdHistoryEntry{cmdLine: record.CmdLine, cmd: record.Command}) |
|
||||||
s.historyCmds = append(s.historyCmds, record.Command) |
|
||||||
// s.historySet[record.CmdLine] = true
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ResetHistory see name
|
|
||||||
func (s *MarkovChainCmd) ResetHistory() error { |
|
||||||
s.Init() |
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,76 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import ( |
|
||||||
"sort" |
|
||||||
"strconv" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
"github.com/mb-14/gomarkov" |
|
||||||
) |
|
||||||
|
|
||||||
// MarkovChain prediction/recommendation strategy
|
|
||||||
type MarkovChain struct { |
|
||||||
Order int |
|
||||||
history []string |
|
||||||
} |
|
||||||
|
|
||||||
type strMarkEntry struct { |
|
||||||
cmdLine string |
|
||||||
transProb float64 |
|
||||||
} |
|
||||||
|
|
||||||
// Init see name
|
|
||||||
func (s *MarkovChain) Init() { |
|
||||||
s.history = nil |
|
||||||
} |
|
||||||
|
|
||||||
// GetTitleAndDescription see name
|
|
||||||
func (s *MarkovChain) GetTitleAndDescription() (string, string) { |
|
||||||
return "markov chain (order " + strconv.Itoa(s.Order) + ")", "Use markov chain to recommend commands" |
|
||||||
} |
|
||||||
|
|
||||||
// GetCandidates see name
|
|
||||||
func (s *MarkovChain) GetCandidates() []string { |
|
||||||
if len(s.history) < s.Order { |
|
||||||
return s.history |
|
||||||
} |
|
||||||
chain := gomarkov.NewChain(s.Order) |
|
||||||
|
|
||||||
chain.Add(s.history) |
|
||||||
|
|
||||||
cmdLinesSet := map[string]bool{} |
|
||||||
var entries []strMarkEntry |
|
||||||
for _, cmdLine := range s.history { |
|
||||||
if cmdLinesSet[cmdLine] { |
|
||||||
continue |
|
||||||
} |
|
||||||
cmdLinesSet[cmdLine] = true |
|
||||||
prob, _ := chain.TransitionProbability(cmdLine, s.history[len(s.history)-s.Order:]) |
|
||||||
entries = append(entries, strMarkEntry{cmdLine: cmdLine, transProb: prob}) |
|
||||||
} |
|
||||||
sort.Slice(entries, func(i int, j int) bool { return entries[i].transProb > entries[j].transProb }) |
|
||||||
var hist []string |
|
||||||
for _, item := range entries { |
|
||||||
hist = append(hist, item.cmdLine) |
|
||||||
} |
|
||||||
// log.Println("################")
|
|
||||||
// log.Println(s.history[len(s.history)-s.order:])
|
|
||||||
// log.Println(" -> ")
|
|
||||||
// x := math.Min(float64(len(hist)), 5)
|
|
||||||
// log.Println(hist[:int(x)])
|
|
||||||
// log.Println("################")
|
|
||||||
return hist |
|
||||||
} |
|
||||||
|
|
||||||
// AddHistoryRecord see name
|
|
||||||
func (s *MarkovChain) AddHistoryRecord(record *records.EnrichedRecord) error { |
|
||||||
s.history = append(s.history, record.CmdLine) |
|
||||||
// s.historySet[record.CmdLine] = true
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ResetHistory see name
|
|
||||||
func (s *MarkovChain) ResetHistory() error { |
|
||||||
s.Init() |
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,57 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import ( |
|
||||||
"math/rand" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
) |
|
||||||
|
|
||||||
// Random prediction/recommendation strategy
|
|
||||||
type Random struct { |
|
||||||
CandidatesSize int |
|
||||||
history []string |
|
||||||
historySet map[string]bool |
|
||||||
} |
|
||||||
|
|
||||||
// Init see name
|
|
||||||
func (s *Random) Init() { |
|
||||||
s.history = nil |
|
||||||
s.historySet = map[string]bool{} |
|
||||||
} |
|
||||||
|
|
||||||
// GetTitleAndDescription see name
|
|
||||||
func (s *Random) GetTitleAndDescription() (string, string) { |
|
||||||
return "random", "Use random commands" |
|
||||||
} |
|
||||||
|
|
||||||
// GetCandidates see name
|
|
||||||
func (s *Random) GetCandidates() []string { |
|
||||||
seed := time.Now().UnixNano() |
|
||||||
rand.Seed(seed) |
|
||||||
var candidates []string |
|
||||||
candidateSet := map[string]bool{} |
|
||||||
for len(candidates) < s.CandidatesSize && len(candidates)*2 < len(s.historySet) { |
|
||||||
x := rand.Intn(len(s.history)) |
|
||||||
candidate := s.history[x] |
|
||||||
if candidateSet[candidate] == false { |
|
||||||
candidateSet[candidate] = true |
|
||||||
candidates = append(candidates, candidate) |
|
||||||
continue |
|
||||||
} |
|
||||||
} |
|
||||||
return candidates |
|
||||||
} |
|
||||||
|
|
||||||
// AddHistoryRecord see name
|
|
||||||
func (s *Random) AddHistoryRecord(record *records.EnrichedRecord) error { |
|
||||||
s.history = append([]string{record.CmdLine}, s.history...) |
|
||||||
s.historySet[record.CmdLine] = true |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ResetHistory see name
|
|
||||||
func (s *Random) ResetHistory() error { |
|
||||||
s.Init() |
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,56 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import "github.com/curusarn/resh/pkg/records" |
|
||||||
|
|
||||||
// RecentBash prediction/recommendation strategy
|
|
||||||
type RecentBash struct { |
|
||||||
histfile []string |
|
||||||
histfileSnapshot map[string][]string |
|
||||||
history map[string][]string |
|
||||||
} |
|
||||||
|
|
||||||
// Init see name
|
|
||||||
func (s *RecentBash) Init() { |
|
||||||
s.histfileSnapshot = map[string][]string{} |
|
||||||
s.history = map[string][]string{} |
|
||||||
} |
|
||||||
|
|
||||||
// GetTitleAndDescription see name
|
|
||||||
func (s *RecentBash) GetTitleAndDescription() (string, string) { |
|
||||||
return "recent (bash-like)", "Behave like bash" |
|
||||||
} |
|
||||||
|
|
||||||
// GetCandidates see name
|
|
||||||
func (s *RecentBash) GetCandidates(strippedRecord records.EnrichedRecord) []string { |
|
||||||
// populate the local history from histfile
|
|
||||||
if s.histfileSnapshot[strippedRecord.SessionID] == nil { |
|
||||||
s.histfileSnapshot[strippedRecord.SessionID] = s.histfile |
|
||||||
} |
|
||||||
return append(s.history[strippedRecord.SessionID], s.histfileSnapshot[strippedRecord.SessionID]...) |
|
||||||
} |
|
||||||
|
|
||||||
// AddHistoryRecord see name
|
|
||||||
func (s *RecentBash) AddHistoryRecord(record *records.EnrichedRecord) error { |
|
||||||
// remove previous occurance of record
|
|
||||||
for i, cmd := range s.history[record.SessionID] { |
|
||||||
if cmd == record.CmdLine { |
|
||||||
s.history[record.SessionID] = append(s.history[record.SessionID][:i], s.history[record.SessionID][i+1:]...) |
|
||||||
} |
|
||||||
} |
|
||||||
// append new record
|
|
||||||
s.history[record.SessionID] = append([]string{record.CmdLine}, s.history[record.SessionID]...) |
|
||||||
|
|
||||||
if record.LastRecordOfSession { |
|
||||||
// append history of the session to histfile and clear session history
|
|
||||||
s.histfile = append(s.history[record.SessionID], s.histfile...) |
|
||||||
s.histfileSnapshot[record.SessionID] = nil |
|
||||||
s.history[record.SessionID] = nil |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ResetHistory see name
|
|
||||||
func (s *RecentBash) ResetHistory() error { |
|
||||||
s.Init() |
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,37 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import "github.com/curusarn/resh/pkg/records" |
|
||||||
|
|
||||||
// Recent prediction/recommendation strategy
|
|
||||||
type Recent struct { |
|
||||||
history []string |
|
||||||
} |
|
||||||
|
|
||||||
// GetTitleAndDescription see name
|
|
||||||
func (s *Recent) GetTitleAndDescription() (string, string) { |
|
||||||
return "recent", "Use recent commands" |
|
||||||
} |
|
||||||
|
|
||||||
// GetCandidates see name
|
|
||||||
func (s *Recent) GetCandidates() []string { |
|
||||||
return s.history |
|
||||||
} |
|
||||||
|
|
||||||
// AddHistoryRecord see name
|
|
||||||
func (s *Recent) AddHistoryRecord(record *records.EnrichedRecord) error { |
|
||||||
// remove previous occurance of record
|
|
||||||
for i, cmd := range s.history { |
|
||||||
if cmd == record.CmdLine { |
|
||||||
s.history = append(s.history[:i], s.history[i+1:]...) |
|
||||||
} |
|
||||||
} |
|
||||||
// append new record
|
|
||||||
s.history = append([]string{record.CmdLine}, s.history...) |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ResetHistory see name
|
|
||||||
func (s *Recent) ResetHistory() error { |
|
||||||
s.history = nil |
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,70 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import ( |
|
||||||
"sort" |
|
||||||
"strconv" |
|
||||||
|
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
) |
|
||||||
|
|
||||||
// RecordDistance prediction/recommendation strategy
|
|
||||||
type RecordDistance struct { |
|
||||||
history []records.EnrichedRecord |
|
||||||
DistParams records.DistParams |
|
||||||
MaxDepth int |
|
||||||
Label string |
|
||||||
} |
|
||||||
|
|
||||||
type strDistEntry struct { |
|
||||||
cmdLine string |
|
||||||
distance float64 |
|
||||||
} |
|
||||||
|
|
||||||
// Init see name
|
|
||||||
func (s *RecordDistance) Init() { |
|
||||||
s.history = nil |
|
||||||
} |
|
||||||
|
|
||||||
// GetTitleAndDescription see name
|
|
||||||
func (s *RecordDistance) GetTitleAndDescription() (string, string) { |
|
||||||
return "record distance (depth:" + strconv.Itoa(s.MaxDepth) + ";" + s.Label + ")", "Use record distance to recommend commands" |
|
||||||
} |
|
||||||
|
|
||||||
// GetCandidates see name
|
|
||||||
func (s *RecordDistance) GetCandidates(strippedRecord records.EnrichedRecord) []string { |
|
||||||
if len(s.history) == 0 { |
|
||||||
return nil |
|
||||||
} |
|
||||||
var mapItems []strDistEntry |
|
||||||
for i, record := range s.history { |
|
||||||
if s.MaxDepth != 0 && i > s.MaxDepth { |
|
||||||
break |
|
||||||
} |
|
||||||
distance := record.DistanceTo(strippedRecord, s.DistParams) |
|
||||||
mapItems = append(mapItems, strDistEntry{record.CmdLine, distance}) |
|
||||||
} |
|
||||||
sort.SliceStable(mapItems, func(i int, j int) bool { return mapItems[i].distance < mapItems[j].distance }) |
|
||||||
var hist []string |
|
||||||
histSet := map[string]bool{} |
|
||||||
for _, item := range mapItems { |
|
||||||
if histSet[item.cmdLine] { |
|
||||||
continue |
|
||||||
} |
|
||||||
histSet[item.cmdLine] = true |
|
||||||
hist = append(hist, item.cmdLine) |
|
||||||
} |
|
||||||
return hist |
|
||||||
} |
|
||||||
|
|
||||||
// AddHistoryRecord see name
|
|
||||||
func (s *RecordDistance) AddHistoryRecord(record *records.EnrichedRecord) error { |
|
||||||
// append record to front
|
|
||||||
s.history = append([]records.EnrichedRecord{*record}, s.history...) |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// ResetHistory see name
|
|
||||||
func (s *RecordDistance) ResetHistory() error { |
|
||||||
s.Init() |
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,46 +0,0 @@ |
|||||||
package strat |
|
||||||
|
|
||||||
import ( |
|
||||||
"github.com/curusarn/resh/pkg/records" |
|
||||||
) |
|
||||||
|
|
||||||
// ISimpleStrategy interface
|
|
||||||
type ISimpleStrategy interface { |
|
||||||
GetTitleAndDescription() (string, string) |
|
||||||
GetCandidates() []string |
|
||||||
AddHistoryRecord(record *records.EnrichedRecord) error |
|
||||||
ResetHistory() error |
|
||||||
} |
|
||||||
|
|
||||||
// IStrategy interface
|
|
||||||
type IStrategy interface { |
|
||||||
GetTitleAndDescription() (string, string) |
|
||||||
GetCandidates(r records.EnrichedRecord) []string |
|
||||||
AddHistoryRecord(record *records.EnrichedRecord) error |
|
||||||
ResetHistory() error |
|
||||||
} |
|
||||||
|
|
||||||
type simpleStrategyWrapper struct { |
|
||||||
strategy ISimpleStrategy |
|
||||||
} |
|
||||||
|
|
||||||
// NewSimpleStrategyWrapper returns IStrategy created by wrapping given ISimpleStrategy
|
|
||||||
func NewSimpleStrategyWrapper(strategy ISimpleStrategy) *simpleStrategyWrapper { |
|
||||||
return &simpleStrategyWrapper{strategy: strategy} |
|
||||||
} |
|
||||||
|
|
||||||
func (s *simpleStrategyWrapper) GetTitleAndDescription() (string, string) { |
|
||||||
return s.strategy.GetTitleAndDescription() |
|
||||||
} |
|
||||||
|
|
||||||
func (s *simpleStrategyWrapper) GetCandidates(r records.EnrichedRecord) []string { |
|
||||||
return s.strategy.GetCandidates() |
|
||||||
} |
|
||||||
|
|
||||||
func (s *simpleStrategyWrapper) AddHistoryRecord(r *records.EnrichedRecord) error { |
|
||||||
return s.strategy.AddHistoryRecord(r) |
|
||||||
} |
|
||||||
|
|
||||||
func (s *simpleStrategyWrapper) ResetHistory() error { |
|
||||||
return s.strategy.ResetHistory() |
|
||||||
} |
|
||||||
@ -0,0 +1,88 @@ |
|||||||
|
package record |
||||||
|
|
||||||
|
type Legacy struct { |
||||||
|
// core
|
||||||
|
CmdLine string `json:"cmdLine"` |
||||||
|
ExitCode int `json:"exitCode"` |
||||||
|
Shell string `json:"shell"` |
||||||
|
Uname string `json:"uname"` |
||||||
|
SessionID string `json:"sessionId"` |
||||||
|
RecordID string `json:"recordId"` |
||||||
|
|
||||||
|
// posix
|
||||||
|
Home string `json:"home"` |
||||||
|
Lang string `json:"lang"` |
||||||
|
LcAll string `json:"lcAll"` |
||||||
|
Login string `json:"login"` |
||||||
|
Pwd string `json:"pwd"` |
||||||
|
PwdAfter string `json:"pwdAfter"` |
||||||
|
ShellEnv string `json:"shellEnv"` |
||||||
|
Term string `json:"term"` |
||||||
|
|
||||||
|
// non-posix"`
|
||||||
|
RealPwd string `json:"realPwd"` |
||||||
|
RealPwdAfter string `json:"realPwdAfter"` |
||||||
|
Pid int `json:"pid"` |
||||||
|
SessionPID int `json:"sessionPid"` |
||||||
|
Host string `json:"host"` |
||||||
|
Hosttype string `json:"hosttype"` |
||||||
|
Ostype string `json:"ostype"` |
||||||
|
Machtype string `json:"machtype"` |
||||||
|
Shlvl int `json:"shlvl"` |
||||||
|
|
||||||
|
// before after
|
||||||
|
TimezoneBefore string `json:"timezoneBefore"` |
||||||
|
TimezoneAfter string `json:"timezoneAfter"` |
||||||
|
|
||||||
|
RealtimeBefore float64 `json:"realtimeBefore"` |
||||||
|
RealtimeAfter float64 `json:"realtimeAfter"` |
||||||
|
RealtimeBeforeLocal float64 `json:"realtimeBeforeLocal"` |
||||||
|
RealtimeAfterLocal float64 `json:"realtimeAfterLocal"` |
||||||
|
|
||||||
|
RealtimeDuration float64 `json:"realtimeDuration"` |
||||||
|
RealtimeSinceSessionStart float64 `json:"realtimeSinceSessionStart"` |
||||||
|
RealtimeSinceBoot float64 `json:"realtimeSinceBoot"` |
||||||
|
|
||||||
|
GitDir string `json:"gitDir"` |
||||||
|
GitRealDir string `json:"gitRealDir"` |
||||||
|
GitOriginRemote string `json:"gitOriginRemote"` |
||||||
|
GitDirAfter string `json:"gitDirAfter"` |
||||||
|
GitRealDirAfter string `json:"gitRealDirAfter"` |
||||||
|
GitOriginRemoteAfter string `json:"gitOriginRemoteAfter"` |
||||||
|
MachineID string `json:"machineId"` |
||||||
|
|
||||||
|
OsReleaseID string `json:"osReleaseId"` |
||||||
|
OsReleaseVersionID string `json:"osReleaseVersionId"` |
||||||
|
OsReleaseIDLike string `json:"osReleaseIdLike"` |
||||||
|
OsReleaseName string `json:"osReleaseName"` |
||||||
|
OsReleasePrettyName string `json:"osReleasePrettyName"` |
||||||
|
|
||||||
|
ReshUUID string `json:"reshUuid"` |
||||||
|
ReshVersion string `json:"reshVersion"` |
||||||
|
ReshRevision string `json:"reshRevision"` |
||||||
|
|
||||||
|
// records come in two parts (collect and postcollect)
|
||||||
|
PartOne bool `json:"partOne,omitempty"` // false => part two
|
||||||
|
PartsMerged bool `json:"partsMerged"` |
||||||
|
// special flag -> not an actual record but an session end
|
||||||
|
SessionExit bool `json:"sessionExit,omitempty"` |
||||||
|
|
||||||
|
// recall metadata
|
||||||
|
Recalled bool `json:"recalled"` |
||||||
|
RecallHistno int `json:"recallHistno,omitempty"` |
||||||
|
RecallStrategy string `json:"recallStrategy,omitempty"` |
||||||
|
RecallActionsRaw string `json:"recallActionsRaw,omitempty"` |
||||||
|
RecallActions []string `json:"recallActions,omitempty"` |
||||||
|
RecallLastCmdLine string `json:"recallLastCmdLine"` |
||||||
|
|
||||||
|
// recall command
|
||||||
|
RecallPrefix string `json:"recallPrefix,omitempty"` |
||||||
|
|
||||||
|
// added by sanitizatizer
|
||||||
|
Sanitized bool `json:"sanitized,omitempty"` |
||||||
|
CmdLength int `json:"cmdLength,omitempty"` |
||||||
|
|
||||||
|
// fields that are string here and int in older resh verisons
|
||||||
|
Cols interface{} `json:"cols"` |
||||||
|
Lines interface{} `json:"lines"` |
||||||
|
} |
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue