detached 030c8ba

fix(cef): address CodeRabbit review findings across CEF engine

- Remove debug stderr prints from main() startup path
- Use strings.HasPrefix for CEF subprocess detection
- Consolidate duplicate resolveEngineType into config.ResolveEngineType()
- Remove vestigial error return from UpdateHistoryTitle
- Guard persistTitleUpdate against empty titles
- Simplify factory adapter returns to direct delegation
- Fix recordBrowserCreateRequest log bug (Msg method capture)
- Make pumpWorkCount atomic to prevent potential races
- Document os.Args mutation thread-safety in engine_init
- Log warning in CreateRelated about unsupported session sharing
- Add error/type logging in pool Prewarm
- Use int64 intermediates in render buffer size calculation
- Distinguish left/right modifier VK codes in input bridge
- Close pendingURI race by holding lock through LoadURL
- Simplify double lock/unlock in webview state update helpers
commit
030c8ba724d89e5656aefaf4bf73c8310576d12c
author
brice · · 2026-03-20 15:12 UTC
committer
brice · · 2026-03-28 04:36 UTC
parent
a004a88
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
15 files · +67 -77
M cmd/dumber/main.go +3 -19
@@ -7,6 +7,7 @@ import (
77 "os"
88 "os/signal"
99 "runtime"
10+ "strings"
1011 "syscall"
1112
1213 "github.com/bnema/dumber/internal/application/port"
@@ -55,9 +56,6 @@ var restoreSessionID string
5556 func main() {
5657 enableCrashForensics()
5758
58- // Debug: dump args at entry to identify subprocess leakage.
59- fmt.Fprintf(os.Stderr, "cef-debug: main() entry, pid=%d args=%v\n", os.Getpid(), os.Args)
60-
6159 // CEF subprocess handling: when CEF re-launches the binary with
6260 // --type=renderer/gpu/etc, we must call ExecuteProcess before anything
6361 // else (Cobra, config, arg stripping). We detect subprocesses by the
@@ -65,9 +63,7 @@ func main() {
6563 // var, because subprocesses may not inherit the environment and the
6664 // engine can also be selected via config.
6765 if isCEFSubprocess() {
68- fmt.Fprintf(os.Stderr, "cef: detected subprocess, args=%v\n", os.Args)
6966 cef.MaybeExitSubprocess()
70- fmt.Fprintf(os.Stderr, "cef: MaybeExitSubprocess returned (browser process)\n")
7167 }
7268
7369 // Run GUI mode for browse command
@@ -113,7 +109,7 @@ func runGUI() int {
113109 }
114110 timer.MarkDuration("parallel_phase", initResult.Duration)
115111
116- if cfg.Engine.CEF.CEFMultiThreadedMessageLoop() && resolveEngineType(cfg) == "cef" {
112+ if cfg.Engine.CEF.CEFMultiThreadedMessageLoop() && cfg.Engine.ResolveEngineType() == "cef" {
117113 logging.FromContext(ctx).Info().Msg("pre-initializing libadwaita before CEF multi-threaded loop")
118114 ui.EnsureAdwaitaInitialized()
119115 if initResult.AdwaitaDetector != nil {
@@ -252,18 +248,6 @@ func initStartupContext(cfg *config.Config) context.Context {
252248 return ctx
253249 }
254250
255-// resolveEngineType returns the effective engine type from config + env override.
256-func resolveEngineType(cfg *config.Config) string {
257- engineType := cfg.Engine.Type
258- if engineType == "" {
259- engineType = "webkit"
260- }
261- if envEngine := os.Getenv("DUMBER_ENGINE"); envEngine != "" {
262- engineType = envEngine
263- }
264- return engineType
265-}
266-
267251 func initStackAndRepos(
268252 ctx context.Context,
269253 cfg *config.Config,
@@ -538,7 +522,7 @@ func createUseCases(repos *repositories, cfg *config.Config) *useCases {
538522 // this process was spawned by CEF as a renderer, GPU, or utility subprocess.
539523 func isCEFSubprocess() bool {
540524 for _, arg := range os.Args {
541- if len(arg) > 7 && arg[:7] == "--type=" {
525+ if strings.HasPrefix(arg, "--type=") {
542526 return true
543527 }
544528 }
@@ -7,6 +7,7 @@ import (
7 "os"7 "os"
8 "os/signal"8 "os/signal"
9 "runtime"9 "runtime"
10+ "strings"
10 "syscall"11 "syscall"
11 12
12 "github.com/bnema/dumber/internal/application/port"13 "github.com/bnema/dumber/internal/application/port"
@@ -55,9 +56,6 @@ var restoreSessionID string
55 func main() {56 func main() {
56 enableCrashForensics()57 enableCrashForensics()
57 58
58- // Debug: dump args at entry to identify subprocess leakage.
59- fmt.Fprintf(os.Stderr, "cef-debug: main() entry, pid=%d args=%v\n", os.Getpid(), os.Args)
60-
61 // CEF subprocess handling: when CEF re-launches the binary with59 // CEF subprocess handling: when CEF re-launches the binary with
62 // --type=renderer/gpu/etc, we must call ExecuteProcess before anything60 // --type=renderer/gpu/etc, we must call ExecuteProcess before anything
63 // else (Cobra, config, arg stripping). We detect subprocesses by the61 // else (Cobra, config, arg stripping). We detect subprocesses by the
@@ -65,9 +63,7 @@ func main() {
65 // var, because subprocesses may not inherit the environment and the63 // var, because subprocesses may not inherit the environment and the
66 // engine can also be selected via config.64 // engine can also be selected via config.
67 if isCEFSubprocess() {65 if isCEFSubprocess() {
68- fmt.Fprintf(os.Stderr, "cef: detected subprocess, args=%v\n", os.Args)
69 cef.MaybeExitSubprocess()66 cef.MaybeExitSubprocess()
70- fmt.Fprintf(os.Stderr, "cef: MaybeExitSubprocess returned (browser process)\n")
71 }67 }
72 68
73 // Run GUI mode for browse command69 // Run GUI mode for browse command
@@ -113,7 +109,7 @@ func runGUI() int {
113 }109 }
114 timer.MarkDuration("parallel_phase", initResult.Duration)110 timer.MarkDuration("parallel_phase", initResult.Duration)
115 111
116- if cfg.Engine.CEF.CEFMultiThreadedMessageLoop() && resolveEngineType(cfg) == "cef" {112+ if cfg.Engine.CEF.CEFMultiThreadedMessageLoop() && cfg.Engine.ResolveEngineType() == "cef" {
117 logging.FromContext(ctx).Info().Msg("pre-initializing libadwaita before CEF multi-threaded loop")113 logging.FromContext(ctx).Info().Msg("pre-initializing libadwaita before CEF multi-threaded loop")
118 ui.EnsureAdwaitaInitialized()114 ui.EnsureAdwaitaInitialized()
119 if initResult.AdwaitaDetector != nil {115 if initResult.AdwaitaDetector != nil {
@@ -252,18 +248,6 @@ func initStartupContext(cfg *config.Config) context.Context {
252 return ctx248 return ctx
253 }249 }
254 250
255-// resolveEngineType returns the effective engine type from config + env override.
256-func resolveEngineType(cfg *config.Config) string {
257- engineType := cfg.Engine.Type
258- if engineType == "" {
259- engineType = "webkit"
260- }
261- if envEngine := os.Getenv("DUMBER_ENGINE"); envEngine != "" {
262- engineType = envEngine
263- }
264- return engineType
265-}
266-
267 func initStackAndRepos(251 func initStackAndRepos(
268 ctx context.Context,252 ctx context.Context,
269 cfg *config.Config,253 cfg *config.Config,
@@ -538,7 +522,7 @@ func createUseCases(repos *repositories, cfg *config.Config) *useCases {
538 // this process was spawned by CEF as a renderer, GPU, or utility subprocess.522 // this process was spawned by CEF as a renderer, GPU, or utility subprocess.
539 func isCEFSubprocess() bool {523 func isCEFSubprocess() bool {
540 for _, arg := range os.Args {524 for _, arg := range os.Args {
541- if len(arg) > 7 && arg[:7] == "--type=" {525+ if strings.HasPrefix(arg, "--type=") {
542 return true526 return true
543 }527 }
544 }528 }
M internal/application/usecase/navigate.go +6 -3
@@ -330,6 +330,10 @@ func (uc *NavigateUseCase) persistHistory(ctx context.Context, record historyRec
330330 func (uc *NavigateUseCase) persistTitleUpdate(ctx context.Context, historyURL, title string) {
331331 log := logging.FromContext(ctx)
332332
333+ if title == "" {
334+ return
335+ }
336+
333337 entry, err := uc.historyRepo.FindByURL(ctx, historyURL)
334338 if err != nil {
335339 log.Warn().Err(err).Str("url", historyURL).Msg("failed to find history entry for title update")
@@ -354,12 +358,12 @@ func (uc *NavigateUseCase) persistTitleUpdate(ctx context.Context, historyURL, t
354358 // UpdateHistoryTitle queues a title update for a history entry. The actual
355359 // DB write happens in the background historyWorker to avoid concurrent
356360 // repo access from the GTK main thread and the worker goroutine.
357-func (uc *NavigateUseCase) UpdateHistoryTitle(_ context.Context, historyURL, title string) error {
361+func (uc *NavigateUseCase) UpdateHistoryTitle(_ context.Context, historyURL, title string) {
358362 historyURL = canonicalizeURLForHistory(historyURL, historyCanonicalizationOptions{
359363 StripTrackingParams: stripTrackingParamsForHistoryDedup,
360364 })
361365 if historyURL == "" {
362- return nil
366+ return
363367 }
364368
365369 // Non-blocking send — same queue as visit records.
@@ -368,7 +372,6 @@ func (uc *NavigateUseCase) UpdateHistoryTitle(_ context.Context, historyURL, tit
368372 default:
369373 // Queue full — title update is best-effort.
370374 }
371- return nil
372375 }
373376
374377 // Reload reloads the current page.
@@ -330,6 +330,10 @@ func (uc *NavigateUseCase) persistHistory(ctx context.Context, record historyRec
330 func (uc *NavigateUseCase) persistTitleUpdate(ctx context.Context, historyURL, title string) {330 func (uc *NavigateUseCase) persistTitleUpdate(ctx context.Context, historyURL, title string) {
331 log := logging.FromContext(ctx)331 log := logging.FromContext(ctx)
332 332
333+ if title == "" {
334+ return
335+ }
336+
333 entry, err := uc.historyRepo.FindByURL(ctx, historyURL)337 entry, err := uc.historyRepo.FindByURL(ctx, historyURL)
334 if err != nil {338 if err != nil {
335 log.Warn().Err(err).Str("url", historyURL).Msg("failed to find history entry for title update")339 log.Warn().Err(err).Str("url", historyURL).Msg("failed to find history entry for title update")
@@ -354,12 +358,12 @@ func (uc *NavigateUseCase) persistTitleUpdate(ctx context.Context, historyURL, t
354 // UpdateHistoryTitle queues a title update for a history entry. The actual358 // UpdateHistoryTitle queues a title update for a history entry. The actual
355 // DB write happens in the background historyWorker to avoid concurrent359 // DB write happens in the background historyWorker to avoid concurrent
356 // repo access from the GTK main thread and the worker goroutine.360 // repo access from the GTK main thread and the worker goroutine.
357-func (uc *NavigateUseCase) UpdateHistoryTitle(_ context.Context, historyURL, title string) error {361+func (uc *NavigateUseCase) UpdateHistoryTitle(_ context.Context, historyURL, title string) {
358 historyURL = canonicalizeURLForHistory(historyURL, historyCanonicalizationOptions{362 historyURL = canonicalizeURLForHistory(historyURL, historyCanonicalizationOptions{
359 StripTrackingParams: stripTrackingParamsForHistoryDedup,363 StripTrackingParams: stripTrackingParamsForHistoryDedup,
360 })364 })
361 if historyURL == "" {365 if historyURL == "" {
362- return nil366+ return
363 }367 }
364 368
365 // Non-blocking send — same queue as visit records.369 // Non-blocking send — same queue as visit records.
@@ -368,7 +372,6 @@ func (uc *NavigateUseCase) UpdateHistoryTitle(_ context.Context, historyURL, tit
368 default:372 default:
369 // Queue full — title update is best-effort.373 // Queue full — title update is best-effort.
370 }374 }
371- return nil
372 }375 }
373 376
374 // Reload reloads the current page.377 // Reload reloads the current page.
M internal/application/usecase/navigate_test.go +1 -2
@@ -79,8 +79,7 @@ func TestUpdateHistoryTitle_UsesMetadataUpdateWithoutIncrementingVisits(t *testi
7979 require.NotNil(t, before)
8080
8181 uc := NewNavigateUseCase(repo, nil, entity.ZoomDefault)
82- err = uc.UpdateHistoryTitle(ctx, "https://example.com/article", "New")
83- require.NoError(t, err)
82+ uc.UpdateHistoryTitle(ctx, "https://example.com/article", "New")
8483 uc.Close()
8584
8685 after, err := repo.FindByURL(ctx, "https://example.com/article")
@@ -79,8 +79,7 @@ func TestUpdateHistoryTitle_UsesMetadataUpdateWithoutIncrementingVisits(t *testi
79 require.NotNil(t, before)79 require.NotNil(t, before)
80 80
81 uc := NewNavigateUseCase(repo, nil, entity.ZoomDefault)81 uc := NewNavigateUseCase(repo, nil, entity.ZoomDefault)
82- err = uc.UpdateHistoryTitle(ctx, "https://example.com/article", "New")82+ uc.UpdateHistoryTitle(ctx, "https://example.com/article", "New")
83- require.NoError(t, err)
84 uc.Close()83 uc.Close()
85 84
86 after, err := repo.FindByURL(ctx, "https://example.com/article")85 after, err := repo.FindByURL(ctx, "https://example.com/article")
M internal/bootstrap/engine.go +1 -9
@@ -3,7 +3,6 @@ package bootstrap
33 import (
44 "context"
55 "fmt"
6- "os"
76
87 "github.com/bnema/dumber/internal/application/port"
98 "github.com/bnema/dumber/internal/application/usecase"
@@ -29,14 +28,7 @@ type EngineInput struct {
2928 // BuildEngine constructs a port.Engine for the engine type specified in cfg.Engine.Type.
3029 func BuildEngine(input EngineInput) (port.Engine, error) {
3130 cfg := input.Config
32- engineType := cfg.Engine.Type
33- if engineType == "" {
34- engineType = "webkit"
35- }
36- // Allow env var override for smoke testing: DUMBER_ENGINE=cef
37- if envEngine := os.Getenv("DUMBER_ENGINE"); envEngine != "" {
38- engineType = envEngine
39- }
31+ engineType := cfg.Engine.ResolveEngineType()
4032 switch engineType {
4133 case "webkit":
4234 opts := port.EngineOptions{
@@ -3,7 +3,6 @@ package bootstrap
3 import (3 import (
4 "context"4 "context"
5 "fmt"5 "fmt"
6- "os"
7 6
8 "github.com/bnema/dumber/internal/application/port"7 "github.com/bnema/dumber/internal/application/port"
9 "github.com/bnema/dumber/internal/application/usecase"8 "github.com/bnema/dumber/internal/application/usecase"
@@ -29,14 +28,7 @@ type EngineInput struct {
29 // BuildEngine constructs a port.Engine for the engine type specified in cfg.Engine.Type.28 // BuildEngine constructs a port.Engine for the engine type specified in cfg.Engine.Type.
30 func BuildEngine(input EngineInput) (port.Engine, error) {29 func BuildEngine(input EngineInput) (port.Engine, error) {
31 cfg := input.Config30 cfg := input.Config
32- engineType := cfg.Engine.Type31+ engineType := cfg.Engine.ResolveEngineType()
33- if engineType == "" {
34- engineType = "webkit"
35- }
36- // Allow env var override for smoke testing: DUMBER_ENGINE=cef
37- if envEngine := os.Getenv("DUMBER_ENGINE"); envEngine != "" {
38- engineType = envEngine
39- }
40 switch engineType {32 switch engineType {
41 case "webkit":33 case "webkit":
42 opts := port.EngineOptions{34 opts := port.EngineOptions{
M internal/infrastructure/cef/engine.go +7 -9
@@ -32,7 +32,7 @@ type Engine struct {
3232 pumpTimerSource atomic.Uint64
3333
3434 // Diagnostic counters.
35- pumpWorkCount uint64
35+ pumpWorkCount atomic.Uint64
3636 scheduleCount atomic.Uint64 // OnScheduleMessagePumpWork call count (currently always 0)
3737
3838 contextInitializedCount atomic.Uint64
@@ -171,12 +171,12 @@ func (e *Engine) recordBrowserCreateRequest(width, height, result int32) {
171171 e.browserCreateLastWidth.Store(width)
172172 e.browserCreateLastHeight.Store(height)
173173
174- log := logging.FromContext(e.ctx).Debug().
174+ logging.FromContext(e.ctx).Debug().
175175 Uint64("request_count", count).
176176 Int32("width", width).
177177 Int32("height", height).
178178 Int32("result", result).
179- Msg
179+ Msg("cef: BrowserHostCreateBrowser returned")
180180 if result != 1 {
181181 logging.FromContext(e.ctx).Warn().
182182 Uint64("request_count", count).
@@ -184,9 +184,7 @@ func (e *Engine) recordBrowserCreateRequest(width, height, result int32) {
184184 Int32("height", height).
185185 Int32("result", result).
186186 Msg("cef: BrowserHostCreateBrowser returned non-success")
187- return
188187 }
189- log("cef: BrowserHostCreateBrowser returned")
190188 }
191189
192190 func (e *Engine) recordBrowserAfterCreated(browser purecef.Browser) {
@@ -226,7 +224,7 @@ func (e *Engine) maybeLogBrowserCreateStall() {
226224 Int32("last_create_result", e.browserCreateLastResult.Load()).
227225 Int32("last_create_width", e.browserCreateLastWidth.Load()).
228226 Int32("last_create_height", e.browserCreateLastHeight.Load()).
229- Uint64("pump_work", e.pumpWorkCount).
227+ Uint64("pump_work", e.pumpWorkCount.Load()).
230228 Uint64("schedule_calls", e.scheduleCount.Load()).
231229 Uint64("context_initialized", e.contextInitializedCount.Load()).
232230 Uint64("renderer_launches", e.childLaunchRenderer.Load()).
@@ -269,11 +267,11 @@ func (e *Engine) startManualMessagePump() {
269267 return glib.SOURCE_REMOVE
270268 }
271269
272- e.pumpWorkCount++
270+ count := e.pumpWorkCount.Add(1)
273271 e.maybeLogBrowserCreateStall()
274- if e.pumpWorkCount <= 20 || e.pumpWorkCount%100 == 0 {
272+ if count <= 20 || count%100 == 0 {
275273 logging.FromContext(e.ctx).Debug().
276- Uint64("work", e.pumpWorkCount).
274+ Uint64("work", count).
277275 Int64("interval_ms", intervalMs).
278276 Msg("cef: manual pump doing work")
279277 }
@@ -32,7 +32,7 @@ type Engine struct {
32 pumpTimerSource atomic.Uint6432 pumpTimerSource atomic.Uint64
33 33
34 // Diagnostic counters.34 // Diagnostic counters.
35- pumpWorkCount uint6435+ pumpWorkCount atomic.Uint64
36 scheduleCount atomic.Uint64 // OnScheduleMessagePumpWork call count (currently always 0)36 scheduleCount atomic.Uint64 // OnScheduleMessagePumpWork call count (currently always 0)
37 37
38 contextInitializedCount atomic.Uint6438 contextInitializedCount atomic.Uint64
@@ -171,12 +171,12 @@ func (e *Engine) recordBrowserCreateRequest(width, height, result int32) {
171 e.browserCreateLastWidth.Store(width)171 e.browserCreateLastWidth.Store(width)
172 e.browserCreateLastHeight.Store(height)172 e.browserCreateLastHeight.Store(height)
173 173
174- log := logging.FromContext(e.ctx).Debug().174+ logging.FromContext(e.ctx).Debug().
175 Uint64("request_count", count).175 Uint64("request_count", count).
176 Int32("width", width).176 Int32("width", width).
177 Int32("height", height).177 Int32("height", height).
178 Int32("result", result).178 Int32("result", result).
179- Msg179+ Msg("cef: BrowserHostCreateBrowser returned")
180 if result != 1 {180 if result != 1 {
181 logging.FromContext(e.ctx).Warn().181 logging.FromContext(e.ctx).Warn().
182 Uint64("request_count", count).182 Uint64("request_count", count).
@@ -184,9 +184,7 @@ func (e *Engine) recordBrowserCreateRequest(width, height, result int32) {
184 Int32("height", height).184 Int32("height", height).
185 Int32("result", result).185 Int32("result", result).
186 Msg("cef: BrowserHostCreateBrowser returned non-success")186 Msg("cef: BrowserHostCreateBrowser returned non-success")
187- return
188 }187 }
189- log("cef: BrowserHostCreateBrowser returned")
190 }188 }
191 189
192 func (e *Engine) recordBrowserAfterCreated(browser purecef.Browser) {190 func (e *Engine) recordBrowserAfterCreated(browser purecef.Browser) {
@@ -226,7 +224,7 @@ func (e *Engine) maybeLogBrowserCreateStall() {
226 Int32("last_create_result", e.browserCreateLastResult.Load()).224 Int32("last_create_result", e.browserCreateLastResult.Load()).
227 Int32("last_create_width", e.browserCreateLastWidth.Load()).225 Int32("last_create_width", e.browserCreateLastWidth.Load()).
228 Int32("last_create_height", e.browserCreateLastHeight.Load()).226 Int32("last_create_height", e.browserCreateLastHeight.Load()).
229- Uint64("pump_work", e.pumpWorkCount).227+ Uint64("pump_work", e.pumpWorkCount.Load()).
230 Uint64("schedule_calls", e.scheduleCount.Load()).228 Uint64("schedule_calls", e.scheduleCount.Load()).
231 Uint64("context_initialized", e.contextInitializedCount.Load()).229 Uint64("context_initialized", e.contextInitializedCount.Load()).
232 Uint64("renderer_launches", e.childLaunchRenderer.Load()).230 Uint64("renderer_launches", e.childLaunchRenderer.Load()).
@@ -269,11 +267,11 @@ func (e *Engine) startManualMessagePump() {
269 return glib.SOURCE_REMOVE267 return glib.SOURCE_REMOVE
270 }268 }
271 269
272- e.pumpWorkCount++270+ count := e.pumpWorkCount.Add(1)
273 e.maybeLogBrowserCreateStall()271 e.maybeLogBrowserCreateStall()
274- if e.pumpWorkCount <= 20 || e.pumpWorkCount%100 == 0 {272+ if count <= 20 || count%100 == 0 {
275 logging.FromContext(e.ctx).Debug().273 logging.FromContext(e.ctx).Debug().
276- Uint64("work", e.pumpWorkCount).274+ Uint64("work", count).
277 Int64("interval_ms", intervalMs).275 Int64("interval_ms", intervalMs).
278 Msg("cef: manual pump doing work")276 Msg("cef: manual pump doing work")
279 }277 }
M internal/infrastructure/cef/engine_adapters.go +2 -10
@@ -18,19 +18,11 @@ type webViewFactoryAdapter struct {
1818 }
1919
2020 func (a *webViewFactoryAdapter) Create(ctx context.Context) (port.WebView, error) {
21- wv, err := a.factory.Create(ctx)
22- if err != nil {
23- return nil, err
24- }
25- return wv, nil
21+ return a.factory.Create(ctx)
2622 }
2723
2824 func (a *webViewFactoryAdapter) CreateRelated(ctx context.Context, parentID port.WebViewID) (port.WebView, error) {
29- wv, err := a.factory.CreateRelated(ctx, parentID)
30- if err != nil {
31- return nil, err
32- }
33- return wv, nil
25+ return a.factory.CreateRelated(ctx, parentID)
3426 }
3527
3628 // --- WebViewPool adapter ---
@@ -18,19 +18,11 @@ type webViewFactoryAdapter struct {
18 }18 }
19 19
20 func (a *webViewFactoryAdapter) Create(ctx context.Context) (port.WebView, error) {20 func (a *webViewFactoryAdapter) Create(ctx context.Context) (port.WebView, error) {
21- wv, err := a.factory.Create(ctx)21+ return a.factory.Create(ctx)
22- if err != nil {
23- return nil, err
24- }
25- return wv, nil
26 }22 }
27 23
28 func (a *webViewFactoryAdapter) CreateRelated(ctx context.Context, parentID port.WebViewID) (port.WebView, error) {24 func (a *webViewFactoryAdapter) CreateRelated(ctx context.Context, parentID port.WebViewID) (port.WebView, error) {
29- wv, err := a.factory.CreateRelated(ctx, parentID)25+ return a.factory.CreateRelated(ctx, parentID)
30- if err != nil {
31- return nil, err
32- }
33- return wv, nil
34 }26 }
35 27
36 // --- WebViewPool adapter ---28 // --- WebViewPool adapter ---
M internal/infrastructure/cef/engine_init.go +3 -0
@@ -56,6 +56,9 @@ func NewEngine(ctx context.Context, cfg config.CEFEngineConfig) (*Engine, error)
5656 // --no-zygote: Disable the zygote process which uses fork(). Go's runtime
5757 // does not survive fork cleanly, causing renderer crashes. Without zygote,
5858 // CEF launches each subprocess as a fresh exec of the helper binary.
59+ //
60+ // Thread-safety: safe because this runs during single-threaded startup
61+ // before any concurrent goroutines are spawned.
5962 savedArgs := os.Args
6063 os.Args = appendIfMissing(os.Args, "--no-zygote")
6164
@@ -56,6 +56,9 @@ func NewEngine(ctx context.Context, cfg config.CEFEngineConfig) (*Engine, error)
56 // --no-zygote: Disable the zygote process which uses fork(). Go's runtime56 // --no-zygote: Disable the zygote process which uses fork(). Go's runtime
57 // does not survive fork cleanly, causing renderer crashes. Without zygote,57 // does not survive fork cleanly, causing renderer crashes. Without zygote,
58 // CEF launches each subprocess as a fresh exec of the helper binary.58 // CEF launches each subprocess as a fresh exec of the helper binary.
59+ //
60+ // Thread-safety: safe because this runs during single-threaded startup
61+ // before any concurrent goroutines are spawned.
59 savedArgs := os.Args62 savedArgs := os.Args
60 os.Args = appendIfMissing(os.Args, "--no-zygote")63 os.Args = appendIfMissing(os.Args, "--no-zygote")
61 64
M internal/infrastructure/cef/factory.go +1 -0
@@ -124,5 +124,6 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) {
124124 // TODO(phase2): look up the parent browser by parentID and use the same
125125 // request context so cookies and session state are shared.
126126 func (f *WebViewFactory) CreateRelated(ctx context.Context, _ port.WebViewID) (port.WebView, error) {
127+ logging.FromContext(ctx).Warn().Msg("cef: CreateRelated does not yet support session sharing, creating independent WebView")
127128 return f.Create(ctx)
128129 }
@@ -124,5 +124,6 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) {
124 // TODO(phase2): look up the parent browser by parentID and use the same124 // TODO(phase2): look up the parent browser by parentID and use the same
125 // request context so cookies and session state are shared.125 // request context so cookies and session state are shared.
126 func (f *WebViewFactory) CreateRelated(ctx context.Context, _ port.WebViewID) (port.WebView, error) {126 func (f *WebViewFactory) CreateRelated(ctx context.Context, _ port.WebViewID) (port.WebView, error) {
127+ logging.FromContext(ctx).Warn().Msg("cef: CreateRelated does not yet support session sharing, creating independent WebView")
127 return f.Create(ctx)128 return f.Create(ctx)
128 }129 }
M internal/infrastructure/cef/input_bridge.go +12 -6
@@ -409,12 +409,18 @@ func gdkKeyvalToVKSwitch(keyval uint) int32 {
409409 return 0x21
410410 case gdkKeyPageDown:
411411 return 0x22
412- case 0xffe1, 0xffe2: // Shift_L, Shift_R
413- return 0xA0
414- case 0xffe3, 0xffe4: // Control_L, Control_R
415- return 0xA2
416- case 0xffe9, 0xffea: // Alt_L, Alt_R
417- return 0xA4
412+ case 0xffe1: // Shift_L
413+ return 0xA0 // VK_LSHIFT
414+ case 0xffe2: // Shift_R
415+ return 0xA1 // VK_RSHIFT
416+ case 0xffe3: // Control_L
417+ return 0xA2 // VK_LCONTROL
418+ case 0xffe4: // Control_R
419+ return 0xA3 // VK_RCONTROL
420+ case 0xffe9: // Alt_L
421+ return 0xA4 // VK_LMENU
422+ case 0xffea: // Alt_R
423+ return 0xA5 // VK_RMENU
418424 default:
419425 // For unmapped keys, return the keyval if it fits in a reasonable range.
420426 if keyval < 0x100 {
@@ -409,12 +409,18 @@ func gdkKeyvalToVKSwitch(keyval uint) int32 {
409 return 0x21409 return 0x21
410 case gdkKeyPageDown:410 case gdkKeyPageDown:
411 return 0x22411 return 0x22
412- case 0xffe1, 0xffe2: // Shift_L, Shift_R412+ case 0xffe1: // Shift_L
413- return 0xA0413+ return 0xA0 // VK_LSHIFT
414- case 0xffe3, 0xffe4: // Control_L, Control_R414+ case 0xffe2: // Shift_R
415- return 0xA2415+ return 0xA1 // VK_RSHIFT
416- case 0xffe9, 0xffea: // Alt_L, Alt_R416+ case 0xffe3: // Control_L
417- return 0xA4417+ return 0xA2 // VK_LCONTROL
418+ case 0xffe4: // Control_R
419+ return 0xA3 // VK_RCONTROL
420+ case 0xffe9: // Alt_L
421+ return 0xA4 // VK_LMENU
422+ case 0xffea: // Alt_R
423+ return 0xA5 // VK_RMENU
418 default:424 default:
419 // For unmapped keys, return the keyval if it fits in a reasonable range.425 // For unmapped keys, return the keyval if it fits in a reasonable range.
420 if keyval < 0x100 {426 if keyval < 0x100 {
M internal/infrastructure/cef/pool.go +9 -2
@@ -3,9 +3,11 @@ package cef
33 import (
44 "context"
55 "errors"
6+ "fmt"
67 "sync"
78
89 "github.com/bnema/dumber/internal/application/port"
10+ "github.com/bnema/dumber/internal/logging"
911 )
1012
1113 // Compile-time interface check.
@@ -67,15 +69,20 @@ func (p *WebViewPool) Prewarm(count int) {
6769 }
6870
6971 ctx := context.Background()
72+ log := logging.FromContext(ctx)
7073 views := make([]*WebView, 0, count)
7174 for range count {
7275 wv, err := p.factory.Create(ctx)
7376 if err != nil {
77+ log.Warn().Err(err).Msg("cef: pool prewarm failed to create WebView")
7478 continue
7579 }
76- if cefWV, ok := wv.(*WebView); ok {
77- views = append(views, cefWV)
80+ cefWV, ok := wv.(*WebView)
81+ if !ok {
82+ log.Warn().Str("concrete_type", fmt.Sprintf("%T", wv)).Msg("cef: pool prewarm got unexpected WebView type")
83+ continue
7884 }
85+ views = append(views, cefWV)
7986 }
8087
8188 p.mu.Lock()
@@ -3,9 +3,11 @@ package cef
3 import (3 import (
4 "context"4 "context"
5 "errors"5 "errors"
6+ "fmt"
6 "sync"7 "sync"
7 8
8 "github.com/bnema/dumber/internal/application/port"9 "github.com/bnema/dumber/internal/application/port"
10+ "github.com/bnema/dumber/internal/logging"
9 )11 )
10 12
11 // Compile-time interface check.13 // Compile-time interface check.
@@ -67,15 +69,20 @@ func (p *WebViewPool) Prewarm(count int) {
67 }69 }
68 70
69 ctx := context.Background()71 ctx := context.Background()
72+ log := logging.FromContext(ctx)
70 views := make([]*WebView, 0, count)73 views := make([]*WebView, 0, count)
71 for range count {74 for range count {
72 wv, err := p.factory.Create(ctx)75 wv, err := p.factory.Create(ctx)
73 if err != nil {76 if err != nil {
77+ log.Warn().Err(err).Msg("cef: pool prewarm failed to create WebView")
74 continue78 continue
75 }79 }
76- if cefWV, ok := wv.(*WebView); ok {80+ cefWV, ok := wv.(*WebView)
77- views = append(views, cefWV)81+ if !ok {
82+ log.Warn().Str("concrete_type", fmt.Sprintf("%T", wv)).Msg("cef: pool prewarm got unexpected WebView type")
83+ continue
78 }84 }
85+ views = append(views, cefWV)
79 }86 }
80 87
81 p.mu.Lock()88 p.mu.Lock()
M internal/infrastructure/cef/render_pipeline.go +1 -1
@@ -159,7 +159,7 @@ func (rp *renderPipeline) handlePaint(buffer unsafe.Pointer, width, height int32
159159 startedAt := time.Now()
160160 rp.mu.Lock()
161161
162- bufSize := int(width) * int(height) * 4
162+ bufSize := int(int64(width) * int64(height) * 4)
163163 srcSlice := unsafe.Slice((*byte)(buffer), bufSize)
164164 copiedBytes := uint64(0)
165165
@@ -159,7 +159,7 @@ func (rp *renderPipeline) handlePaint(buffer unsafe.Pointer, width, height int32
159 startedAt := time.Now()159 startedAt := time.Now()
160 rp.mu.Lock()160 rp.mu.Lock()
161 161
162- bufSize := int(width) * int(height) * 4162+ bufSize := int(int64(width) * int64(height) * 4)
163 srcSlice := unsafe.Slice((*byte)(buffer), bufSize)163 srcSlice := unsafe.Slice((*byte)(buffer), bufSize)
164 copiedBytes := uint64(0)164 copiedBytes := uint64(0)
165 165
M internal/infrastructure/cef/webview.go +3 -9
@@ -380,11 +380,9 @@ func (wv *WebView) NativeWidget() uintptr {
380380 func (wv *WebView) updateURI(uri string) {
381381 wv.mu.Lock()
382382 wv.uri = uri
383+ cb := wv.callbacks
383384 wv.mu.Unlock()
384385
385- wv.mu.RLock()
386- cb := wv.callbacks
387- wv.mu.RUnlock()
388386 if cb != nil && cb.OnURIChanged != nil {
389387 wv.runOnGTK(func() {
390388 cb.OnURIChanged(uri)
@@ -395,11 +393,9 @@ func (wv *WebView) updateURI(uri string) {
395393 func (wv *WebView) updateTitle(title string) {
396394 wv.mu.Lock()
397395 wv.title = title
396+ cb := wv.callbacks
398397 wv.mu.Unlock()
399398
400- wv.mu.RLock()
401- cb := wv.callbacks
402- wv.mu.RUnlock()
403399 if cb != nil && cb.OnTitleChanged != nil {
404400 wv.runOnGTK(func() {
405401 cb.OnTitleChanged(title)
@@ -410,11 +406,9 @@ func (wv *WebView) updateTitle(title string) {
410406 func (wv *WebView) updateProgress(progress float64) {
411407 wv.mu.Lock()
412408 wv.progress = progress
409+ cb := wv.callbacks
413410 wv.mu.Unlock()
414411
415- wv.mu.RLock()
416- cb := wv.callbacks
417- wv.mu.RUnlock()
418412 if cb != nil && cb.OnProgressChanged != nil {
419413 wv.runOnGTK(func() {
420414 cb.OnProgressChanged(progress)
@@ -380,11 +380,9 @@ func (wv *WebView) NativeWidget() uintptr {
380 func (wv *WebView) updateURI(uri string) {380 func (wv *WebView) updateURI(uri string) {
381 wv.mu.Lock()381 wv.mu.Lock()
382 wv.uri = uri382 wv.uri = uri
383+ cb := wv.callbacks
383 wv.mu.Unlock()384 wv.mu.Unlock()
384 385
385- wv.mu.RLock()
386- cb := wv.callbacks
387- wv.mu.RUnlock()
388 if cb != nil && cb.OnURIChanged != nil {386 if cb != nil && cb.OnURIChanged != nil {
389 wv.runOnGTK(func() {387 wv.runOnGTK(func() {
390 cb.OnURIChanged(uri)388 cb.OnURIChanged(uri)
@@ -395,11 +393,9 @@ func (wv *WebView) updateURI(uri string) {
395 func (wv *WebView) updateTitle(title string) {393 func (wv *WebView) updateTitle(title string) {
396 wv.mu.Lock()394 wv.mu.Lock()
397 wv.title = title395 wv.title = title
396+ cb := wv.callbacks
398 wv.mu.Unlock()397 wv.mu.Unlock()
399 398
400- wv.mu.RLock()
401- cb := wv.callbacks
402- wv.mu.RUnlock()
403 if cb != nil && cb.OnTitleChanged != nil {399 if cb != nil && cb.OnTitleChanged != nil {
404 wv.runOnGTK(func() {400 wv.runOnGTK(func() {
405 cb.OnTitleChanged(title)401 cb.OnTitleChanged(title)
@@ -410,11 +406,9 @@ func (wv *WebView) updateTitle(title string) {
410 func (wv *WebView) updateProgress(progress float64) {406 func (wv *WebView) updateProgress(progress float64) {
411 wv.mu.Lock()407 wv.mu.Lock()
412 wv.progress = progress408 wv.progress = progress
409+ cb := wv.callbacks
413 wv.mu.Unlock()410 wv.mu.Unlock()
414 411
415- wv.mu.RLock()
416- cb := wv.callbacks
417- wv.mu.RUnlock()
418 if cb != nil && cb.OnProgressChanged != nil {412 if cb != nil && cb.OnProgressChanged != nil {
419 wv.runOnGTK(func() {413 wv.runOnGTK(func() {
420 cb.OnProgressChanged(progress)414 cb.OnProgressChanged(progress)
M internal/infrastructure/cef/webview_handlers.go +1 -2
@@ -357,14 +357,13 @@ func (h *handlerSet) OnAfterCreated(browser purecef.Browser) {
357357 h.wv.host = host
358358 uri := h.wv.pendingURI
359359 h.wv.pendingURI = ""
360- h.wv.mu.Unlock()
361-
362360 h.wv.input.setHost(host)
363361
364362 // Replay any navigation that was requested before the browser existed.
365363 if uri != "" {
366364 browser.GetMainFrame().LoadURL(uri)
367365 }
366+ h.wv.mu.Unlock()
368367 }
369368
370369 // DoClose returns false to allow the default close behavior.
@@ -357,14 +357,13 @@ func (h *handlerSet) OnAfterCreated(browser purecef.Browser) {
357 h.wv.host = host357 h.wv.host = host
358 uri := h.wv.pendingURI358 uri := h.wv.pendingURI
359 h.wv.pendingURI = ""359 h.wv.pendingURI = ""
360- h.wv.mu.Unlock()
361-
362 h.wv.input.setHost(host)360 h.wv.input.setHost(host)
363 361
364 // Replay any navigation that was requested before the browser existed.362 // Replay any navigation that was requested before the browser existed.
365 if uri != "" {363 if uri != "" {
366 browser.GetMainFrame().LoadURL(uri)364 browser.GetMainFrame().LoadURL(uri)
367 }365 }
366+ h.wv.mu.Unlock()
368 }367 }
369 368
370 // DoClose returns false to allow the default close behavior.369 // DoClose returns false to allow the default close behavior.
M internal/infrastructure/config/engine.go +16 -0
@@ -1,5 +1,21 @@
11 package config
22
3+import "os"
4+
5+// ResolveEngineType returns the effective engine type from config + env override.
6+// It defaults to "webkit" when no type is configured, and allows DUMBER_ENGINE
7+// to override the configured value for smoke testing.
8+func (e *EngineConfig) ResolveEngineType() string {
9+ engineType := e.Type
10+ if engineType == "" {
11+ engineType = "webkit"
12+ }
13+ if envEngine := os.Getenv("DUMBER_ENGINE"); envEngine != "" {
14+ engineType = envEngine
15+ }
16+ return engineType
17+}
18+
319 // EngineConfig holds engine selection and universal engine options.
420 type EngineConfig struct {
521 Type string `mapstructure:"type" toml:"type" yaml:"type"`
@@ -1,5 +1,21 @@
1 package config1 package config
2 2
3+import "os"
4+
5+// ResolveEngineType returns the effective engine type from config + env override.
6+// It defaults to "webkit" when no type is configured, and allows DUMBER_ENGINE
7+// to override the configured value for smoke testing.
8+func (e *EngineConfig) ResolveEngineType() string {
9+ engineType := e.Type
10+ if engineType == "" {
11+ engineType = "webkit"
12+ }
13+ if envEngine := os.Getenv("DUMBER_ENGINE"); envEngine != "" {
14+ engineType = envEngine
15+ }
16+ return engineType
17+}
18+
3 // EngineConfig holds engine selection and universal engine options.19 // EngineConfig holds engine selection and universal engine options.
4 type EngineConfig struct {20 type EngineConfig struct {
5 Type string `mapstructure:"type" toml:"type" yaml:"type"`21 Type string `mapstructure:"type" toml:"type" yaml:"type"`
M internal/ui/coordinator/navigation.go +1 -5
@@ -241,15 +241,11 @@ func (c *NavigationCoordinator) PrintPage(ctx context.Context) error {
241241
242242 // UpdateHistoryTitle updates the title of a history entry after page load.
243243 func (c *NavigationCoordinator) UpdateHistoryTitle(ctx context.Context, paneID entity.PaneID, url, title string) {
244- log := logging.FromContext(ctx)
245-
246244 if c.navigateUC == nil {
247245 return
248246 }
249247
250- if err := c.navigateUC.UpdateHistoryTitle(ctx, url, title); err != nil {
251- log.Warn().Err(err).Str("url", url).Msg("failed to update history title")
252- }
248+ c.navigateUC.UpdateHistoryTitle(ctx, url, title)
253249 }
254250
255251 // RecordHistory records a URL in history on page commit.
@@ -241,15 +241,11 @@ func (c *NavigationCoordinator) PrintPage(ctx context.Context) error {
241 241
242 // UpdateHistoryTitle updates the title of a history entry after page load.242 // UpdateHistoryTitle updates the title of a history entry after page load.
243 func (c *NavigationCoordinator) UpdateHistoryTitle(ctx context.Context, paneID entity.PaneID, url, title string) {243 func (c *NavigationCoordinator) UpdateHistoryTitle(ctx context.Context, paneID entity.PaneID, url, title string) {
244- log := logging.FromContext(ctx)
245-
246 if c.navigateUC == nil {244 if c.navigateUC == nil {
247 return245 return
248 }246 }
249 247
250- if err := c.navigateUC.UpdateHistoryTitle(ctx, url, title); err != nil {248+ c.navigateUC.UpdateHistoryTitle(ctx, url, title)
251- log.Warn().Err(err).Str("url", url).Msg("failed to update history title")
252- }
253 }249 }
254 250
255 // RecordHistory records a URL in history on page commit.251 // RecordHistory records a URL in history on page commit.

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