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