mirror of https://github.com/curusarn/resh
commit
ebfc4565d6
@ -1,255 +1,91 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"flag" |
||||
"fmt" |
||||
"log" |
||||
"os" |
||||
|
||||
"github.com/BurntSushi/toml" |
||||
"github.com/curusarn/resh/pkg/cfg" |
||||
"github.com/curusarn/resh/pkg/collect" |
||||
"github.com/curusarn/resh/pkg/records" |
||||
"github.com/curusarn/resh/internal/cfg" |
||||
"github.com/curusarn/resh/internal/collect" |
||||
"github.com/curusarn/resh/internal/logger" |
||||
"github.com/curusarn/resh/internal/opt" |
||||
"github.com/curusarn/resh/internal/output" |
||||
"github.com/curusarn/resh/internal/recordint" |
||||
"github.com/curusarn/resh/record" |
||||
"github.com/spf13/pflag" |
||||
"go.uber.org/zap" |
||||
|
||||
// "os/exec"
|
||||
"os/user" |
||||
"path/filepath" |
||||
"strconv" |
||||
) |
||||
|
||||
// version tag from git set during build
|
||||
// info passed during build
|
||||
var version string |
||||
|
||||
// Commit hash from git set during build
|
||||
var commit string |
||||
var development string |
||||
|
||||
func main() { |
||||
usr, _ := user.Current() |
||||
dir := usr.HomeDir |
||||
configPath := filepath.Join(dir, "/.config/resh.toml") |
||||
reshUUIDPath := filepath.Join(dir, "/.resh/resh-uuid") |
||||
|
||||
machineIDPath := "/etc/machine-id" |
||||
|
||||
var config cfg.Config |
||||
if _, err := toml.DecodeFile(configPath, &config); err != nil { |
||||
log.Fatal("Error reading config:", err) |
||||
} |
||||
// recall command
|
||||
recall := flag.Bool("recall", false, "Recall command on position --histno") |
||||
recallHistno := flag.Int("histno", 0, "Recall command on position --histno") |
||||
recallPrefix := flag.String("prefix-search", "", "Recall command based on prefix --prefix-search") |
||||
|
||||
// version
|
||||
showVersion := flag.Bool("version", false, "Show version and exit") |
||||
showRevision := flag.Bool("revision", false, "Show git revision and exit") |
||||
|
||||
requireVersion := flag.String("requireVersion", "", "abort if version doesn't match") |
||||
requireRevision := flag.String("requireRevision", "", "abort if revision doesn't match") |
||||
|
||||
// core
|
||||
cmdLine := flag.String("cmdLine", "", "command line") |
||||
exitCode := flag.Int("exitCode", -1, "exit code") |
||||
shell := flag.String("shell", "", "actual shell") |
||||
uname := flag.String("uname", "", "uname") |
||||
sessionID := flag.String("sessionId", "", "resh generated session id") |
||||
recordID := flag.String("recordId", "", "resh generated record id") |
||||
|
||||
// recall metadata
|
||||
recallActions := flag.String("recall-actions", "", "recall actions that took place before executing the command") |
||||
recallStrategy := flag.String("recall-strategy", "", "recall strategy used during recall actions") |
||||
recallLastCmdLine := flag.String("recall-last-cmdline", "", "last recalled cmdline") |
||||
|
||||
// posix variables
|
||||
cols := flag.String("cols", "-1", "$COLUMNS") |
||||
lines := flag.String("lines", "-1", "$LINES") |
||||
home := flag.String("home", "", "$HOME") |
||||
lang := flag.String("lang", "", "$LANG") |
||||
lcAll := flag.String("lcAll", "", "$LC_ALL") |
||||
login := flag.String("login", "", "$LOGIN") |
||||
// path := flag.String("path", "", "$PATH")
|
||||
pwd := flag.String("pwd", "", "$PWD - present working directory") |
||||
shellEnv := flag.String("shellEnv", "", "$SHELL") |
||||
term := flag.String("term", "", "$TERM") |
||||
|
||||
// non-posix
|
||||
pid := flag.Int("pid", -1, "$$") |
||||
sessionPid := flag.Int("sessionPid", -1, "$$ at session start") |
||||
shlvl := flag.Int("shlvl", -1, "$SHLVL") |
||||
|
||||
host := flag.String("host", "", "$HOSTNAME") |
||||
hosttype := flag.String("hosttype", "", "$HOSTTYPE") |
||||
ostype := flag.String("ostype", "", "$OSTYPE") |
||||
machtype := flag.String("machtype", "", "$MACHTYPE") |
||||
gitCdup := flag.String("gitCdup", "", "git rev-parse --show-cdup") |
||||
gitRemote := flag.String("gitRemote", "", "git remote get-url origin") |
||||
|
||||
gitCdupExitCode := flag.Int("gitCdupExitCode", -1, "... $?") |
||||
gitRemoteExitCode := flag.Int("gitRemoteExitCode", -1, "... $?") |
||||
|
||||
// before after
|
||||
timezoneBefore := flag.String("timezoneBefore", "", "") |
||||
|
||||
osReleaseID := flag.String("osReleaseId", "", "/etc/os-release ID") |
||||
osReleaseVersionID := flag.String("osReleaseVersionId", "", |
||||
"/etc/os-release ID") |
||||
osReleaseIDLike := flag.String("osReleaseIdLike", "", "/etc/os-release ID") |
||||
osReleaseName := flag.String("osReleaseName", "", "/etc/os-release ID") |
||||
osReleasePrettyName := flag.String("osReleasePrettyName", "", |
||||
"/etc/os-release ID") |
||||
|
||||
rtb := flag.String("realtimeBefore", "-1", "before $EPOCHREALTIME") |
||||
rtsess := flag.String("realtimeSession", "-1", |
||||
"on session start $EPOCHREALTIME") |
||||
rtsessboot := flag.String("realtimeSessSinceBoot", "-1", |
||||
"on session start $EPOCHREALTIME") |
||||
flag.Parse() |
||||
|
||||
if *showVersion == true { |
||||
fmt.Println(version) |
||||
os.Exit(0) |
||||
} |
||||
if *showRevision == true { |
||||
fmt.Println(commit) |
||||
os.Exit(0) |
||||
} |
||||
if *requireVersion != "" && *requireVersion != version { |
||||
fmt.Println("Please restart/reload this terminal session " + |
||||
"(resh version: " + version + |
||||
"; resh version of this terminal session: " + *requireVersion + |
||||
")") |
||||
os.Exit(3) |
||||
} |
||||
if *requireRevision != "" && *requireRevision != commit { |
||||
fmt.Println("Please restart/reload this terminal session " + |
||||
"(resh revision: " + commit + |
||||
"; resh revision of this terminal session: " + *requireRevision + |
||||
")") |
||||
os.Exit(3) |
||||
} |
||||
if *recallPrefix != "" && *recall == false { |
||||
log.Println("Option '--prefix-search' only works with '--recall' option - exiting!") |
||||
os.Exit(4) |
||||
} |
||||
|
||||
realtimeBefore, err := strconv.ParseFloat(*rtb, 64) |
||||
config, errCfg := cfg.New() |
||||
logger, err := logger.New("collect", config.LogLevel, development) |
||||
if err != nil { |
||||
log.Fatal("Flag Parsing error (rtb):", err) |
||||
fmt.Printf("Error while creating logger: %v", err) |
||||
} |
||||
realtimeSessionStart, err := strconv.ParseFloat(*rtsess, 64) |
||||
if err != nil { |
||||
log.Fatal("Flag Parsing error (rt sess):", err) |
||||
defer logger.Sync() // flushes buffer, if any
|
||||
if errCfg != nil { |
||||
logger.Error("Error while getting configuration", zap.Error(errCfg)) |
||||
} |
||||
realtimeSessSinceBoot, err := strconv.ParseFloat(*rtsessboot, 64) |
||||
out := output.New(logger, "resh-collect ERROR") |
||||
|
||||
args := opt.HandleVersionOpts(out, os.Args, version, commit) |
||||
|
||||
flags := pflag.NewFlagSet("", pflag.ExitOnError) |
||||
cmdLine := flags.String("cmd-line", "", "Command line") |
||||
gitRemote := flags.String("git-remote", "", "> git remote get-url origin") |
||||
home := flags.String("home", "", "$HOME") |
||||
pwd := flags.String("pwd", "", "$PWD - present working directory") |
||||
recordID := flags.String("record-id", "", "Resh generated record ID") |
||||
sessionID := flags.String("session-id", "", "Resh generated session ID") |
||||
sessionPID := flags.Int("session-pid", -1, "$$ - Shell session PID") |
||||
shell := flags.String("shell", "", "Current shell") |
||||
shlvl := flags.Int("shlvl", -1, "$SHLVL") |
||||
timeStr := flags.String("time", "-1", "$EPOCHREALTIME") |
||||
flags.Parse(args) |
||||
|
||||
time, err := strconv.ParseFloat(*timeStr, 64) |
||||
if err != nil { |
||||
log.Fatal("Flag Parsing error (rt sess boot):", err) |
||||
out.FatalE("Error while parsing flag --time", err) |
||||
} |
||||
realtimeSinceSessionStart := realtimeBefore - realtimeSessionStart |
||||
realtimeSinceBoot := realtimeSessSinceBoot + realtimeSinceSessionStart |
||||
|
||||
timezoneBeforeOffset := collect.GetTimezoneOffsetInSeconds(*timezoneBefore) |
||||
realtimeBeforeLocal := realtimeBefore + timezoneBeforeOffset |
||||
|
||||
realPwd, err := filepath.EvalSymlinks(*pwd) |
||||
if err != nil { |
||||
log.Println("err while handling pwd realpath:", err) |
||||
out.ErrorE("Error while evaluating symlinks in PWD", err) |
||||
realPwd = "" |
||||
} |
||||
|
||||
gitDir, gitRealDir := collect.GetGitDirs(*gitCdup, *gitCdupExitCode, *pwd) |
||||
if *gitRemoteExitCode != 0 { |
||||
*gitRemote = "" |
||||
} |
||||
rec := recordint.Collect{ |
||||
SessionID: *sessionID, |
||||
Shlvl: *shlvl, |
||||
SessionPID: *sessionPID, |
||||
|
||||
Shell: *shell, |
||||
|
||||
Rec: record.V1{ |
||||
SessionID: *sessionID, |
||||
RecordID: *recordID, |
||||
|
||||
CmdLine: *cmdLine, |
||||
|
||||
// if *osReleaseID == "" {
|
||||
// *osReleaseID = "linux"
|
||||
// }
|
||||
// if *osReleaseName == "" {
|
||||
// *osReleaseName = "Linux"
|
||||
// }
|
||||
// if *osReleasePrettyName == "" {
|
||||
// *osReleasePrettyName = "Linux"
|
||||
// }
|
||||
|
||||
if *recall { |
||||
rec := records.SlimRecord{ |
||||
SessionID: *sessionID, |
||||
RecallHistno: *recallHistno, |
||||
RecallPrefix: *recallPrefix, |
||||
} |
||||
str, found := collect.SendRecallRequest(rec, strconv.Itoa(config.Port)) |
||||
if found == false { |
||||
os.Exit(1) |
||||
} |
||||
fmt.Println(str) |
||||
} else { |
||||
rec := records.Record{ |
||||
// posix
|
||||
Cols: *cols, |
||||
Lines: *lines, |
||||
// core
|
||||
BaseRecord: records.BaseRecord{ |
||||
RecallHistno: *recallHistno, |
||||
|
||||
CmdLine: *cmdLine, |
||||
ExitCode: *exitCode, |
||||
Shell: *shell, |
||||
Uname: *uname, |
||||
SessionID: *sessionID, |
||||
RecordID: *recordID, |
||||
|
||||
// posix
|
||||
Home: *home, |
||||
Lang: *lang, |
||||
LcAll: *lcAll, |
||||
Login: *login, |
||||
// Path: *path,
|
||||
Pwd: *pwd, |
||||
ShellEnv: *shellEnv, |
||||
Term: *term, |
||||
|
||||
// non-posix
|
||||
RealPwd: realPwd, |
||||
Pid: *pid, |
||||
SessionPID: *sessionPid, |
||||
Host: *host, |
||||
Hosttype: *hosttype, |
||||
Ostype: *ostype, |
||||
Machtype: *machtype, |
||||
Shlvl: *shlvl, |
||||
|
||||
// before after
|
||||
TimezoneBefore: *timezoneBefore, |
||||
|
||||
RealtimeBefore: realtimeBefore, |
||||
RealtimeBeforeLocal: realtimeBeforeLocal, |
||||
|
||||
RealtimeSinceSessionStart: realtimeSinceSessionStart, |
||||
RealtimeSinceBoot: realtimeSinceBoot, |
||||
|
||||
GitDir: gitDir, |
||||
GitRealDir: gitRealDir, |
||||
GitOriginRemote: *gitRemote, |
||||
MachineID: collect.ReadFileContent(machineIDPath), |
||||
|
||||
OsReleaseID: *osReleaseID, |
||||
OsReleaseVersionID: *osReleaseVersionID, |
||||
OsReleaseIDLike: *osReleaseIDLike, |
||||
OsReleaseName: *osReleaseName, |
||||
OsReleasePrettyName: *osReleasePrettyName, |
||||
|
||||
PartOne: true, |
||||
|
||||
ReshUUID: collect.ReadFileContent(reshUUIDPath), |
||||
ReshVersion: version, |
||||
ReshRevision: commit, |
||||
|
||||
RecallActionsRaw: *recallActions, |
||||
RecallPrefix: *recallPrefix, |
||||
RecallStrategy: *recallStrategy, |
||||
RecallLastCmdLine: *recallLastCmdLine, |
||||
}, |
||||
} |
||||
collect.SendRecord(rec, strconv.Itoa(config.Port), "/record") |
||||
Home: *home, |
||||
Pwd: *pwd, |
||||
RealPwd: realPwd, |
||||
|
||||
GitOriginRemote: *gitRemote, |
||||
|
||||
Time: fmt.Sprintf("%.4f", time), |
||||
|
||||
PartOne: true, |
||||
PartsNotMerged: true, |
||||
}, |
||||
} |
||||
collect.SendRecord(out, rec, strconv.Itoa(config.Port), "/record") |
||||
} |
||||
|
||||
@ -1,48 +0,0 @@ |
||||
package cmd |
||||
|
||||
import ( |
||||
"os" |
||||
|
||||
"github.com/curusarn/resh/cmd/control/status" |
||||
"github.com/spf13/cobra" |
||||
) |
||||
|
||||
// completionCmd represents the completion command
|
||||
var completionCmd = &cobra.Command{ |
||||
Use: "completion", |
||||
Short: "generate bash/zsh completion scripts", |
||||
Long: `To load completion run |
||||
|
||||
. <(reshctl completion bash)
|
||||
|
||||
OR
|
||||
|
||||
. <(reshctl completion zsh) && compdef _reshctl reshctl |
||||
`, |
||||
} |
||||
|
||||
var completionBashCmd = &cobra.Command{ |
||||
Use: "bash", |
||||
Short: "generate bash completion scripts", |
||||
Long: `To load completion run |
||||
|
||||
. <(reshctl completion bash)
|
||||
`, |
||||
Run: func(cmd *cobra.Command, args []string) { |
||||
rootCmd.GenBashCompletion(os.Stdout) |
||||
exitCode = status.Success |
||||
}, |
||||
} |
||||
|
||||
var completionZshCmd = &cobra.Command{ |
||||
Use: "zsh", |
||||
Short: "generate zsh completion scripts", |
||||
Long: `To load completion run |
||||
|
||||
. <(reshctl completion zsh) && compdef _reshctl reshctl |
||||
`, |
||||
Run: func(cmd *cobra.Command, args []string) { |
||||
rootCmd.GenZshCompletion(os.Stdout) |
||||
exitCode = status.Success |
||||
}, |
||||
} |
||||
@ -1,66 +0,0 @@ |
||||
package cmd |
||||
|
||||
import ( |
||||
"fmt" |
||||
"io/ioutil" |
||||
"os" |
||||
"path/filepath" |
||||
|
||||
"github.com/curusarn/resh/cmd/control/status" |
||||
"github.com/spf13/cobra" |
||||
) |
||||
|
||||
var debugCmd = &cobra.Command{ |
||||
Use: "debug", |
||||
Short: "debug utils for resh", |
||||
Long: "Reloads resh rc files. Shows logs and output from last runs of resh", |
||||
} |
||||
|
||||
var debugReloadCmd = &cobra.Command{ |
||||
Use: "reload", |
||||
Short: "reload resh rc files", |
||||
Long: "Reload resh rc files", |
||||
Run: func(cmd *cobra.Command, args []string) { |
||||
exitCode = status.ReloadRcFiles |
||||
}, |
||||
} |
||||
|
||||
var debugInspectCmd = &cobra.Command{ |
||||
Use: "inspect", |
||||
Short: "inspect session history", |
||||
Run: func(cmd *cobra.Command, args []string) { |
||||
exitCode = status.InspectSessionHistory |
||||
}, |
||||
} |
||||
|
||||
var debugOutputCmd = &cobra.Command{ |
||||
Use: "output", |
||||
Short: "shows output from last runs of resh", |
||||
Long: "Shows output from last runs of resh", |
||||
Run: func(cmd *cobra.Command, args []string) { |
||||
files := []string{ |
||||
"daemon_last_run_out.txt", |
||||
"collect_last_run_out.txt", |
||||
"postcollect_last_run_out.txt", |
||||
"session_init_last_run_out.txt", |
||||
"cli_last_run_out.txt", |
||||
} |
||||
dir := os.Getenv("__RESH_XDG_CACHE_HOME") |
||||
for _, fpath := range files { |
||||
fpath := filepath.Join(dir, fpath) |
||||
debugReadFile(fpath) |
||||
} |
||||
exitCode = status.Success |
||||
}, |
||||
} |
||||
|
||||
func debugReadFile(path string) { |
||||
fmt.Println("============================================================") |
||||
fmt.Println(" filepath:", path) |
||||
fmt.Println("============================================================") |
||||
dat, err := ioutil.ReadFile(path) |
||||
if err != nil { |
||||
fmt.Println("ERROR while reading file:", err) |
||||
} |
||||
fmt.Println(string(dat)) |
||||
} |
||||
@ -0,0 +1,147 @@ |
||||
package cmd |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
"os/exec" |
||||
"time" |
||||
|
||||
"github.com/curusarn/resh/internal/cfg" |
||||
"github.com/curusarn/resh/internal/check" |
||||
"github.com/curusarn/resh/internal/msg" |
||||
"github.com/curusarn/resh/internal/status" |
||||
"github.com/spf13/cobra" |
||||
"go.uber.org/zap" |
||||
) |
||||
|
||||
func doctorCmdFunc(config cfg.Config) func(*cobra.Command, []string) { |
||||
return func(cmd *cobra.Command, args []string) { |
||||
allOK := true |
||||
if !checkDaemon(config) { |
||||
allOK = false |
||||
printDivider() |
||||
} |
||||
if !checkShellSession() { |
||||
allOK = false |
||||
printDivider() |
||||
} |
||||
if !checkShells() { |
||||
allOK = false |
||||
printDivider() |
||||
} |
||||
|
||||
if allOK { |
||||
out.Info("Everything looks good.") |
||||
} |
||||
} |
||||
} |
||||
|
||||
func printDivider() { |
||||
fmt.Printf("\n") |
||||
} |
||||
|
||||
var msgFailedDaemonStart = `Failed to start RESH daemon. |
||||
-> Start RESH daemon manually - run: resh-daemon-start |
||||
-> Or restart this terminal window to bring RESH daemon back up |
||||
-> You can check 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
|
||||
` |
||||
|
||||
func checkDaemon(config cfg.Config) bool { |
||||
ok := true |
||||
resp, err := status.GetDaemonStatus(config.Port) |
||||
if err != nil { |
||||
out.InfoE("RESH Daemon is not running", err) |
||||
out.Info("Attempting to start RESH daemon ...") |
||||
resp, err = startDaemon(config.Port, 5, 200*time.Millisecond) |
||||
if err != nil { |
||||
out.InfoE(msgFailedDaemonStart, err) |
||||
return false |
||||
} |
||||
ok = false |
||||
out.Info("Successfully started daemon.") |
||||
} |
||||
if version != resp.Version { |
||||
out.InfoDaemonVersionMismatch(version, resp.Version) |
||||
return false |
||||
} |
||||
return ok |
||||
} |
||||
|
||||
func startDaemon(port int, maxRetries int, backoff time.Duration) (*msg.StatusResponse, error) { |
||||
err := exec.Command("resh-daemon-start").Run() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
var resp *msg.StatusResponse |
||||
retry := 0 |
||||
for { |
||||
time.Sleep(backoff) |
||||
resp, err = status.GetDaemonStatus(port) |
||||
if err == nil { |
||||
break |
||||
} |
||||
if retry == maxRetries { |
||||
return nil, err |
||||
} |
||||
out.Logger.Error("Failed to get daemon status - retrying", zap.Error(err), zap.Int("retry", retry)) |
||||
retry++ |
||||
continue |
||||
} |
||||
return resp, nil |
||||
} |
||||
|
||||
var msgShellFilesNotLoaded = `RESH shell files were not properly loaded in this terminal |
||||
-> Try restarting this terminal to see if the issue persists |
||||
-> Check your shell rc files (e.g. .zshrc, .bashrc, ...) |
||||
-> You can create an issue at: https://github.com/curusarn/resh/issues
|
||||
` |
||||
|
||||
func checkShellSession() bool { |
||||
versionEnv, found := os.LookupEnv("__RESH_VERSION") |
||||
if !found { |
||||
out.Info(msgShellFilesNotLoaded) |
||||
return false |
||||
} |
||||
if version != versionEnv { |
||||
out.InfoTerminalVersionMismatch(version, versionEnv) |
||||
return false |
||||
} |
||||
return true |
||||
} |
||||
|
||||
func checkShells() bool { |
||||
allOK := true |
||||
|
||||
msg, err := check.LoginShell() |
||||
if err != nil { |
||||
out.InfoE("Failed to get login shell", err) |
||||
allOK = false |
||||
} |
||||
if msg != "" { |
||||
out.Info(msg) |
||||
allOK = false |
||||
} |
||||
|
||||
msg, err = check.ZshVersion() |
||||
if err != nil { |
||||
out.InfoE("Failed to check zsh version", err) |
||||
allOK = false |
||||
} |
||||
if msg != "" { |
||||
out.Info(msg) |
||||
allOK = false |
||||
} |
||||
|
||||
msg, err = check.BashVersion() |
||||
if err != nil { |
||||
out.InfoE("Failed to check bash version", err) |
||||
allOK = false |
||||
} |
||||
if msg != "" { |
||||
out.Info(msg) |
||||
allOK = false |
||||
} |
||||
|
||||
return allOK |
||||
} |
||||
@ -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,45 @@ |
||||
package cmd |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
|
||||
"github.com/curusarn/resh/internal/cfg" |
||||
"github.com/curusarn/resh/internal/status" |
||||
"github.com/spf13/cobra" |
||||
) |
||||
|
||||
func versionCmdFunc(config cfg.Config) func(*cobra.Command, []string) { |
||||
return func(cmd *cobra.Command, args []string) { |
||||
|
||||
fmt.Printf("Installed: %s\n", version) |
||||
|
||||
versionEnv := getEnvVarWithDefault("__RESH_VERSION", "<unknown>") |
||||
fmt.Printf("This terminal session: %s\n", version) |
||||
|
||||
resp, err := status.GetDaemonStatus(config.Port) |
||||
if err != nil { |
||||
fmt.Printf("Running checks: %s\n", version) |
||||
out.ErrorDaemonNotRunning(err) |
||||
return |
||||
} |
||||
fmt.Printf("Currently running daemon: %s\n", resp.Version) |
||||
|
||||
if version != resp.Version { |
||||
out.ErrorDaemonVersionMismatch(version, resp.Version) |
||||
return |
||||
} |
||||
if version != versionEnv { |
||||
out.ErrorTerminalVersionMismatch(version, versionEnv) |
||||
return |
||||
} |
||||
} |
||||
} |
||||
|
||||
func getEnvVarWithDefault(varName, defaultValue string) string { |
||||
val, found := os.LookupEnv(varName) |
||||
if !found { |
||||
return defaultValue |
||||
} |
||||
return val |
||||
} |
||||
@ -1,17 +1,13 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"os" |
||||
|
||||
"github.com/curusarn/resh/cmd/control/cmd" |
||||
) |
||||
|
||||
// version from git set during build
|
||||
var version string |
||||
|
||||
// commit from git set during build
|
||||
var commit string |
||||
var development string |
||||
|
||||
func main() { |
||||
os.Exit(int(cmd.Execute(version, commit))) |
||||
cmd.Execute(version, commit, development) |
||||
} |
||||
|
||||
@ -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,165 @@ |
||||
package main |
||||
|
||||
import ( |
||||
//"flag"
|
||||
|
||||
"io/ioutil" |
||||
"log" |
||||
"fmt" |
||||
"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/datadir" |
||||
"github.com/curusarn/resh/internal/device" |
||||
"github.com/curusarn/resh/internal/logger" |
||||
"github.com/curusarn/resh/internal/status" |
||||
"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 |
||||
var development string |
||||
|
||||
const helpMsg = `ERROR: resh-daemon doesn't accept any arguments |
||||
|
||||
WARNING: |
||||
You shouldn't typically need to start RESH daemon yourself. |
||||
Unless its already running, RESH daemon is started when a new terminal is opened. |
||||
RESH daemon will not start if it's already running even when you run it manually. |
||||
|
||||
USAGE: |
||||
$ resh-daemon |
||||
Runs the daemon as foreground process. You can kill it with CTRL+C. |
||||
|
||||
$ resh-daemon-start |
||||
Runs the daemon as background process detached from terminal. |
||||
|
||||
LOGS & DEBUGGING: |
||||
Logs are located in: |
||||
${XDG_DATA_HOME}/resh/log.json (if XDG_DATA_HOME is set) |
||||
~/.local/share/resh/log.json (otherwise - more common) |
||||
|
||||
A good way to see the logs as they are being produced is: |
||||
$ tail -f ~/.local/share/resh/log.json |
||||
|
||||
// Debug switch
|
||||
var Debug = false |
||||
MORE INFO: |
||||
https://github.com/curusarn/resh/
|
||||
` |
||||
|
||||
func main() { |
||||
log.Println("Daemon starting... \n" + |
||||
"version: " + version + |
||||
" commit: " + commit) |
||||
usr, _ := user.Current() |
||||
dir := usr.HomeDir |
||||
pidfilePath := filepath.Join(dir, ".resh/resh.pid") |
||||
configPath := filepath.Join(dir, ".config/resh.toml") |
||||
reshHistoryPath := filepath.Join(dir, ".resh_history.json") |
||||
bashHistoryPath := filepath.Join(dir, ".bash_history") |
||||
zshHistoryPath := filepath.Join(dir, ".zsh_history") |
||||
logPath := filepath.Join(dir, ".resh/daemon.log") |
||||
|
||||
f, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) |
||||
if len(os.Args) > 1 { |
||||
fmt.Fprint(os.Stderr, helpMsg) |
||||
os.Exit(1) |
||||
} |
||||
config, errCfg := cfg.New() |
||||
logger, err := logger.New("daemon", config.LogLevel, development) |
||||
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)) |
||||
} |
||||
sugar := logger.Sugar() |
||||
d := daemon{sugar: sugar} |
||||
sugar.Infow("Daemon starting ...", |
||||
"version", version, |
||||
"commit", commit, |
||||
) |
||||
dataDir, err := datadir.MakePath() |
||||
if err != nil { |
||||
sugar.Fatalw("Could not get user data directory", zap.Error(err)) |
||||
} |
||||
homeDir, err := os.UserHomeDir() |
||||
if err != nil { |
||||
sugar.Fatalw("Could not get user home directory", zap.Error(err)) |
||||
} |
||||
if config.Debug { |
||||
Debug = true |
||||
log.SetFlags(log.LstdFlags | log.Lmicroseconds) |
||||
// TODO: These paths should be probably defined in a package
|
||||
pidFile := filepath.Join(dataDir, "daemon.pid") |
||||
reshHistoryPath := filepath.Join(dataDir, datadir.HistoryFileName) |
||||
bashHistoryPath := filepath.Join(homeDir, ".bash_history") |
||||
zshHistoryPath := filepath.Join(homeDir, ".zsh_history") |
||||
deviceID, err := device.GetID(dataDir) |
||||
if err != nil { |
||||
sugar.Fatalw("Could not get resh device ID", zap.Error(err)) |
||||
} |
||||
deviceName, err := device.GetName(dataDir) |
||||
if err != nil { |
||||
sugar.Fatalw("Could not get resh device name", zap.Error(err)) |
||||
} |
||||
|
||||
res, err := isDaemonRunning(config.Port) |
||||
sugar = sugar.With(zap.Int("daemonPID", os.Getpid())) |
||||
|
||||
res, err := status.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.Warnw("PID file exists", |
||||
"PIDFile", pidFile) |
||||
// kill daemon
|
||||
err = killDaemon(pidfilePath) |
||||
err = d.killDaemon(pidFile) |
||||
if err != nil { |
||||
log.Printf("Error while killing daemon: %v\n", err) |
||||
sugar.Errorw("Could not kill daemon", |
||||
"error", err, |
||||
) |
||||
} |
||||
} |
||||
err = ioutil.WriteFile(pidfilePath, []byte(strconv.Itoa(os.Getpid())), 0644) |
||||
err = os.WriteFile(pidFile, []byte(strconv.Itoa(os.Getpid())), 0644) |
||||
if err != nil { |
||||
sugar.Fatalw("Could not create PID file", |
||||
"error", err, |
||||
"PIDFile", pidFile, |
||||
) |
||||
} |
||||
server := Server{ |
||||
sugar: sugar, |
||||
config: config, |
||||
reshHistoryPath: reshHistoryPath, |
||||
bashHistoryPath: bashHistoryPath, |
||||
zshHistoryPath: zshHistoryPath, |
||||
|
||||
deviceID: deviceID, |
||||
deviceName: deviceName, |
||||
} |
||||
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) killDaemon(pidFile string) error { |
||||
dat, err := os.ReadFile(pidFile) |
||||
if err != nil { |
||||
d.sugar.Errorw("Reading PID file failed", |
||||
"PIDFile", pidFile, |
||||
"error", err) |
||||
} |
||||
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) |
||||
err = exec.Command("kill", "-SIGTERM", fmt.Sprintf("%d", pid)).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") |
||||
} |
||||
@ -0,0 +1,26 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
|
||||
"github.com/google/uuid" |
||||
) |
||||
|
||||
// Small utility to generate UUID's using google/uuid golang package
|
||||
// Doesn't check arguments
|
||||
// Exits with status 1 on error
|
||||
func main() { |
||||
rnd, err := uuid.NewRandom() |
||||
if err != nil { |
||||
fmt.Fprintf(os.Stderr, "ERROR: could not get new random source: %v", err) |
||||
os.Exit(1) |
||||
} |
||||
id := rnd.String() |
||||
if id == "" { |
||||
fmt.Fprintf(os.Stderr, "ERROR: got invalid UUID from package") |
||||
os.Exit(1) |
||||
} |
||||
// No newline
|
||||
fmt.Print(id) |
||||
} |
||||
@ -0,0 +1,14 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
|
||||
"github.com/curusarn/resh/internal/epochtime" |
||||
) |
||||
|
||||
// Small utility to get epochtime in millisecond precision
|
||||
// Doesn't check arguments
|
||||
// Exits with status 1 on error
|
||||
func main() { |
||||
fmt.Printf("%s", epochtime.Now()) |
||||
} |
||||
@ -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,28 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
|
||||
"github.com/curusarn/resh/internal/datadir" |
||||
"github.com/curusarn/resh/internal/device" |
||||
"github.com/curusarn/resh/internal/output" |
||||
) |
||||
|
||||
func setupDevice(out *output.Output) { |
||||
dataDir, err := datadir.MakePath() |
||||
if err != nil { |
||||
fmt.Fprintf(os.Stderr, "ERROR: Failed to get/setup data directory: %v\n", err) |
||||
os.Exit(1) |
||||
} |
||||
err = device.SetupName(out, dataDir) |
||||
if err != nil { |
||||
fmt.Fprintf(os.Stderr, "ERROR: Failed to check/setup device name: %v\n", err) |
||||
os.Exit(1) |
||||
} |
||||
err = device.SetupID(dataDir) |
||||
if err != nil { |
||||
fmt.Fprintf(os.Stderr, "ERROR: Failed to check/setup device ID: %v\n", err) |
||||
os.Exit(1) |
||||
} |
||||
} |
||||
@ -0,0 +1,67 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
|
||||
"github.com/curusarn/resh/internal/cfg" |
||||
"github.com/curusarn/resh/internal/logger" |
||||
"github.com/curusarn/resh/internal/output" |
||||
"go.uber.org/zap" |
||||
) |
||||
|
||||
// info passed during build
|
||||
var version string |
||||
var commit string |
||||
var development string |
||||
|
||||
func main() { |
||||
config, errCfg := cfg.New() |
||||
logger, err := logger.New("install-utils", config.LogLevel, development) |
||||
if err != nil { |
||||
fmt.Printf("Error while creating logger: %v", err) |
||||
} |
||||
defer logger.Sync() // flushes buffer, if any
|
||||
if errCfg != nil { |
||||
logger.Error("Error while getting configuration", zap.Error(errCfg)) |
||||
} |
||||
sugar := logger.Sugar() |
||||
sugar.Infow("Install-utils invoked ...", |
||||
"version", version, |
||||
"commit", commit, |
||||
) |
||||
out := output.New(logger, "install-utils ERROR") |
||||
|
||||
if len(os.Args) < 2 { |
||||
out.Error("ERROR: Not enough arguments\n") |
||||
printUsage(os.Stderr) |
||||
os.Exit(1) |
||||
} |
||||
command := os.Args[1] |
||||
switch command { |
||||
case "setup-device": |
||||
setupDevice(out) |
||||
case "migrate-all": |
||||
migrateAll(out) |
||||
case "help": |
||||
printUsage(os.Stdout) |
||||
default: |
||||
out.Error(fmt.Sprintf("ERROR: Unknown command: %s\n", command)) |
||||
printUsage(os.Stderr) |
||||
os.Exit(1) |
||||
} |
||||
} |
||||
|
||||
func printUsage(f *os.File) { |
||||
usage := ` |
||||
USAGE: ./install-utils COMMAND |
||||
Utils used during RESH installation. |
||||
|
||||
COMMANDS: |
||||
setup-device setup device name and device ID |
||||
migrate-all update config and history to latest format |
||||
help show this help |
||||
|
||||
` |
||||
fmt.Fprint(f, usage) |
||||
} |
||||
@ -0,0 +1,195 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
"path" |
||||
|
||||
"github.com/curusarn/resh/internal/cfg" |
||||
"github.com/curusarn/resh/internal/datadir" |
||||
"github.com/curusarn/resh/internal/futil" |
||||
"github.com/curusarn/resh/internal/output" |
||||
"github.com/curusarn/resh/internal/recio" |
||||
) |
||||
|
||||
func printRecoveryInfo(rf *futil.RestorableFile) { |
||||
fmt.Printf(" -> Backup is '%s'\n"+ |
||||
" -> Original file location is '%s'\n"+ |
||||
" -> Please copy the backup over the file - run: cp -f '%s' '%s'\n\n", |
||||
rf.PathBackup, rf.Path, |
||||
rf.PathBackup, rf.Path, |
||||
) |
||||
} |
||||
|
||||
func migrateAll(out *output.Output) { |
||||
cfgBackup, err := migrateConfig(out) |
||||
if err != nil { |
||||
// out.InfoE("Failed to update config file format", err)
|
||||
out.FatalE("Failed to update config file format", err) |
||||
} |
||||
err = migrateHistory(out) |
||||
if err != nil { |
||||
errHist := err |
||||
out.InfoE("Failed to update RESH history", errHist) |
||||
out.Info("Restoring config from backup ...") |
||||
err = cfgBackup.Restore() |
||||
if err != nil { |
||||
out.InfoE("FAILED TO RESTORE CONFIG FROM BACKUP!", err) |
||||
printRecoveryInfo(cfgBackup) |
||||
} else { |
||||
out.Info("Config file was restored successfully") |
||||
} |
||||
out.FatalE("Failed to update history", errHist) |
||||
} |
||||
} |
||||
|
||||
func migrateConfig(out *output.Output) (*futil.RestorableFile, error) { |
||||
cfgPath, err := cfg.GetPath() |
||||
if err != nil { |
||||
return nil, fmt.Errorf("could not get config file path: %w", err) |
||||
} |
||||
|
||||
// Touch config to get rid of edge-cases
|
||||
created, err := futil.TouchFile(cfgPath) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed to touch config file: %w", err) |
||||
} |
||||
|
||||
// Backup
|
||||
backup, err := futil.BackupFile(cfgPath) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("could not backup config file: %w", err) |
||||
} |
||||
|
||||
// Migrate
|
||||
changes, err := cfg.Migrate() |
||||
if err != nil { |
||||
// Restore
|
||||
errMigrate := err |
||||
errMigrateWrap := fmt.Errorf("failed to update config file: %w", errMigrate) |
||||
out.InfoE("Failed to update config file format", errMigrate) |
||||
out.Info("Restoring config from backup ...") |
||||
err = backup.Restore() |
||||
if err != nil { |
||||
out.InfoE("FAILED TO RESTORE CONFIG FROM BACKUP!", err) |
||||
printRecoveryInfo(backup) |
||||
} else { |
||||
out.Info("Config file was restored successfully") |
||||
} |
||||
// We are returning the root cause - there might be a better solution how to report the errors
|
||||
return nil, errMigrateWrap |
||||
} |
||||
if created { |
||||
out.Info(fmt.Sprintf("RESH config created in '%s'", cfgPath)) |
||||
} else if changes { |
||||
out.Info("RESH config file format has changed since last update - your config was updated to reflect the changes.") |
||||
} |
||||
return backup, nil |
||||
} |
||||
|
||||
func migrateHistory(out *output.Output) error { |
||||
err := migrateHistoryLocation(out) |
||||
if err != nil { |
||||
return fmt.Errorf("failed to move history to new location %w", err) |
||||
} |
||||
return migrateHistoryFormat(out) |
||||
} |
||||
|
||||
// Find first existing history and use it
|
||||
// Don't bother with merging of history in multiple locations - it could get messy and it shouldn't be necessary
|
||||
func migrateHistoryLocation(out *output.Output) error { |
||||
dataDir, err := datadir.MakePath() |
||||
if err != nil { |
||||
return fmt.Errorf("failed to get data directory: %w", err) |
||||
} |
||||
historyPath := path.Join(dataDir, datadir.HistoryFileName) |
||||
|
||||
exists, err := futil.FileExists(historyPath) |
||||
if err != nil { |
||||
return fmt.Errorf("failed to check history file: %w", err) |
||||
} |
||||
if exists { |
||||
// TODO: get rid of this output (later)
|
||||
out.Info(fmt.Sprintf("Found history file in '%s' - nothing to move", historyPath)) |
||||
return nil |
||||
} |
||||
|
||||
homeDir, err := os.UserHomeDir() |
||||
if err != nil { |
||||
return fmt.Errorf("failed to get user home directory: %w", err) |
||||
} |
||||
|
||||
legacyHistoryPaths := []string{ |
||||
path.Join(homeDir, ".resh_history.json"), |
||||
path.Join(homeDir, ".resh/history.json"), |
||||
} |
||||
for _, path := range legacyHistoryPaths { |
||||
exists, err = futil.FileExists(path) |
||||
if err != nil { |
||||
return fmt.Errorf("failed to check existence of legacy history file: %w", err) |
||||
} |
||||
if exists { |
||||
// TODO: maybe get rid of this output later
|
||||
out.Info(fmt.Sprintf("Copying history file to new location: '%s' -> '%s' ...", path, historyPath)) |
||||
err = futil.CopyFile(path, historyPath) |
||||
if err != nil { |
||||
return fmt.Errorf("failed to copy history file: %w", err) |
||||
} |
||||
out.Info("History file copied successfully") |
||||
return nil |
||||
} |
||||
} |
||||
// out.Info("WARNING: No RESH history file found (this is normal during new installation)")
|
||||
return nil |
||||
} |
||||
|
||||
func migrateHistoryFormat(out *output.Output) error { |
||||
dataDir, err := datadir.MakePath() |
||||
if err != nil { |
||||
return fmt.Errorf("could not get user data directory: %w", err) |
||||
} |
||||
historyPath := path.Join(dataDir, datadir.HistoryFileName) |
||||
|
||||
exists, err := futil.FileExists(historyPath) |
||||
if err != nil { |
||||
return fmt.Errorf("failed to check existence of history file: %w", err) |
||||
} |
||||
if !exists { |
||||
out.Error("There is no RESH history file - this is normal if you are installing RESH for the first time on this device") |
||||
_, err = futil.TouchFile(historyPath) |
||||
if err != nil { |
||||
return fmt.Errorf("failed to touch history file: %w", err) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
backup, err := futil.BackupFile(historyPath) |
||||
if err != nil { |
||||
return fmt.Errorf("could not back up history file: %w", err) |
||||
} |
||||
|
||||
rio := recio.New(out.Logger.Sugar()) |
||||
|
||||
recs, err := rio.ReadAndFixFile(historyPath, 3) |
||||
if err != nil { |
||||
return fmt.Errorf("could not load history file: %w", err) |
||||
} |
||||
err = rio.OverwriteFile(historyPath, recs) |
||||
if err != nil { |
||||
// Restore
|
||||
errMigrate := err |
||||
errMigrateWrap := fmt.Errorf("failed to update format of history file: %w", errMigrate) |
||||
out.InfoE("Failed to update RESH history file format", errMigrate) |
||||
out.Info("Restoring RESH history from backup ...") |
||||
err = backup.Restore() |
||||
if err != nil { |
||||
out.InfoE("FAILED TO RESTORE RESH HISTORY FROM BACKUP!", err) |
||||
printRecoveryInfo(backup) |
||||
} else { |
||||
out.Info("RESH history file was restored successfully") |
||||
} |
||||
// We are returning the root cause - there might be a better solution how to report the errors
|
||||
return errMigrateWrap |
||||
} |
||||
return nil |
||||
} |
||||
@ -1,154 +1,74 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"flag" |
||||
"fmt" |
||||
"log" |
||||
"os" |
||||
|
||||
"github.com/BurntSushi/toml" |
||||
"github.com/curusarn/resh/pkg/cfg" |
||||
"github.com/curusarn/resh/pkg/collect" |
||||
"github.com/curusarn/resh/pkg/records" |
||||
"github.com/curusarn/resh/internal/cfg" |
||||
"github.com/curusarn/resh/internal/collect" |
||||
"github.com/curusarn/resh/internal/logger" |
||||
"github.com/curusarn/resh/internal/opt" |
||||
"github.com/curusarn/resh/internal/output" |
||||
"github.com/curusarn/resh/internal/recordint" |
||||
"github.com/curusarn/resh/record" |
||||
"github.com/spf13/pflag" |
||||
"go.uber.org/zap" |
||||
|
||||
// "os/exec"
|
||||
"os/user" |
||||
"path/filepath" |
||||
"strconv" |
||||
) |
||||
|
||||
// version from git set during build
|
||||
// info passed during build
|
||||
var version string |
||||
|
||||
// commit from git set during build
|
||||
var commit string |
||||
var development string |
||||
|
||||
func main() { |
||||
usr, _ := user.Current() |
||||
dir := usr.HomeDir |
||||
configPath := filepath.Join(dir, "/.config/resh.toml") |
||||
reshUUIDPath := filepath.Join(dir, "/.resh/resh-uuid") |
||||
|
||||
machineIDPath := "/etc/machine-id" |
||||
|
||||
var config cfg.Config |
||||
if _, err := toml.DecodeFile(configPath, &config); err != nil { |
||||
log.Fatal("Error reading config:", err) |
||||
config, errCfg := cfg.New() |
||||
logger, err := logger.New("postcollect", config.LogLevel, development) |
||||
if err != nil { |
||||
fmt.Printf("Error while creating logger: %v", err) |
||||
} |
||||
showVersion := flag.Bool("version", false, "Show version and exit") |
||||
showRevision := flag.Bool("revision", false, "Show git revision and exit") |
||||
|
||||
requireVersion := flag.String("requireVersion", "", "abort if version doesn't match") |
||||
requireRevision := flag.String("requireRevision", "", "abort if revision doesn't match") |
||||
|
||||
cmdLine := flag.String("cmdLine", "", "command line") |
||||
exitCode := flag.Int("exitCode", -1, "exit code") |
||||
sessionID := flag.String("sessionId", "", "resh generated session id") |
||||
recordID := flag.String("recordId", "", "resh generated record id") |
||||
|
||||
shlvl := flag.Int("shlvl", -1, "$SHLVL") |
||||
shell := flag.String("shell", "", "actual shell") |
||||
|
||||
// posix variables
|
||||
pwdAfter := flag.String("pwdAfter", "", "$PWD after command") |
||||
|
||||
// non-posix
|
||||
// sessionPid := flag.Int("sessionPid", -1, "$$ at session start")
|
||||
|
||||
gitCdupAfter := flag.String("gitCdupAfter", "", "git rev-parse --show-cdup") |
||||
gitRemoteAfter := flag.String("gitRemoteAfter", "", "git remote get-url origin") |
||||
|
||||
gitCdupExitCodeAfter := flag.Int("gitCdupExitCodeAfter", -1, "... $?") |
||||
gitRemoteExitCodeAfter := flag.Int("gitRemoteExitCodeAfter", -1, "... $?") |
||||
defer logger.Sync() // flushes buffer, if any
|
||||
if errCfg != nil { |
||||
logger.Error("Error while getting configuration", zap.Error(errCfg)) |
||||
} |
||||
out := output.New(logger, "resh-postcollect ERROR") |
||||
|
||||
// before after
|
||||
timezoneAfter := flag.String("timezoneAfter", "", "") |
||||
args := opt.HandleVersionOpts(out, os.Args, version, commit) |
||||
|
||||
rtb := flag.String("realtimeBefore", "-1", "before $EPOCHREALTIME") |
||||
rta := flag.String("realtimeAfter", "-1", "after $EPOCHREALTIME") |
||||
flag.Parse() |
||||
flags := pflag.NewFlagSet("", pflag.ExitOnError) |
||||
exitCode := flags.Int("exit-code", -1, "Exit code") |
||||
sessionID := flags.String("session-id", "", "Resh generated session ID") |
||||
recordID := flags.String("record-id", "", "Resh generated record ID") |
||||
shlvl := flags.Int("shlvl", -1, "$SHLVL") |
||||
rtb := flags.String("time-before", "-1", "Before $EPOCHREALTIME") |
||||
rta := flags.String("time-after", "-1", "After $EPOCHREALTIME") |
||||
flags.Parse(args) |
||||
|
||||
if *showVersion == true { |
||||
fmt.Println(version) |
||||
os.Exit(0) |
||||
} |
||||
if *showRevision == true { |
||||
fmt.Println(commit) |
||||
os.Exit(0) |
||||
} |
||||
if *requireVersion != "" && *requireVersion != version { |
||||
fmt.Println("Please restart/reload this terminal session " + |
||||
"(resh version: " + version + |
||||
"; resh version of this terminal session: " + *requireVersion + |
||||
")") |
||||
os.Exit(3) |
||||
} |
||||
if *requireRevision != "" && *requireRevision != commit { |
||||
fmt.Println("Please restart/reload this terminal session " + |
||||
"(resh revision: " + commit + |
||||
"; resh revision of this terminal session: " + *requireRevision + |
||||
")") |
||||
os.Exit(3) |
||||
} |
||||
realtimeAfter, err := strconv.ParseFloat(*rta, 64) |
||||
timeAfter, err := strconv.ParseFloat(*rta, 64) |
||||
if err != nil { |
||||
log.Fatal("Flag Parsing error (rta):", err) |
||||
out.FatalE("Error while parsing flag --time-after", err) |
||||
} |
||||
realtimeBefore, err := strconv.ParseFloat(*rtb, 64) |
||||
timeBefore, err := strconv.ParseFloat(*rtb, 64) |
||||
if err != nil { |
||||
log.Fatal("Flag Parsing error (rtb):", err) |
||||
out.FatalE("Error while parsing flag --time-before", err) |
||||
} |
||||
realtimeDuration := realtimeAfter - realtimeBefore |
||||
|
||||
timezoneAfterOffset := collect.GetTimezoneOffsetInSeconds(*timezoneAfter) |
||||
realtimeAfterLocal := realtimeAfter + timezoneAfterOffset |
||||
duration := timeAfter - timeBefore |
||||
|
||||
realPwdAfter, err := filepath.EvalSymlinks(*pwdAfter) |
||||
if err != nil { |
||||
log.Println("err while handling pwdAfter realpath:", err) |
||||
realPwdAfter = "" |
||||
} |
||||
// FIXME: use recordint.Postcollect
|
||||
rec := recordint.Collect{ |
||||
SessionID: *sessionID, |
||||
Shlvl: *shlvl, |
||||
|
||||
gitDirAfter, gitRealDirAfter := collect.GetGitDirs(*gitCdupAfter, *gitCdupExitCodeAfter, *pwdAfter) |
||||
if *gitRemoteExitCodeAfter != 0 { |
||||
*gitRemoteAfter = "" |
||||
} |
||||
|
||||
rec := records.Record{ |
||||
// core
|
||||
BaseRecord: records.BaseRecord{ |
||||
CmdLine: *cmdLine, |
||||
ExitCode: *exitCode, |
||||
SessionID: *sessionID, |
||||
Rec: record.V1{ |
||||
RecordID: *recordID, |
||||
Shlvl: *shlvl, |
||||
Shell: *shell, |
||||
|
||||
PwdAfter: *pwdAfter, |
||||
|
||||
// non-posix
|
||||
RealPwdAfter: realPwdAfter, |
||||
|
||||
// before after
|
||||
TimezoneAfter: *timezoneAfter, |
||||
|
||||
RealtimeBefore: realtimeBefore, |
||||
RealtimeAfter: realtimeAfter, |
||||
RealtimeAfterLocal: realtimeAfterLocal, |
||||
|
||||
RealtimeDuration: realtimeDuration, |
||||
|
||||
GitDirAfter: gitDirAfter, |
||||
GitRealDirAfter: gitRealDirAfter, |
||||
GitOriginRemoteAfter: *gitRemoteAfter, |
||||
MachineID: collect.ReadFileContent(machineIDPath), |
||||
SessionID: *sessionID, |
||||
|
||||
PartOne: false, |
||||
ExitCode: *exitCode, |
||||
Duration: fmt.Sprintf("%.4f", duration), |
||||
|
||||
ReshUUID: collect.ReadFileContent(reshUUIDPath), |
||||
ReshVersion: version, |
||||
ReshRevision: commit, |
||||
PartsNotMerged: true, |
||||
}, |
||||
} |
||||
collect.SendRecord(rec, strconv.Itoa(config.Port), "/record") |
||||
collect.SendRecord(out, rec, strconv.Itoa(config.Port), "/record") |
||||
} |
||||
|
||||
@ -1,523 +0,0 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"bufio" |
||||
"crypto/sha256" |
||||
"encoding/binary" |
||||
"encoding/hex" |
||||
"encoding/json" |
||||
"errors" |
||||
"flag" |
||||
"fmt" |
||||
"log" |
||||
"math" |
||||
"net/url" |
||||
"os" |
||||
"os/user" |
||||
"path" |
||||
"path/filepath" |
||||
"strconv" |
||||
"strings" |
||||
"unicode" |
||||
|
||||
"github.com/coreos/go-semver/semver" |
||||
"github.com/curusarn/resh/pkg/records" |
||||
giturls "github.com/whilp/git-urls" |
||||
) |
||||
|
||||
// version from git set during build
|
||||
var version string |
||||
|
||||
// commit from git set during build
|
||||
var commit string |
||||
|
||||
func main() { |
||||
usr, _ := user.Current() |
||||
dir := usr.HomeDir |
||||
historyPath := filepath.Join(dir, ".resh_history.json") |
||||
// outputPath := filepath.Join(dir, "resh_history_sanitized.json")
|
||||
sanitizerDataPath := filepath.Join(dir, ".resh", "sanitizer_data") |
||||
|
||||
showVersion := flag.Bool("version", false, "Show version and exit") |
||||
showRevision := flag.Bool("revision", false, "Show git revision and exit") |
||||
trimHashes := flag.Int("trim-hashes", 12, "Trim hashes to N characters, '0' turns off trimming") |
||||
inputPath := flag.String("input", historyPath, "Input file") |
||||
outputPath := flag.String("output", "", "Output file (default: use stdout)") |
||||
|
||||
flag.Parse() |
||||
|
||||
if *showVersion == true { |
||||
fmt.Println(version) |
||||
os.Exit(0) |
||||
} |
||||
if *showRevision == true { |
||||
fmt.Println(commit) |
||||
os.Exit(0) |
||||
} |
||||
sanitizer := sanitizer{hashLength: *trimHashes} |
||||
err := sanitizer.init(sanitizerDataPath) |
||||
if err != nil { |
||||
log.Fatal("Sanitizer init() error:", err) |
||||
} |
||||
|
||||
inputFile, err := os.Open(*inputPath) |
||||
if err != nil { |
||||
log.Fatal("Open() resh history file error:", err) |
||||
} |
||||
defer inputFile.Close() |
||||
|
||||
var writer *bufio.Writer |
||||
if *outputPath == "" { |
||||
writer = bufio.NewWriter(os.Stdout) |
||||
} else { |
||||
outputFile, err := os.Create(*outputPath) |
||||
if err != nil { |
||||
log.Fatal("Create() output file error:", err) |
||||
} |
||||
defer outputFile.Close() |
||||
writer = bufio.NewWriter(outputFile) |
||||
} |
||||
defer writer.Flush() |
||||
|
||||
scanner := bufio.NewScanner(inputFile) |
||||
for scanner.Scan() { |
||||
record := records.Record{} |
||||
fallbackRecord := records.FallbackRecord{} |
||||
line := scanner.Text() |
||||
err = json.Unmarshal([]byte(line), &record) |
||||
if err != nil { |
||||
err = json.Unmarshal([]byte(line), &fallbackRecord) |
||||
if err != nil { |
||||
log.Println("Line:", line) |
||||
log.Fatal("Decoding error:", err) |
||||
} |
||||
record = records.Convert(&fallbackRecord) |
||||
} |
||||
err = sanitizer.sanitizeRecord(&record) |
||||
if err != nil { |
||||
log.Println("Line:", line) |
||||
log.Fatal("Sanitization error:", err) |
||||
} |
||||
outLine, err := json.Marshal(&record) |
||||
if err != nil { |
||||
log.Println("Line:", line) |
||||
log.Fatal("Encoding error:", err) |
||||
} |
||||
// fmt.Println(string(outLine))
|
||||
n, err := writer.WriteString(string(outLine) + "\n") |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
if n == 0 { |
||||
log.Fatal("Nothing was written", n) |
||||
} |
||||
} |
||||
} |
||||
|
||||
type sanitizer struct { |
||||
hashLength int |
||||
whitelist map[string]bool |
||||
} |
||||
|
||||
func (s *sanitizer) init(dataPath string) error { |
||||
globalData := path.Join(dataPath, "whitelist.txt") |
||||
s.whitelist = loadData(globalData) |
||||
return nil |
||||
} |
||||
|
||||
func loadData(fname string) map[string]bool { |
||||
file, err := os.Open(fname) |
||||
if err != nil { |
||||
log.Fatal("Open() file error:", err) |
||||
} |
||||
defer file.Close() |
||||
|
||||
scanner := bufio.NewScanner(file) |
||||
data := make(map[string]bool) |
||||
for scanner.Scan() { |
||||
line := scanner.Text() |
||||
data[line] = true |
||||
} |
||||
return data |
||||
} |
||||
|
||||
func (s *sanitizer) sanitizeRecord(record *records.Record) error { |
||||
// hash directories of the paths
|
||||
record.Pwd = s.sanitizePath(record.Pwd) |
||||
record.RealPwd = s.sanitizePath(record.RealPwd) |
||||
record.PwdAfter = s.sanitizePath(record.PwdAfter) |
||||
record.RealPwdAfter = s.sanitizePath(record.RealPwdAfter) |
||||
record.GitDir = s.sanitizePath(record.GitDir) |
||||
record.GitDirAfter = s.sanitizePath(record.GitDirAfter) |
||||
record.GitRealDir = s.sanitizePath(record.GitRealDir) |
||||
record.GitRealDirAfter = s.sanitizePath(record.GitRealDirAfter) |
||||
record.Home = s.sanitizePath(record.Home) |
||||
record.ShellEnv = s.sanitizePath(record.ShellEnv) |
||||
|
||||
// hash the most sensitive info, do not tokenize
|
||||
record.Host = s.hashToken(record.Host) |
||||
record.Login = s.hashToken(record.Login) |
||||
record.MachineID = s.hashToken(record.MachineID) |
||||
|
||||
var err error |
||||
// this changes git url a bit but I'm still happy with the result
|
||||
// e.g. "git@github.com:curusarn/resh" becomes "ssh://git@github.com/3385162f14d7/5a7b2909005c"
|
||||
// notice the "ssh://" prefix
|
||||
record.GitOriginRemote, err = s.sanitizeGitURL(record.GitOriginRemote) |
||||
if err != nil { |
||||
log.Println("Error while snitizing GitOriginRemote url", record.GitOriginRemote, ":", err) |
||||
return err |
||||
} |
||||
record.GitOriginRemoteAfter, err = s.sanitizeGitURL(record.GitOriginRemoteAfter) |
||||
if err != nil { |
||||
log.Println("Error while snitizing GitOriginRemoteAfter url", record.GitOriginRemoteAfter, ":", err) |
||||
return err |
||||
} |
||||
|
||||
// sanitization destroys original CmdLine length -> save it
|
||||
record.CmdLength = len(record.CmdLine) |
||||
|
||||
record.CmdLine, err = s.sanitizeCmdLine(record.CmdLine) |
||||
if err != nil { |
||||
log.Fatal("Cmd:", record.CmdLine, "; sanitization error:", err) |
||||
} |
||||
record.RecallLastCmdLine, err = s.sanitizeCmdLine(record.RecallLastCmdLine) |
||||
if err != nil { |
||||
log.Fatal("RecallLastCmdLine:", record.RecallLastCmdLine, "; sanitization error:", err) |
||||
} |
||||
|
||||
if len(record.RecallActionsRaw) > 0 { |
||||
record.RecallActionsRaw, err = s.sanitizeRecallActions(record.RecallActionsRaw, record.ReshVersion) |
||||
if err != nil { |
||||
log.Println("RecallActionsRaw:", record.RecallActionsRaw, "; sanitization error:", err) |
||||
} |
||||
} |
||||
// add a flag to signify that the record has been sanitized
|
||||
record.Sanitized = true |
||||
return nil |
||||
} |
||||
|
||||
func fixSeparator(str string) string { |
||||
if len(str) > 0 && str[0] == ';' { |
||||
return "|||" + str[1:] |
||||
} |
||||
return str |
||||
} |
||||
|
||||
func minIndex(str string, substrs []string) (idx, substrIdx int) { |
||||
minMatch := math.MaxInt32 |
||||
for i, sep := range substrs { |
||||
match := strings.Index(str, sep) |
||||
if match != -1 && match < minMatch { |
||||
minMatch = match |
||||
substrIdx = i |
||||
} |
||||
} |
||||
idx = minMatch |
||||
return |
||||
} |
||||
|
||||
// sanitizes the recall actions by replacing the recall prefix with it's length
|
||||
func (s *sanitizer) sanitizeRecallActions(str string, reshVersion string) (string, error) { |
||||
if len(str) == 0 { |
||||
return "", nil |
||||
} |
||||
var separators []string |
||||
seps := []string{"|||"} |
||||
refVersion, err := semver.NewVersion("2.5.14") |
||||
if err != nil { |
||||
return str, fmt.Errorf("sanitizeRecallActions: semver error: %s", err.Error()) |
||||
} |
||||
if len(reshVersion) == 0 { |
||||
return str, errors.New("sanitizeRecallActions: record.ReshVersion is an empty string") |
||||
} |
||||
if reshVersion == "dev" { |
||||
reshVersion = "0.0.0" |
||||
} |
||||
if reshVersion[0] == 'v' { |
||||
reshVersion = reshVersion[1:] |
||||
} |
||||
recordVersion, err := semver.NewVersion(reshVersion) |
||||
if err != nil { |
||||
return str, fmt.Errorf("sanitizeRecallActions: semver error: %s; version string: %s", err.Error(), reshVersion) |
||||
} |
||||
if recordVersion.LessThan(*refVersion) { |
||||
seps = append(seps, ";") |
||||
} |
||||
|
||||
actions := []string{"arrow_up", "arrow_down", "control_R"} |
||||
for _, sep := range seps { |
||||
for _, action := range actions { |
||||
separators = append(separators, sep+action+":") |
||||
} |
||||
} |
||||
/* |
||||
- find any of {|||,;}{arrow_up,arrow_down,control_R}: in the recallActions (on the lowest index) |
||||
- use found substring to parse out the next prefix |
||||
- sanitize prefix |
||||
- add fixed substring and sanitized prefix to output |
||||
*/ |
||||
doBreak := false |
||||
sanStr := "" |
||||
idx := 0 |
||||
var currSeparator string |
||||
tokenLen, sepIdx := minIndex(str, separators) |
||||
if tokenLen != 0 { |
||||
return str, errors.New("sanitizeReacallActions: unexpected string before first action/separator") |
||||
} |
||||
currSeparator = separators[sepIdx] |
||||
idx += len(currSeparator) |
||||
for !doBreak { |
||||
tokenLen, sepIdx := minIndex(str[idx:], separators) |
||||
if tokenLen > len(str[idx:]) { |
||||
tokenLen = len(str[idx:]) |
||||
doBreak = true |
||||
} |
||||
// token := str[idx : idx+tokenLen]
|
||||
sanStr += fixSeparator(currSeparator) + strconv.Itoa(tokenLen) |
||||
currSeparator = separators[sepIdx] |
||||
idx += tokenLen + len(currSeparator) |
||||
} |
||||
return sanStr, nil |
||||
} |
||||
|
||||
func (s *sanitizer) sanitizeCmdLine(cmdLine string) (string, error) { |
||||
const optionEndingChars = "\"$'\\#[]!><|;{}()*,?~&=`:@^/+%." // all bash control characters, '=', ...
|
||||
const optionAllowedChars = "-_" // characters commonly found inside of options
|
||||
sanCmdLine := "" |
||||
buff := "" |
||||
|
||||
// simple options shouldn't be sanitized
|
||||
// 1) whitespace 2) "-" or "--" 3) letters, digits, "-", "_" 4) ending whitespace or any of "=;)"
|
||||
var optionDetected bool |
||||
|
||||
prevR3 := ' ' |
||||
prevR2 := ' ' |
||||
prevR := ' ' |
||||
for _, r := range cmdLine { |
||||
switch optionDetected { |
||||
case true: |
||||
if unicode.IsSpace(r) || strings.ContainsRune(optionEndingChars, r) { |
||||
// whitespace or option ends the option
|
||||
// => add option unsanitized
|
||||
optionDetected = false |
||||
if len(buff) > 0 { |
||||
sanCmdLine += buff |
||||
buff = "" |
||||
} |
||||
sanCmdLine += string(r) |
||||
} else if unicode.IsLetter(r) == false && unicode.IsDigit(r) == false && |
||||
strings.ContainsRune(optionAllowedChars, r) == false { |
||||
// r is not any of allowed chars for an option: letter, digit, "-" or "_"
|
||||
// => sanitize
|
||||
if len(buff) > 0 { |
||||
sanToken, err := s.sanitizeCmdToken(buff) |
||||
if err != nil { |
||||
log.Println("WARN: got error while sanitizing cmdLine:", cmdLine) |
||||
// return cmdLine, err
|
||||
} |
||||
sanCmdLine += sanToken |
||||
buff = "" |
||||
} |
||||
sanCmdLine += string(r) |
||||
} else { |
||||
buff += string(r) |
||||
} |
||||
case false: |
||||
// split command on all non-letter and non-digit characters
|
||||
if unicode.IsLetter(r) == false && unicode.IsDigit(r) == false { |
||||
// split token
|
||||
if len(buff) > 0 { |
||||
sanToken, err := s.sanitizeCmdToken(buff) |
||||
if err != nil { |
||||
log.Println("WARN: got error while sanitizing cmdLine:", cmdLine) |
||||
// return cmdLine, err
|
||||
} |
||||
sanCmdLine += sanToken |
||||
buff = "" |
||||
} |
||||
sanCmdLine += string(r) |
||||
} else { |
||||
if (unicode.IsSpace(prevR2) && prevR == '-') || |
||||
(unicode.IsSpace(prevR3) && prevR2 == '-' && prevR == '-') { |
||||
optionDetected = true |
||||
} |
||||
buff += string(r) |
||||
} |
||||
} |
||||
prevR3 = prevR2 |
||||
prevR2 = prevR |
||||
prevR = r |
||||
} |
||||
if len(buff) <= 0 { |
||||
// nothing in the buffer => work is done
|
||||
return sanCmdLine, nil |
||||
} |
||||
if optionDetected { |
||||
// option detected => dont sanitize
|
||||
sanCmdLine += buff |
||||
return sanCmdLine, nil |
||||
} |
||||
// sanitize
|
||||
sanToken, err := s.sanitizeCmdToken(buff) |
||||
if err != nil { |
||||
log.Println("WARN: got error while sanitizing cmdLine:", cmdLine) |
||||
// return cmdLine, err
|
||||
} |
||||
sanCmdLine += sanToken |
||||
return sanCmdLine, nil |
||||
} |
||||
|
||||
func (s *sanitizer) sanitizeGitURL(rawURL string) (string, error) { |
||||
if len(rawURL) <= 0 { |
||||
return rawURL, nil |
||||
} |
||||
parsedURL, err := giturls.Parse(rawURL) |
||||
if err != nil { |
||||
return rawURL, err |
||||
} |
||||
return s.sanitizeParsedURL(parsedURL) |
||||
} |
||||
|
||||
func (s *sanitizer) sanitizeURL(rawURL string) (string, error) { |
||||
if len(rawURL) <= 0 { |
||||
return rawURL, nil |
||||
} |
||||
parsedURL, err := url.Parse(rawURL) |
||||
if err != nil { |
||||
return rawURL, err |
||||
} |
||||
return s.sanitizeParsedURL(parsedURL) |
||||
} |
||||
|
||||
func (s *sanitizer) sanitizeParsedURL(parsedURL *url.URL) (string, error) { |
||||
parsedURL.Opaque = s.sanitizeToken(parsedURL.Opaque) |
||||
|
||||
userinfo := parsedURL.User.Username() // only get username => password won't even make it to the sanitized data
|
||||
if len(userinfo) > 0 { |
||||
parsedURL.User = url.User(s.sanitizeToken(userinfo)) |
||||
} else { |
||||
// we need to do this because `gitUrls.Parse()` sets `User` to `url.User("")` instead of `nil`
|
||||
parsedURL.User = nil |
||||
} |
||||
var err error |
||||
parsedURL.Host, err = s.sanitizeTwoPartToken(parsedURL.Host, ":") |
||||
if err != nil { |
||||
return parsedURL.String(), err |
||||
} |
||||
parsedURL.Path = s.sanitizePath(parsedURL.Path) |
||||
// ForceQuery bool
|
||||
parsedURL.RawQuery = s.sanitizeToken(parsedURL.RawQuery) |
||||
parsedURL.Fragment = s.sanitizeToken(parsedURL.Fragment) |
||||
|
||||
return parsedURL.String(), nil |
||||
} |
||||
|
||||
func (s *sanitizer) sanitizePath(path string) string { |
||||
var sanPath string |
||||
for _, token := range strings.Split(path, "/") { |
||||
if s.whitelist[token] != true { |
||||
token = s.hashToken(token) |
||||
} |
||||
sanPath += token + "/" |
||||
} |
||||
if len(sanPath) > 0 { |
||||
sanPath = sanPath[:len(sanPath)-1] |
||||
} |
||||
return sanPath |
||||
} |
||||
|
||||
func (s *sanitizer) sanitizeTwoPartToken(token string, delimeter string) (string, error) { |
||||
tokenParts := strings.Split(token, delimeter) |
||||
if len(tokenParts) <= 1 { |
||||
return s.sanitizeToken(token), nil |
||||
} |
||||
if len(tokenParts) == 2 { |
||||
return s.sanitizeToken(tokenParts[0]) + delimeter + s.sanitizeToken(tokenParts[1]), nil |
||||
} |
||||
return token, errors.New("Token has more than two parts") |
||||
} |
||||
|
||||
func (s *sanitizer) sanitizeCmdToken(token string) (string, error) { |
||||
// there shouldn't be tokens with letters or digits mixed together with symbols
|
||||
if len(token) <= 1 { |
||||
// NOTE: do not sanitize single letter tokens
|
||||
return token, nil |
||||
} |
||||
if s.isInWhitelist(token) == true { |
||||
return token, nil |
||||
} |
||||
|
||||
isLettersOrDigits := true |
||||
// isDigits := true
|
||||
isOtherCharacters := true |
||||
for _, r := range token { |
||||
if unicode.IsDigit(r) == false && unicode.IsLetter(r) == false { |
||||
isLettersOrDigits = false |
||||
// isDigits = false
|
||||
} |
||||
// if unicode.IsDigit(r) == false {
|
||||
// isDigits = false
|
||||
// }
|
||||
if unicode.IsDigit(r) || unicode.IsLetter(r) { |
||||
isOtherCharacters = false |
||||
} |
||||
} |
||||
// NOTE: I decided that I don't want a special sanitization for numbers
|
||||
// if isDigits {
|
||||
// return s.hashNumericToken(token), nil
|
||||
// }
|
||||
if isLettersOrDigits { |
||||
return s.hashToken(token), nil |
||||
} |
||||
if isOtherCharacters { |
||||
return token, nil |
||||
} |
||||
log.Println("WARN: cmd token is made of mix of letters or digits and other characters; token:", token) |
||||
// return token, errors.New("cmd token is made of mix of letters or digits and other characters")
|
||||
return s.hashToken(token), errors.New("cmd token is made of mix of letters or digits and other characters") |
||||
} |
||||
|
||||
func (s *sanitizer) sanitizeToken(token string) string { |
||||
if len(token) <= 1 { |
||||
// NOTE: do not sanitize single letter tokens
|
||||
return token |
||||
} |
||||
if s.isInWhitelist(token) { |
||||
return token |
||||
} |
||||
return s.hashToken(token) |
||||
} |
||||
|
||||
func (s *sanitizer) hashToken(token string) string { |
||||
if len(token) <= 0 { |
||||
return token |
||||
} |
||||
// hash with sha256
|
||||
sum := sha256.Sum256([]byte(token)) |
||||
return s.trimHash(hex.EncodeToString(sum[:])) |
||||
} |
||||
|
||||
func (s *sanitizer) hashNumericToken(token string) string { |
||||
if len(token) <= 0 { |
||||
return token |
||||
} |
||||
sum := sha256.Sum256([]byte(token)) |
||||
sumInt := int(binary.LittleEndian.Uint64(sum[:])) |
||||
if sumInt < 0 { |
||||
return strconv.Itoa(sumInt * -1) |
||||
} |
||||
return s.trimHash(strconv.Itoa(sumInt)) |
||||
} |
||||
|
||||
func (s *sanitizer) trimHash(hash string) string { |
||||
length := s.hashLength |
||||
if length <= 0 || len(hash) < length { |
||||
length = len(hash) |
||||
} |
||||
return hash[:length] |
||||
} |
||||
|
||||
func (s *sanitizer) isInWhitelist(token string) bool { |
||||
return s.whitelist[strings.ToLower(token)] == true |
||||
} |
||||
@ -1,186 +1,48 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"flag" |
||||
"fmt" |
||||
"log" |
||||
"os" |
||||
|
||||
"github.com/BurntSushi/toml" |
||||
"github.com/curusarn/resh/pkg/cfg" |
||||
"github.com/curusarn/resh/pkg/collect" |
||||
"github.com/curusarn/resh/pkg/records" |
||||
"github.com/curusarn/resh/internal/cfg" |
||||
"github.com/curusarn/resh/internal/collect" |
||||
"github.com/curusarn/resh/internal/logger" |
||||
"github.com/curusarn/resh/internal/opt" |
||||
"github.com/curusarn/resh/internal/output" |
||||
"github.com/curusarn/resh/internal/recordint" |
||||
"github.com/spf13/pflag" |
||||
"go.uber.org/zap" |
||||
|
||||
"os/user" |
||||
"path/filepath" |
||||
"strconv" |
||||
) |
||||
|
||||
// version from git set during build
|
||||
// info passed during build
|
||||
var version string |
||||
|
||||
// commit from git set during build
|
||||
var commit string |
||||
var development string |
||||
|
||||
func main() { |
||||
usr, _ := user.Current() |
||||
dir := usr.HomeDir |
||||
configPath := filepath.Join(dir, "/.config/resh.toml") |
||||
reshUUIDPath := filepath.Join(dir, "/.resh/resh-uuid") |
||||
|
||||
machineIDPath := "/etc/machine-id" |
||||
|
||||
var config cfg.Config |
||||
if _, err := toml.DecodeFile(configPath, &config); err != nil { |
||||
log.Fatal("Error reading config:", err) |
||||
} |
||||
showVersion := flag.Bool("version", false, "Show version and exit") |
||||
showRevision := flag.Bool("revision", false, "Show git revision and exit") |
||||
|
||||
requireVersion := flag.String("requireVersion", "", "abort if version doesn't match") |
||||
requireRevision := flag.String("requireRevision", "", "abort if revision doesn't match") |
||||
|
||||
shell := flag.String("shell", "", "actual shell") |
||||
uname := flag.String("uname", "", "uname") |
||||
sessionID := flag.String("sessionId", "", "resh generated session id") |
||||
|
||||
// posix variables
|
||||
cols := flag.String("cols", "-1", "$COLUMNS") |
||||
lines := flag.String("lines", "-1", "$LINES") |
||||
home := flag.String("home", "", "$HOME") |
||||
lang := flag.String("lang", "", "$LANG") |
||||
lcAll := flag.String("lcAll", "", "$LC_ALL") |
||||
login := flag.String("login", "", "$LOGIN") |
||||
shellEnv := flag.String("shellEnv", "", "$SHELL") |
||||
term := flag.String("term", "", "$TERM") |
||||
|
||||
// non-posix
|
||||
pid := flag.Int("pid", -1, "$$") |
||||
sessionPid := flag.Int("sessionPid", -1, "$$ at session start") |
||||
shlvl := flag.Int("shlvl", -1, "$SHLVL") |
||||
|
||||
host := flag.String("host", "", "$HOSTNAME") |
||||
hosttype := flag.String("hosttype", "", "$HOSTTYPE") |
||||
ostype := flag.String("ostype", "", "$OSTYPE") |
||||
machtype := flag.String("machtype", "", "$MACHTYPE") |
||||
|
||||
// before after
|
||||
timezoneBefore := flag.String("timezoneBefore", "", "") |
||||
|
||||
osReleaseID := flag.String("osReleaseId", "", "/etc/os-release ID") |
||||
osReleaseVersionID := flag.String("osReleaseVersionId", "", |
||||
"/etc/os-release ID") |
||||
osReleaseIDLike := flag.String("osReleaseIdLike", "", "/etc/os-release ID") |
||||
osReleaseName := flag.String("osReleaseName", "", "/etc/os-release ID") |
||||
osReleasePrettyName := flag.String("osReleasePrettyName", "", |
||||
"/etc/os-release ID") |
||||
|
||||
rtb := flag.String("realtimeBefore", "-1", "before $EPOCHREALTIME") |
||||
rtsess := flag.String("realtimeSession", "-1", |
||||
"on session start $EPOCHREALTIME") |
||||
rtsessboot := flag.String("realtimeSessSinceBoot", "-1", |
||||
"on session start $EPOCHREALTIME") |
||||
flag.Parse() |
||||
|
||||
if *showVersion == true { |
||||
fmt.Println(version) |
||||
os.Exit(0) |
||||
} |
||||
if *showRevision == true { |
||||
fmt.Println(commit) |
||||
os.Exit(0) |
||||
} |
||||
if *requireVersion != "" && *requireVersion != version { |
||||
fmt.Println("Please restart/reload this terminal session " + |
||||
"(resh version: " + version + |
||||
"; resh version of this terminal session: " + *requireVersion + |
||||
")") |
||||
os.Exit(3) |
||||
} |
||||
if *requireRevision != "" && *requireRevision != commit { |
||||
fmt.Println("Please restart/reload this terminal session " + |
||||
"(resh revision: " + commit + |
||||
"; resh revision of this terminal session: " + *requireRevision + |
||||
")") |
||||
os.Exit(3) |
||||
} |
||||
realtimeBefore, err := strconv.ParseFloat(*rtb, 64) |
||||
if err != nil { |
||||
log.Fatal("Flag Parsing error (rtb):", err) |
||||
} |
||||
realtimeSessionStart, err := strconv.ParseFloat(*rtsess, 64) |
||||
if err != nil { |
||||
log.Fatal("Flag Parsing error (rt sess):", err) |
||||
} |
||||
realtimeSessSinceBoot, err := strconv.ParseFloat(*rtsessboot, 64) |
||||
config, errCfg := cfg.New() |
||||
logger, err := logger.New("session-init", config.LogLevel, development) |
||||
if err != nil { |
||||
log.Fatal("Flag Parsing error (rt sess boot):", err) |
||||
} |
||||
realtimeSinceSessionStart := realtimeBefore - realtimeSessionStart |
||||
realtimeSinceBoot := realtimeSessSinceBoot + realtimeSinceSessionStart |
||||
|
||||
timezoneBeforeOffset := collect.GetTimezoneOffsetInSeconds(*timezoneBefore) |
||||
realtimeBeforeLocal := realtimeBefore + timezoneBeforeOffset |
||||
|
||||
if *osReleaseID == "" { |
||||
*osReleaseID = "linux" |
||||
fmt.Printf("Error while creating logger: %v", err) |
||||
} |
||||
if *osReleaseName == "" { |
||||
*osReleaseName = "Linux" |
||||
defer logger.Sync() // flushes buffer, if any
|
||||
if errCfg != nil { |
||||
logger.Error("Error while getting configuration", zap.Error(errCfg)) |
||||
} |
||||
if *osReleasePrettyName == "" { |
||||
*osReleasePrettyName = "Linux" |
||||
} |
||||
|
||||
rec := records.Record{ |
||||
// posix
|
||||
Cols: *cols, |
||||
Lines: *lines, |
||||
// core
|
||||
BaseRecord: records.BaseRecord{ |
||||
Shell: *shell, |
||||
Uname: *uname, |
||||
SessionID: *sessionID, |
||||
|
||||
// posix
|
||||
Home: *home, |
||||
Lang: *lang, |
||||
LcAll: *lcAll, |
||||
Login: *login, |
||||
// Path: *path,
|
||||
ShellEnv: *shellEnv, |
||||
Term: *term, |
||||
|
||||
// non-posix
|
||||
Pid: *pid, |
||||
SessionPID: *sessionPid, |
||||
Host: *host, |
||||
Hosttype: *hosttype, |
||||
Ostype: *ostype, |
||||
Machtype: *machtype, |
||||
Shlvl: *shlvl, |
||||
|
||||
// before after
|
||||
TimezoneBefore: *timezoneBefore, |
||||
|
||||
RealtimeBefore: realtimeBefore, |
||||
RealtimeBeforeLocal: realtimeBeforeLocal, |
||||
|
||||
RealtimeSinceSessionStart: realtimeSinceSessionStart, |
||||
RealtimeSinceBoot: realtimeSinceBoot, |
||||
out := output.New(logger, "resh-collect ERROR") |
||||
|
||||
MachineID: collect.ReadFileContent(machineIDPath), |
||||
args := opt.HandleVersionOpts(out, os.Args, version, commit) |
||||
|
||||
OsReleaseID: *osReleaseID, |
||||
OsReleaseVersionID: *osReleaseVersionID, |
||||
OsReleaseIDLike: *osReleaseIDLike, |
||||
OsReleaseName: *osReleaseName, |
||||
OsReleasePrettyName: *osReleasePrettyName, |
||||
flags := pflag.NewFlagSet("", pflag.ExitOnError) |
||||
sessionID := flags.String("session-id", "", "Resh generated session ID") |
||||
sessionPID := flags.Int("session-pid", -1, "$$ - Shell session PID") |
||||
flags.Parse(args) |
||||
|
||||
ReshUUID: collect.ReadFileContent(reshUUIDPath), |
||||
ReshVersion: version, |
||||
ReshRevision: commit, |
||||
}, |
||||
rec := recordint.SessionInit{ |
||||
SessionID: *sessionID, |
||||
SessionPID: *sessionPID, |
||||
} |
||||
collect.SendRecord(rec, strconv.Itoa(config.Port), "/session_init") |
||||
collect.SendSessionInit(out, rec, strconv.Itoa(config.Port)) |
||||
} |
||||
|
||||
@ -1,5 +0,0 @@ |
||||
port = 2627 |
||||
sesswatchPeriodSeconds = 120 |
||||
sesshistInitHistorySize = 1000 |
||||
debug = false |
||||
bindControlR = true |
||||
@ -1,7 +0,0 @@ |
||||
# copyright information |
||||
|
||||
Whitelist contains content from variety of sources. |
||||
|
||||
Part of the whitelist (`./whitelist.txt`) is made of copyrighted content from [FileInfo.com](https://fileinfo.com/filetypes/common). |
||||
|
||||
This content was used with permission from FileInfo.com. |
||||
File diff suppressed because it is too large
Load Diff
@ -1,23 +1,30 @@ |
||||
module github.com/curusarn/resh |
||||
|
||||
go 1.16 |
||||
go 1.19 |
||||
|
||||
require ( |
||||
github.com/BurntSushi/toml v0.4.1 |
||||
github.com/awesome-gocui/gocui v1.0.0 |
||||
github.com/coreos/go-semver v0.3.0 |
||||
github.com/gdamore/tcell/v2 v2.4.0 // indirect |
||||
github.com/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629 |
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect |
||||
github.com/mattn/go-runewidth v0.0.13 // indirect |
||||
github.com/mattn/go-shellwords v1.0.12 |
||||
github.com/mb-14/gomarkov v0.0.0-20210216094942-a5b484cc0243 |
||||
github.com/BurntSushi/toml v1.2.1 |
||||
github.com/awesome-gocui/gocui v1.1.0 |
||||
github.com/google/uuid v1.3.0 |
||||
github.com/mattn/go-isatty v0.0.17 |
||||
github.com/mitchellh/go-ps v1.0.0 |
||||
github.com/schollz/progressbar v1.0.0 |
||||
github.com/spf13/cobra v1.2.1 |
||||
github.com/spf13/cobra v1.6.1 |
||||
github.com/spf13/pflag v1.0.5 |
||||
github.com/whilp/git-urls v1.0.0 |
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 |
||||
golang.org/x/sys v0.0.0-20210903071746-97244b99971b // indirect |
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect |
||||
golang.org/x/text v0.3.7 // indirect |
||||
go.uber.org/zap v1.24.0 |
||||
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 |
||||
) |
||||
|
||||
require ( |
||||
github.com/gdamore/encoding v1.0.0 // indirect |
||||
github.com/gdamore/tcell/v2 v2.6.0 // indirect |
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect |
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect |
||||
github.com/mattn/go-runewidth v0.0.14 // indirect |
||||
github.com/rivo/uniseg v0.4.4 // indirect |
||||
go.uber.org/atomic v1.10.0 // indirect |
||||
go.uber.org/multierr v1.9.0 // indirect |
||||
golang.org/x/sys v0.5.0 // indirect |
||||
golang.org/x/term v0.5.0 // indirect |
||||
golang.org/x/text v0.7.0 // indirect |
||||
) |
||||
|
||||
@ -0,0 +1,191 @@ |
||||
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 |
||||
} |
||||
|
||||
// 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 because both memory overhead of watched sessions
|
||||
// and the CPU overhead of checking them are quite 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 |
||||
} |
||||
|
||||
// defaults for config
|
||||
var defaults = Config{ |
||||
Port: 2627, |
||||
LogLevel: zap.InfoLevel, |
||||
BindControlR: true, |
||||
|
||||
Debug: false, |
||||
SessionWatchPeriodSeconds: 600, |
||||
ReshHistoryMinSize: 1000, |
||||
} |
||||
|
||||
const headerComment = `## |
||||
###################### |
||||
## RESH config (v1) ## |
||||
###################### |
||||
## Here you can find info about RESH configuration options. |
||||
## You can uncomment the options and customize them. |
||||
|
||||
## Required. |
||||
## The config format can change in future versions. |
||||
## ConfigVersion helps us seamlessly 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 startup |
||||
# 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 workaround, 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 |
||||
|
||||
` |
||||
|
||||
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 |
||||
} |
||||
|
||||
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 |
||||
} |
||||
|
||||
// GetPath returns path to config
|
||||
// Shouldn't be necessary for basic use
|
||||
func GetPath() (string, error) { |
||||
return getConfigPath() |
||||
} |
||||
@ -0,0 +1,100 @@ |
||||
package cfg |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
|
||||
"github.com/BurntSushi/toml" |
||||
) |
||||
|
||||
// Migrate old config versions to current config version
|
||||
// returns true if any changes were made to the config
|
||||
func Migrate() (bool, error) { |
||||
fpath, err := getConfigPath() |
||||
if err != nil { |
||||
return false, fmt.Errorf("could not get config file path: %w", err) |
||||
} |
||||
configF, err := readConfig(fpath) |
||||
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, fpath) |
||||
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,92 @@ |
||||
package check |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
"os/exec" |
||||
"strconv" |
||||
"strings" |
||||
) |
||||
|
||||
func LoginShell() (string, error) { |
||||
shellPath, found := os.LookupEnv("SHELL") |
||||
if !found { |
||||
return "", fmt.Errorf("env variable $SHELL is not set") |
||||
} |
||||
parts := strings.Split(shellPath, "/") |
||||
shell := parts[len(parts)-1] |
||||
if shell != "bash" && shell != "zsh" { |
||||
return fmt.Sprintf("Current shell (%s) is unsupported\n", shell), nil |
||||
} |
||||
return "", nil |
||||
} |
||||
|
||||
func msgShellVersion(shell, expectedVer, actualVer string) string { |
||||
return fmt.Sprintf( |
||||
"Minimal supported %s version is %s. You have %s.\n"+ |
||||
" -> Update to %s %s+ if you want to use RESH with it", |
||||
shell, expectedVer, actualVer, |
||||
shell, expectedVer, |
||||
) |
||||
} |
||||
|
||||
func BashVersion() (string, error) { |
||||
out, err := exec.Command("bash", "-c", "echo $BASH_VERSION").Output() |
||||
if err != nil { |
||||
return "", fmt.Errorf("command failed: %w", err) |
||||
} |
||||
verStr := strings.TrimSuffix(string(out), "\n") |
||||
ver, err := parseVersion(verStr) |
||||
if err != nil { |
||||
return "", fmt.Errorf("failed to parse version: %w", err) |
||||
} |
||||
|
||||
if ver.Major < 4 || (ver.Major == 4 && ver.Minor < 3) { |
||||
return msgShellVersion("bash", "4.3", verStr), nil |
||||
} |
||||
return "", nil |
||||
} |
||||
|
||||
func ZshVersion() (string, error) { |
||||
out, err := exec.Command("zsh", "-c", "echo $ZSH_VERSION").Output() |
||||
if err != nil { |
||||
return "", fmt.Errorf("command failed: %w", err) |
||||
} |
||||
verStr := strings.TrimSuffix(string(out), "\n") |
||||
ver, err := parseVersion(string(out)) |
||||
if err != nil { |
||||
return "", fmt.Errorf("failed to parse version: %w", err) |
||||
} |
||||
|
||||
if ver.Major < 5 { |
||||
return msgShellVersion("zsh", "5.0", verStr), nil |
||||
} |
||||
return "", nil |
||||
} |
||||
|
||||
type version struct { |
||||
Major int |
||||
Minor int |
||||
Rest string |
||||
} |
||||
|
||||
func parseVersion(str string) (version, error) { |
||||
parts := strings.SplitN(str, ".", 3) |
||||
if len(parts) < 3 { |
||||
return version{}, fmt.Errorf("not enough parts") |
||||
} |
||||
major, err := strconv.Atoi(parts[0]) |
||||
if err != nil { |
||||
return version{}, fmt.Errorf("failed to parse major version: %w", err) |
||||
} |
||||
minor, err := strconv.Atoi(parts[1]) |
||||
if err != nil { |
||||
return version{}, fmt.Errorf("failed to parse minor version: %w", err) |
||||
} |
||||
ver := version{ |
||||
Major: major, |
||||
Minor: minor, |
||||
Rest: parts[2], |
||||
} |
||||
return ver, 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.FatalE("Error while encoding record", err) |
||||
} |
||||
|
||||
req, err := http.NewRequest("POST", "http://localhost:"+port+path, |
||||
bytes.NewBuffer(recJSON)) |
||||
if err != nil { |
||||
out.FatalE("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.FatalE("Error while encoding record", err) |
||||
} |
||||
|
||||
req, err := http.NewRequest("POST", "http://localhost:"+port+"/session_init", |
||||
bytes.NewBuffer(recJSON)) |
||||
if err != nil { |
||||
out.FatalE("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,36 @@ |
||||
package datadir |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
"path" |
||||
) |
||||
|
||||
// Maybe there is a better place for this constant
|
||||
const HistoryFileName = "history.reshjson" |
||||
|
||||
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,145 @@ |
||||
// device implements helpers that get/set device config files
|
||||
package device |
||||
|
||||
import ( |
||||
"bufio" |
||||
"fmt" |
||||
"os" |
||||
"path" |
||||
"strings" |
||||
|
||||
"github.com/curusarn/resh/internal/futil" |
||||
"github.com/curusarn/resh/internal/output" |
||||
"github.com/google/uuid" |
||||
isatty "github.com/mattn/go-isatty" |
||||
) |
||||
|
||||
const fnameID = "device-id" |
||||
const fnameName = "device-name" |
||||
|
||||
const fpathIDLegacy = ".resh/resh-uuid" |
||||
|
||||
const filePerm = 0644 |
||||
|
||||
// Getters
|
||||
|
||||
func GetID(dataDir string) (string, error) { |
||||
return readValue(dataDir, fnameID) |
||||
} |
||||
|
||||
func GetName(dataDir string) (string, error) { |
||||
return readValue(dataDir, fnameName) |
||||
} |
||||
|
||||
// Install helpers
|
||||
|
||||
func SetupID(dataDir string) error { |
||||
return setIDIfUnset(dataDir) |
||||
} |
||||
|
||||
func SetupName(out *output.Output, dataDir string) error { |
||||
return promptAndWriteNameIfUnset(out, dataDir) |
||||
} |
||||
|
||||
func readValue(dataDir, fname string) (string, error) { |
||||
fpath := path.Join(dataDir, fname) |
||||
dat, err := os.ReadFile(fpath) |
||||
if err != nil { |
||||
return "", fmt.Errorf("could not read file with %s: %w", fname, err) |
||||
} |
||||
val := strings.TrimRight(string(dat), "\n") |
||||
return val, nil |
||||
} |
||||
|
||||
func setIDIfUnset(dataDir string) error { |
||||
fpath := path.Join(dataDir, fnameID) |
||||
exists, err := futil.FileExists(fpath) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if exists { |
||||
return nil |
||||
} |
||||
|
||||
// Try copy device ID from legacy location
|
||||
homeDir, err := os.UserHomeDir() |
||||
if err != nil { |
||||
return fmt.Errorf("could not get user home: %w", err) |
||||
} |
||||
fpathLegacy := path.Join(homeDir, fpathIDLegacy) |
||||
exists, err = futil.FileExists(fpath) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if exists { |
||||
futil.CopyFile(fpathLegacy, fpath) |
||||
if err != nil { |
||||
return fmt.Errorf("could not copy device ID from legacy location: %w", err) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// Generate new device ID
|
||||
rnd, err := uuid.NewRandom() |
||||
if err != nil { |
||||
return fmt.Errorf("could not get new random source: %w", err) |
||||
} |
||||
id := rnd.String() |
||||
if id == "" { |
||||
return fmt.Errorf("got invalid UUID from package") |
||||
} |
||||
err = os.WriteFile(fpath, []byte(id), filePerm) |
||||
if err != nil { |
||||
return fmt.Errorf("could not write generated ID to file: %w", err) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func promptAndWriteNameIfUnset(out *output.Output, dataDir string) error { |
||||
fpath := path.Join(dataDir, fnameName) |
||||
exists, err := futil.FileExists(fpath) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if exists { |
||||
return nil |
||||
} |
||||
|
||||
name, err := promptForName(out, fpath) |
||||
if err != nil { |
||||
return fmt.Errorf("error while prompting for input: %w", err) |
||||
} |
||||
err = os.WriteFile(fpath, []byte(name), filePerm) |
||||
if err != nil { |
||||
return fmt.Errorf("could not write name to file: %w", err) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func promptForName(out *output.Output, fpath string) (string, error) { |
||||
// This function should be only ran from install-utils with attached terminal
|
||||
if !isatty.IsTerminal(os.Stdout.Fd()) { |
||||
return "", fmt.Errorf("output is not a terminal - write name of this device to '%s' to bypass this error", fpath) |
||||
} |
||||
host, err := os.Hostname() |
||||
if err != nil { |
||||
return "", fmt.Errorf("could not get hostname (prompt default): %w", err) |
||||
} |
||||
hostStub := strings.Split(host, ".")[0] |
||||
fmt.Printf("\nPlease choose a short name for this device (default: '%s'): ", hostStub) |
||||
var input string |
||||
scanner := bufio.NewScanner(os.Stdin) |
||||
if scanner.Scan() { |
||||
input = scanner.Text() |
||||
} |
||||
if err = scanner.Err(); err != nil { |
||||
return "", fmt.Errorf("scanner error: %w", err) |
||||
} |
||||
if input == "" { |
||||
out.Info("Got no input - using default ...") |
||||
input = hostStub |
||||
} |
||||
out.Info(fmt.Sprintf("Device name set to '%s'", input)) |
||||
fmt.Printf("You can change the device name at any time by editing '%s' file\n", fpath) |
||||
return input, nil |
||||
} |
||||
@ -0,0 +1,14 @@ |
||||
package epochtime |
||||
|
||||
import ( |
||||
"fmt" |
||||
"time" |
||||
) |
||||
|
||||
func TimeToString(t time.Time) string { |
||||
return fmt.Sprintf("%.2f", float64(t.UnixMilli())/1000) |
||||
} |
||||
|
||||
func Now() string { |
||||
return TimeToString(time.Now()) |
||||
} |
||||
@ -0,0 +1,18 @@ |
||||
package epochtime |
||||
|
||||
import ( |
||||
"strconv" |
||||
"testing" |
||||
"time" |
||||
) |
||||
|
||||
func TestConversion(t *testing.T) { |
||||
epochTime := "1672702332.64" |
||||
seconds, err := strconv.ParseFloat(epochTime, 64) |
||||
if err != nil { |
||||
t.Fatal("Test setup failed: Failed to convert constant") |
||||
} |
||||
if TimeToString(time.UnixMilli(int64(seconds*1000))) != epochTime { |
||||
t.Fatal("EpochTime changed during conversion") |
||||
} |
||||
} |
||||
@ -0,0 +1,113 @@ |
||||
// futil implements common file-related utilities
|
||||
package futil |
||||
|
||||
import ( |
||||
"fmt" |
||||
"io" |
||||
"os" |
||||
"time" |
||||
) |
||||
|
||||
func CopyFile(source, dest string) error { |
||||
from, err := os.Open(source) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer from.Close() |
||||
|
||||
// This is equivalent to: os.OpenFile(dest, os.O_RDWR|os.O_CREATE, 0666)
|
||||
to, err := os.Create(dest) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = io.Copy(to, from) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return to.Close() |
||||
} |
||||
|
||||
func FileExists(fpath string) (bool, error) { |
||||
_, err := os.Stat(fpath) |
||||
if err == nil { |
||||
// File exists
|
||||
return true, nil |
||||
} |
||||
if os.IsNotExist(err) { |
||||
// File doesn't exist
|
||||
return false, nil |
||||
} |
||||
// Any other error
|
||||
return false, fmt.Errorf("could not stat file: %w", err) |
||||
} |
||||
|
||||
// TouchFile touches file
|
||||
// Returns true if file was created false otherwise
|
||||
func TouchFile(fpath string) (bool, error) { |
||||
exists, err := FileExists(fpath) |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
|
||||
file, err := os.OpenFile(fpath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666) |
||||
if err != nil { |
||||
return false, fmt.Errorf("could not open/create file: %w", err) |
||||
} |
||||
err = file.Close() |
||||
if err != nil { |
||||
return false, fmt.Errorf("could not close file: %w", err) |
||||
} |
||||
return !exists, nil |
||||
} |
||||
|
||||
func getBackupPath(fpath string) string { |
||||
ext := fmt.Sprintf(".backup-%d", time.Now().Unix()) |
||||
return fpath + ext |
||||
} |
||||
|
||||
// BackupFile backups file using unique suffix
|
||||
// Returns path to backup
|
||||
func BackupFile(fpath string) (*RestorableFile, error) { |
||||
fpathBackup := getBackupPath(fpath) |
||||
exists, err := FileExists(fpathBackup) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if exists { |
||||
return nil, fmt.Errorf("backup already exists in the determined path") |
||||
} |
||||
err = CopyFile(fpath, fpathBackup) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed to copy file: %w ", err) |
||||
} |
||||
rf := RestorableFile{ |
||||
Path: fpath, |
||||
PathBackup: fpathBackup, |
||||
} |
||||
return &rf, nil |
||||
} |
||||
|
||||
type RestorableFile struct { |
||||
Path string |
||||
PathBackup string |
||||
} |
||||
|
||||
func (r RestorableFile) Restore() error { |
||||
return restoreFileFromBackup(r.Path, r.PathBackup) |
||||
} |
||||
|
||||
func restoreFileFromBackup(fpath, fpathBak string) error { |
||||
exists, err := FileExists(fpathBak) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if !exists { |
||||
return fmt.Errorf("backup not found in given path: no such file or directory: %s", fpathBak) |
||||
} |
||||
err = CopyFile(fpathBak, fpath) |
||||
if err != nil { |
||||
return fmt.Errorf("failed to copy file: %w ", err) |
||||
} |
||||
return nil |
||||
} |
||||
@ -1,31 +1,34 @@ |
||||
package histcli |
||||
|
||||
import ( |
||||
"github.com/curusarn/resh/pkg/records" |
||||
"github.com/curusarn/resh/internal/recordint" |
||||
"github.com/curusarn/resh/record" |
||||
"go.uber.org/zap" |
||||
) |
||||
|
||||
// Histcli is a dump of history preprocessed for resh cli purposes
|
||||
type Histcli struct { |
||||
// list of records
|
||||
List []records.CliRecord |
||||
List []recordint.SearchApp |
||||
|
||||
sugar *zap.SugaredLogger |
||||
} |
||||
|
||||
// New Histcli
|
||||
func New() Histcli { |
||||
func New(sugar *zap.SugaredLogger) Histcli { |
||||
return Histcli{} |
||||
} |
||||
|
||||
// AddRecord to the histcli
|
||||
func (h *Histcli) AddRecord(record records.Record) { |
||||
enriched := records.Enriched(record) |
||||
cli := records.NewCliRecord(enriched) |
||||
func (h *Histcli) AddRecord(rec *record.V1) { |
||||
cli := recordint.NewSearchApp(h.sugar, rec) |
||||
|
||||
h.List = append(h.List, cli) |
||||
} |
||||
|
||||
// AddCmdLine to the histcli
|
||||
func (h *Histcli) AddCmdLine(cmdline string) { |
||||
cli := records.NewCliRecordFromCmdLine(cmdline) |
||||
cli := recordint.NewSearchAppFromCmdLine(cmdline) |
||||
|
||||
h.List = append(h.List, cli) |
||||
} |
||||
@ -0,0 +1,283 @@ |
||||
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 goroutines
|
||||
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) *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.New(sugar), |
||||
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 []record.V1) { |
||||
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.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.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.Fatalf("Failed to read history file: %v", 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 { |
||||
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 |
||||
} |
||||
|
||||
recV1 := record.V1(rec) |
||||
func() { |
||||
cmdLine := rec.CmdLine |
||||
h.bashCmdLines.AddCmdLine(cmdLine) |
||||
h.zshCmdLines.AddCmdLine(cmdLine) |
||||
h.cliRecords.AddRecord(&recV1) |
||||
}() |
||||
|
||||
h.rio.AppendToFile(h.historyPath, []record.V1{recV1}) |
||||
} |
||||
|
||||
// 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
|
||||
// }
|
||||
// }
|
||||
|
||||
// DumpCliRecords returns enriched records
|
||||
func (h *Histfile) DumpCliRecords() histcli.Histcli { |
||||
// don't forget locks in the future
|
||||
return h.cliRecords |
||||
} |
||||
|
||||
func loadCmdLines(sugar *zap.SugaredLogger, recs []record.V1) 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].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/record" |
||||
"go.uber.org/zap" |
||||
) |
||||
|
||||
type histfile struct { |
||||
sugar *zap.SugaredLogger |
||||
// deviceID string
|
||||
path string |
||||
|
||||
mu sync.RWMutex |
||||
data []record.V1 |
||||
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,43 @@ |
||||
package histio |
||||
|
||||
import ( |
||||
"path" |
||||
|
||||
"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, development string) (*zap.Logger, error) { |
||||
dataDir, err := datadir.MakePath() |
||||
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 = development == "true" // DPanic panics in development
|
||||
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,31 @@ |
||||
package normalize |
||||
|
||||
import ( |
||||
"net/url" |
||||
"strings" |
||||
|
||||
giturls "github.com/whilp/git-urls" |
||||
"go.uber.org/zap" |
||||
) |
||||
|
||||
// GitRemote helper
|
||||
// Returns normalized git remote - valid even on error
|
||||
func GitRemote(sugar *zap.SugaredLogger, gitRemote string) string { |
||||
if len(gitRemote) == 0 { |
||||
return "" |
||||
} |
||||
gitRemote = strings.TrimSuffix(gitRemote, ".git") |
||||
parsedURL, err := giturls.Parse(gitRemote) |
||||
if err != nil { |
||||
sugar.Errorw("Failed to parse git remote", zap.Error(err), |
||||
"gitRemote", gitRemote, |
||||
) |
||||
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,51 @@ |
||||
package normalize_test |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/curusarn/resh/internal/normalize" |
||||
"go.uber.org/zap" |
||||
) |
||||
|
||||
// TestLeftCutPadString
|
||||
func TestGitRemote(t *testing.T) { |
||||
sugar := zap.NewNop().Sugar() |
||||
|
||||
data := [][]string{ |
||||
{ |
||||
"git@github.com:curusarn/resh.git", // git
|
||||
"git@github.com:curusarn/resh", // git no ".git"
|
||||
"http://github.com/curusarn/resh.git", // http
|
||||
"https://github.com/curusarn/resh.git", // https
|
||||
"ssh://git@github.com/curusarn/resh.git", // ssh
|
||||
"git+ssh://git@github.com/curusarn/resh.git", // git+ssh
|
||||
}, |
||||
{ |
||||
"git@host.example.com:org/user/repo.git", // git
|
||||
"git@host.example.com:org/user/repo", // git no ".git"
|
||||
"http://host.example.com/org/user/repo.git", // http
|
||||
"https://host.example.com/org/user/repo.git", // https
|
||||
"ssh://git@host.example.com/org/user/repo.git", // ssh
|
||||
"git+ssh://git@host.example.com/org/user/repo.git", // git+ssh
|
||||
}, |
||||
} |
||||
|
||||
for _, arr := range data { |
||||
n := len(arr) |
||||
for i := 0; i < n-1; i++ { |
||||
for j := i + 1; j < n; j++ { |
||||
one := normalize.GitRemote(sugar, arr[i]) |
||||
two := normalize.GitRemote(sugar, arr[j]) |
||||
if one != two { |
||||
t.Fatalf("Normalized git remotes should match for '%s' and '%s'\n -> got '%s' != '%s'", |
||||
arr[i], arr[j], one, two) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
empty := normalize.GitRemote(sugar, "") |
||||
if len(empty) != 0 { |
||||
t.Fatalf("Normalized git remotes for '' should be ''\n -> got '%s'", empty) |
||||
} |
||||
} |
||||
@ -0,0 +1,36 @@ |
||||
package opt |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
|
||||
"github.com/curusarn/resh/internal/output" |
||||
) |
||||
|
||||
// HandleVersionOpts reads the first option and handles it
|
||||
// This is a helper for resh-{collect,postcollect,session-init} commands
|
||||
func HandleVersionOpts(out *output.Output, args []string, version, commit string) []string { |
||||
if len(os.Args) == 0 { |
||||
return os.Args[1:] |
||||
} |
||||
// We use go-like options because of backwards compatibility.
|
||||
// Not ideal but we should support them because they have worked once
|
||||
// and adding "more correct" variants would mean supporting more variants.
|
||||
switch os.Args[1] { |
||||
case "-version": |
||||
fmt.Print(version) |
||||
os.Exit(0) |
||||
case "-revision": |
||||
fmt.Print(commit) |
||||
os.Exit(0) |
||||
case "-requireVersion": |
||||
if len(os.Args) < 3 { |
||||
out.FatalTerminalVersionMismatch(version, "") |
||||
} |
||||
if os.Args[2] != version { |
||||
out.FatalTerminalVersionMismatch(version, os.Args[2]) |
||||
} |
||||
return os.Args[3:] |
||||
} |
||||
return os.Args[1:] |
||||
} |
||||
@ -0,0 +1,143 @@ |
||||
package output |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
|
||||
"go.uber.org/zap" |
||||
) |
||||
|
||||
// Output wrapper for writing 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, |
||||
} |
||||
} |
||||
|
||||
// Info outputs string to stdout and to log (as info)
|
||||
// This is how we write output to users from interactive commands
|
||||
// This way we have full record in logs
|
||||
func (f *Output) Info(msg string) { |
||||
fmt.Printf("%s\n", msg) |
||||
f.Logger.Info(msg) |
||||
} |
||||
|
||||
// InfoE outputs string to stdout and to log (as error)
|
||||
// Passed error is only written to log
|
||||
// This is how we output errors to users from interactive commands
|
||||
// This way we have errors in logs
|
||||
func (f *Output) InfoE(msg string, err error) { |
||||
fmt.Printf("%s\n", msg) |
||||
f.Logger.Error(msg, zap.Error(err)) |
||||
} |
||||
|
||||
// Error outputs string to stderr and to log (as error)
|
||||
// This is how we output errors from non-interactive commands
|
||||
func (f *Output) Error(msg string) { |
||||
fmt.Fprintf(os.Stderr, "%s: %s\n", f.ErrPrefix, msg) |
||||
f.Logger.Error(msg) |
||||
} |
||||
|
||||
// ErrorE outputs string and error to stderr and to log (as error)
|
||||
// This is how we output errors from non-interactive commands
|
||||
func (f *Output) ErrorE(msg string, err error) { |
||||
fmt.Fprintf(os.Stderr, "%s: %s: %v\n", f.ErrPrefix, msg, err) |
||||
f.Logger.Error(msg, zap.Error(err)) |
||||
} |
||||
|
||||
// FatalE outputs string and error to stderr and to log (as fatal)
|
||||
// This is how we raise fatal errors from non-interactive commands
|
||||
func (f *Output) FatalE(msg string, err error) { |
||||
fmt.Fprintf(os.Stderr, "%s: %s: %v\n", f.ErrPrefix, msg, err) |
||||
f.Logger.Fatal(msg, zap.Error(err)) |
||||
} |
||||
|
||||
var msgDaemonNotRunning = `RESH daemon didn't respond - it's probably not running. |
||||
-> Start RESH daemon manually - run: resh-daemon-start |
||||
-> Or restart this terminal window to bring RESH daemon back up |
||||
-> You can check 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 msgTerminalVersionMismatch = `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 |
||||
|
||||
` |
||||
|
||||
var msgDaemonVersionMismatch = `RESH daemon is running in different version than is installed now. |
||||
It looks like something went wrong during RESH update. |
||||
-> Kill resh-daemon and then launch a new terminal window to fix that: killall resh-daemon |
||||
-> You can create an issue at: https://github.com/curusarn/resh/issues
|
||||
|
||||
` |
||||
|
||||
func (f *Output) InfoDaemonNotRunning(err error) { |
||||
fmt.Printf("%s", msgDaemonNotRunning) |
||||
f.Logger.Error("Daemon is not running", zap.Error(err)) |
||||
} |
||||
|
||||
func (f *Output) ErrorDaemonNotRunning(err error) { |
||||
fmt.Fprintf(os.Stderr, "%s: %s", f.ErrPrefix, msgDaemonNotRunning) |
||||
f.Logger.Error("Daemon is not running", zap.Error(err)) |
||||
} |
||||
|
||||
func (f *Output) FatalDaemonNotRunning(err error) { |
||||
fmt.Fprintf(os.Stderr, "%s: %s", f.ErrPrefix, msgDaemonNotRunning) |
||||
f.Logger.Fatal("Daemon is not running", zap.Error(err)) |
||||
} |
||||
|
||||
func (f *Output) InfoTerminalVersionMismatch(installedVer, terminalVer string) { |
||||
fmt.Printf("%s(installed version: %s, this terminal version: %s)\n\n", |
||||
msgTerminalVersionMismatch, installedVer, terminalVer) |
||||
f.Logger.Fatal("Version mismatch", |
||||
zap.String("installed", installedVer), |
||||
zap.String("terminal", terminalVer)) |
||||
} |
||||
|
||||
func (f *Output) ErrorTerminalVersionMismatch(installedVer, terminalVer string) { |
||||
fmt.Fprintf(os.Stderr, "%s: %s(installed version: %s, this terminal version: %s)\n\n", |
||||
f.ErrPrefix, msgTerminalVersionMismatch, installedVer, terminalVer) |
||||
f.Logger.Fatal("Version mismatch", |
||||
zap.String("installed", installedVer), |
||||
zap.String("terminal", terminalVer)) |
||||
} |
||||
|
||||
func (f *Output) FatalTerminalVersionMismatch(installedVer, terminalVer string) { |
||||
fmt.Fprintf(os.Stderr, "%s: %s(installed version: %s, this terminal version: %s)\n\n", |
||||
f.ErrPrefix, msgTerminalVersionMismatch, installedVer, terminalVer) |
||||
f.Logger.Fatal("Version mismatch", |
||||
zap.String("installed", installedVer), |
||||
zap.String("terminal", terminalVer)) |
||||
} |
||||
|
||||
func (f *Output) InfoDaemonVersionMismatch(installedVer, daemonVer string) { |
||||
fmt.Printf("%s(installed version: %s, running daemon version: %s)\n\n", |
||||
msgDaemonVersionMismatch, installedVer, daemonVer) |
||||
f.Logger.Error("Version mismatch", |
||||
zap.String("installed", installedVer), |
||||
zap.String("daemon", daemonVer)) |
||||
} |
||||
|
||||
func (f *Output) ErrorDaemonVersionMismatch(installedVer, daemonVer string) { |
||||
fmt.Fprintf(os.Stderr, "%s: %s(installed version: %s, running daemon version: %s)\n\n", |
||||
f.ErrPrefix, msgDaemonVersionMismatch, installedVer, daemonVer) |
||||
f.Logger.Error("Version mismatch", |
||||
zap.String("installed", installedVer), |
||||
zap.String("daemon", daemonVer)) |
||||
} |
||||
|
||||
func (f *Output) FatalDaemonVersionMismatch(installedVer, daemonVer string) { |
||||
fmt.Fprintf(os.Stderr, "%s: %s(installed version: %s, running daemon version: %s)\n\n", |
||||
f.ErrPrefix, msgDaemonVersionMismatch, installedVer, daemonVer) |
||||
f.Logger.Fatal("Version mismatch", |
||||
zap.String("installed", installedVer), |
||||
zap.String("daemon", daemonVer)) |
||||
} |
||||
@ -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,144 @@ |
||||
package recio |
||||
|
||||
import ( |
||||
"bufio" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
"os" |
||||
"strings" |
||||
|
||||
"github.com/curusarn/resh/internal/futil" |
||||
"github.com/curusarn/resh/internal/recconv" |
||||
"github.com/curusarn/resh/record" |
||||
"go.uber.org/zap" |
||||
) |
||||
|
||||
func (r *RecIO) ReadAndFixFile(fpath string, maxErrors int) ([]record.V1, error) { |
||||
recs, decodeErrs, err := r.ReadFile(fpath) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
numErrs := len(decodeErrs) |
||||
if numErrs > maxErrors { |
||||
r.sugar.Errorw("Encountered too many decoding errors", |
||||
"errorsCount", numErrs, |
||||
"individualErrors", "<Search 'Error while decoding line' to see individual errors>", |
||||
) |
||||
return nil, fmt.Errorf("encountered too many decoding errors, last error: %w", decodeErrs[len(decodeErrs)-1]) |
||||
} |
||||
if numErrs == 0 { |
||||
return recs, nil |
||||
} |
||||
|
||||
r.sugar.Warnw("Some history records could not be decoded - fixing RESH history file by dropping them", |
||||
"corruptedRecords", numErrs, |
||||
"lastError", decodeErrs[len(decodeErrs)-1], |
||||
"individualErrors", "<Search 'Error while decoding line' to see individual errors>", |
||||
) |
||||
|
||||
fpathBak := fpath + ".bak" |
||||
r.sugar.Infow("Backing up current corrupted history file", |
||||
"historyFileBackup", fpathBak, |
||||
) |
||||
err = futil.CopyFile(fpath, fpathBak) |
||||
if err != nil { |
||||
r.sugar.Errorw("Failed to create a backup history file - aborting fixing history file", |
||||
"historyFileBackup", fpathBak, |
||||
zap.Error(err), |
||||
) |
||||
return recs, nil |
||||
} |
||||
r.sugar.Info("Writing resh history file without errors ...") |
||||
err = r.OverwriteFile(fpath, recs) |
||||
if err != nil { |
||||
r.sugar.Errorw("Failed write fixed history file - restoring history file from backup", |
||||
"historyFile", fpath, |
||||
zap.Error(err), |
||||
) |
||||
|
||||
err = futil.CopyFile(fpathBak, fpath) |
||||
if err != nil { |
||||
r.sugar.Errorw("Failed restore history file from backup", |
||||
"historyFile", fpath, |
||||
"HistoryFileBackup", fpathBak, |
||||
zap.Error(err), |
||||
) |
||||
} |
||||
} |
||||
return recs, nil |
||||
} |
||||
|
||||
func (r *RecIO) ReadFile(fpath string) ([]record.V1, []error, error) { |
||||
var recs []record.V1 |
||||
file, err := os.Open(fpath) |
||||
if err != nil { |
||||
return nil, nil, fmt.Errorf("failed to open history file: %w", err) |
||||
} |
||||
defer file.Close() |
||||
|
||||
reader := bufio.NewReader(file) |
||||
var decodeErrs []error |
||||
for { |
||||
var line string |
||||
line, err = reader.ReadString('\n') |
||||
if err != nil { |
||||
break |
||||
} |
||||
rec, err := r.decodeLine(line) |
||||
if err != nil { |
||||
r.sugar.Errorw("Error while decoding line", zap.Error(err), |
||||
"filePath", fpath, |
||||
"line", line, |
||||
) |
||||
decodeErrs = append(decodeErrs, err) |
||||
continue |
||||
} |
||||
recs = append(recs, *rec) |
||||
} |
||||
if err != io.EOF { |
||||
r.sugar.Error("Error while reading file", zap.Error(err)) |
||||
return recs, decodeErrs, err |
||||
} |
||||
r.sugar.Infow("Loaded resh history records", |
||||
"recordCount", len(recs), |
||||
) |
||||
return recs, decodeErrs, nil |
||||
} |
||||
|
||||
func (r *RecIO) decodeLine(line string) (*record.V1, error) { |
||||
idx := strings.Index(line, "{") |
||||
if idx == -1 { |
||||
return nil, fmt.Errorf("no opening 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,64 @@ |
||||
package recio |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"os" |
||||
|
||||
"github.com/curusarn/resh/record" |
||||
) |
||||
|
||||
func (r *RecIO) OverwriteFile(fpath string, recs []record.V1) error { |
||||
file, err := os.Create(fpath) |
||||
if err != nil { |
||||
return fmt.Errorf("could not create/truncate file: %w", err) |
||||
} |
||||
err = writeRecords(file, recs) |
||||
if err != nil { |
||||
return fmt.Errorf("error while writing records: %w", err) |
||||
} |
||||
err = file.Close() |
||||
if err != nil { |
||||
return fmt.Errorf("could not close file: %w", err) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
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 fmt.Errorf("could not open/create file: %w", err) |
||||
} |
||||
err = writeRecords(file, recs) |
||||
if err != nil { |
||||
return fmt.Errorf("error while writing records: %w", err) |
||||
} |
||||
err = file.Close() |
||||
if err != nil { |
||||
return fmt.Errorf("could not close file: %w", err) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func writeRecords(file *os.File, recs []record.V1) error { |
||||
for _, rec := range recs { |
||||
jsn, err := encodeV1Record(rec) |
||||
if err != nil { |
||||
return fmt.Errorf("could not encode record: %w", err) |
||||
} |
||||
_, err = file.Write(jsn) |
||||
if err != nil { |
||||
return fmt.Errorf("could not write json: %w", 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,2 @@ |
||||
// Package recordint provides internal record types that are passed between resh components
|
||||
package recordint |
||||
@ -0,0 +1,56 @@ |
||||
package recordint |
||||
|
||||
import ( |
||||
"strconv" |
||||
|
||||
"github.com/curusarn/resh/internal/normalize" |
||||
"github.com/curusarn/resh/record" |
||||
"go.uber.org/zap" |
||||
) |
||||
|
||||
// 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 |
||||
} |
||||
|
||||
func NewSearchAppFromCmdLine(cmdLine string) SearchApp { |
||||
return SearchApp{ |
||||
IsRaw: true, |
||||
CmdLine: cmdLine, |
||||
} |
||||
} |
||||
|
||||
// The error handling here could be better
|
||||
func NewSearchApp(sugar *zap.SugaredLogger, r *record.V1) SearchApp { |
||||
time, err := strconv.ParseFloat(r.Time, 64) |
||||
if err != nil { |
||||
sugar.Errorw("Error while parsing time as float", zap.Error(err), |
||||
"time", time) |
||||
} |
||||
return SearchApp{ |
||||
IsRaw: false, |
||||
SessionID: r.SessionID, |
||||
CmdLine: r.CmdLine, |
||||
Host: r.Device, |
||||
Pwd: r.Pwd, |
||||
Home: r.Home, |
||||
// TODO: is this the right place to normalize the git remote?
|
||||
GitOriginRemote: normalize.GitRemote(sugar, r.GitOriginRemote), |
||||
ExitCode: r.ExitCode, |
||||
Time: time, |
||||
} |
||||
} |
||||
@ -0,0 +1,84 @@ |
||||
package records |
||||
|
||||
// DEPRECATION NOTICE: This package should be removed in favor of:
|
||||
// - record: public record definitions
|
||||
// - recordint: internal record definitions
|
||||
// - recutil: record-related utils
|
||||
|
||||
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 new 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 session - 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,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("Timeouted 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, syscall.SIGHUP) |
||||
|
||||
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,49 @@ |
||||
package status |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
"net/http" |
||||
"strconv" |
||||
|
||||
"github.com/curusarn/resh/internal/httpclient" |
||||
"github.com/curusarn/resh/internal/msg" |
||||
) |
||||
|
||||
func get(port int) (*http.Response, error) { |
||||
url := "http://localhost:" + strconv.Itoa(port) + "/status" |
||||
client := httpclient.New() |
||||
resp, err := client.Get(url) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("error while GET'ing daemon /status: %w", err) |
||||
} |
||||
return resp, nil |
||||
} |
||||
|
||||
func IsDaemonRunning(port int) (bool, error) { |
||||
resp, err := get(port) |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
defer resp.Body.Close() |
||||
return true, nil |
||||
} |
||||
|
||||
func GetDaemonStatus(port int) (*msg.StatusResponse, error) { |
||||
resp, err := get(port) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
jsn, err := io.ReadAll(resp.Body) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("error while reading 'daemon /status' response: %w", err) |
||||
} |
||||
var msgResp msg.StatusResponse |
||||
err = json.Unmarshal(jsn, &msgResp) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("error while decoding 'daemon /status' response: %w", err) |
||||
} |
||||
return &msgResp, 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,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 |
||||
} |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue