detached 8d33e03

feat(webkit): implement persistent storage for cookies and session data

- Add NetworkSession management with idempotent initialization
- Add DataDir/CacheDir to webkit.Config
- Configure CookieManager on WebView's actual session
- Store window reference in WebView for proper lifecycle
- Verify session is non-ephemeral before use
commit
8d33e03aa2a255225e9822b83868e8020db734a4
author
bamen · · 2025-10-08 18:54 UTC
committer
bamen · · 2025-10-08 17:47 UTC
parent
7a8b759
signature
unsigned
3 files · +161 -17
A pkg/webkit/network_session.go +99 -0
@@ -0,0 +1,99 @@
1+package webkit
2+
3+import (
4+ "fmt"
5+ "log"
6+
7+ webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
8+)
9+
10+var (
11+ // globalNetworkSession holds the persistent network session to prevent garbage collection
12+ // This must be kept alive for the entire application lifetime
13+ globalNetworkSession *webkit.NetworkSession
14+)
15+
16+// InitPersistentSession creates and initializes a persistent NetworkSession
17+// with the provided data and cache directories.
18+// This should be called before creating any WebViews to ensure they use persistent storage.
19+// Returns an error if initialization fails.
20+//
21+// IMPORTANT: Once created, this session is stored globally and will be used by all
22+// WebViews created afterward, as per WebKitGTK 6.0's design where the first created
23+// session becomes the default for the network process.
24+func InitPersistentSession(dataDir, cacheDir string) error {
25+ // If session already exists, return success (idempotent)
26+ // This ensures we only create ONE session for the entire application
27+ if globalNetworkSession != nil {
28+ log.Printf("[webkit] Using existing persistent network session")
29+ return nil
30+ }
31+
32+ // Create persistent network session using gotk4 API
33+ // Per WebKitGTK 6.0 docs: "The first WebKitNetworkSession created becomes the default"
34+ session := webkit.NewNetworkSession(dataDir, cacheDir)
35+ if session == nil {
36+ return fmt.Errorf("failed to create persistent network session")
37+ }
38+
39+ // Store the session globally to prevent garbage collection
40+ // This is CRITICAL - if the session is GC'd, WebKit falls back to ephemeral storage
41+ globalNetworkSession = session
42+
43+ log.Printf("[webkit] Persistent network session created: data=%s, cache=%s", dataDir, cacheDir)
44+
45+ // Verify the session is not ephemeral
46+ if session.IsEphemeral() {
47+ return fmt.Errorf("created session is ephemeral despite providing data directories")
48+ }
49+
50+ // Verify the WebsiteDataManager is not ephemeral
51+ websiteDataManager := session.WebsiteDataManager()
52+ if websiteDataManager == nil {
53+ return fmt.Errorf("failed to get website data manager from network session")
54+ }
55+ if websiteDataManager.IsEphemeral() {
56+ return fmt.Errorf("website data manager is ephemeral despite providing data directories")
57+ }
58+ log.Printf("[webkit] WebsiteDataManager verified as non-ephemeral")
59+
60+ // Configure CookieManager for persistent cookie storage
61+ // This is REQUIRED - without this, cookies are not persisted to disk
62+ cookieManager := session.CookieManager()
63+ if cookieManager == nil {
64+ return fmt.Errorf("failed to get cookie manager from network session")
65+ }
66+
67+ // Set cookie persistent storage to SQLite database
68+ // Format: filepath.Join(dataDir, "cookies.db")
69+ cookiePath := dataDir + "/cookies.db"
70+ cookieManager.SetPersistentStorage(cookiePath, webkit.CookiePersistentStorageSqlite)
71+
72+ // Set cookie accept policy to no third-party (default)
73+ cookieManager.SetAcceptPolicy(webkit.CookiePolicyAcceptNoThirdParty)
74+
75+ log.Printf("[webkit] Cookie manager configured: storage=%s, policy=no-third-party", cookiePath)
76+
77+ // Enable persistent credential storage (HTTP auth, etc.)
78+ session.SetPersistentCredentialStorageEnabled(true)
79+
80+ // Verify this is indeed the default session now
81+ defaultSession := webkit.NetworkSessionGetDefault()
82+ if defaultSession == nil {
83+ return fmt.Errorf("default network session is nil after creating persistent session")
84+ }
85+
86+ // Check if they're the same session by comparing ephemeral status
87+ if defaultSession.IsEphemeral() {
88+ return fmt.Errorf("default network session is ephemeral after creating persistent session")
89+ }
90+
91+ log.Printf("[webkit] Network session verified as persistent and set as default")
92+ return nil
93+}
94+
95+// GetGlobalNetworkSession returns the global network session.
96+// Returns nil if InitPersistentSession hasn't been called successfully.
97+func GetGlobalNetworkSession() *webkit.NetworkSession {
98+ return globalNetworkSession
99+}
@@ -0,0 +1,99 @@
1+package webkit
2+
3+import (
4+ "fmt"
5+ "log"
6+
7+ webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
8+)
9+
10+var (
11+ // globalNetworkSession holds the persistent network session to prevent garbage collection
12+ // This must be kept alive for the entire application lifetime
13+ globalNetworkSession *webkit.NetworkSession
14+)
15+
16+// InitPersistentSession creates and initializes a persistent NetworkSession
17+// with the provided data and cache directories.
18+// This should be called before creating any WebViews to ensure they use persistent storage.
19+// Returns an error if initialization fails.
20+//
21+// IMPORTANT: Once created, this session is stored globally and will be used by all
22+// WebViews created afterward, as per WebKitGTK 6.0's design where the first created
23+// session becomes the default for the network process.
24+func InitPersistentSession(dataDir, cacheDir string) error {
25+ // If session already exists, return success (idempotent)
26+ // This ensures we only create ONE session for the entire application
27+ if globalNetworkSession != nil {
28+ log.Printf("[webkit] Using existing persistent network session")
29+ return nil
30+ }
31+
32+ // Create persistent network session using gotk4 API
33+ // Per WebKitGTK 6.0 docs: "The first WebKitNetworkSession created becomes the default"
34+ session := webkit.NewNetworkSession(dataDir, cacheDir)
35+ if session == nil {
36+ return fmt.Errorf("failed to create persistent network session")
37+ }
38+
39+ // Store the session globally to prevent garbage collection
40+ // This is CRITICAL - if the session is GC'd, WebKit falls back to ephemeral storage
41+ globalNetworkSession = session
42+
43+ log.Printf("[webkit] Persistent network session created: data=%s, cache=%s", dataDir, cacheDir)
44+
45+ // Verify the session is not ephemeral
46+ if session.IsEphemeral() {
47+ return fmt.Errorf("created session is ephemeral despite providing data directories")
48+ }
49+
50+ // Verify the WebsiteDataManager is not ephemeral
51+ websiteDataManager := session.WebsiteDataManager()
52+ if websiteDataManager == nil {
53+ return fmt.Errorf("failed to get website data manager from network session")
54+ }
55+ if websiteDataManager.IsEphemeral() {
56+ return fmt.Errorf("website data manager is ephemeral despite providing data directories")
57+ }
58+ log.Printf("[webkit] WebsiteDataManager verified as non-ephemeral")
59+
60+ // Configure CookieManager for persistent cookie storage
61+ // This is REQUIRED - without this, cookies are not persisted to disk
62+ cookieManager := session.CookieManager()
63+ if cookieManager == nil {
64+ return fmt.Errorf("failed to get cookie manager from network session")
65+ }
66+
67+ // Set cookie persistent storage to SQLite database
68+ // Format: filepath.Join(dataDir, "cookies.db")
69+ cookiePath := dataDir + "/cookies.db"
70+ cookieManager.SetPersistentStorage(cookiePath, webkit.CookiePersistentStorageSqlite)
71+
72+ // Set cookie accept policy to no third-party (default)
73+ cookieManager.SetAcceptPolicy(webkit.CookiePolicyAcceptNoThirdParty)
74+
75+ log.Printf("[webkit] Cookie manager configured: storage=%s, policy=no-third-party", cookiePath)
76+
77+ // Enable persistent credential storage (HTTP auth, etc.)
78+ session.SetPersistentCredentialStorageEnabled(true)
79+
80+ // Verify this is indeed the default session now
81+ defaultSession := webkit.NetworkSessionGetDefault()
82+ if defaultSession == nil {
83+ return fmt.Errorf("default network session is nil after creating persistent session")
84+ }
85+
86+ // Check if they're the same session by comparing ephemeral status
87+ if defaultSession.IsEphemeral() {
88+ return fmt.Errorf("default network session is ephemeral after creating persistent session")
89+ }
90+
91+ log.Printf("[webkit] Network session verified as persistent and set as default")
92+ return nil
93+}
94+
95+// GetGlobalNetworkSession returns the global network session.
96+// Returns nil if InitPersistentSession hasn't been called successfully.
97+func GetGlobalNetworkSession() *webkit.NetworkSession {
98+ return globalNetworkSession
99+}
M pkg/webkit/types.go +6 -0
@@ -44,6 +44,12 @@ type Config struct {
4444
4545 // MinimumFontSize in pixels
4646 MinimumFontSize int
47+
48+ // DataDir is the directory for persistent data (cookies, localStorage, etc.)
49+ DataDir string
50+
51+ // CacheDir is the directory for HTTP cache
52+ CacheDir string
4753 }
4854
4955 // GetDefaultConfig returns a Config with sensible defaults
@@ -44,6 +44,12 @@ type Config struct {
44 44
45 // MinimumFontSize in pixels45 // MinimumFontSize in pixels
46 MinimumFontSize int46 MinimumFontSize int
47+
48+ // DataDir is the directory for persistent data (cookies, localStorage, etc.)
49+ DataDir string
50+
51+ // CacheDir is the directory for HTTP cache
52+ CacheDir string
47 }53 }
48 54
49 // GetDefaultConfig returns a Config with sensible defaults55 // GetDefaultConfig returns a Config with sensible defaults
M pkg/webkit/webview.go +56 -17
@@ -18,8 +18,9 @@ var (
1818
1919 // WebView wraps a WebKitGTK WebView
2020 type WebView struct {
21- view *webkit.WebView
22- id uint64
21+ view *webkit.WebView
22+ window *Window
23+ id uint64
2324
2425 // State
2526 config *Config
@@ -47,17 +48,55 @@ func NewWebView(cfg *Config) (*WebView, error) {
4748
4849 InitMainThread()
4950
50- // Create WebKitGTK WebView
51+ // Create GTK Window
52+ window, err := NewWindow("Dumb Browser")
53+ if err != nil {
54+ return nil, err
55+ }
56+
57+ // Initialize persistent session - REQUIRED, no ephemeral fallback
58+ // This must be done BEFORE creating the WebView
59+ if cfg.DataDir == "" || cfg.CacheDir == "" {
60+ return nil, fmt.Errorf("data and cache directories are required for persistent storage")
61+ }
62+
63+ if err := InitPersistentSession(cfg.DataDir, cfg.CacheDir); err != nil {
64+ return nil, fmt.Errorf("failed to initialize persistent storage: %w", err)
65+ }
66+
67+ // Create WebView - it will automatically use the persistent session
68+ // Per WebKitGTK 6.0: newly created session becomes the default for all WebViews
5169 wkView := webkit.NewWebView()
5270 if wkView == nil {
5371 return nil, ErrWebViewNotInitialized
5472 }
5573
74+ // Verify the WebView is using the persistent session
75+ viewSession := wkView.NetworkSession()
76+ if viewSession == nil {
77+ return nil, fmt.Errorf("WebView has no network session")
78+ }
79+ if viewSession.IsEphemeral() {
80+ return nil, fmt.Errorf("WebView is using ephemeral session despite persistent session initialization")
81+ }
82+
83+ // CRITICAL: Configure CookieManager on the WebView's actual session
84+ // This ensures cookies are persisted regardless of whether the WebView
85+ // uses our global session or creates its own
86+ cookieManager := viewSession.CookieManager()
87+ if cookieManager == nil {
88+ return nil, fmt.Errorf("failed to get cookie manager from WebView's network session")
89+ }
90+ cookiePath := cfg.DataDir + "/cookies.db"
91+ cookieManager.SetPersistentStorage(cookiePath, webkit.CookiePersistentStorageSqlite)
92+ cookieManager.SetAcceptPolicy(webkit.CookiePolicyAcceptNoThirdParty)
93+
5694 // Generate unique ID
5795 id := atomic.AddUint64(&viewIDCounter, 1)
5896
5997 wv := &WebView{
6098 view: wkView,
99+ window: window,
61100 id: id,
62101 config: cfg,
63102 }
@@ -70,6 +109,9 @@ func NewWebView(cfg *Config) (*WebView, error) {
70109 // Setup event handlers
71110 wv.setupEventHandlers()
72111
112+ // Add WebView as child of window
113+ window.SetChild(wkView)
114+
73115 // Register in global registry
74116 viewMu.Lock()
75117 viewRegistry[id] = wv
@@ -107,6 +149,9 @@ func (w *WebView) applyConfig() error {
107149 settings.SetUserAgent(w.config.UserAgent)
108150 }
109151
152+ // Enable developer tools (F12, inspector)
153+ settings.SetEnableDeveloperExtras(true)
154+
110155 // Enable hardware acceleration if configured
111156 settings.SetHardwareAccelerationPolicy(webkit.HardwareAccelerationPolicyAlways)
112157
@@ -246,7 +291,14 @@ func (w *WebView) Show() error {
246291 return ErrWebViewDestroyed
247292 }
248293
294+ // Make the WebView widget visible
249295 w.view.SetVisible(true)
296+
297+ // Show the window containing the WebView
298+ if w.window != nil {
299+ w.window.Show()
300+ }
301+
250302 return nil
251303 }
252304
@@ -543,20 +595,7 @@ func (w *WebView) Window() *Window {
543595 return nil
544596 }
545597
546- // Get the root window
547- widget := getWidget(w.view)
548- if widget != nil {
549- root := widget.Root()
550- if root != nil {
551- // Try to cast the native widget to a gtk.Window
552- if obj := root.Cast(); obj != nil {
553- if gtkWin, ok := obj.(*gtk.Window); ok {
554- return &Window{win: gtkWin}
555- }
556- }
557- }
558- }
559- return nil
598+ return w.window
560599 }
561600
562601 // UpdateContentFilters updates the content filtering rules
@@ -18,8 +18,9 @@ var (
18 18
19 // WebView wraps a WebKitGTK WebView19 // WebView wraps a WebKitGTK WebView
20 type WebView struct {20 type WebView struct {
21- view *webkit.WebView21+ view *webkit.WebView
22- id uint6422+ window *Window
23+ id uint64
23 24
24 // State25 // State
25 config *Config26 config *Config
@@ -47,17 +48,55 @@ func NewWebView(cfg *Config) (*WebView, error) {
47 48
48 InitMainThread()49 InitMainThread()
49 50
50- // Create WebKitGTK WebView51+ // Create GTK Window
52+ window, err := NewWindow("Dumb Browser")
53+ if err != nil {
54+ return nil, err
55+ }
56+
57+ // Initialize persistent session - REQUIRED, no ephemeral fallback
58+ // This must be done BEFORE creating the WebView
59+ if cfg.DataDir == "" || cfg.CacheDir == "" {
60+ return nil, fmt.Errorf("data and cache directories are required for persistent storage")
61+ }
62+
63+ if err := InitPersistentSession(cfg.DataDir, cfg.CacheDir); err != nil {
64+ return nil, fmt.Errorf("failed to initialize persistent storage: %w", err)
65+ }
66+
67+ // Create WebView - it will automatically use the persistent session
68+ // Per WebKitGTK 6.0: newly created session becomes the default for all WebViews
51 wkView := webkit.NewWebView()69 wkView := webkit.NewWebView()
52 if wkView == nil {70 if wkView == nil {
53 return nil, ErrWebViewNotInitialized71 return nil, ErrWebViewNotInitialized
54 }72 }
55 73
74+ // Verify the WebView is using the persistent session
75+ viewSession := wkView.NetworkSession()
76+ if viewSession == nil {
77+ return nil, fmt.Errorf("WebView has no network session")
78+ }
79+ if viewSession.IsEphemeral() {
80+ return nil, fmt.Errorf("WebView is using ephemeral session despite persistent session initialization")
81+ }
82+
83+ // CRITICAL: Configure CookieManager on the WebView's actual session
84+ // This ensures cookies are persisted regardless of whether the WebView
85+ // uses our global session or creates its own
86+ cookieManager := viewSession.CookieManager()
87+ if cookieManager == nil {
88+ return nil, fmt.Errorf("failed to get cookie manager from WebView's network session")
89+ }
90+ cookiePath := cfg.DataDir + "/cookies.db"
91+ cookieManager.SetPersistentStorage(cookiePath, webkit.CookiePersistentStorageSqlite)
92+ cookieManager.SetAcceptPolicy(webkit.CookiePolicyAcceptNoThirdParty)
93+
56 // Generate unique ID94 // Generate unique ID
57 id := atomic.AddUint64(&viewIDCounter, 1)95 id := atomic.AddUint64(&viewIDCounter, 1)
58 96
59 wv := &WebView{97 wv := &WebView{
60 view: wkView,98 view: wkView,
99+ window: window,
61 id: id,100 id: id,
62 config: cfg,101 config: cfg,
63 }102 }
@@ -70,6 +109,9 @@ func NewWebView(cfg *Config) (*WebView, error) {
70 // Setup event handlers109 // Setup event handlers
71 wv.setupEventHandlers()110 wv.setupEventHandlers()
72 111
112+ // Add WebView as child of window
113+ window.SetChild(wkView)
114+
73 // Register in global registry115 // Register in global registry
74 viewMu.Lock()116 viewMu.Lock()
75 viewRegistry[id] = wv117 viewRegistry[id] = wv
@@ -107,6 +149,9 @@ func (w *WebView) applyConfig() error {
107 settings.SetUserAgent(w.config.UserAgent)149 settings.SetUserAgent(w.config.UserAgent)
108 }150 }
109 151
152+ // Enable developer tools (F12, inspector)
153+ settings.SetEnableDeveloperExtras(true)
154+
110 // Enable hardware acceleration if configured155 // Enable hardware acceleration if configured
111 settings.SetHardwareAccelerationPolicy(webkit.HardwareAccelerationPolicyAlways)156 settings.SetHardwareAccelerationPolicy(webkit.HardwareAccelerationPolicyAlways)
112 157
@@ -246,7 +291,14 @@ func (w *WebView) Show() error {
246 return ErrWebViewDestroyed291 return ErrWebViewDestroyed
247 }292 }
248 293
294+ // Make the WebView widget visible
249 w.view.SetVisible(true)295 w.view.SetVisible(true)
296+
297+ // Show the window containing the WebView
298+ if w.window != nil {
299+ w.window.Show()
300+ }
301+
250 return nil302 return nil
251 }303 }
252 304
@@ -543,20 +595,7 @@ func (w *WebView) Window() *Window {
543 return nil595 return nil
544 }596 }
545 597
546- // Get the root window598+ return w.window
547- widget := getWidget(w.view)
548- if widget != nil {
549- root := widget.Root()
550- if root != nil {
551- // Try to cast the native widget to a gtk.Window
552- if obj := root.Cast(); obj != nil {
553- if gtkWin, ok := obj.(*gtk.Window); ok {
554- return &Window{win: gtkWin}
555- }
556- }
557- }
558- }
559- return nil
560 }599 }
561 600
562 // UpdateContentFilters updates the content filtering rules601 // UpdateContentFilters updates the content filtering rules

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close