fix(webkit): implement graceful shutdown on window close with GTK4 close-request signal handler and OS signal handling to prevent process hanging
- commit
- b9db1df87bbd22d84f39601fbf8aec570a73ae05
- author
- Brice · · 2025-09-10 19:57 UTC
- parent
- 3b55dd8
- signature
- unsigned
3 files · +38 -2
M main.go +16 -0
| @@ -10,10 +10,12 @@ import ( | |||
| 10 | 10 | neturl "net/url" | |
| 11 | 11 | "os" | |
| 12 | 12 | "os/exec" | |
| 13 | + | "os/signal" | |
| 13 | 14 | "path/filepath" | |
| 14 | 15 | "runtime" | |
| 15 | 16 | "strconv" | |
| 16 | 17 | "strings" | |
| 18 | + | "syscall" | |
| 17 | 19 | ||
| 18 | 20 | "github.com/bnema/dumber/internal/cli" | |
| 19 | 21 | "github.com/bnema/dumber/internal/config" | |
| @@ -566,13 +568,27 @@ func runBrowser() { | |||
| 566 | 568 | // Zoom handled natively in webkit package (built-in shortcuts) | |
| 567 | 569 | } | |
| 568 | 570 | ||
| 571 | + | // Set up signal handling for graceful shutdown | |
| 572 | + | sigChan := make(chan os.Signal, 1) | |
| 573 | + | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) | |
| 574 | + | ||
| 569 | 575 | // Enter GTK main loop only when native backend is available. | |
| 570 | 576 | if webkit.IsNativeAvailable() { | |
| 577 | + | // Handle signals in a goroutine to quit the main loop | |
| 578 | + | go func() { | |
| 579 | + | sig := <-sigChan | |
| 580 | + | log.Printf("Received signal %v - shutting down gracefully", sig) | |
| 581 | + | webkit.QuitMainLoop() | |
| 582 | + | }() | |
| 583 | + | ||
| 571 | 584 | log.Printf("Entering GTK main loop…") | |
| 572 | 585 | webkit.RunMainLoop() | |
| 573 | 586 | log.Printf("GTK main loop exited") | |
| 574 | 587 | } else { | |
| 575 | 588 | log.Printf("Not entering GUI loop (non-CGO build)") | |
| 589 | + | // In non-CGO mode, just wait for signals | |
| 590 | + | sig := <-sigChan | |
| 591 | + | log.Printf("Received signal %v - exiting", sig) | |
| 576 | 592 | } | |
| 577 | 593 | } | |
| 578 | 594 | ||
| @@ -10,10 +10,12 @@ import ( | |||||
| 10 | neturl "net/url" | 10 | neturl "net/url" | ||
| 11 | "os" | 11 | "os" | ||
| 12 | "os/exec" | 12 | "os/exec" | ||
| 13 | + | "os/signal" | |||
| 13 | "path/filepath" | 14 | "path/filepath" | ||
| 14 | "runtime" | 15 | "runtime" | ||
| 15 | "strconv" | 16 | "strconv" | ||
| 16 | "strings" | 17 | "strings" | ||
| 18 | + | "syscall" | |||
| 17 | 19 | ||||
| 18 | "github.com/bnema/dumber/internal/cli" | 20 | "github.com/bnema/dumber/internal/cli" | ||
| 19 | "github.com/bnema/dumber/internal/config" | 21 | "github.com/bnema/dumber/internal/config" | ||
| @@ -566,13 +568,27 @@ func runBrowser() { | |||||
| 566 | // Zoom handled natively in webkit package (built-in shortcuts) | 568 | // Zoom handled natively in webkit package (built-in shortcuts) | ||
| 567 | } | 569 | } | ||
| 568 | 570 | ||||
| 571 | + | // Set up signal handling for graceful shutdown | |||
| 572 | + | sigChan := make(chan os.Signal, 1) | |||
| 573 | + | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) | |||
| 574 | + | ||||
| 569 | // Enter GTK main loop only when native backend is available. | 575 | // Enter GTK main loop only when native backend is available. | ||
| 570 | if webkit.IsNativeAvailable() { | 576 | if webkit.IsNativeAvailable() { | ||
| 577 | + | // Handle signals in a goroutine to quit the main loop | |||
| 578 | + | go func() { | |||
| 579 | + | sig := <-sigChan | |||
| 580 | + | log.Printf("Received signal %v - shutting down gracefully", sig) | |||
| 581 | + | webkit.QuitMainLoop() | |||
| 582 | + | }() | |||
| 583 | + | ||||
| 571 | log.Printf("Entering GTK main loop…") | 584 | log.Printf("Entering GTK main loop…") | ||
| 572 | webkit.RunMainLoop() | 585 | webkit.RunMainLoop() | ||
| 573 | log.Printf("GTK main loop exited") | 586 | log.Printf("GTK main loop exited") | ||
| 574 | } else { | 587 | } else { | ||
| 575 | log.Printf("Not entering GUI loop (non-CGO build)") | 588 | log.Printf("Not entering GUI loop (non-CGO build)") | ||
| 589 | + | // In non-CGO mode, just wait for signals | |||
| 590 | + | sig := <-sigChan | |||
| 591 | + | log.Printf("Received signal %v - exiting", sig) | |||
| 576 | } | 592 | } | ||
| 577 | } | 593 | } | ||
| 578 | 594 | ||||
M pkg/webkit/keyboard_cgo.go +6 -0
| @@ -350,3 +350,9 @@ func goOnButtonPress(id C.ulong, button C.guint, state C.GdkModifierType) { | |||
| 350 | 350 | _ = vw.GoForward() | |
| 351 | 351 | } | |
| 352 | 352 | } | |
| 353 | + | ||
| 354 | + | //export goQuitMainLoop | |
| 355 | + | func goQuitMainLoop() { | |
| 356 | + | log.Printf("[webkit] Window close requested - quitting main loop") | |
| 357 | + | QuitMainLoop() | |
| 358 | + | } | |
| @@ -350,3 +350,9 @@ func goOnButtonPress(id C.ulong, button C.guint, state C.GdkModifierType) { | |||||
| 350 | _ = vw.GoForward() | 350 | _ = vw.GoForward() | ||
| 351 | } | 351 | } | ||
| 352 | } | 352 | } | ||
| 353 | + | ||||
| 354 | + | //export goQuitMainLoop | |||
| 355 | + | func goQuitMainLoop() { | |||
| 356 | + | log.Printf("[webkit] Window close requested - quitting main loop") | |||
| 357 | + | QuitMainLoop() | |||
| 358 | + | } | |||
M pkg/webkit/webview_cgo.go +16 -2
| @@ -14,8 +14,22 @@ package webkit | |||
| 14 | 14 | #include <gio/gio.h> | |
| 15 | 15 | ||
| 16 | 16 | static GtkWidget* new_window() { return GTK_WIDGET(gtk_window_new()); } | |
| 17 | - | // GTK4: gtk_main_quit removed; leave as no-op for now (Quit handled in Go) | |
| 18 | - | static void connect_destroy_quit(GtkWidget* w) { (void)w; } | |
| 17 | + | ||
| 18 | + | // Forward declaration | |
| 19 | + | extern void goQuitMainLoop(); | |
| 20 | + | ||
| 21 | + | // GTK4 close-request signal handler | |
| 22 | + | static gboolean on_close_request(GtkWindow* window, gpointer user_data) { | |
| 23 | + | (void)window; (void)user_data; | |
| 24 | + | goQuitMainLoop(); | |
| 25 | + | return FALSE; // Allow the window to close | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // Connect window close signal to quit main loop | |
| 29 | + | static void connect_destroy_quit(GtkWidget* w) { | |
| 30 | + | if (!w) return; | |
| 31 | + | g_signal_connect(G_OBJECT(w), "close-request", G_CALLBACK(on_close_request), NULL); | |
| 32 | + | } | |
| 19 | 33 | static WebKitWebView* as_webview(GtkWidget* w) { return WEBKIT_WEB_VIEW(w); } | |
| 20 | 34 | // WebsiteDataManager creation will be handled via GTK4/WebKit6 APIs in Go code. | |
| 21 | 35 | ||
| @@ -14,8 +14,22 @@ package webkit | |||||
| 14 | #include <gio/gio.h> | 14 | #include <gio/gio.h> | ||
| 15 | 15 | ||||
| 16 | static GtkWidget* new_window() { return GTK_WIDGET(gtk_window_new()); } | 16 | static GtkWidget* new_window() { return GTK_WIDGET(gtk_window_new()); } | ||
| 17 | - | // GTK4: gtk_main_quit removed; leave as no-op for now (Quit handled in Go) | 17 | + | |
| 18 | - | static void connect_destroy_quit(GtkWidget* w) { (void)w; } | 18 | + | // Forward declaration |
| 19 | + | extern void goQuitMainLoop(); | |||
| 20 | + | ||||
| 21 | + | // GTK4 close-request signal handler | |||
| 22 | + | static gboolean on_close_request(GtkWindow* window, gpointer user_data) { | |||
| 23 | + | (void)window; (void)user_data; | |||
| 24 | + | goQuitMainLoop(); | |||
| 25 | + | return FALSE; // Allow the window to close | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // Connect window close signal to quit main loop | |||
| 29 | + | static void connect_destroy_quit(GtkWidget* w) { | |||
| 30 | + | if (!w) return; | |||
| 31 | + | g_signal_connect(G_OBJECT(w), "close-request", G_CALLBACK(on_close_request), NULL); | |||
| 32 | + | } | |||
| 19 | static WebKitWebView* as_webview(GtkWidget* w) { return WEBKIT_WEB_VIEW(w); } | 33 | static WebKitWebView* as_webview(GtkWidget* w) { return WEBKIT_WEB_VIEW(w); } | ||
| 20 | // WebsiteDataManager creation will be handled via GTK4/WebKit6 APIs in Go code. | 34 | // WebsiteDataManager creation will be handled via GTK4/WebKit6 APIs in Go code. | ||
| 21 | 35 | ||||