diff --git a/cmd/server/main-server.go b/cmd/server/main-server.go index b52d97491c..30540e351f 100644 --- a/cmd/server/main-server.go +++ b/cmd/server/main-server.go @@ -465,6 +465,10 @@ func main() { return } + err = shellutil.FixupWaveZshHistory() + if err != nil { + log.Printf("error fixing up wave zsh history: %v\n", err) + } createMainWshClient() sigutil.InstallShutdownSignalHandlers(doShutdown) sigutil.InstallSIGUSR1Handler() diff --git a/go.mod b/go.mod index ef1927874f..6711ded66a 100644 --- a/go.mod +++ b/go.mod @@ -81,6 +81,7 @@ require ( github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/outrigdev/goid v0.3.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sirupsen/logrus v1.9.3 // indirect diff --git a/go.sum b/go.sum index ee73e96e87..ea416cd09a 100644 --- a/go.sum +++ b/go.sum @@ -146,6 +146,8 @@ github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuE github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/outrigdev/goid v0.3.0 h1:t/otQD3EXc45cLtQVPUnNgEyRaTQA4cPeu3qVcrsIws= +github.com/outrigdev/goid v0.3.0/go.mod h1:hEH7f27ypN/GHWt/7gvkRoFYR0LZizfUBIAbak4neVE= github.com/photostorm/pty v1.1.19-0.20230903182454-31354506054b h1:cLGKfKb1uk0hxI0Q8L83UAJPpeJ+gSpn3cCU/tjd3eg= github.com/photostorm/pty v1.1.19-0.20230903182454-31354506054b/go.mod h1:KO+FcPtyLAiRC0hJwreJVvfwc7vnNz77UxBTIGHdPVk= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= diff --git a/pkg/telemetry/telemetry.go b/pkg/telemetry/telemetry.go index 3a208055b9..24c7541b54 100644 --- a/pkg/telemetry/telemetry.go +++ b/pkg/telemetry/telemetry.go @@ -254,10 +254,13 @@ func RecordTEvent(ctx context.Context, tevent *telemetrydata.TEvent) error { } func CleanOldTEvents(ctx context.Context) error { + daysToKeep := 7 + if !IsTelemetryEnabled() { + daysToKeep = 1 + } + olderThan := time.Now().AddDate(0, 0, -daysToKeep).UnixMilli() return wstore.WithTx(ctx, func(tx *wstore.TxWrap) error { - // delete events older than 28 days query := `DELETE FROM db_tevent WHERE ts < ?` - olderThan := time.Now().AddDate(0, 0, -28).UnixMilli() tx.Exec(query, olderThan) return nil }) diff --git a/pkg/util/envutil/envutil.go b/pkg/util/envutil/envutil.go index c2505e7c47..dff40c1842 100644 --- a/pkg/util/envutil/envutil.go +++ b/pkg/util/envutil/envutil.go @@ -67,3 +67,27 @@ func RmEnv(envStr string, key string) string { delete(envMap, key) return MapToEnv(envMap) } + +func SliceToEnv(env []string) string { + var sb strings.Builder + for _, envVar := range env { + if len(envVar) == 0 { + continue + } + sb.WriteString(envVar) + sb.WriteByte('\x00') + } + return sb.String() +} + +func EnvToSlice(envStr string) []string { + envLines := strings.Split(envStr, "\x00") + result := make([]string, 0, len(envLines)) + for _, line := range envLines { + if len(line) == 0 { + continue + } + result = append(result, line) + } + return result +} diff --git a/pkg/util/shellutil/shellintegration/zsh_zshrc.sh b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh index f24abee181..0c0271a29d 100644 --- a/pkg/util/shellutil/shellintegration/zsh_zshrc.sh +++ b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh @@ -18,6 +18,11 @@ if [[ -n ${_comps+x} ]]; then source <(wsh completion zsh) fi +# fix history (macos) +if [[ "$HISTFILE" == "$WAVETERM_ZDOTDIR/.zsh_history" ]]; then + HISTFILE="$HOME/.zsh_history" +fi + typeset -g _WAVETERM_SI_FIRSTPRECMD=1 # shell integration diff --git a/pkg/util/shellutil/shellutil.go b/pkg/util/shellutil/shellutil.go index dd9fe697a1..a6268bf5dd 100644 --- a/pkg/util/shellutil/shellutil.go +++ b/pkg/util/shellutil/shellutil.go @@ -18,6 +18,7 @@ import ( "sync" "time" + "github.com/wavetermdev/waveterm/pkg/util/envutil" "github.com/wavetermdev/waveterm/pkg/util/utilfn" "github.com/wavetermdev/waveterm/pkg/wavebase" "github.com/wavetermdev/waveterm/pkg/waveobj" @@ -47,6 +48,8 @@ var ( //go:embed shellintegration/pwsh_wavepwsh.sh PwshStartup_wavepwsh string + + ZshExtendedHistoryPattern = regexp.MustCompile(`^: [0-9]+:`) ) const DefaultTermType = "xterm-256color" @@ -74,6 +77,7 @@ const ( PwshIntegrationDir = "shell/pwsh" FishIntegrationDir = "shell/fish" WaveHomeBinDir = "bin" + ZshHistoryFileName = ".zsh_history" ) func DetectLocalShellPath() string { @@ -208,6 +212,46 @@ func GetLocalZshZDotDir() string { return filepath.Join(wavebase.GetWaveDataDir(), ZshIntegrationDir) } +func HasWaveZshHistory() (bool, int64) { + zshDir := GetLocalZshZDotDir() + historyFile := filepath.Join(zshDir, ZshHistoryFileName) + fileInfo, err := os.Stat(historyFile) + if err != nil { + return false, 0 + } + return true, fileInfo.Size() +} + +func IsExtendedZshHistoryFile(fileName string) (bool, error) { + file, err := os.Open(fileName) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, err + } + defer file.Close() + + buf := make([]byte, 1024) + n, err := file.Read(buf) + if err != nil { + return false, err + } + + content := string(buf[:n]) + lines := strings.Split(content, "\n") + + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + return ZshExtendedHistoryPattern.MatchString(line), nil + } + + return false, nil +} + func GetLocalWshBinaryPath(version string, goos string, goarch string) (string, error) { ext := "" if goarch == "amd64" { @@ -422,6 +466,80 @@ func getShellVersion(shellPath string, shellType string) (string, error) { return matches[1], nil } +func FixupWaveZshHistory() error { + if runtime.GOOS != "darwin" { + return nil + } + + hasHistory, size := HasWaveZshHistory() + if !hasHistory { + return nil + } + + zshDir := GetLocalZshZDotDir() + waveHistFile := filepath.Join(zshDir, ZshHistoryFileName) + + if size == 0 { + err := os.Remove(waveHistFile) + if err != nil { + log.Printf("error removing wave zsh history file %s: %v\n", waveHistFile, err) + } + return nil + } + + log.Printf("merging wave zsh history %s into ~/.zsh_history\n", waveHistFile) + + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("error getting home directory: %w", err) + } + realHistFile := filepath.Join(homeDir, ".zsh_history") + + isExtended, err := IsExtendedZshHistoryFile(realHistFile) + if err != nil { + return fmt.Errorf("error checking if history is extended: %w", err) + } + + hasExtendedStr := "false" + if isExtended { + hasExtendedStr = "true" + } + + quotedWaveHistFile := utilfn.ShellQuote(waveHistFile, true, -1) + + script := fmt.Sprintf(` + HISTFILE=~/.zsh_history + HISTSIZE=999999 + SAVEHIST=999999 + has_extended_history=%s + [[ $has_extended_history == true ]] && setopt EXTENDED_HISTORY + fc -RI + fc -RI %s + fc -W + `, hasExtendedStr, quotedWaveHistFile) + + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + cmd := exec.CommandContext(ctx, "zsh", "-f", "-i", "-c", script) + cmd.Stdin = nil + envStr := envutil.SliceToEnv(os.Environ()) + envStr = envutil.RmEnv(envStr, "ZDOTDIR") + cmd.Env = envutil.EnvToSlice(envStr) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("error executing zsh history fixup script: %w, output: %s", err, string(output)) + } + + err = os.Remove(waveHistFile) + if err != nil { + log.Printf("error removing wave zsh history file %s: %v\n", waveHistFile, err) + } + log.Printf("successfully merged wave zsh history %s into ~/.zsh_history\n", waveHistFile) + + return nil +} + func FormatOSC(oscNum int, parts ...string) string { if len(parts) == 0 { return fmt.Sprintf("\x1b]%d\x07", oscNum) diff --git a/pkg/wcloud/wcloud.go b/pkg/wcloud/wcloud.go index 9101b34469..455acd8944 100644 --- a/pkg/wcloud/wcloud.go +++ b/pkg/wcloud/wcloud.go @@ -214,11 +214,11 @@ func sendTEvents(clientId string) (int, error) { } func SendAllTelemetry(clientId string) error { - defer func() { - ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) - defer cancelFn() - telemetry.CleanOldTEvents(ctx) - }() + ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) + defer cancelFn() + if err := telemetry.CleanOldTEvents(ctx); err != nil { + log.Printf("error cleaning old telemetry events: %v\n", err) + } if !telemetry.IsTelemetryEnabled() { log.Printf("telemetry disabled, not sending\n") return nil