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