detached 15ae90d

chore: guard GTK thread calls and add CB kill switch for segv triage

commit
15ae90d3259b942f7a930a68859c2520ec3909f3
author
bnema · · 2025-12-07 21:40 UTC
parent
4f987b8
signature
unsigned
5 files · +148 -104
M internal/app/browser/browser.go +11 -7
@@ -359,13 +359,17 @@ func (app *BrowserApp) setupSignalHandling() {
359359 // runMainLoop starts the appropriate main loop based on WebKit availability
360360 func (app *BrowserApp) runMainLoop() {
361361 if webkit.IsNativeAvailable() {
362- // Schedule content blocking initialization to run on the first main loop iteration
363- // This ensures GTK is fully ready before we start loading filters
364- webkit.RunOnMainThread(func() {
365- if err := app.setupContentBlocking(); err != nil {
366- logging.Warn(fmt.Sprintf("Warning: failed to setup content blocking: %v", err))
367- }
368- })
362+ if os.Getenv("DUMBER_DISABLE_CONTENT_BLOCKING") != "1" {
363+ // Schedule content blocking initialization to run on the first main loop iteration
364+ // This ensures GTK is fully ready before we start loading filters
365+ webkit.RunOnMainThread(func() {
366+ if err := app.setupContentBlocking(); err != nil {
367+ logging.Warn(fmt.Sprintf("Warning: failed to setup content blocking: %v", err))
368+ }
369+ })
370+ } else {
371+ logging.Warn("Content blocking skipped (DUMBER_DISABLE_CONTENT_BLOCKING=1)")
372+ }
369373
370374 logging.Info(fmt.Sprintf("Entering GTK main loop…"))
371375 webkit.RunMainLoop()
@@ -359,13 +359,17 @@ func (app *BrowserApp) setupSignalHandling() {
359 // runMainLoop starts the appropriate main loop based on WebKit availability359 // runMainLoop starts the appropriate main loop based on WebKit availability
360 func (app *BrowserApp) runMainLoop() {360 func (app *BrowserApp) runMainLoop() {
361 if webkit.IsNativeAvailable() {361 if webkit.IsNativeAvailable() {
362- // Schedule content blocking initialization to run on the first main loop iteration362+ if os.Getenv("DUMBER_DISABLE_CONTENT_BLOCKING") != "1" {
363- // This ensures GTK is fully ready before we start loading filters363+ // Schedule content blocking initialization to run on the first main loop iteration
364- webkit.RunOnMainThread(func() {364+ // This ensures GTK is fully ready before we start loading filters
365- if err := app.setupContentBlocking(); err != nil {365+ webkit.RunOnMainThread(func() {
366- logging.Warn(fmt.Sprintf("Warning: failed to setup content blocking: %v", err))366+ if err := app.setupContentBlocking(); err != nil {
367- }367+ logging.Warn(fmt.Sprintf("Warning: failed to setup content blocking: %v", err))
368- })368+ }
369+ })
370+ } else {
371+ logging.Warn("Content blocking skipped (DUMBER_DISABLE_CONTENT_BLOCKING=1)")
372+ }
369 373
370 logging.Info(fmt.Sprintf("Entering GTK main loop…"))374 logging.Info(fmt.Sprintf("Entering GTK main loop…"))
371 webkit.RunMainLoop()375 webkit.RunMainLoop()
M internal/app/browser/webview.go +7 -0
@@ -373,6 +373,13 @@ func (app *BrowserApp) initializeFaviconService() error {
373373 func (app *BrowserApp) setupContentBlocking() error {
374374 logging.Info(fmt.Sprintf("Initializing content blocking system..."))
375375
376+ // Temporary kill-switch for crash triage: set env DUMBER_DISABLE_CONTENT_BLOCKING=1
377+ // to skip all blocking setup and run bare WebViews.
378+ if os.Getenv("DUMBER_DISABLE_CONTENT_BLOCKING") == "1" {
379+ logging.Warn("Content blocking disabled via DUMBER_DISABLE_CONTENT_BLOCKING=1 (test mode)")
380+ return nil
381+ }
382+
376383 // Enable WebKit debug logging if requested
377384 webkit.SetupWebKitDebugLogging()
378385
@@ -373,6 +373,13 @@ func (app *BrowserApp) initializeFaviconService() error {
373 func (app *BrowserApp) setupContentBlocking() error {373 func (app *BrowserApp) setupContentBlocking() error {
374 logging.Info(fmt.Sprintf("Initializing content blocking system..."))374 logging.Info(fmt.Sprintf("Initializing content blocking system..."))
375 375
376+ // Temporary kill-switch for crash triage: set env DUMBER_DISABLE_CONTENT_BLOCKING=1
377+ // to skip all blocking setup and run bare WebViews.
378+ if os.Getenv("DUMBER_DISABLE_CONTENT_BLOCKING") == "1" {
379+ logging.Warn("Content blocking disabled via DUMBER_DISABLE_CONTENT_BLOCKING=1 (test mode)")
380+ return nil
381+ }
382+
376 // Enable WebKit debug logging if requested383 // Enable WebKit debug logging if requested
377 webkit.SetupWebKitDebugLogging()384 webkit.SetupWebKitDebugLogging()
378 385
M internal/filtering/service.go +73 -50
@@ -124,6 +124,11 @@ func (s *ContentBlockingService) SetFiltersReady(networkJSON []byte) {
124124 hash := fmt.Sprintf("%x", sha256.Sum256(networkJSON))
125125
126126 s.mu.Lock()
127+ // Update current payload
128+ s.networkFilterJSON = networkJSON
129+ s.networkHash = hash
130+ s.filtersReady = true
131+
127132 // If a compile is already in progress:
128133 if s.compileInProgress {
129134 // If it's the same payload, just ensure cosmetics are injected and return.
@@ -210,6 +215,10 @@ func (s *ContentBlockingService) SetFiltersReady(networkJSON []byte) {
210215 // New payload, no compile in progress: create a fresh completion channel
211216 s.compileDone = make(chan struct{})
212217 s.compileInProgress = true
218+ webViews = make([]*pkgwebkit.WebView, 0, len(s.webViews))
219+ for wv := range s.webViews {
220+ webViews = append(webViews, wv)
221+ }
213222 s.mu.Unlock()
214223
215224 // Inject cosmetic scripts IMMEDIATELY (don't wait for network filter compilation)
@@ -251,6 +260,9 @@ func (s *ContentBlockingService) SetFiltersReady(networkJSON []byte) {
251260 if done != nil {
252261 close(done)
253262 }
263+
264+ // Apply compiled filters to all registered WebViews
265+ s.applyFiltersToRegisteredWebViews()
254266 })
255267 })
256268 }
@@ -283,12 +295,7 @@ func (s *ContentBlockingService) RegisterWebView(wv *pkgwebkit.WebView) {
283295 }
284296
285297 // If network filters are already compiled, apply them immediately (non-blocking)
286- if s.contentBlockingMgr.IsFilterCompiled() {
287- webView := wv // capture for closure
288- s.runWhenWidgetReady(webView, func() {
289- s.applyFiltersToWebView(webView)
290- })
291- }
298+ s.applyFiltersToWebView(wv)
292299 }
293300
294301 // injectCosmeticBaseScript injects the base cosmetic filtering script into a WebView.
@@ -387,24 +394,11 @@ func (s *ContentBlockingService) applyFiltersToWebView(wv *pkgwebkit.WebView) {
387394 s.mu.Unlock()
388395 return
389396 }
390- // Mark as applying to avoid double AddFilter races
391- state.networkApplied = true
392- s.webViews[wv] = state
393- networkJSON := s.networkFilterJSON
394397 s.mu.Unlock()
395398
396- if len(networkJSON) == 0 {
397- // Nothing to apply; reset flag
398- s.mu.Lock()
399- state.networkApplied = false
400- s.webViews[wv] = state
401- s.mu.Unlock()
402- return
403- }
404-
405399 webView := wv // capture for closure
406400 s.runWhenWidgetReady(webView, func() {
407- s.applyNetworkFilters(webView, networkJSON)
401+ s.applyNetworkFilters(webView, s.networkFilterJSON)
408402 })
409403 }
410404
@@ -416,19 +410,9 @@ func (s *ContentBlockingService) applyNetworkFilters(wv *pkgwebkit.WebView, filt
416410
417411 // Double-check compile state before proceeding
418412 if !s.contentBlockingMgr.IsFilterCompiled() {
419- return
420- }
421-
422- gtkView := wv.GetWebView()
423- if gtkView == nil {
424- return
425- }
426-
427- ucm := gtkView.UserContentManager()
428- if ucm == nil {
429- // Reset state so a future ready WebView can retry
413+ // Ensure we retry later once compilation finishes
430414 s.mu.Lock()
431- if state, exists := s.webViews[wv]; exists {
415+ if state, ok := s.webViews[wv]; ok {
432416 state.networkApplied = false
433417 s.webViews[wv] = state
434418 }
@@ -436,30 +420,55 @@ func (s *ContentBlockingService) applyNetworkFilters(wv *pkgwebkit.WebView, filt
436420 return
437421 }
438422
439- // Use shared identifier so all WebViews reuse the same compiled filter
440- const identifier = "adblock-filters"
423+ // Run the application on the GTK main thread to avoid CGO crashes.
424+ pkgwebkit.RunOnMainThread(func() {
425+ if wv.IsDestroyed() {
426+ return
427+ }
441428
442- // Try to apply cached compiled filter first (instant)
443- s.applyMu.Lock()
444- defer s.applyMu.Unlock()
429+ gtkView := wv.GetWebView()
430+ if gtkView == nil {
431+ return
432+ }
433+
434+ ucm := gtkView.UserContentManager()
435+ if ucm == nil {
436+ // Reset state so a future ready WebView can retry
437+ s.mu.Lock()
438+ if state, exists := s.webViews[wv]; exists {
439+ state.networkApplied = false
440+ s.webViews[wv] = state
441+ }
442+ s.mu.Unlock()
443+ return
444+ }
445+
446+ // Use shared identifier so all WebViews reuse the same compiled filter
447+ const identifier = "adblock-filters"
448+
449+ // Try to apply cached compiled filter first (instant)
450+ s.applyMu.Lock()
451+ defer s.applyMu.Unlock()
452+
453+ if !s.contentBlockingMgr.IsFilterCompiled() {
454+ return
455+ }
445456
446- if s.contentBlockingMgr.IsFilterCompiled() {
447457 if err := s.contentBlockingMgr.ApplyCompiledFilter(ucm); err != nil {
448458 logging.Error(fmt.Sprintf("[filtering] Failed to apply cached filter to WebView %d: %v", wv.ID(), err))
449459 return
450460 }
451- } else if len(filterJSON) > 0 {
452- // Fallback: compile if not yet compiled (first WebView)
453- err := s.contentBlockingMgr.ApplyFiltersFromJSON(ucm, identifier, filterJSON)
454- if err != nil {
455- logging.Error(fmt.Sprintf("[filtering] Failed to apply network filters to WebView %d: %v", wv.ID(), err))
456- return
461+
462+ // Mark applied
463+ s.mu.Lock()
464+ if state, ok := s.webViews[wv]; ok {
465+ state.networkApplied = true
466+ s.webViews[wv] = state
457467 }
458- } else {
459- return
460- }
468+ s.mu.Unlock()
461469
462- logging.Debug(fmt.Sprintf("[filtering] Applied network filters to WebView %d", wv.ID()))
470+ logging.Debug(fmt.Sprintf("[filtering] Applied network filters to WebView %d", wv.ID()))
471+ })
463472 }
464473
465474 // setupCosmeticHooks sets up hooks for cosmetic filtering on navigation events.
@@ -476,12 +485,26 @@ func (s *ContentBlockingService) setupCosmeticHooks(wv *pkgwebkit.WebView) {
476485 }
477486 s.runWhenWidgetReady(webView, func() {
478487 s.injectCosmeticRules(webView, uri)
479- // Apply network filters lazily on navigation when compilation is ready
480- s.applyFiltersToWebView(webView)
481488 })
482489 })
483490 }
484491
492+// applyFiltersToRegisteredWebViews applies compiled network filters to all registered WebViews.
493+func (s *ContentBlockingService) applyFiltersToRegisteredWebViews() {
494+ s.mu.RLock()
495+ webViews := make([]*pkgwebkit.WebView, 0, len(s.webViews))
496+ for wv := range s.webViews {
497+ webViews = append(webViews, wv)
498+ }
499+ s.mu.RUnlock()
500+
501+ for _, wv := range webViews {
502+ if wv != nil && !wv.IsDestroyed() {
503+ s.applyFiltersToWebView(wv)
504+ }
505+ }
506+}
507+
485508 // injectCosmeticRules injects cosmetic filtering rules for the current page.
486509 func (s *ContentBlockingService) injectCosmeticRules(wv *pkgwebkit.WebView, uri string) {
487510 if wv == nil || wv.IsDestroyed() || uri == "" {
@@ -124,6 +124,11 @@ func (s *ContentBlockingService) SetFiltersReady(networkJSON []byte) {
124 hash := fmt.Sprintf("%x", sha256.Sum256(networkJSON))124 hash := fmt.Sprintf("%x", sha256.Sum256(networkJSON))
125 125
126 s.mu.Lock()126 s.mu.Lock()
127+ // Update current payload
128+ s.networkFilterJSON = networkJSON
129+ s.networkHash = hash
130+ s.filtersReady = true
131+
127 // If a compile is already in progress:132 // If a compile is already in progress:
128 if s.compileInProgress {133 if s.compileInProgress {
129 // If it's the same payload, just ensure cosmetics are injected and return.134 // If it's the same payload, just ensure cosmetics are injected and return.
@@ -210,6 +215,10 @@ func (s *ContentBlockingService) SetFiltersReady(networkJSON []byte) {
210 // New payload, no compile in progress: create a fresh completion channel215 // New payload, no compile in progress: create a fresh completion channel
211 s.compileDone = make(chan struct{})216 s.compileDone = make(chan struct{})
212 s.compileInProgress = true217 s.compileInProgress = true
218+ webViews = make([]*pkgwebkit.WebView, 0, len(s.webViews))
219+ for wv := range s.webViews {
220+ webViews = append(webViews, wv)
221+ }
213 s.mu.Unlock()222 s.mu.Unlock()
214 223
215 // Inject cosmetic scripts IMMEDIATELY (don't wait for network filter compilation)224 // Inject cosmetic scripts IMMEDIATELY (don't wait for network filter compilation)
@@ -251,6 +260,9 @@ func (s *ContentBlockingService) SetFiltersReady(networkJSON []byte) {
251 if done != nil {260 if done != nil {
252 close(done)261 close(done)
253 }262 }
263+
264+ // Apply compiled filters to all registered WebViews
265+ s.applyFiltersToRegisteredWebViews()
254 })266 })
255 })267 })
256 }268 }
@@ -283,12 +295,7 @@ func (s *ContentBlockingService) RegisterWebView(wv *pkgwebkit.WebView) {
283 }295 }
284 296
285 // If network filters are already compiled, apply them immediately (non-blocking)297 // If network filters are already compiled, apply them immediately (non-blocking)
286- if s.contentBlockingMgr.IsFilterCompiled() {298+ s.applyFiltersToWebView(wv)
287- webView := wv // capture for closure
288- s.runWhenWidgetReady(webView, func() {
289- s.applyFiltersToWebView(webView)
290- })
291- }
292 }299 }
293 300
294 // injectCosmeticBaseScript injects the base cosmetic filtering script into a WebView.301 // injectCosmeticBaseScript injects the base cosmetic filtering script into a WebView.
@@ -387,24 +394,11 @@ func (s *ContentBlockingService) applyFiltersToWebView(wv *pkgwebkit.WebView) {
387 s.mu.Unlock()394 s.mu.Unlock()
388 return395 return
389 }396 }
390- // Mark as applying to avoid double AddFilter races
391- state.networkApplied = true
392- s.webViews[wv] = state
393- networkJSON := s.networkFilterJSON
394 s.mu.Unlock()397 s.mu.Unlock()
395 398
396- if len(networkJSON) == 0 {
397- // Nothing to apply; reset flag
398- s.mu.Lock()
399- state.networkApplied = false
400- s.webViews[wv] = state
401- s.mu.Unlock()
402- return
403- }
404-
405 webView := wv // capture for closure399 webView := wv // capture for closure
406 s.runWhenWidgetReady(webView, func() {400 s.runWhenWidgetReady(webView, func() {
407- s.applyNetworkFilters(webView, networkJSON)401+ s.applyNetworkFilters(webView, s.networkFilterJSON)
408 })402 })
409 }403 }
410 404
@@ -416,19 +410,9 @@ func (s *ContentBlockingService) applyNetworkFilters(wv *pkgwebkit.WebView, filt
416 410
417 // Double-check compile state before proceeding411 // Double-check compile state before proceeding
418 if !s.contentBlockingMgr.IsFilterCompiled() {412 if !s.contentBlockingMgr.IsFilterCompiled() {
419- return413+ // Ensure we retry later once compilation finishes
420- }
421-
422- gtkView := wv.GetWebView()
423- if gtkView == nil {
424- return
425- }
426-
427- ucm := gtkView.UserContentManager()
428- if ucm == nil {
429- // Reset state so a future ready WebView can retry
430 s.mu.Lock()414 s.mu.Lock()
431- if state, exists := s.webViews[wv]; exists {415+ if state, ok := s.webViews[wv]; ok {
432 state.networkApplied = false416 state.networkApplied = false
433 s.webViews[wv] = state417 s.webViews[wv] = state
434 }418 }
@@ -436,30 +420,55 @@ func (s *ContentBlockingService) applyNetworkFilters(wv *pkgwebkit.WebView, filt
436 return420 return
437 }421 }
438 422
439- // Use shared identifier so all WebViews reuse the same compiled filter423+ // Run the application on the GTK main thread to avoid CGO crashes.
440- const identifier = "adblock-filters"424+ pkgwebkit.RunOnMainThread(func() {
425+ if wv.IsDestroyed() {
426+ return
427+ }
441 428
442- // Try to apply cached compiled filter first (instant)429+ gtkView := wv.GetWebView()
443- s.applyMu.Lock()430+ if gtkView == nil {
444- defer s.applyMu.Unlock()431+ return
432+ }
433+
434+ ucm := gtkView.UserContentManager()
435+ if ucm == nil {
436+ // Reset state so a future ready WebView can retry
437+ s.mu.Lock()
438+ if state, exists := s.webViews[wv]; exists {
439+ state.networkApplied = false
440+ s.webViews[wv] = state
441+ }
442+ s.mu.Unlock()
443+ return
444+ }
445+
446+ // Use shared identifier so all WebViews reuse the same compiled filter
447+ const identifier = "adblock-filters"
448+
449+ // Try to apply cached compiled filter first (instant)
450+ s.applyMu.Lock()
451+ defer s.applyMu.Unlock()
452+
453+ if !s.contentBlockingMgr.IsFilterCompiled() {
454+ return
455+ }
445 456
446- if s.contentBlockingMgr.IsFilterCompiled() {
447 if err := s.contentBlockingMgr.ApplyCompiledFilter(ucm); err != nil {457 if err := s.contentBlockingMgr.ApplyCompiledFilter(ucm); err != nil {
448 logging.Error(fmt.Sprintf("[filtering] Failed to apply cached filter to WebView %d: %v", wv.ID(), err))458 logging.Error(fmt.Sprintf("[filtering] Failed to apply cached filter to WebView %d: %v", wv.ID(), err))
449 return459 return
450 }460 }
451- } else if len(filterJSON) > 0 {461+
452- // Fallback: compile if not yet compiled (first WebView)462+ // Mark applied
453- err := s.contentBlockingMgr.ApplyFiltersFromJSON(ucm, identifier, filterJSON)463+ s.mu.Lock()
454- if err != nil {464+ if state, ok := s.webViews[wv]; ok {
455- logging.Error(fmt.Sprintf("[filtering] Failed to apply network filters to WebView %d: %v", wv.ID(), err))465+ state.networkApplied = true
456- return466+ s.webViews[wv] = state
457 }467 }
458- } else {468+ s.mu.Unlock()
459- return
460- }
461 469
462- logging.Debug(fmt.Sprintf("[filtering] Applied network filters to WebView %d", wv.ID()))470+ logging.Debug(fmt.Sprintf("[filtering] Applied network filters to WebView %d", wv.ID()))
471+ })
463 }472 }
464 473
465 // setupCosmeticHooks sets up hooks for cosmetic filtering on navigation events.474 // setupCosmeticHooks sets up hooks for cosmetic filtering on navigation events.
@@ -476,12 +485,26 @@ func (s *ContentBlockingService) setupCosmeticHooks(wv *pkgwebkit.WebView) {
476 }485 }
477 s.runWhenWidgetReady(webView, func() {486 s.runWhenWidgetReady(webView, func() {
478 s.injectCosmeticRules(webView, uri)487 s.injectCosmeticRules(webView, uri)
479- // Apply network filters lazily on navigation when compilation is ready
480- s.applyFiltersToWebView(webView)
481 })488 })
482 })489 })
483 }490 }
484 491
492+// applyFiltersToRegisteredWebViews applies compiled network filters to all registered WebViews.
493+func (s *ContentBlockingService) applyFiltersToRegisteredWebViews() {
494+ s.mu.RLock()
495+ webViews := make([]*pkgwebkit.WebView, 0, len(s.webViews))
496+ for wv := range s.webViews {
497+ webViews = append(webViews, wv)
498+ }
499+ s.mu.RUnlock()
500+
501+ for _, wv := range webViews {
502+ if wv != nil && !wv.IsDestroyed() {
503+ s.applyFiltersToWebView(wv)
504+ }
505+ }
506+}
507+
485 // injectCosmeticRules injects cosmetic filtering rules for the current page.508 // injectCosmeticRules injects cosmetic filtering rules for the current page.
486 func (s *ContentBlockingService) injectCosmeticRules(wv *pkgwebkit.WebView, uri string) {509 func (s *ContentBlockingService) injectCosmeticRules(wv *pkgwebkit.WebView, uri string) {
487 if wv == nil || wv.IsDestroyed() || uri == "" {510 if wv == nil || wv.IsDestroyed() || uri == "" {
M pkg/webkit/content_blocking.go +46 -45
@@ -62,33 +62,36 @@ func (cbm *ContentBlockingManager) CompileFilters(identifier string, jsonRules [
6262 // Compile asynchronously - does not block main thread
6363 ctx := context.Background()
6464 cbm.filterStore.Save(ctx, identifier, gBytes, func(result gio.AsyncResulter) {
65- filter, err := cbm.filterStore.SaveFinish(result)
66- if err != nil {
67- logging.Error(fmt.Sprintf("[webkit] Failed to compile content filter: %v", err))
68- // Don't store nil filter - prevents GC from trying to unref it
69- if onComplete != nil {
70- onComplete(fmt.Errorf("failed to compile filter: %w", err))
65+ // Ensure all GLib/GObject interactions happen on the GTK main thread.
66+ RunOnMainThread(func() {
67+ filter, err := cbm.filterStore.SaveFinish(result)
68+ if err != nil {
69+ logging.Error(fmt.Sprintf("[webkit] Failed to compile content filter: %v", err))
70+ // Don't store nil filter - prevents GC from trying to unref it
71+ if onComplete != nil {
72+ onComplete(fmt.Errorf("failed to compile filter: %w", err))
73+ }
74+ return
7175 }
72- return
73- }
7476
75- if filter == nil {
76- logging.Error(fmt.Sprintf("[webkit] Filter compilation returned nil"))
77- if onComplete != nil {
78- onComplete(fmt.Errorf("filter compilation returned nil"))
77+ if filter == nil {
78+ logging.Error(fmt.Sprintf("[webkit] Filter compilation returned nil"))
79+ if onComplete != nil {
80+ onComplete(fmt.Errorf("filter compilation returned nil"))
81+ }
82+ return
7983 }
80- return
81- }
8284
83- // Cache the compiled filter (only if valid)
84- cbm.mu.Lock()
85- cbm.compiledFilter = filter
86- cbm.mu.Unlock()
85+ // Cache the compiled filter (only if valid)
86+ cbm.mu.Lock()
87+ cbm.compiledFilter = filter
88+ cbm.mu.Unlock()
8789
88- logging.Debug(fmt.Sprintf("[webkit] Filter compilation complete: %s", identifier))
89- if onComplete != nil {
90- onComplete(nil)
91- }
90+ logging.Debug(fmt.Sprintf("[webkit] Filter compilation complete: %s", identifier))
91+ if onComplete != nil {
92+ onComplete(nil)
93+ }
94+ })
9295 })
9396 }
9497
@@ -150,32 +153,30 @@ func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentM
150153 done := make(chan error, 1)
151154
152155 cbm.filterStore.Save(ctx, identifier, gBytes, func(result gio.AsyncResulter) {
153- filter, err := cbm.filterStore.SaveFinish(result)
154- if err != nil {
155- logging.Error(fmt.Sprintf("[webkit] Failed to compile content filter: %v", err))
156- done <- fmt.Errorf("failed to compile filter: %w", err)
157- // Explicitly nil to prevent GC issues with partial objects
158- filter = nil
159- return
160- }
156+ // Ensure GLib/GObject operations run on the GTK main thread.
157+ RunOnMainThread(func() {
158+ filter, err := cbm.filterStore.SaveFinish(result)
159+ if err != nil {
160+ logging.Error(fmt.Sprintf("[webkit] Failed to compile content filter: %v", err))
161+ done <- fmt.Errorf("failed to compile filter: %w", err)
162+ // Explicitly nil to prevent GC issues with partial objects
163+ return
164+ }
161165
162- if filter == nil {
163- logging.Error(fmt.Sprintf("[webkit] Filter compilation returned nil"))
164- done <- fmt.Errorf("filter compilation returned nil")
165- return
166- }
166+ if filter == nil {
167+ logging.Error(fmt.Sprintf("[webkit] Filter compilation returned nil"))
168+ done <- fmt.Errorf("filter compilation returned nil")
169+ return
170+ }
167171
168- // Cache for future use (only valid filters)
169- cbm.mu.Lock()
170- cbm.compiledFilter = filter
171- cbm.filterID = identifier
172- cbm.mu.Unlock()
172+ // Cache for future use (only valid filters)
173+ cbm.mu.Lock()
174+ cbm.compiledFilter = filter
175+ cbm.filterID = identifier
176+ cbm.mu.Unlock()
173177
174- RunOnMainThread(func() {
175- if filter != nil {
176- ucm.AddFilter(filter)
177- logging.Debug(fmt.Sprintf("[webkit] Successfully added content filter: %s", identifier))
178- }
178+ ucm.AddFilter(filter)
179+ logging.Debug(fmt.Sprintf("[webkit] Successfully added content filter: %s", identifier))
179180 done <- nil
180181 })
181182 })
@@ -62,33 +62,36 @@ func (cbm *ContentBlockingManager) CompileFilters(identifier string, jsonRules [
62 // Compile asynchronously - does not block main thread62 // Compile asynchronously - does not block main thread
63 ctx := context.Background()63 ctx := context.Background()
64 cbm.filterStore.Save(ctx, identifier, gBytes, func(result gio.AsyncResulter) {64 cbm.filterStore.Save(ctx, identifier, gBytes, func(result gio.AsyncResulter) {
65- filter, err := cbm.filterStore.SaveFinish(result)65+ // Ensure all GLib/GObject interactions happen on the GTK main thread.
66- if err != nil {66+ RunOnMainThread(func() {
67- logging.Error(fmt.Sprintf("[webkit] Failed to compile content filter: %v", err))67+ filter, err := cbm.filterStore.SaveFinish(result)
68- // Don't store nil filter - prevents GC from trying to unref it68+ if err != nil {
69- if onComplete != nil {69+ logging.Error(fmt.Sprintf("[webkit] Failed to compile content filter: %v", err))
70- onComplete(fmt.Errorf("failed to compile filter: %w", err))70+ // Don't store nil filter - prevents GC from trying to unref it
71+ if onComplete != nil {
72+ onComplete(fmt.Errorf("failed to compile filter: %w", err))
73+ }
74+ return
71 }75 }
72- return
73- }
74 76
75- if filter == nil {77+ if filter == nil {
76- logging.Error(fmt.Sprintf("[webkit] Filter compilation returned nil"))78+ logging.Error(fmt.Sprintf("[webkit] Filter compilation returned nil"))
77- if onComplete != nil {79+ if onComplete != nil {
78- onComplete(fmt.Errorf("filter compilation returned nil"))80+ onComplete(fmt.Errorf("filter compilation returned nil"))
81+ }
82+ return
79 }83 }
80- return
81- }
82 84
83- // Cache the compiled filter (only if valid)85+ // Cache the compiled filter (only if valid)
84- cbm.mu.Lock()86+ cbm.mu.Lock()
85- cbm.compiledFilter = filter87+ cbm.compiledFilter = filter
86- cbm.mu.Unlock()88+ cbm.mu.Unlock()
87 89
88- logging.Debug(fmt.Sprintf("[webkit] Filter compilation complete: %s", identifier))90+ logging.Debug(fmt.Sprintf("[webkit] Filter compilation complete: %s", identifier))
89- if onComplete != nil {91+ if onComplete != nil {
90- onComplete(nil)92+ onComplete(nil)
91- }93+ }
94+ })
92 })95 })
93 }96 }
94 97
@@ -150,32 +153,30 @@ func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentM
150 done := make(chan error, 1)153 done := make(chan error, 1)
151 154
152 cbm.filterStore.Save(ctx, identifier, gBytes, func(result gio.AsyncResulter) {155 cbm.filterStore.Save(ctx, identifier, gBytes, func(result gio.AsyncResulter) {
153- filter, err := cbm.filterStore.SaveFinish(result)156+ // Ensure GLib/GObject operations run on the GTK main thread.
154- if err != nil {157+ RunOnMainThread(func() {
155- logging.Error(fmt.Sprintf("[webkit] Failed to compile content filter: %v", err))158+ filter, err := cbm.filterStore.SaveFinish(result)
156- done <- fmt.Errorf("failed to compile filter: %w", err)159+ if err != nil {
157- // Explicitly nil to prevent GC issues with partial objects160+ logging.Error(fmt.Sprintf("[webkit] Failed to compile content filter: %v", err))
158- filter = nil161+ done <- fmt.Errorf("failed to compile filter: %w", err)
159- return162+ // Explicitly nil to prevent GC issues with partial objects
160- }163+ return
164+ }
161 165
162- if filter == nil {166+ if filter == nil {
163- logging.Error(fmt.Sprintf("[webkit] Filter compilation returned nil"))167+ logging.Error(fmt.Sprintf("[webkit] Filter compilation returned nil"))
164- done <- fmt.Errorf("filter compilation returned nil")168+ done <- fmt.Errorf("filter compilation returned nil")
165- return169+ return
166- }170+ }
167 171
168- // Cache for future use (only valid filters)172+ // Cache for future use (only valid filters)
169- cbm.mu.Lock()173+ cbm.mu.Lock()
170- cbm.compiledFilter = filter174+ cbm.compiledFilter = filter
171- cbm.filterID = identifier175+ cbm.filterID = identifier
172- cbm.mu.Unlock()176+ cbm.mu.Unlock()
173 177
174- RunOnMainThread(func() {178+ ucm.AddFilter(filter)
175- if filter != nil {179+ logging.Debug(fmt.Sprintf("[webkit] Successfully added content filter: %s", identifier))
176- ucm.AddFilter(filter)
177- logging.Debug(fmt.Sprintf("[webkit] Successfully added content filter: %s", identifier))
178- }
179 done <- nil180 done <- nil
180 })181 })
181 })182 })
M pkg/webkit/mainloop.go +11 -2
@@ -10,6 +10,7 @@ import (
1010 var (
1111 mainLoop *glib.MainLoop
1212 isInitialized bool
13+ mainContext *glib.MainContext
1314 )
1415
1516 // InitMainThread locks the current goroutine to the OS thread for GTK operations
@@ -21,6 +22,9 @@ func InitMainThread() {
2122 // Initialize GTK - this is required before creating any GTK widgets
2223 gtk.Init()
2324
25+ // Cache the default main context so we can check ownership later.
26+ mainContext = glib.MainContextDefault()
27+
2428 isInitialized = true
2529 }
2630 }
@@ -46,8 +50,13 @@ func QuitMainLoop() {
4650
4751 // IsMainThread returns true if called from the GTK main thread.
4852 func IsMainThread() bool {
49- // In gotk4, we're always on the main thread since we lock it
50- return isInitialized
53+ if !isInitialized {
54+ return false
55+ }
56+ if mainContext == nil {
57+ mainContext = glib.MainContextDefault()
58+ }
59+ return mainContext != nil && mainContext.IsOwner()
5160 }
5261
5362 // RunOnMainThread executes a function on the GTK main thread.
@@ -10,6 +10,7 @@ import (
10 var (10 var (
11 mainLoop *glib.MainLoop11 mainLoop *glib.MainLoop
12 isInitialized bool12 isInitialized bool
13+ mainContext *glib.MainContext
13 )14 )
14 15
15 // InitMainThread locks the current goroutine to the OS thread for GTK operations16 // InitMainThread locks the current goroutine to the OS thread for GTK operations
@@ -21,6 +22,9 @@ func InitMainThread() {
21 // Initialize GTK - this is required before creating any GTK widgets22 // Initialize GTK - this is required before creating any GTK widgets
22 gtk.Init()23 gtk.Init()
23 24
25+ // Cache the default main context so we can check ownership later.
26+ mainContext = glib.MainContextDefault()
27+
24 isInitialized = true28 isInitialized = true
25 }29 }
26 }30 }
@@ -46,8 +50,13 @@ func QuitMainLoop() {
46 50
47 // IsMainThread returns true if called from the GTK main thread.51 // IsMainThread returns true if called from the GTK main thread.
48 func IsMainThread() bool {52 func IsMainThread() bool {
49- // In gotk4, we're always on the main thread since we lock it53+ if !isInitialized {
50- return isInitialized54+ return false
55+ }
56+ if mainContext == nil {
57+ mainContext = glib.MainContextDefault()
58+ }
59+ return mainContext != nil && mainContext.IsOwner()
51 }60 }
52 61
53 // RunOnMainThread executes a function on the GTK main thread.62 // RunOnMainThread executes a function on the GTK main thread.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close