package root import ( "fmt" "os" "path/filepath" "runtime" "strings" "sync" "syscall" "time" "github.com/spf13/cobra" "github.com/versenilvis/iris/internal/config" ) // startRescueShell starts a fallback shell if the application crashes to keep the terminal open func startRescueShell() { shell := os.Getenv("SHELL") if shell == "" { shell = "/bin/sh" } env := append(os.Environ(), "IRIS_RESCUE=1") _ = syscall.Exec(shell, []string{shell}, env) } var ( lastCrashFile string lastCrashMu sync.Mutex ) // writeCrashLog writes the crash info and stack trace to a new log file func WriteCrashLog(err any) { dir, errDir := config.CrashDir() if errDir != nil { return } _ = os.MkdirAll(dir, 0755) logFile := filepath.Join(dir, fmt.Sprintf("crash_%s.log", time.Now().Format("20060102_150405"))) lastCrashMu.Lock() lastCrashFile = logFile lastCrashMu.Unlock() f, errOpen := os.OpenFile(logFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) if errOpen != nil { return } defer f.Close() _, _ = fmt.Fprintf(f, "=== IRIS CRASH %s ===\n", time.Now().Format(time.RFC3339)) _, _ = fmt.Fprintf(f, "version: %s\nos: %s/%s\n\n", Version, runtime.GOOS, runtime.GOARCH) _, _ = fmt.Fprintf(f, "panic: %v\n\n", err) var n int buf := make([]byte, 1024) for { n = runtime.Stack(buf, true) if n < len(buf) { break } buf = make([]byte, 2*len(buf)) } _, _ = f.Write(buf[:n]) _, _ = fmt.Fprintln(f) } // getLatestCrashLog returns the path to the newest crash log file func getLatestCrashLog() string { dir, errDir := config.CrashDir() if errDir != nil { return "" } files, err := os.ReadDir(dir) if err != nil || len(files) == 0 { home, errHome := os.UserHomeDir() if errHome == nil { oldLog := filepath.Join(home, ".iris", "crash.log") if _, err := os.Stat(oldLog); err == nil { return oldLog } oldCrashes := filepath.Join(home, ".iris", "crashes") if oldFiles, errOld := os.ReadDir(oldCrashes); errOld == nil && len(oldFiles) > 0 { var latestOld string for _, f := range oldFiles { if f.IsDir() { continue } name := f.Name() if strings.HasPrefix(name, "crash_") && strings.HasSuffix(name, ".log") { if name > latestOld { latestOld = name } } } if latestOld != "" { return filepath.Join(oldCrashes, latestOld) } } } return "" } var latest string for _, f := range files { if f.IsDir() { continue } name := f.Name() if strings.HasPrefix(name, "crash_") && strings.HasSuffix(name, ".log") { if name > latest { latest = name } } } if latest == "" { home, errHome := os.UserHomeDir() if errHome == nil { oldLog := filepath.Join(home, ".iris", "crash.log") if _, err := os.Stat(oldLog); err == nil { return oldLog } } return "" } return filepath.Join(dir, latest) } // printCrashNotice prints the crash notice with absolute path to the log file func printCrashNotice() { lastCrashMu.Lock() logFile := lastCrashFile lastCrashMu.Unlock() if logFile == "" { logFile = getLatestCrashLog() } _, _ = fmt.Fprintf(os.Stderr, "\n\033[31m[IRIS] crashed, report saved to %s\033[0m\n", logFile) } var ( // crashCmd is the cobra command to manage crash logs CrashCmd = &cobra.Command{ Use: "crash-log", Short: "manage iris crash logs", Run: func(cmd *cobra.Command, args []string) { if ClearLog { dir, errDir := config.CrashDir() if errDir == nil { _ = os.RemoveAll(dir) } home, errHome := os.UserHomeDir() if errHome == nil { _ = os.RemoveAll(filepath.Join(home, ".iris", "crashes")) _ = os.Remove(filepath.Join(home, ".iris", "crash.log")) } cmd.Println("crash log cleared") return } logFile := getLatestCrashLog() if logFile == "" { cmd.Println("no crash log found") return } cmd.Println(logFile) }, } // clearLog is the flag to clear the crash log ClearLog bool ) func init() { CrashCmd.Flags().BoolVar(&ClearLog, "clear", false, "clear the crash log") rootCmd.AddCommand(CrashCmd) }