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