wip: attempt to stabilize filtering after splits
- commit
- fefcc69d586cb6907ea78aea3d25d6e24a4a7192
- author
- bnema · · 2025-12-07 07:52 UTC
- committer
- bnema · · 2025-12-07 18:45 UTC
- parent
- 1cbf486
- signature
- unsigned
3 files · +133 -37
M internal/app/browser/browser.go +3 -0
| @@ -8,6 +8,7 @@ import ( | |||
| 8 | 8 | "os" | |
| 9 | 9 | "os/signal" | |
| 10 | 10 | "runtime" | |
| 11 | + | "sync" | |
| 11 | 12 | "syscall" | |
| 12 | 13 | "time" | |
| 13 | 14 | ||
| @@ -60,6 +61,8 @@ type BrowserApp struct { | |||
| 60 | 61 | filterManager *filtering.FilterManager | |
| 61 | 62 | contentBlockingService *filtering.ContentBlockingService | |
| 62 | 63 | bypassRegistry *filtering.BypassRegistry | |
| 64 | + | pendingFilteringMu sync.Mutex | |
| 65 | + | pendingFiltering []*webkit.WebView | |
| 63 | 66 | ||
| 64 | 67 | // Handlers | |
| 65 | 68 | schemeHandler *api.SchemeHandler | |
| @@ -8,6 +8,7 @@ import ( | |||||
| 8 | "os" | 8 | "os" | ||
| 9 | "os/signal" | 9 | "os/signal" | ||
| 10 | "runtime" | 10 | "runtime" | ||
| 11 | + | "sync" | |||
| 11 | "syscall" | 12 | "syscall" | ||
| 12 | "time" | 13 | "time" | ||
| 13 | 14 | ||||
| @@ -60,6 +61,8 @@ type BrowserApp struct { | |||||
| 60 | filterManager *filtering.FilterManager | 61 | filterManager *filtering.FilterManager | ||
| 61 | contentBlockingService *filtering.ContentBlockingService | 62 | contentBlockingService *filtering.ContentBlockingService | ||
| 62 | bypassRegistry *filtering.BypassRegistry | 63 | bypassRegistry *filtering.BypassRegistry | ||
| 64 | + | pendingFilteringMu sync.Mutex | |||
| 65 | + | pendingFiltering []*webkit.WebView | |||
| 63 | 66 | ||||
| 64 | // Handlers | 67 | // Handlers | ||
| 65 | schemeHandler *api.SchemeHandler | 68 | schemeHandler *api.SchemeHandler | ||
M internal/app/browser/webview.go +43 -3
| @@ -259,6 +259,9 @@ func (app *BrowserApp) createWebView() error { | |||
| 259 | 259 | return err | |
| 260 | 260 | } | |
| 261 | 261 | ||
| 262 | + | // Ensure root WebView participates in content blocking even if the service initializes later | |
| 263 | + | app.RegisterWebViewForFiltering(view) | |
| 264 | + | ||
| 262 | 265 | pane, err := app.createPaneForView(view) | |
| 263 | 266 | if err != nil { | |
| 264 | 267 | return err | |
| @@ -404,6 +407,16 @@ func (app *BrowserApp) setupContentBlocking() error { | |||
| 404 | 407 | } | |
| 405 | 408 | app.contentBlockingService = cbService | |
| 406 | 409 | ||
| 410 | + | // Register any WebViews created before content blocking was ready | |
| 411 | + | app.pendingFilteringMu.Lock() | |
| 412 | + | for _, pending := range app.pendingFiltering { | |
| 413 | + | if pending != nil && !pending.IsDestroyed() { | |
| 414 | + | cbService.RegisterWebView(pending) | |
| 415 | + | } | |
| 416 | + | } | |
| 417 | + | app.pendingFiltering = nil | |
| 418 | + | app.pendingFilteringMu.Unlock() | |
| 419 | + | ||
| 407 | 420 | // Set up callback to apply network filters when they become ready | |
| 408 | 421 | // This callback will be called from the async filter loading process | |
| 409 | 422 | filterManager.SetFiltersReadyCallback(func() { | |
| @@ -453,14 +466,41 @@ func (app *BrowserApp) setupContentBlocking() error { | |||
| 453 | 466 | // RegisterWebViewForFiltering registers a WebView with the content blocking service. | |
| 454 | 467 | // This should be called for every new WebView created in the application. | |
| 455 | 468 | func (app *BrowserApp) RegisterWebViewForFiltering(wv *webkit.WebView) { | |
| 456 | - | if app.contentBlockingService != nil && wv != nil { | |
| 457 | - | app.contentBlockingService.RegisterWebView(wv) | |
| 469 | + | if wv == nil || wv.IsDestroyed() { | |
| 470 | + | return | |
| 458 | 471 | } | |
| 472 | + | ||
| 473 | + | if app.contentBlockingService == nil { | |
| 474 | + | app.pendingFilteringMu.Lock() | |
| 475 | + | app.pendingFiltering = append(app.pendingFiltering, wv) | |
| 476 | + | app.pendingFilteringMu.Unlock() | |
| 477 | + | return | |
| 478 | + | } | |
| 479 | + | ||
| 480 | + | app.contentBlockingService.RegisterWebView(wv) | |
| 459 | 481 | } | |
| 460 | 482 | ||
| 461 | 483 | // UnregisterWebViewFromFiltering removes a WebView from the content blocking service. | |
| 462 | 484 | func (app *BrowserApp) UnregisterWebViewFromFiltering(wv *webkit.WebView) { | |
| 463 | - | if app.contentBlockingService != nil && wv != nil { | |
| 485 | + | if wv == nil { | |
| 486 | + | return | |
| 487 | + | } | |
| 488 | + | ||
| 489 | + | if app.contentBlockingService == nil { | |
| 490 | + | // Drop from pending list to avoid registering destroyed WebViews later | |
| 491 | + | app.pendingFilteringMu.Lock() | |
| 492 | + | filtered := app.pendingFiltering[:0] | |
| 493 | + | for _, pending := range app.pendingFiltering { | |
| 494 | + | if pending != nil && pending != wv { | |
| 495 | + | filtered = append(filtered, pending) | |
| 496 | + | } | |
| 497 | + | } | |
| 498 | + | app.pendingFiltering = filtered | |
| 499 | + | app.pendingFilteringMu.Unlock() | |
| 500 | + | return | |
| 501 | + | } | |
| 502 | + | ||
| 503 | + | if !wv.IsDestroyed() { | |
| 464 | 504 | app.contentBlockingService.UnregisterWebView(wv) | |
| 465 | 505 | } | |
| 466 | 506 | } | |
| @@ -259,6 +259,9 @@ func (app *BrowserApp) createWebView() error { | |||||
| 259 | return err | 259 | return err | ||
| 260 | } | 260 | } | ||
| 261 | 261 | ||||
| 262 | + | // Ensure root WebView participates in content blocking even if the service initializes later | |||
| 263 | + | app.RegisterWebViewForFiltering(view) | |||
| 264 | + | ||||
| 262 | pane, err := app.createPaneForView(view) | 265 | pane, err := app.createPaneForView(view) | ||
| 263 | if err != nil { | 266 | if err != nil { | ||
| 264 | return err | 267 | return err | ||
| @@ -404,6 +407,16 @@ func (app *BrowserApp) setupContentBlocking() error { | |||||
| 404 | } | 407 | } | ||
| 405 | app.contentBlockingService = cbService | 408 | app.contentBlockingService = cbService | ||
| 406 | 409 | ||||
| 410 | + | // Register any WebViews created before content blocking was ready | |||
| 411 | + | app.pendingFilteringMu.Lock() | |||
| 412 | + | for _, pending := range app.pendingFiltering { | |||
| 413 | + | if pending != nil && !pending.IsDestroyed() { | |||
| 414 | + | cbService.RegisterWebView(pending) | |||
| 415 | + | } | |||
| 416 | + | } | |||
| 417 | + | app.pendingFiltering = nil | |||
| 418 | + | app.pendingFilteringMu.Unlock() | |||
| 419 | + | ||||
| 407 | // Set up callback to apply network filters when they become ready | 420 | // Set up callback to apply network filters when they become ready | ||
| 408 | // This callback will be called from the async filter loading process | 421 | // This callback will be called from the async filter loading process | ||
| 409 | filterManager.SetFiltersReadyCallback(func() { | 422 | filterManager.SetFiltersReadyCallback(func() { | ||
| @@ -453,14 +466,41 @@ func (app *BrowserApp) setupContentBlocking() error { | |||||
| 453 | // RegisterWebViewForFiltering registers a WebView with the content blocking service. | 466 | // RegisterWebViewForFiltering registers a WebView with the content blocking service. | ||
| 454 | // This should be called for every new WebView created in the application. | 467 | // This should be called for every new WebView created in the application. | ||
| 455 | func (app *BrowserApp) RegisterWebViewForFiltering(wv *webkit.WebView) { | 468 | func (app *BrowserApp) RegisterWebViewForFiltering(wv *webkit.WebView) { | ||
| 456 | - | if app.contentBlockingService != nil && wv != nil { | 469 | + | if wv == nil || wv.IsDestroyed() { |
| 457 | - | app.contentBlockingService.RegisterWebView(wv) | 470 | + | return |
| 458 | } | 471 | } | ||
| 472 | + | ||||
| 473 | + | if app.contentBlockingService == nil { | |||
| 474 | + | app.pendingFilteringMu.Lock() | |||
| 475 | + | app.pendingFiltering = append(app.pendingFiltering, wv) | |||
| 476 | + | app.pendingFilteringMu.Unlock() | |||
| 477 | + | return | |||
| 478 | + | } | |||
| 479 | + | ||||
| 480 | + | app.contentBlockingService.RegisterWebView(wv) | |||
| 459 | } | 481 | } | ||
| 460 | 482 | ||||
| 461 | // UnregisterWebViewFromFiltering removes a WebView from the content blocking service. | 483 | // UnregisterWebViewFromFiltering removes a WebView from the content blocking service. | ||
| 462 | func (app *BrowserApp) UnregisterWebViewFromFiltering(wv *webkit.WebView) { | 484 | func (app *BrowserApp) UnregisterWebViewFromFiltering(wv *webkit.WebView) { | ||
| 463 | - | if app.contentBlockingService != nil && wv != nil { | 485 | + | if wv == nil { |
| 486 | + | return | |||
| 487 | + | } | |||
| 488 | + | ||||
| 489 | + | if app.contentBlockingService == nil { | |||
| 490 | + | // Drop from pending list to avoid registering destroyed WebViews later | |||
| 491 | + | app.pendingFilteringMu.Lock() | |||
| 492 | + | filtered := app.pendingFiltering[:0] | |||
| 493 | + | for _, pending := range app.pendingFiltering { | |||
| 494 | + | if pending != nil && pending != wv { | |||
| 495 | + | filtered = append(filtered, pending) | |||
| 496 | + | } | |||
| 497 | + | } | |||
| 498 | + | app.pendingFiltering = filtered | |||
| 499 | + | app.pendingFilteringMu.Unlock() | |||
| 500 | + | return | |||
| 501 | + | } | |||
| 502 | + | ||||
| 503 | + | if !wv.IsDestroyed() { | |||
| 464 | app.contentBlockingService.UnregisterWebView(wv) | 504 | app.contentBlockingService.UnregisterWebView(wv) | ||
| 465 | } | 505 | } | ||
| 466 | } | 506 | } | ||
M internal/filtering/service.go +87 -34
| @@ -7,12 +7,58 @@ import ( | |||
| 7 | 7 | "sync" | |
| 8 | 8 | ||
| 9 | 9 | webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6" | |
| 10 | + | glib "github.com/diamondburned/gotk4/pkg/glib/v2" | |
| 10 | 11 | ||
| 11 | 12 | "github.com/bnema/dumber/internal/filtering/cosmetic" | |
| 12 | 13 | "github.com/bnema/dumber/internal/logging" | |
| 13 | 14 | pkgwebkit "github.com/bnema/dumber/pkg/webkit" | |
| 14 | 15 | ) | |
| 15 | 16 | ||
| 17 | + | // runWhenWidgetReady waits until the WebView exposes a valid UserContentManager before invoking fn on the GTK main thread. | |
| 18 | + | // This avoids racing freshly split panes whose underlying widgets aren't fully initialized yet. | |
| 19 | + | func (s *ContentBlockingService) runWhenWidgetReady(wv *pkgwebkit.WebView, fn func()) { | |
| 20 | + | if wv == nil || wv.IsDestroyed() || fn == nil { | |
| 21 | + | return | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | const maxAttempts = 50 // up to ~1s total | |
| 25 | + | const retryDelayMS = uint(20) // 20ms between checks | |
| 26 | + | ||
| 27 | + | var tryApply func(attempt int) bool | |
| 28 | + | tryApply = func(attempt int) bool { | |
| 29 | + | if wv.IsDestroyed() { | |
| 30 | + | return false | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | gtkView := wv.GetWebView() | |
| 34 | + | if gtkView == nil { | |
| 35 | + | return attempt < maxAttempts | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | if ucm := gtkView.UserContentManager(); ucm == nil { | |
| 39 | + | return attempt < maxAttempts | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | pkgwebkit.RunOnMainThread(func() { | |
| 43 | + | if wv.IsDestroyed() { | |
| 44 | + | return | |
| 45 | + | } | |
| 46 | + | fn() | |
| 47 | + | }) | |
| 48 | + | return false | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | attempts := 0 | |
| 52 | + | if !tryApply(attempts) { | |
| 53 | + | return | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | glib.TimeoutAdd(retryDelayMS, func() bool { | |
| 57 | + | attempts++ | |
| 58 | + | return tryApply(attempts) | |
| 59 | + | }) | |
| 60 | + | } | |
| 61 | + | ||
| 16 | 62 | // ContentBlockingService manages content blocking for all WebViews. | |
| 17 | 63 | // It ensures network filters and cosmetic rules are applied consistently | |
| 18 | 64 | // across all WebViews in the application. | |
| @@ -79,10 +125,7 @@ func (s *ContentBlockingService) SetFiltersReady(networkJSON []byte) { | |||
| 79 | 125 | // This hides ads visually within milliseconds while network filters compile in background | |
| 80 | 126 | for _, wv := range webViews { | |
| 81 | 127 | webView := wv // capture for closure | |
| 82 | - | pkgwebkit.RunOnMainThread(func() { | |
| 83 | - | if webView.IsDestroyed() { | |
| 84 | - | return | |
| 85 | - | } | |
| 128 | + | s.runWhenWidgetReady(webView, func() { | |
| 86 | 129 | s.injectCosmeticBaseScript(webView) | |
| 87 | 130 | if uri := webView.GetCurrentURL(); uri != "" { | |
| 88 | 131 | s.injectCosmeticRules(webView, uri) | |
| @@ -117,10 +160,7 @@ func (s *ContentBlockingService) SetFiltersReady(networkJSON []byte) { | |||
| 117 | 160 | // Apply network filters to all registered WebViews | |
| 118 | 161 | for _, wv := range currentWebViews { | |
| 119 | 162 | webView := wv // capture for closure | |
| 120 | - | pkgwebkit.RunOnMainThread(func() { | |
| 121 | - | if webView.IsDestroyed() { | |
| 122 | - | return | |
| 123 | - | } | |
| 163 | + | s.runWhenWidgetReady(webView, func() { | |
| 124 | 164 | s.applyFiltersToWebView(webView) | |
| 125 | 165 | }) | |
| 126 | 166 | } | |
| @@ -154,13 +194,10 @@ func (s *ContentBlockingService) RegisterWebView(wv *pkgwebkit.WebView) { | |||
| 154 | 194 | s.injectCosmeticBaseScript(wv) | |
| 155 | 195 | } | |
| 156 | 196 | ||
| 157 | - | // Apply network filters if already compiled (instant) | |
| 197 | + | // If network filters are already compiled, apply them immediately (non-blocking) | |
| 158 | 198 | if s.contentBlockingMgr.IsFilterCompiled() { | |
| 159 | 199 | webView := wv // capture for closure | |
| 160 | - | pkgwebkit.RunOnMainThread(func() { | |
| 161 | - | if webView.IsDestroyed() { | |
| 162 | - | return | |
| 163 | - | } | |
| 200 | + | s.runWhenWidgetReady(webView, func() { | |
| 164 | 201 | s.applyFiltersToWebView(webView) | |
| 165 | 202 | }) | |
| 166 | 203 | } | |
| @@ -231,21 +268,36 @@ func (s *ContentBlockingService) applyFiltersToWebView(wv *pkgwebkit.WebView) { | |||
| 231 | 268 | return | |
| 232 | 269 | } | |
| 233 | 270 | ||
| 234 | - | s.mu.RLock() | |
| 235 | - | networkJSON := s.networkFilterJSON | |
| 271 | + | // Avoid triggering compilation on the GTK thread; only apply once compiled. | |
| 272 | + | if !s.contentBlockingMgr.IsFilterCompiled() { | |
| 273 | + | return | |
| 274 | + | } | |
| 275 | + | ||
| 276 | + | s.mu.Lock() | |
| 236 | 277 | state := s.webViews[wv] | |
| 237 | - | s.mu.RUnlock() | |
| 278 | + | if state.networkApplied { | |
| 279 | + | s.mu.Unlock() | |
| 280 | + | return | |
| 281 | + | } | |
| 282 | + | // Mark as applying to avoid double AddFilter races | |
| 283 | + | state.networkApplied = true | |
| 284 | + | s.webViews[wv] = state | |
| 285 | + | networkJSON := s.networkFilterJSON | |
| 286 | + | s.mu.Unlock() | |
| 238 | 287 | ||
| 239 | - | // Apply network filters if not already applied | |
| 240 | - | if !state.networkApplied && len(networkJSON) > 0 { | |
| 241 | - | webView := wv // capture for closure | |
| 242 | - | pkgwebkit.RunOnMainThread(func() { | |
| 243 | - | if webView.IsDestroyed() { | |
| 244 | - | return | |
| 245 | - | } | |
| 246 | - | s.applyNetworkFilters(webView, networkJSON) | |
| 247 | - | }) | |
| 288 | + | if len(networkJSON) == 0 { | |
| 289 | + | // Nothing to apply; reset flag | |
| 290 | + | s.mu.Lock() | |
| 291 | + | state.networkApplied = false | |
| 292 | + | s.webViews[wv] = state | |
| 293 | + | s.mu.Unlock() | |
| 294 | + | return | |
| 248 | 295 | } | |
| 296 | + | ||
| 297 | + | webView := wv // capture for closure | |
| 298 | + | s.runWhenWidgetReady(webView, func() { | |
| 299 | + | s.applyNetworkFilters(webView, networkJSON) | |
| 300 | + | }) | |
| 249 | 301 | } | |
| 250 | 302 | ||
| 251 | 303 | // applyNetworkFilters applies network blocking rules to a WebView. | |
| @@ -261,6 +313,13 @@ func (s *ContentBlockingService) applyNetworkFilters(wv *pkgwebkit.WebView, filt | |||
| 261 | 313 | ||
| 262 | 314 | ucm := gtkView.UserContentManager() | |
| 263 | 315 | if ucm == nil { | |
| 316 | + | // Reset state so a future ready WebView can retry | |
| 317 | + | s.mu.Lock() | |
| 318 | + | if state, exists := s.webViews[wv]; exists { | |
| 319 | + | state.networkApplied = false | |
| 320 | + | s.webViews[wv] = state | |
| 321 | + | } | |
| 322 | + | s.mu.Unlock() | |
| 264 | 323 | return | |
| 265 | 324 | } | |
| 266 | 325 | ||
| @@ -284,14 +343,6 @@ func (s *ContentBlockingService) applyNetworkFilters(wv *pkgwebkit.WebView, filt | |||
| 284 | 343 | return | |
| 285 | 344 | } | |
| 286 | 345 | ||
| 287 | - | // Mark as applied | |
| 288 | - | s.mu.Lock() | |
| 289 | - | if state, exists := s.webViews[wv]; exists { | |
| 290 | - | state.networkApplied = true | |
| 291 | - | s.webViews[wv] = state | |
| 292 | - | } | |
| 293 | - | s.mu.Unlock() | |
| 294 | - | ||
| 295 | 346 | logging.Debug(fmt.Sprintf("[filtering] Applied network filters to WebView %d", wv.ID())) | |
| 296 | 347 | } | |
| 297 | 348 | ||
| @@ -307,7 +358,9 @@ func (s *ContentBlockingService) setupCosmeticHooks(wv *pkgwebkit.WebView) { | |||
| 307 | 358 | if webView.IsDestroyed() { | |
| 308 | 359 | return | |
| 309 | 360 | } | |
| 310 | - | s.injectCosmeticRules(webView, uri) | |
| 361 | + | s.runWhenWidgetReady(webView, func() { | |
| 362 | + | s.injectCosmeticRules(webView, uri) | |
| 363 | + | }) | |
| 311 | 364 | }) | |
| 312 | 365 | } | |
| 313 | 366 | ||
| @@ -7,12 +7,58 @@ import ( | |||||
| 7 | "sync" | 7 | "sync" | ||
| 8 | 8 | ||||
| 9 | webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6" | 9 | webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6" | ||
| 10 | + | glib "github.com/diamondburned/gotk4/pkg/glib/v2" | |||
| 10 | 11 | ||||
| 11 | "github.com/bnema/dumber/internal/filtering/cosmetic" | 12 | "github.com/bnema/dumber/internal/filtering/cosmetic" | ||
| 12 | "github.com/bnema/dumber/internal/logging" | 13 | "github.com/bnema/dumber/internal/logging" | ||
| 13 | pkgwebkit "github.com/bnema/dumber/pkg/webkit" | 14 | pkgwebkit "github.com/bnema/dumber/pkg/webkit" | ||
| 14 | ) | 15 | ) | ||
| 15 | 16 | ||||
| 17 | + | // runWhenWidgetReady waits until the WebView exposes a valid UserContentManager before invoking fn on the GTK main thread. | |||
| 18 | + | // This avoids racing freshly split panes whose underlying widgets aren't fully initialized yet. | |||
| 19 | + | func (s *ContentBlockingService) runWhenWidgetReady(wv *pkgwebkit.WebView, fn func()) { | |||
| 20 | + | if wv == nil || wv.IsDestroyed() || fn == nil { | |||
| 21 | + | return | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | const maxAttempts = 50 // up to ~1s total | |||
| 25 | + | const retryDelayMS = uint(20) // 20ms between checks | |||
| 26 | + | ||||
| 27 | + | var tryApply func(attempt int) bool | |||
| 28 | + | tryApply = func(attempt int) bool { | |||
| 29 | + | if wv.IsDestroyed() { | |||
| 30 | + | return false | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | gtkView := wv.GetWebView() | |||
| 34 | + | if gtkView == nil { | |||
| 35 | + | return attempt < maxAttempts | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | if ucm := gtkView.UserContentManager(); ucm == nil { | |||
| 39 | + | return attempt < maxAttempts | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | pkgwebkit.RunOnMainThread(func() { | |||
| 43 | + | if wv.IsDestroyed() { | |||
| 44 | + | return | |||
| 45 | + | } | |||
| 46 | + | fn() | |||
| 47 | + | }) | |||
| 48 | + | return false | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | attempts := 0 | |||
| 52 | + | if !tryApply(attempts) { | |||
| 53 | + | return | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | glib.TimeoutAdd(retryDelayMS, func() bool { | |||
| 57 | + | attempts++ | |||
| 58 | + | return tryApply(attempts) | |||
| 59 | + | }) | |||
| 60 | + | } | |||
| 61 | + | ||||
| 16 | // ContentBlockingService manages content blocking for all WebViews. | 62 | // ContentBlockingService manages content blocking for all WebViews. | ||
| 17 | // It ensures network filters and cosmetic rules are applied consistently | 63 | // It ensures network filters and cosmetic rules are applied consistently | ||
| 18 | // across all WebViews in the application. | 64 | // across all WebViews in the application. | ||
| @@ -79,10 +125,7 @@ func (s *ContentBlockingService) SetFiltersReady(networkJSON []byte) { | |||||
| 79 | // This hides ads visually within milliseconds while network filters compile in background | 125 | // This hides ads visually within milliseconds while network filters compile in background | ||
| 80 | for _, wv := range webViews { | 126 | for _, wv := range webViews { | ||
| 81 | webView := wv // capture for closure | 127 | webView := wv // capture for closure | ||
| 82 | - | pkgwebkit.RunOnMainThread(func() { | 128 | + | s.runWhenWidgetReady(webView, func() { |
| 83 | - | if webView.IsDestroyed() { | |||
| 84 | - | return | |||
| 85 | - | } | |||
| 86 | s.injectCosmeticBaseScript(webView) | 129 | s.injectCosmeticBaseScript(webView) | ||
| 87 | if uri := webView.GetCurrentURL(); uri != "" { | 130 | if uri := webView.GetCurrentURL(); uri != "" { | ||
| 88 | s.injectCosmeticRules(webView, uri) | 131 | s.injectCosmeticRules(webView, uri) | ||
| @@ -117,10 +160,7 @@ func (s *ContentBlockingService) SetFiltersReady(networkJSON []byte) { | |||||
| 117 | // Apply network filters to all registered WebViews | 160 | // Apply network filters to all registered WebViews | ||
| 118 | for _, wv := range currentWebViews { | 161 | for _, wv := range currentWebViews { | ||
| 119 | webView := wv // capture for closure | 162 | webView := wv // capture for closure | ||
| 120 | - | pkgwebkit.RunOnMainThread(func() { | 163 | + | s.runWhenWidgetReady(webView, func() { |
| 121 | - | if webView.IsDestroyed() { | |||
| 122 | - | return | |||
| 123 | - | } | |||
| 124 | s.applyFiltersToWebView(webView) | 164 | s.applyFiltersToWebView(webView) | ||
| 125 | }) | 165 | }) | ||
| 126 | } | 166 | } | ||
| @@ -154,13 +194,10 @@ func (s *ContentBlockingService) RegisterWebView(wv *pkgwebkit.WebView) { | |||||
| 154 | s.injectCosmeticBaseScript(wv) | 194 | s.injectCosmeticBaseScript(wv) | ||
| 155 | } | 195 | } | ||
| 156 | 196 | ||||
| 157 | - | // Apply network filters if already compiled (instant) | 197 | + | // If network filters are already compiled, apply them immediately (non-blocking) |
| 158 | if s.contentBlockingMgr.IsFilterCompiled() { | 198 | if s.contentBlockingMgr.IsFilterCompiled() { | ||
| 159 | webView := wv // capture for closure | 199 | webView := wv // capture for closure | ||
| 160 | - | pkgwebkit.RunOnMainThread(func() { | 200 | + | s.runWhenWidgetReady(webView, func() { |
| 161 | - | if webView.IsDestroyed() { | |||
| 162 | - | return | |||
| 163 | - | } | |||
| 164 | s.applyFiltersToWebView(webView) | 201 | s.applyFiltersToWebView(webView) | ||
| 165 | }) | 202 | }) | ||
| 166 | } | 203 | } | ||
| @@ -231,21 +268,36 @@ func (s *ContentBlockingService) applyFiltersToWebView(wv *pkgwebkit.WebView) { | |||||
| 231 | return | 268 | return | ||
| 232 | } | 269 | } | ||
| 233 | 270 | ||||
| 234 | - | s.mu.RLock() | 271 | + | // Avoid triggering compilation on the GTK thread; only apply once compiled. |
| 235 | - | networkJSON := s.networkFilterJSON | 272 | + | if !s.contentBlockingMgr.IsFilterCompiled() { |
| 273 | + | return | |||
| 274 | + | } | |||
| 275 | + | ||||
| 276 | + | s.mu.Lock() | |||
| 236 | state := s.webViews[wv] | 277 | state := s.webViews[wv] | ||
| 237 | - | s.mu.RUnlock() | 278 | + | if state.networkApplied { |
| 279 | + | s.mu.Unlock() | |||
| 280 | + | return | |||
| 281 | + | } | |||
| 282 | + | // Mark as applying to avoid double AddFilter races | |||
| 283 | + | state.networkApplied = true | |||
| 284 | + | s.webViews[wv] = state | |||
| 285 | + | networkJSON := s.networkFilterJSON | |||
| 286 | + | s.mu.Unlock() | |||
| 238 | 287 | ||||
| 239 | - | // Apply network filters if not already applied | 288 | + | if len(networkJSON) == 0 { |
| 240 | - | if !state.networkApplied && len(networkJSON) > 0 { | 289 | + | // Nothing to apply; reset flag |
| 241 | - | webView := wv // capture for closure | 290 | + | s.mu.Lock() |
| 242 | - | pkgwebkit.RunOnMainThread(func() { | 291 | + | state.networkApplied = false |
| 243 | - | if webView.IsDestroyed() { | 292 | + | s.webViews[wv] = state |
| 244 | - | return | 293 | + | s.mu.Unlock() |
| 245 | - | } | 294 | + | return |
| 246 | - | s.applyNetworkFilters(webView, networkJSON) | |||
| 247 | - | }) | |||
| 248 | } | 295 | } | ||
| 296 | + | ||||
| 297 | + | webView := wv // capture for closure | |||
| 298 | + | s.runWhenWidgetReady(webView, func() { | |||
| 299 | + | s.applyNetworkFilters(webView, networkJSON) | |||
| 300 | + | }) | |||
| 249 | } | 301 | } | ||
| 250 | 302 | ||||
| 251 | // applyNetworkFilters applies network blocking rules to a WebView. | 303 | // applyNetworkFilters applies network blocking rules to a WebView. | ||
| @@ -261,6 +313,13 @@ func (s *ContentBlockingService) applyNetworkFilters(wv *pkgwebkit.WebView, filt | |||||
| 261 | 313 | ||||
| 262 | ucm := gtkView.UserContentManager() | 314 | ucm := gtkView.UserContentManager() | ||
| 263 | if ucm == nil { | 315 | if ucm == nil { | ||
| 316 | + | // Reset state so a future ready WebView can retry | |||
| 317 | + | s.mu.Lock() | |||
| 318 | + | if state, exists := s.webViews[wv]; exists { | |||
| 319 | + | state.networkApplied = false | |||
| 320 | + | s.webViews[wv] = state | |||
| 321 | + | } | |||
| 322 | + | s.mu.Unlock() | |||
| 264 | return | 323 | return | ||
| 265 | } | 324 | } | ||
| 266 | 325 | ||||
| @@ -284,14 +343,6 @@ func (s *ContentBlockingService) applyNetworkFilters(wv *pkgwebkit.WebView, filt | |||||
| 284 | return | 343 | return | ||
| 285 | } | 344 | } | ||
| 286 | 345 | ||||
| 287 | - | // Mark as applied | |||
| 288 | - | s.mu.Lock() | |||
| 289 | - | if state, exists := s.webViews[wv]; exists { | |||
| 290 | - | state.networkApplied = true | |||
| 291 | - | s.webViews[wv] = state | |||
| 292 | - | } | |||
| 293 | - | s.mu.Unlock() | |||
| 294 | - | ||||
| 295 | logging.Debug(fmt.Sprintf("[filtering] Applied network filters to WebView %d", wv.ID())) | 346 | logging.Debug(fmt.Sprintf("[filtering] Applied network filters to WebView %d", wv.ID())) | ||
| 296 | } | 347 | } | ||
| 297 | 348 | ||||
| @@ -307,7 +358,9 @@ func (s *ContentBlockingService) setupCosmeticHooks(wv *pkgwebkit.WebView) { | |||||
| 307 | if webView.IsDestroyed() { | 358 | if webView.IsDestroyed() { | ||
| 308 | return | 359 | return | ||
| 309 | } | 360 | } | ||
| 310 | - | s.injectCosmeticRules(webView, uri) | 361 | + | s.runWhenWidgetReady(webView, func() { |
| 362 | + | s.injectCosmeticRules(webView, uri) | |||
| 363 | + | }) | |||
| 311 | }) | 364 | }) | ||
| 312 | } | 365 | } | ||
| 313 | 366 | ||||