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