diff --git a/README.md b/README.md index 442e8a6..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,5 +0,0 @@ -Installation: - -``` -export PROMPT_COMMAND='~/.hishtory-client saveHistoryEntry $? "`history 1`"' -``` diff --git a/clients/local/client.go b/clients/local/client.go index 8e44398..9c10cfd 100644 --- a/clients/local/client.go +++ b/clients/local/client.go @@ -22,7 +22,6 @@ func main() { shared.CheckFatalError(shared.Setup(os.Args)) case "install": shared.CheckFatalError(shared.Install()) - shared.CheckFatalError(shared.Setup(os.Args)) case "enable": shared.CheckFatalError(shared.Enable()) case "disable": diff --git a/clients/remote/client.go b/clients/remote/client.go index 4fe254a..23ddd4a 100644 --- a/clients/remote/client.go +++ b/clients/remote/client.go @@ -25,7 +25,6 @@ func main() { case "init": shared.CheckFatalError(shared.Setup(os.Args)) case "install": - shared.CheckFatalError(shared.Setup(os.Args)) shared.CheckFatalError(shared.Install()) case "enable": shared.CheckFatalError(shared.Enable()) diff --git a/server/server_test.go b/server/server_test.go index 9502c2e..2ef0ea4 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -17,7 +17,7 @@ func TestSubmitThenQuery(t *testing.T) { shared.Check(t, shared.Setup([]string{})) // Submit an entry - entry, err := shared.BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls / "}) + entry, err := shared.BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls / ", "1641774958326745663"}) shared.Check(t, err) reqBody, err := json.Marshal(entry) shared.Check(t, err) @@ -63,7 +63,7 @@ func TestNoUserSecretGivesNoResults(t *testing.T) { shared.Check(t, shared.Setup([]string{})) // Submit an entry - entry, err := shared.BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls / "}) + entry, err := shared.BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls / ", "1641774958326745663"}) shared.Check(t, err) reqBody, err := json.Marshal(entry) shared.Check(t, err) @@ -91,14 +91,14 @@ func TestSearchQuery(t *testing.T) { shared.Check(t, shared.Setup([]string{})) // Submit an entry that we'll match - entry, err := shared.BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls /bar "}) + entry, err := shared.BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls /bar ", "1641774958326745663"}) shared.Check(t, err) reqBody, err := json.Marshal(entry) shared.Check(t, err) submitReq := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(reqBody)) apiSubmitHandler(nil, submitReq) // Submit an entry that we won't match - entry, err = shared.BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls /foo "}) + entry, err = shared.BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls /foo ", "1641774958326745663"}) shared.Check(t, err) reqBody, err = json.Marshal(entry) shared.Check(t, err) diff --git a/shared/client.go b/shared/client.go index 9865896..7c62144 100644 --- a/shared/client.go +++ b/shared/client.go @@ -1,6 +1,8 @@ package shared import ( + _ "embed" + "encoding/json" "fmt" "io" @@ -24,6 +26,11 @@ const ( CONFIG_PATH = ".hishtory.config" ) +var ( + //go:embed config.sh + CONFIG_SH_CONTENTS string +) + func getCwd() (string, error) { cwd, err := os.Getwd() if err != nil { @@ -63,7 +70,12 @@ func BuildHistoryEntry(args []string) (*HistoryEntry, error) { } entry.CurrentWorkingDirectory = cwd - // TODO(ddworken): start time + // start time + nanos, err := strconv.ParseInt(args[4], 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse start time %s as int: %v", args[4], err) + } + entry.StartTime = time.Unix(0, nanos) // end time entry.EndTime = time.Now() @@ -119,17 +131,19 @@ func Setup(args []string) error { func DisplayResults(results []*HistoryEntry, displayHostname bool) { headerFmt := color.New(color.FgGreen, color.Underline).SprintfFunc() - tbl := table.New("CWD", "Timestamp", "Exit Code", "Command") + tbl := table.New("CWD", "Timestamp", "Runtime", "Exit Code", "Command") if displayHostname { - tbl = table.New("Hostname", "CWD", "Timestamp", "Exit Code", "Command") + tbl = table.New("Hostname", "CWD", "Timestamp", "Runtime", "Exit Code", "Command") } tbl.WithHeaderFormatter(headerFmt) for _, result := range results { + timestamp := result.StartTime.Format("Jan 2 2006 15:04:05 MST") + duration := result.EndTime.Sub(result.StartTime).Round(time.Millisecond).String() if displayHostname { - tbl.AddRow(result.Hostname, result.CurrentWorkingDirectory, result.EndTime.Format("Jan 2 2006 15:04:05 MST"), result.ExitCode, result.Command) + tbl.AddRow(result.Hostname, result.CurrentWorkingDirectory, timestamp, duration, result.ExitCode, result.Command) } else { - tbl.AddRow(result.CurrentWorkingDirectory, result.EndTime.Format("Jan 2 2006 15:04:05 MST"), result.ExitCode, result.Command) + tbl.AddRow(result.CurrentWorkingDirectory, timestamp, duration, result.ExitCode, result.Command) } } @@ -206,10 +220,6 @@ func CheckFatalError(err error) { } } -const ( - PROMPT_COMMAND = "export PROMPT_COMMAND='%s saveHistoryEntry $? \"`history 1`\"'" -) - func Install() error { homedir, err := os.UserHomeDir() if err != nil { @@ -219,24 +229,41 @@ func Install() error { if err != nil { return err } - return configureBashrc(homedir, path) + err = configureBashrc(homedir, path) + if err != nil { + return err + } + _, err = GetConfig() + if err != nil { + // No config, so set up a new installation + return Setup(os.Args) + } + return nil } func configureBashrc(homedir, binaryPath string) error { - promptCommand := fmt.Sprintf(PROMPT_COMMAND, binaryPath) + // Check if we need to configure the bashrc bashrc, err := ioutil.ReadFile(path.Join(homedir, ".bashrc")) if err != nil { return fmt.Errorf("failed to read bashrc: %v", err) } - if strings.Contains(string(bashrc), promptCommand) { + if strings.Contains(string(bashrc), "# Hishtory Config:") { return nil } + // Create the file we're going to source in our bashrc + bashConfigPath := path.Join(filepath.Dir(binaryPath), "config.sh") + err = ioutil.WriteFile(bashConfigPath, []byte(CONFIG_SH_CONTENTS), 0644) + if err != nil { + return fmt.Errorf("failed to write config.sh file: %v", err) + } + + // Add to bashrc f, err := os.OpenFile(path.Join(homedir, ".bashrc"), os.O_APPEND|os.O_WRONLY, 0644) if err != nil { return fmt.Errorf("failed to append to bashrc: %v", err) } defer f.Close() - _, err = f.WriteString(string(bashrc) + "\n# Hishtory Config:\nexport PATH=\"$PATH:" + filepath.Dir(binaryPath) + "\"\n" + promptCommand + "\n") + _, err = f.WriteString("\n# Hishtory Config:\nexport PATH=\"$PATH:" + filepath.Dir(binaryPath) + "\"\nsource " + bashConfigPath + "\n") if err != nil { return fmt.Errorf("failed to append to bashrc: %v", err) } diff --git a/shared/client_test.go b/shared/client_test.go index 6fcbb81..03a537d 100644 --- a/shared/client_test.go +++ b/shared/client_test.go @@ -5,6 +5,7 @@ import ( "path" "strings" "testing" + "time" ) func TestSetup(t *testing.T) { @@ -28,7 +29,7 @@ func TestSetup(t *testing.T) { func TestBuildHistoryEntry(t *testing.T) { defer BackupAndRestore(t) Check(t, Setup([]string{})) - entry, err := BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls / "}) + entry, err := BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls / ", "1641774958326745663"}) Check(t, err) if entry.UserSecret == "" || len(entry.UserSecret) < 10 || strings.TrimSpace(entry.UserSecret) != entry.UserSecret { t.Fatalf("history entry has unexpected user secret: %v", entry.UserSecret) @@ -45,6 +46,9 @@ func TestBuildHistoryEntry(t *testing.T) { if entry.Command != "ls /" { t.Fatalf("history entry has unexpected command: %v", entry.Command) } + if entry.StartTime.Format(time.RFC3339) != "2022-01-09T16:35:58-08:00" { + t.Fatalf("history entry has incorrect start time: %v", entry.StartTime.Format(time.RFC3339)) + } } func TestGetUserSecret(t *testing.T) { diff --git a/shared/config.sh b/shared/config.sh new file mode 100644 index 0000000..92eabd8 --- /dev/null +++ b/shared/config.sh @@ -0,0 +1,27 @@ +# This script should be sourced inside of .bashrc to integrate bash with hishtory + +# Implementation of PreCommand and PostCommand based on https://jichu4n.com/posts/debug-trap-and-prompt_command-in-bash/ +function PreCommand() { + if [ -z "$HISHTORY_AT_PROMPT" ]; then + return + fi + unset HISHTORY_AT_PROMPT + + # Run before every command + HISHTORY_START_TIME=`date +%s%N` +} +trap "PreCommand" DEBUG + +HISHTORY_FIRST_PROMPT=1 +function PostCommand() { + HISHTORY_AT_PROMPT=1 + + if [ -n "$HISHTORY_FIRST_PROMPT" ]; then + unset HISHTORY_FIRST_PROMPT + return + fi + + # Run after every prompt + hishtory saveHistoryEntry $? "`history 1`" $HISHTORY_START_TIME +} +PROMPT_COMMAND="PostCommand" diff --git a/shared/data.go b/shared/data.go index 547ae7e..3c855b0 100644 --- a/shared/data.go +++ b/shared/data.go @@ -2,7 +2,6 @@ package shared import ( "fmt" - "log" "os" "path" "strings" @@ -28,7 +27,6 @@ const ( ) func Persist(entry HistoryEntry) error { - log.Printf("Saving %#v to the DB\n", entry) db, err := OpenDB() if err != nil { return err @@ -46,7 +44,7 @@ func OpenDB() (*gorm.DB, error) { } db, err := gorm.Open(sqlite.Open(path.Join(homedir, DB_PATH)), &gorm.Config{}) if err != nil { - panic("failed to connect database") + return nil, fmt.Errorf("failed to connect to the DB: %v", err) } db.AutoMigrate(&HistoryEntry{}) return db, nil @@ -65,6 +63,8 @@ func Search(db *gorm.DB, userSecret, query string, limit int) ([]*HistoryEntry, val := splitToken[1] // tx = tx.Where() panic("TODO(ddworken): Use " + field + val) + } else if strings.HasPrefix(token, "-") { + panic("TODO(ddworken): Implement -foo as filtering out foo") } else { wildcardedToken := "%" + token + "%" tx = tx.Where("(command LIKE ? OR hostname LIKE ? OR current_working_directory LIKE ?)", wildcardedToken, wildcardedToken, wildcardedToken) diff --git a/shared/data_test.go b/shared/data_test.go index 2cf3142..f4d8ddd 100644 --- a/shared/data_test.go +++ b/shared/data_test.go @@ -7,7 +7,7 @@ import ( func TestPersist(t *testing.T) { defer BackupAndRestore(t) Check(t, Setup([]string{})) - entry, err := BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls / "}) + entry, err := BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls / ", "1641774958326745663"}) Check(t, err) Check(t, Persist(*entry)) @@ -30,13 +30,13 @@ func TestSearch(t *testing.T) { Check(t, Setup([]string{})) // Insert data - entry1, err := BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls / "}) + entry1, err := BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls / ", "1641774958326745663"}) Check(t, err) Check(t, Persist(*entry1)) - entry2, err := BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls /foo "}) + entry2, err := BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls /foo ", "1641774958326745663"}) Check(t, err) Check(t, Persist(*entry2)) - entry3, err := BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 echo hi "}) + entry3, err := BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 echo hi ", "1641774958326745663"}) Check(t, err) Check(t, Persist(*entry3))