From 3518e187ee76328df265301b77bc4958b5b3dbf4 Mon Sep 17 00:00:00 2001 From: Stu Leak Date: Wed, 31 Dec 2025 18:00:34 -0500 Subject: [PATCH] feat(logging): Add panic recovery and error logging for UI crashes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added Error() and Fatal() logging functions for non-debug errors - Added Panic() function to log panics with full stack traces - Added RecoverPanic() for defer statements to catch crashes - Added panic recovery to main() function - Added panic recovery to queue job processing goroutine - All panics now logged to videotools.log with timestamps and stack traces - Helps diagnose UI crashes that occur during FFmpeg processing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- internal/logging/logging.go | 48 +++++++++++++++++++++++++++++++++++++ internal/queue/queue.go | 3 +++ main.go | 1 + 3 files changed, 52 insertions(+) diff --git a/internal/logging/logging.go b/internal/logging/logging.go index db71dcc..aade5e4 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "os" + "runtime/debug" "time" ) @@ -80,3 +81,50 @@ func FilePath() string { func History() []string { return history } + +// Error logs an error message with a category (always logged, even when debug is off) +func Error(cat Category, format string, args ...interface{}) { + msg := fmt.Sprintf("%s ERROR: %s", cat, fmt.Sprintf(format, args...)) + timestamp := time.Now().Format(time.RFC3339Nano) + if file != nil { + fmt.Fprintf(file, "%s %s\n", timestamp, msg) + } + history = append(history, fmt.Sprintf("%s %s", timestamp, msg)) + if len(history) > historyMax { + history = history[len(history)-historyMax:] + } + logger.Printf("%s %s", timestamp, msg) +} + +// Fatal logs a fatal error and exits (always logged) +func Fatal(cat Category, format string, args ...interface{}) { + msg := fmt.Sprintf("%s FATAL: %s", cat, fmt.Sprintf(format, args...)) + timestamp := time.Now().Format(time.RFC3339Nano) + if file != nil { + fmt.Fprintf(file, "%s %s\n", timestamp, msg) + file.Sync() + } + logger.Printf("%s %s", timestamp, msg) + os.Exit(1) +} + +// Panic logs a panic with stack trace +func Panic(recovered interface{}) { + msg := fmt.Sprintf("%s PANIC: %v\nStack trace:\n%s", CatSystem, recovered, string(debug.Stack())) + timestamp := time.Now().Format(time.RFC3339Nano) + if file != nil { + fmt.Fprintf(file, "%s %s\n", timestamp, msg) + file.Sync() + } + history = append(history, fmt.Sprintf("%s %s", timestamp, msg)) + logger.Printf("%s %s", timestamp, msg) +} + +// RecoverPanic should be used with defer to catch and log panics +func RecoverPanic() { + if r := recover(); r != nil { + Panic(r) + // Re-panic to let the program crash with the logged info + panic(r) + } +} diff --git a/internal/queue/queue.go b/internal/queue/queue.go index dd852a3..32825d9 100644 --- a/internal/queue/queue.go +++ b/internal/queue/queue.go @@ -8,6 +8,8 @@ import ( "path/filepath" "sync" "time" + + "git.leaktechnologies.dev/stu/VideoTools/internal/logging" ) // JobType represents the type of job to execute @@ -373,6 +375,7 @@ func (q *Queue) ResumeAll() { // processJobs continuously processes pending jobs func (q *Queue) processJobs() { + defer logging.RecoverPanic() // Catch and log any panics in job processing for { q.mu.Lock() if !q.running { diff --git a/main.go b/main.go index 8dd3f14..4d104de 100644 --- a/main.go +++ b/main.go @@ -5945,6 +5945,7 @@ func (s *appState) stopPlayer() { func main() { logging.Init() defer logging.Close() + defer logging.RecoverPanic() // Catch and log any panics with stack trace flag.Parse() logging.SetDebug(*debugFlag || os.Getenv("VIDEOTOOLS_DEBUG") != "")