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() { | |||
| 359 | 359 | // runMainLoop starts the appropriate main loop based on WebKit availability | |
| 360 | 360 | func (app *BrowserApp) runMainLoop() { | |
| 361 | 361 | 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 | + | } | |
| 369 | 373 | ||
| 370 | 374 | logging.Info(fmt.Sprintf("Entering GTK main loop…")) | |
| 371 | 375 | webkit.RunMainLoop() | |
| @@ -359,13 +359,17 @@ func (app *BrowserApp) setupSignalHandling() { | |||||
| 359 | // runMainLoop starts the appropriate main loop based on WebKit availability | 359 | // 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 iteration | 362 | + | if os.Getenv("DUMBER_DISABLE_CONTENT_BLOCKING") != "1" { |
| 363 | - | // This ensures GTK is fully ready before we start loading filters | 363 | + | // 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 { | |||
| 373 | 373 | func (app *BrowserApp) setupContentBlocking() error { | |
| 374 | 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 | 383 | // Enable WebKit debug logging if requested | |
| 377 | 384 | webkit.SetupWebKitDebugLogging() | |
| 378 | 385 | ||
| @@ -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 requested | 383 | // 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) { | |||
| 124 | 124 | hash := fmt.Sprintf("%x", sha256.Sum256(networkJSON)) | |
| 125 | 125 | ||
| 126 | 126 | s.mu.Lock() | |
| 127 | + | // Update current payload | |
| 128 | + | s.networkFilterJSON = networkJSON | |
| 129 | + | s.networkHash = hash | |
| 130 | + | s.filtersReady = true | |
| 131 | + | ||
| 127 | 132 | // If a compile is already in progress: | |
| 128 | 133 | if s.compileInProgress { | |
| 129 | 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 | 215 | // New payload, no compile in progress: create a fresh completion channel | |
| 211 | 216 | s.compileDone = make(chan struct{}) | |
| 212 | 217 | 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 | 222 | s.mu.Unlock() | |
| 214 | 223 | ||
| 215 | 224 | // Inject cosmetic scripts IMMEDIATELY (don't wait for network filter compilation) | |
| @@ -251,6 +260,9 @@ func (s *ContentBlockingService) SetFiltersReady(networkJSON []byte) { | |||
| 251 | 260 | if done != nil { | |
| 252 | 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 | 297 | // 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) | |
| 292 | 299 | } | |
| 293 | 300 | ||
| 294 | 301 | // injectCosmeticBaseScript injects the base cosmetic filtering script into a WebView. | |
| @@ -387,24 +394,11 @@ func (s *ContentBlockingService) applyFiltersToWebView(wv *pkgwebkit.WebView) { | |||
| 387 | 394 | s.mu.Unlock() | |
| 388 | 395 | 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 | 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 | 399 | webView := wv // capture for closure | |
| 406 | 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 | 411 | // Double-check compile state before proceeding | |
| 418 | 412 | 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 | |
| 430 | 414 | s.mu.Lock() | |
| 431 | - | if state, exists := s.webViews[wv]; exists { | |
| 415 | + | if state, ok := s.webViews[wv]; ok { | |
| 432 | 416 | state.networkApplied = false | |
| 433 | 417 | s.webViews[wv] = state | |
| 434 | 418 | } | |
| @@ -436,30 +420,55 @@ func (s *ContentBlockingService) applyNetworkFilters(wv *pkgwebkit.WebView, filt | |||
| 436 | 420 | return | |
| 437 | 421 | } | |
| 438 | 422 | ||
| 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 | + | } | |
| 441 | 428 | ||
| 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 | + | } | |
| 445 | 456 | ||
| 446 | - | if s.contentBlockingMgr.IsFilterCompiled() { | |
| 447 | 457 | if err := s.contentBlockingMgr.ApplyCompiledFilter(ucm); err != nil { | |
| 448 | 458 | logging.Error(fmt.Sprintf("[filtering] Failed to apply cached filter to WebView %d: %v", wv.ID(), err)) | |
| 449 | 459 | return | |
| 450 | 460 | } | |
| 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 | |
| 457 | 467 | } | |
| 458 | - | } else { | |
| 459 | - | return | |
| 460 | - | } | |
| 468 | + | s.mu.Unlock() | |
| 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 | 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 | 486 | s.runWhenWidgetReady(webView, func() { | |
| 478 | 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 | 508 | // injectCosmeticRules injects cosmetic filtering rules for the current page. | |
| 486 | 509 | func (s *ContentBlockingService) injectCosmeticRules(wv *pkgwebkit.WebView, uri string) { | |
| 487 | 510 | 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 channel | 215 | // 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 = true | 217 | 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 | return | 395 | 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 closure | 399 | 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 proceeding | 411 | // Double-check compile state before proceeding | ||
| 418 | if !s.contentBlockingMgr.IsFilterCompiled() { | 412 | if !s.contentBlockingMgr.IsFilterCompiled() { | ||
| 419 | - | return | 413 | + | // 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 = false | 416 | state.networkApplied = false | ||
| 433 | s.webViews[wv] = state | 417 | s.webViews[wv] = state | ||
| 434 | } | 418 | } | ||
| @@ -436,30 +420,55 @@ func (s *ContentBlockingService) applyNetworkFilters(wv *pkgwebkit.WebView, filt | |||||
| 436 | return | 420 | return | ||
| 437 | } | 421 | } | ||
| 438 | 422 | ||||
| 439 | - | // Use shared identifier so all WebViews reuse the same compiled filter | 423 | + | // 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 | return | 459 | 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 | - | return | 466 | + | 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 [ | |||
| 62 | 62 | // Compile asynchronously - does not block main thread | |
| 63 | 63 | ctx := context.Background() | |
| 64 | 64 | 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 | |
| 71 | 75 | } | |
| 72 | - | return | |
| 73 | - | } | |
| 74 | 76 | ||
| 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 | |
| 79 | 83 | } | |
| 80 | - | return | |
| 81 | - | } | |
| 82 | 84 | ||
| 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() | |
| 87 | 89 | ||
| 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 | + | }) | |
| 92 | 95 | }) | |
| 93 | 96 | } | |
| 94 | 97 | ||
| @@ -150,32 +153,30 @@ func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentM | |||
| 150 | 153 | done := make(chan error, 1) | |
| 151 | 154 | ||
| 152 | 155 | 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 | + | } | |
| 161 | 165 | ||
| 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 | + | } | |
| 167 | 171 | ||
| 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() | |
| 173 | 177 | ||
| 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)) | |
| 179 | 180 | done <- nil | |
| 180 | 181 | }) | |
| 181 | 182 | }) | |
| @@ -62,33 +62,36 @@ func (cbm *ContentBlockingManager) CompileFilters(identifier string, jsonRules [ | |||||
| 62 | // Compile asynchronously - does not block main thread | 62 | // 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 it | 68 | + | 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 = filter | 87 | + | 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 objects | 160 | + | logging.Error(fmt.Sprintf("[webkit] Failed to compile content filter: %v", err)) |
| 158 | - | filter = nil | 161 | + | done <- fmt.Errorf("failed to compile filter: %w", err) |
| 159 | - | return | 162 | + | // 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 | - | return | 169 | + | 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 = filter | 174 | + | cbm.compiledFilter = filter |
| 171 | - | cbm.filterID = identifier | 175 | + | 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 <- nil | 180 | done <- nil | ||
| 180 | }) | 181 | }) | ||
| 181 | }) | 182 | }) | ||
M pkg/webkit/mainloop.go +11 -2
| @@ -10,6 +10,7 @@ import ( | |||
| 10 | 10 | var ( | |
| 11 | 11 | mainLoop *glib.MainLoop | |
| 12 | 12 | isInitialized bool | |
| 13 | + | mainContext *glib.MainContext | |
| 13 | 14 | ) | |
| 14 | 15 | ||
| 15 | 16 | // InitMainThread locks the current goroutine to the OS thread for GTK operations | |
| @@ -21,6 +22,9 @@ func InitMainThread() { | |||
| 21 | 22 | // Initialize GTK - this is required before creating any GTK widgets | |
| 22 | 23 | gtk.Init() | |
| 23 | 24 | ||
| 25 | + | // Cache the default main context so we can check ownership later. | |
| 26 | + | mainContext = glib.MainContextDefault() | |
| 27 | + | ||
| 24 | 28 | isInitialized = true | |
| 25 | 29 | } | |
| 26 | 30 | } | |
| @@ -46,8 +50,13 @@ func QuitMainLoop() { | |||
| 46 | 50 | ||
| 47 | 51 | // IsMainThread returns true if called from the GTK main thread. | |
| 48 | 52 | 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() | |
| 51 | 60 | } | |
| 52 | 61 | ||
| 53 | 62 | // RunOnMainThread executes a function on the GTK main thread. | |
| @@ -10,6 +10,7 @@ import ( | |||||
| 10 | var ( | 10 | var ( | ||
| 11 | mainLoop *glib.MainLoop | 11 | mainLoop *glib.MainLoop | ||
| 12 | isInitialized bool | 12 | isInitialized bool | ||
| 13 | + | mainContext *glib.MainContext | |||
| 13 | ) | 14 | ) | ||
| 14 | 15 | ||||
| 15 | // InitMainThread locks the current goroutine to the OS thread for GTK operations | 16 | // 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 widgets | 22 | // 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 = true | 28 | 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 it | 53 | + | if !isInitialized { |
| 50 | - | return isInitialized | 54 | + | 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. | ||