1package webkit
2
3import (
4 "context"
5 "fmt"
6 "path/filepath"
7 "sync"
8
9 "github.com/bnema/dumber/internal/application/port"
10 "github.com/bnema/dumber/internal/logging"
11 "github.com/bnema/puregotk-webkit/webkit"
12 "github.com/rs/zerolog"
13)
14
15// FaviconDatabase is a type alias for webkit.FaviconDatabase.
16// Re-exported for use by UI layer without direct puregotk-webkit import.
17type FaviconDatabase = webkit.FaviconDatabase
18
19// WebKitContext manages the shared WebContext and persistent NetworkSession.
20// IMPORTANT: This MUST be initialized before creating any WebViews.
21type WebKitContext struct {
22 webContext *webkit.WebContext
23 networkSession *webkit.NetworkSession
24 faviconDB *webkit.FaviconDatabase
25
26 dataDir string
27 cacheDir string
28
29 logger zerolog.Logger
30 mu sync.RWMutex
31 initialized bool
32}
33
34// NewWebKitContext creates and initializes a WebKitContext with a persistent NetworkSession.
35// The dataDir and cacheDir are used for cookie storage, cache, and other persistent data.
36// This MUST be called before creating any WebViews to ensure they use persistent storage.
37func NewWebKitContext(ctx context.Context, dataDir, cacheDir string) (*WebKitContext, error) {
38 return NewWebKitContextWithOptions(ctx, port.WebKitContextOptions{
39 DataDir: dataDir,
40 CacheDir: cacheDir,
41 })
42}
43
44// NewWebKitContextWithOptions creates and initializes a WebKitContext with the given options.
45// This allows configuring memory pressure settings for web and network processes.
46func NewWebKitContextWithOptions(ctx context.Context, opts port.WebKitContextOptions) (*WebKitContext, error) {
47 log := logging.FromContext(ctx).With().Str("component", "webkit-context").Logger()
48
49 if opts.DataDir == "" {
50 return nil, fmt.Errorf("data directory cannot be empty")
51 }
52 if opts.CacheDir == "" {
53 return nil, fmt.Errorf("cache directory cannot be empty")
54 }
55
56 wkCtx := &WebKitContext{
57 dataDir: opts.DataDir,
58 cacheDir: opts.CacheDir,
59 logger: log,
60 }
61
62 // Apply network process memory pressure settings BEFORE creating session
63 // Per WebKitGTK docs: must be called before creating any NetworkSession
64 if opts.IsNetworkProcessMemoryConfigured() {
65 applier := NewMemoryPressureApplier()
66 if err := applier.ApplyNetworkProcessSettings(ctx, opts.NetworkProcessMemory); err != nil {
67 log.Warn().Err(err).Msg("failed to apply network process memory pressure settings")
68 }
69 }
70
71 // Create persistent network session FIRST
72 // Per WebKitGTK 6.0 docs: "The first WebKitNetworkSession created becomes the default"
73 if err := wkCtx.initNetworkSession(); err != nil {
74 return nil, fmt.Errorf("failed to init network session: %w", err)
75 }
76
77 // Create WebContext - use custom constructor if memory pressure settings are configured
78 if opts.IsWebProcessMemoryConfigured() {
79 memSettings := buildMemoryPressureSettings(opts.WebProcessMemory)
80 if memSettings != nil {
81 wkCtx.webContext = webkit.NewWebContextWithMemoryPressureSettings(memSettings)
82 if wkCtx.webContext != nil {
83 log.Info().
84 Int("limit_mb", opts.WebProcessMemory.MemoryLimitMB).
85 Float64("poll_sec", opts.WebProcessMemory.PollIntervalSec).
86 Float64("conservative", opts.WebProcessMemory.ConservativeThreshold).
87 Float64("strict", opts.WebProcessMemory.StrictThreshold).
88 Msg("created WebContext with memory pressure settings")
89 }
90 }
91 }
92
93 // Fall back to default WebContext if custom creation failed or wasn't needed
94 if wkCtx.webContext == nil {
95 wkCtx.webContext = webkit.WebContextGetDefault()
96 }
97 if wkCtx.webContext == nil {
98 return nil, fmt.Errorf("failed to create or get WebContext")
99 }
100
101 // Set cache model for browser-style caching
102 wkCtx.webContext.SetCacheModel(webkit.CacheModelWebBrowserValue)
103
104 wkCtx.initialized = true
105 log.Info().
106 Str("data_dir", opts.DataDir).
107 Str("cache_dir", opts.CacheDir).
108 Msg("webkit context initialized")
109
110 return wkCtx, nil
111}
112
113// initNetworkSession creates and configures the persistent network session.
114func (c *WebKitContext) initNetworkSession() error {
115 // Create persistent network session
116 session := webkit.NewNetworkSession(&c.dataDir, &c.cacheDir)
117 if session == nil {
118 return fmt.Errorf("failed to create network session")
119 }
120
121 // Verify session is not ephemeral
122 if session.IsEphemeral() {
123 return fmt.Errorf("network session is ephemeral despite providing data directories")
124 }
125
126 // Verify WebsiteDataManager is not ephemeral
127 dataManager := session.GetWebsiteDataManager()
128 if dataManager == nil {
129 return fmt.Errorf("failed to get website data manager")
130 }
131 if dataManager.IsEphemeral() {
132 return fmt.Errorf("website data manager is ephemeral")
133 }
134
135 // Debug: verify the directories WebKit is actually using
136 actualDataDir := dataManager.GetBaseDataDirectory()
137 actualCacheDir := dataManager.GetBaseCacheDirectory()
138 c.logger.Info().
139 Str("expected_data_dir", c.dataDir).
140 Str("actual_data_dir", actualDataDir).
141 Str("expected_cache_dir", c.cacheDir).
142 Str("actual_cache_dir", actualCacheDir).
143 Msg("webkit directories configured")
144
145 // Enable favicon collection and get database reference
146 dataManager.SetFaviconsEnabled(true)
147 c.faviconDB = dataManager.GetFaviconDatabase()
148 if c.faviconDB != nil {
149 c.logger.Debug().Msg("favicon database enabled")
150 }
151
152 // Configure cookie storage
153 cookieManager := session.GetCookieManager()
154 if cookieManager == nil {
155 return fmt.Errorf("failed to get cookie manager")
156 }
157
158 cookiePath := filepath.Join(c.dataDir, "cookies.db")
159 cookieManager.SetPersistentStorage(cookiePath, webkit.CookiePersistentStorageSqliteValue)
160 cookieManager.SetAcceptPolicy(webkit.CookiePolicyAcceptNoThirdPartyValue)
161
162 c.logger.Debug().
163 Str("cookie_path", cookiePath).
164 Msg("cookie storage configured")
165
166 // Enable persistent credential storage
167 session.SetPersistentCredentialStorageEnabled(true)
168
169 // Configure TLS errors to emit signals (enables load-failed-with-tls-errors)
170 session.SetTlsErrorsPolicy(webkit.TlsErrorsPolicyFailValue)
171
172 // Verify default session
173 defaultSession := webkit.NetworkSessionGetDefault()
174 if defaultSession == nil || defaultSession.IsEphemeral() {
175 c.logger.Warn().Msg("default session may not be persistent - created session might not be used by default")
176 }
177
178 c.networkSession = session
179 c.logger.Debug().Msg("network session initialized as persistent")
180
181 return nil
182}
183
184// Context returns the shared WebContext.
185func (c *WebKitContext) Context() *webkit.WebContext {
186 c.mu.RLock()
187 defer c.mu.RUnlock()
188 return c.webContext
189}
190
191// NetworkSession returns the persistent NetworkSession.
192func (c *WebKitContext) NetworkSession() *webkit.NetworkSession {
193 c.mu.RLock()
194 defer c.mu.RUnlock()
195 return c.networkSession
196}
197
198// FaviconDatabase returns the favicon database for persistent favicon storage.
199func (c *WebKitContext) FaviconDatabase() *webkit.FaviconDatabase {
200 c.mu.RLock()
201 defer c.mu.RUnlock()
202 return c.faviconDB
203}
204
205// DataDir returns the data directory path.
206func (c *WebKitContext) DataDir() string {
207 return c.dataDir
208}
209
210// CacheDir returns the cache directory path.
211func (c *WebKitContext) CacheDir() string {
212 return c.cacheDir
213}
214
215// IsInitialized returns true if the context has been successfully initialized.
216func (c *WebKitContext) IsInitialized() bool {
217 c.mu.RLock()
218 defer c.mu.RUnlock()
219 return c.initialized
220}
221
222// PrefetchDNS prefetches DNS for the given hostname to speed up future requests.
223func (c *WebKitContext) PrefetchDNS(hostname string) {
224 if hostname == "" || c.networkSession == nil {
225 return
226 }
227 c.networkSession.PrefetchDns(hostname)
228}
229
230// Close performs cleanup. Currently a no-op as WebKit handles cleanup internally.
231func (c *WebKitContext) Close() error {
232 c.mu.Lock()
233 defer c.mu.Unlock()
234
235 c.initialized = false
236 c.logger.Debug().Msg("webkit context closed")
237 return nil
238}