fix(cef): address CodeRabbit findings and fix concurrent map crash
Bug fixes: - Guard GetMainFrame() nil in LoadURI, LoadHTML, RunJavaScript, OnAfterCreated - Heap-allocate glib idle callback in runOnGTK to prevent use-after-free - crashCount → atomic.Int32 for thread-safe access - lastStallLoggedCreateSeq → atomic.Uint64 - OnAfterCreated: early return on nil browser - Pool Prewarm: destroy webview on type assert failure - Hostname-pid parsing: use LastIndex to handle hyphenated hostnames - navigate.go: swap pending maps before iteration to fix concurrent map read/write crash in historyWorker Improvements: - InternalSchemePath extracted to package constant - Separated doc comments for findHelperBinary/cleanStaleSingletonLocks - HiDPI detection logs fallback reasons at debug level - Pool uses engine context instead of context.Background() - GetFaviconAsync callback runs asynchronously - Command line logging truncated to 200 chars - CreateRelated warns only once via atomic.Bool - Tests migrated to testify/require - History flush logs pending count at debug level - History queue-full warning logged in UpdateHistoryTitle
- commit
- 60f4f265e3f1d3c39a8f1b043fea15dcd39f6cfc
- author
- brice · · 2026-03-21 06:43 UTC
- committer
- brice · · 2026-03-28 04:37 UTC
- parent
- fed7cca
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
12 files · +117 -86
M internal/application/usecase/navigate.go +18 -7
| @@ -241,16 +241,27 @@ func (uc *NavigateUseCase) historyWorker() { | |||
| 241 | 241 | } | |
| 242 | 242 | ||
| 243 | 243 | flushPending := func() { | |
| 244 | - | ||
| 245 | - | for historyURL, visits := range pendingVisits { | |
| 246 | - | uc.persistHistory(uc.ctx, historyRecord{url: historyURL, visits: visits}) | |
| 244 | + | visitCount := len(pendingVisits) | |
| 245 | + | titleCount := len(pendingTitles) | |
| 246 | + | if visitCount == 0 && titleCount == 0 { | |
| 247 | + | return | |
| 247 | 248 | } | |
| 248 | - | clear(pendingVisits) | |
| 249 | 249 | ||
| 250 | - | for historyURL, title := range pendingTitles { | |
| 250 | + | log.Debug().Int("visits", visitCount).Int("titles", titleCount).Msg("flushing pending history") | |
| 251 | + | ||
| 252 | + | // Swap maps to avoid concurrent iteration/write issues | |
| 253 | + | // (persist callbacks may re-enqueue records). | |
| 254 | + | visits := pendingVisits | |
| 255 | + | titles := pendingTitles | |
| 256 | + | pendingVisits = make(map[string]int) | |
| 257 | + | pendingTitles = make(map[string]string) | |
| 258 | + | ||
| 259 | + | for historyURL, v := range visits { | |
| 260 | + | uc.persistHistory(uc.ctx, historyRecord{url: historyURL, visits: v}) | |
| 261 | + | } | |
| 262 | + | for historyURL, title := range titles { | |
| 251 | 263 | uc.persistTitleUpdate(uc.ctx, historyURL, title) | |
| 252 | 264 | } | |
| 253 | - | clear(pendingTitles) | |
| 254 | 265 | } | |
| 255 | 266 | ||
| 256 | 267 | drainQueue := func() { | |
| @@ -371,7 +382,7 @@ func (uc *NavigateUseCase) UpdateHistoryTitle(_ context.Context, historyURL, tit | |||
| 371 | 382 | select { | |
| 372 | 383 | case uc.historyQueue <- historyRecord{url: historyURL, title: title}: | |
| 373 | 384 | default: | |
| 374 | - | // Queue full — title update is best-effort. | |
| 385 | + | logging.FromContext(uc.ctx).Warn().Str("url", historyURL).Msg("history queue full, title update dropped") | |
| 375 | 386 | } | |
| 376 | 387 | } | |
| 377 | 388 | ||
| @@ -241,16 +241,27 @@ func (uc *NavigateUseCase) historyWorker() { | |||||
| 241 | } | 241 | } | ||
| 242 | 242 | ||||
| 243 | flushPending := func() { | 243 | flushPending := func() { | ||
| 244 | - | 244 | + | visitCount := len(pendingVisits) | |
| 245 | - | for historyURL, visits := range pendingVisits { | 245 | + | titleCount := len(pendingTitles) |
| 246 | - | uc.persistHistory(uc.ctx, historyRecord{url: historyURL, visits: visits}) | 246 | + | if visitCount == 0 && titleCount == 0 { |
| 247 | + | return | |||
| 247 | } | 248 | } | ||
| 248 | - | clear(pendingVisits) | |||
| 249 | 249 | ||||
| 250 | - | for historyURL, title := range pendingTitles { | 250 | + | log.Debug().Int("visits", visitCount).Int("titles", titleCount).Msg("flushing pending history") |
| 251 | + | ||||
| 252 | + | // Swap maps to avoid concurrent iteration/write issues | |||
| 253 | + | // (persist callbacks may re-enqueue records). | |||
| 254 | + | visits := pendingVisits | |||
| 255 | + | titles := pendingTitles | |||
| 256 | + | pendingVisits = make(map[string]int) | |||
| 257 | + | pendingTitles = make(map[string]string) | |||
| 258 | + | ||||
| 259 | + | for historyURL, v := range visits { | |||
| 260 | + | uc.persistHistory(uc.ctx, historyRecord{url: historyURL, visits: v}) | |||
| 261 | + | } | |||
| 262 | + | for historyURL, title := range titles { | |||
| 251 | uc.persistTitleUpdate(uc.ctx, historyURL, title) | 263 | uc.persistTitleUpdate(uc.ctx, historyURL, title) | ||
| 252 | } | 264 | } | ||
| 253 | - | clear(pendingTitles) | |||
| 254 | } | 265 | } | ||
| 255 | 266 | ||||
| 256 | drainQueue := func() { | 267 | drainQueue := func() { | ||
| @@ -371,7 +382,7 @@ func (uc *NavigateUseCase) UpdateHistoryTitle(_ context.Context, historyURL, tit | |||||
| 371 | select { | 382 | select { | ||
| 372 | case uc.historyQueue <- historyRecord{url: historyURL, title: title}: | 383 | case uc.historyQueue <- historyRecord{url: historyURL, title: title}: | ||
| 373 | default: | 384 | default: | ||
| 374 | - | // Queue full — title update is best-effort. | 385 | + | logging.FromContext(uc.ctx).Warn().Str("url", historyURL).Msg("history queue full, title update dropped") |
| 375 | } | 386 | } | ||
| 376 | } | 387 | } | ||
| 377 | 388 | ||||
M internal/infrastructure/cef/app.go +6 -0
| @@ -23,11 +23,17 @@ func newDumberApp(engine *Engine) purecef.App { | |||
| 23 | 23 | return purecef.NewApp(app) | |
| 24 | 24 | } | |
| 25 | 25 | ||
| 26 | + | // maxCmdLineLogLen limits logged command line length to avoid leaking sensitive paths. | |
| 27 | + | const maxCmdLineLogLen = 200 | |
| 28 | + | ||
| 26 | 29 | func (a *dumberApp) OnBeforeCommandLineProcessing(processType string, commandLine purecef.CommandLine) { | |
| 27 | 30 | log := logging.FromContext(a.engine.ctx) | |
| 28 | 31 | cmdline := "" | |
| 29 | 32 | if commandLine != nil { | |
| 30 | 33 | cmdline = commandLine.GetCommandLineString() | |
| 34 | + | if len(cmdline) > maxCmdLineLogLen { | |
| 35 | + | cmdline = cmdline[:maxCmdLineLogLen] + "…" | |
| 36 | + | } | |
| 31 | 37 | } | |
| 32 | 38 | log.Debug(). | |
| 33 | 39 | Str("process_type", processType). | |
| @@ -23,11 +23,17 @@ func newDumberApp(engine *Engine) purecef.App { | |||||
| 23 | return purecef.NewApp(app) | 23 | return purecef.NewApp(app) | ||
| 24 | } | 24 | } | ||
| 25 | 25 | ||||
| 26 | + | // maxCmdLineLogLen limits logged command line length to avoid leaking sensitive paths. | |||
| 27 | + | const maxCmdLineLogLen = 200 | |||
| 28 | + | ||||
| 26 | func (a *dumberApp) OnBeforeCommandLineProcessing(processType string, commandLine purecef.CommandLine) { | 29 | func (a *dumberApp) OnBeforeCommandLineProcessing(processType string, commandLine purecef.CommandLine) { | ||
| 27 | log := logging.FromContext(a.engine.ctx) | 30 | log := logging.FromContext(a.engine.ctx) | ||
| 28 | cmdline := "" | 31 | cmdline := "" | ||
| 29 | if commandLine != nil { | 32 | if commandLine != nil { | ||
| 30 | cmdline = commandLine.GetCommandLineString() | 33 | cmdline = commandLine.GetCommandLineString() | ||
| 34 | + | if len(cmdline) > maxCmdLineLogLen { | |||
| 35 | + | cmdline = cmdline[:maxCmdLineLogLen] + "…" | |||
| 36 | + | } | |||
| 31 | } | 37 | } | ||
| 32 | log.Debug(). | 38 | log.Debug(). | ||
| 33 | Str("process_type", processType). | 39 | Str("process_type", processType). | ||
M internal/infrastructure/cef/engine.go +7 -4
| @@ -46,7 +46,7 @@ type Engine struct { | |||
| 46 | 46 | browserCreateLastResult atomic.Int32 | |
| 47 | 47 | browserCreateLastWidth atomic.Int32 | |
| 48 | 48 | browserCreateLastHeight atomic.Int32 | |
| 49 | - | lastStallLoggedCreateSeq uint64 | |
| 49 | + | lastStallLoggedCreateSeq atomic.Uint64 | |
| 50 | 50 | } | |
| 51 | 51 | ||
| 52 | 52 | // Factory returns the WebViewFactory for creating new WebView instances. | |
| @@ -79,9 +79,12 @@ func (e *Engine) FaviconDatabase() port.FaviconDatabase { | |||
| 79 | 79 | return &noopFaviconDatabase{} | |
| 80 | 80 | } | |
| 81 | 81 | ||
| 82 | + | // internalSchemePath is the URI scheme used for internal app resources. | |
| 83 | + | const internalSchemePath = "cef://dumber/" | |
| 84 | + | ||
| 82 | 85 | // InternalSchemePath returns the URI scheme used for internal app resources. | |
| 83 | 86 | func (e *Engine) InternalSchemePath() string { | |
| 84 | - | return "cef://dumber/" | |
| 87 | + | return internalSchemePath | |
| 85 | 88 | } | |
| 86 | 89 | ||
| 87 | 90 | // Close releases all resources held by the engine. | |
| @@ -205,7 +208,7 @@ func (e *Engine) recordBrowserAfterCreated(browser purecef.Browser) { | |||
| 205 | 208 | ||
| 206 | 209 | func (e *Engine) maybeLogBrowserCreateStall() { | |
| 207 | 210 | createCount := e.browserCreateRequests.Load() | |
| 208 | - | if createCount == 0 || createCount == e.browserAfterCreated.Load() || createCount == e.lastStallLoggedCreateSeq { | |
| 211 | + | if createCount == 0 || createCount == e.browserAfterCreated.Load() || createCount == e.lastStallLoggedCreateSeq.Load() { | |
| 209 | 212 | return | |
| 210 | 213 | } | |
| 211 | 214 | ||
| @@ -219,7 +222,7 @@ func (e *Engine) maybeLogBrowserCreateStall() { | |||
| 219 | 222 | return | |
| 220 | 223 | } | |
| 221 | 224 | ||
| 222 | - | e.lastStallLoggedCreateSeq = createCount | |
| 225 | + | e.lastStallLoggedCreateSeq.Store(createCount) | |
| 223 | 226 | logging.FromContext(e.ctx).Warn(). | |
| 224 | 227 | Uint64("create_requests", createCount). | |
| 225 | 228 | Uint64("after_created", e.browserAfterCreated.Load()). | |
| @@ -46,7 +46,7 @@ type Engine struct { | |||||
| 46 | browserCreateLastResult atomic.Int32 | 46 | browserCreateLastResult atomic.Int32 | ||
| 47 | browserCreateLastWidth atomic.Int32 | 47 | browserCreateLastWidth atomic.Int32 | ||
| 48 | browserCreateLastHeight atomic.Int32 | 48 | browserCreateLastHeight atomic.Int32 | ||
| 49 | - | lastStallLoggedCreateSeq uint64 | 49 | + | lastStallLoggedCreateSeq atomic.Uint64 |
| 50 | } | 50 | } | ||
| 51 | 51 | ||||
| 52 | // Factory returns the WebViewFactory for creating new WebView instances. | 52 | // Factory returns the WebViewFactory for creating new WebView instances. | ||
| @@ -79,9 +79,12 @@ func (e *Engine) FaviconDatabase() port.FaviconDatabase { | |||||
| 79 | return &noopFaviconDatabase{} | 79 | return &noopFaviconDatabase{} | ||
| 80 | } | 80 | } | ||
| 81 | 81 | ||||
| 82 | + | // internalSchemePath is the URI scheme used for internal app resources. | |||
| 83 | + | const internalSchemePath = "cef://dumber/" | |||
| 84 | + | ||||
| 82 | // InternalSchemePath returns the URI scheme used for internal app resources. | 85 | // InternalSchemePath returns the URI scheme used for internal app resources. | ||
| 83 | func (e *Engine) InternalSchemePath() string { | 86 | func (e *Engine) InternalSchemePath() string { | ||
| 84 | - | return "cef://dumber/" | 87 | + | return internalSchemePath |
| 85 | } | 88 | } | ||
| 86 | 89 | ||||
| 87 | // Close releases all resources held by the engine. | 90 | // Close releases all resources held by the engine. | ||
| @@ -205,7 +208,7 @@ func (e *Engine) recordBrowserAfterCreated(browser purecef.Browser) { | |||||
| 205 | 208 | ||||
| 206 | func (e *Engine) maybeLogBrowserCreateStall() { | 209 | func (e *Engine) maybeLogBrowserCreateStall() { | ||
| 207 | createCount := e.browserCreateRequests.Load() | 210 | createCount := e.browserCreateRequests.Load() | ||
| 208 | - | if createCount == 0 || createCount == e.browserAfterCreated.Load() || createCount == e.lastStallLoggedCreateSeq { | 211 | + | if createCount == 0 || createCount == e.browserAfterCreated.Load() || createCount == e.lastStallLoggedCreateSeq.Load() { |
| 209 | return | 212 | return | ||
| 210 | } | 213 | } | ||
| 211 | 214 | ||||
| @@ -219,7 +222,7 @@ func (e *Engine) maybeLogBrowserCreateStall() { | |||||
| 219 | return | 222 | return | ||
| 220 | } | 223 | } | ||
| 221 | 224 | ||||
| 222 | - | e.lastStallLoggedCreateSeq = createCount | 225 | + | e.lastStallLoggedCreateSeq.Store(createCount) |
| 223 | logging.FromContext(e.ctx).Warn(). | 226 | logging.FromContext(e.ctx).Warn(). | ||
| 224 | Uint64("create_requests", createCount). | 227 | Uint64("create_requests", createCount). | ||
| 225 | Uint64("after_created", e.browserAfterCreated.Load()). | 228 | Uint64("after_created", e.browserAfterCreated.Load()). | ||
M internal/infrastructure/cef/engine_adapters.go +1 -1
| @@ -97,5 +97,5 @@ func (n *noopSettingsApplier) ApplyToAll(_ context.Context, _ []port.WebView) {} | |||
| 97 | 97 | type noopFaviconDatabase struct{} | |
| 98 | 98 | ||
| 99 | 99 | func (n *noopFaviconDatabase) GetFaviconAsync(_ string, callback func(port.Texture)) { | |
| 100 | - | callback(nil) | |
| 100 | + | go callback(nil) | |
| 101 | 101 | } | |
| @@ -97,5 +97,5 @@ func (n *noopSettingsApplier) ApplyToAll(_ context.Context, _ []port.WebView) {} | |||||
| 97 | type noopFaviconDatabase struct{} | 97 | type noopFaviconDatabase struct{} | ||
| 98 | 98 | ||||
| 99 | func (n *noopFaviconDatabase) GetFaviconAsync(_ string, callback func(port.Texture)) { | 99 | func (n *noopFaviconDatabase) GetFaviconAsync(_ string, callback func(port.Texture)) { | ||
| 100 | - | callback(nil) | 100 | + | go callback(nil) |
| 101 | } | 101 | } | ||
M internal/infrastructure/cef/engine_init.go +21 -17
| @@ -105,17 +105,20 @@ func NewEngine(ctx context.Context, cfg config.CEFEngineConfig) (*Engine, error) | |||
| 105 | 105 | ||
| 106 | 106 | // 4. Detect HiDPI scale from the primary monitor. | |
| 107 | 107 | scale := int32(1) | |
| 108 | - | if display := gdk.DisplayGetDefault(); display != nil { | |
| 109 | - | if monitors := display.GetMonitors(); monitors != nil { | |
| 110 | - | obj := monitors.GetObject(0) | |
| 111 | - | if obj != nil { | |
| 112 | - | mon := &gdk.Monitor{} | |
| 113 | - | mon.SetGoPointer(obj.GoPointer()) | |
| 114 | - | if s := mon.GetScaleFactor(); s > 0 { | |
| 115 | - | scale = int32(s) | |
| 116 | - | logger.Info().Int32("scale", scale).Msg("cef: detected HiDPI scale from monitor") | |
| 117 | - | } | |
| 118 | - | } | |
| 108 | + | if display := gdk.DisplayGetDefault(); display == nil { | |
| 109 | + | logger.Debug().Msg("cef: no GDK display available, using scale=1") | |
| 110 | + | } else if monitors := display.GetMonitors(); monitors == nil { | |
| 111 | + | logger.Debug().Msg("cef: no monitors found, using scale=1") | |
| 112 | + | } else if obj := monitors.GetObject(0); obj == nil { | |
| 113 | + | logger.Debug().Msg("cef: primary monitor not available, using scale=1") | |
| 114 | + | } else { | |
| 115 | + | mon := &gdk.Monitor{} | |
| 116 | + | mon.SetGoPointer(obj.GoPointer()) | |
| 117 | + | if s := mon.GetScaleFactor(); s > 0 { | |
| 118 | + | scale = int32(s) | |
| 119 | + | logger.Info().Int32("scale", scale).Msg("cef: detected HiDPI scale from monitor") | |
| 120 | + | } else { | |
| 121 | + | logger.Debug().Msg("cef: monitor scale factor <= 0, using scale=1") | |
| 119 | 122 | } | |
| 120 | 123 | } | |
| 121 | 124 | ||
| @@ -140,8 +143,6 @@ func appendIfMissing(args []string, flag string) []string { | |||
| 140 | 143 | return append(args, flag) | |
| 141 | 144 | } | |
| 142 | 145 | ||
| 143 | - | // findHelperBinary looks for the cef-helper binary next to the running | |
| 144 | - | // executable. Returns empty string if not found. | |
| 145 | 146 | // cleanStaleSingletonLocks removes CEF's SingletonLock/Socket/Cookie files | |
| 146 | 147 | // if the owning process is no longer running. CEF leaves these behind on | |
| 147 | 148 | // unclean shutdown (SIGKILL, SIGSEGV) and the next instance crashes trying | |
| @@ -156,12 +157,13 @@ func cleanStaleSingletonLocks(logger *zerolog.Logger) { | |||
| 156 | 157 | return // No lock file or not a symlink — nothing to clean. | |
| 157 | 158 | } | |
| 158 | 159 | ||
| 159 | - | // SingletonLock is a symlink to "hostname-pid". | |
| 160 | - | parts := strings.SplitN(target, "-", 2) | |
| 161 | - | if len(parts) != 2 { | |
| 160 | + | // SingletonLock is a symlink to "hostname-pid". Split from the right | |
| 161 | + | // because hostnames may contain hyphens. | |
| 162 | + | lastDash := strings.LastIndex(target, "-") | |
| 163 | + | if lastDash < 0 || lastDash == len(target)-1 { | |
| 162 | 164 | return | |
| 163 | 165 | } | |
| 164 | - | pid, err := strconv.Atoi(parts[1]) | |
| 166 | + | pid, err := strconv.Atoi(target[lastDash+1:]) | |
| 165 | 167 | if err != nil || pid <= 0 { | |
| 166 | 168 | return | |
| 167 | 169 | } | |
| @@ -184,6 +186,8 @@ func cleanStaleSingletonLocks(logger *zerolog.Logger) { | |||
| 184 | 186 | } | |
| 185 | 187 | } | |
| 186 | 188 | ||
| 189 | + | // findHelperBinary looks for the cef-helper binary next to the running | |
| 190 | + | // executable. Returns empty string if not found. | |
| 187 | 191 | func findHelperBinary() string { | |
| 188 | 192 | exe, err := os.Executable() | |
| 189 | 193 | if err != nil { | |
| @@ -105,17 +105,20 @@ func NewEngine(ctx context.Context, cfg config.CEFEngineConfig) (*Engine, error) | |||||
| 105 | 105 | ||||
| 106 | // 4. Detect HiDPI scale from the primary monitor. | 106 | // 4. Detect HiDPI scale from the primary monitor. | ||
| 107 | scale := int32(1) | 107 | scale := int32(1) | ||
| 108 | - | if display := gdk.DisplayGetDefault(); display != nil { | 108 | + | if display := gdk.DisplayGetDefault(); display == nil { |
| 109 | - | if monitors := display.GetMonitors(); monitors != nil { | 109 | + | logger.Debug().Msg("cef: no GDK display available, using scale=1") |
| 110 | - | obj := monitors.GetObject(0) | 110 | + | } else if monitors := display.GetMonitors(); monitors == nil { |
| 111 | - | if obj != nil { | 111 | + | logger.Debug().Msg("cef: no monitors found, using scale=1") |
| 112 | - | mon := &gdk.Monitor{} | 112 | + | } else if obj := monitors.GetObject(0); obj == nil { |
| 113 | - | mon.SetGoPointer(obj.GoPointer()) | 113 | + | logger.Debug().Msg("cef: primary monitor not available, using scale=1") |
| 114 | - | if s := mon.GetScaleFactor(); s > 0 { | 114 | + | } else { |
| 115 | - | scale = int32(s) | 115 | + | mon := &gdk.Monitor{} |
| 116 | - | logger.Info().Int32("scale", scale).Msg("cef: detected HiDPI scale from monitor") | 116 | + | mon.SetGoPointer(obj.GoPointer()) |
| 117 | - | } | 117 | + | if s := mon.GetScaleFactor(); s > 0 { |
| 118 | - | } | 118 | + | scale = int32(s) |
| 119 | + | logger.Info().Int32("scale", scale).Msg("cef: detected HiDPI scale from monitor") | |||
| 120 | + | } else { | |||
| 121 | + | logger.Debug().Msg("cef: monitor scale factor <= 0, using scale=1") | |||
| 119 | } | 122 | } | ||
| 120 | } | 123 | } | ||
| 121 | 124 | ||||
| @@ -140,8 +143,6 @@ func appendIfMissing(args []string, flag string) []string { | |||||
| 140 | return append(args, flag) | 143 | return append(args, flag) | ||
| 141 | } | 144 | } | ||
| 142 | 145 | ||||
| 143 | - | // findHelperBinary looks for the cef-helper binary next to the running | |||
| 144 | - | // executable. Returns empty string if not found. | |||
| 145 | // cleanStaleSingletonLocks removes CEF's SingletonLock/Socket/Cookie files | 146 | // cleanStaleSingletonLocks removes CEF's SingletonLock/Socket/Cookie files | ||
| 146 | // if the owning process is no longer running. CEF leaves these behind on | 147 | // if the owning process is no longer running. CEF leaves these behind on | ||
| 147 | // unclean shutdown (SIGKILL, SIGSEGV) and the next instance crashes trying | 148 | // unclean shutdown (SIGKILL, SIGSEGV) and the next instance crashes trying | ||
| @@ -156,12 +157,13 @@ func cleanStaleSingletonLocks(logger *zerolog.Logger) { | |||||
| 156 | return // No lock file or not a symlink — nothing to clean. | 157 | return // No lock file or not a symlink — nothing to clean. | ||
| 157 | } | 158 | } | ||
| 158 | 159 | ||||
| 159 | - | // SingletonLock is a symlink to "hostname-pid". | 160 | + | // SingletonLock is a symlink to "hostname-pid". Split from the right |
| 160 | - | parts := strings.SplitN(target, "-", 2) | 161 | + | // because hostnames may contain hyphens. |
| 161 | - | if len(parts) != 2 { | 162 | + | lastDash := strings.LastIndex(target, "-") |
| 163 | + | if lastDash < 0 || lastDash == len(target)-1 { | |||
| 162 | return | 164 | return | ||
| 163 | } | 165 | } | ||
| 164 | - | pid, err := strconv.Atoi(parts[1]) | 166 | + | pid, err := strconv.Atoi(target[lastDash+1:]) |
| 165 | if err != nil || pid <= 0 { | 167 | if err != nil || pid <= 0 { | ||
| 166 | return | 168 | return | ||
| 167 | } | 169 | } | ||
| @@ -184,6 +186,8 @@ func cleanStaleSingletonLocks(logger *zerolog.Logger) { | |||||
| 184 | } | 186 | } | ||
| 185 | } | 187 | } | ||
| 186 | 188 | ||||
| 189 | + | // findHelperBinary looks for the cef-helper binary next to the running | |||
| 190 | + | // executable. Returns empty string if not found. | |||
| 187 | func findHelperBinary() string { | 191 | func findHelperBinary() string { | ||
| 188 | exe, err := os.Executable() | 192 | exe, err := os.Executable() | ||
| 189 | if err != nil { | 193 | if err != nil { | ||
M internal/infrastructure/cef/factory.go +6 -1
| @@ -146,10 +146,15 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) { | |||
| 146 | 146 | return wv, nil | |
| 147 | 147 | } | |
| 148 | 148 | ||
| 149 | + | // createRelatedWarned ensures the session sharing warning is logged only once. | |
| 150 | + | var createRelatedWarned atomic.Bool | |
| 151 | + | ||
| 149 | 152 | // CreateRelated creates a WebView that shares session/cookies with the parent. | |
| 150 | 153 | // TODO(phase2): look up the parent browser by parentID and use the same | |
| 151 | 154 | // request context so cookies and session state are shared. | |
| 152 | 155 | func (f *WebViewFactory) CreateRelated(ctx context.Context, _ port.WebViewID) (port.WebView, error) { | |
| 153 | - | logging.FromContext(ctx).Warn().Msg("cef: CreateRelated does not yet support session sharing, creating independent WebView") | |
| 156 | + | if createRelatedWarned.CompareAndSwap(false, true) { | |
| 157 | + | logging.FromContext(ctx).Warn().Msg("cef: CreateRelated does not yet support session sharing, creating independent WebView") | |
| 158 | + | } | |
| 154 | 159 | return f.Create(ctx) | |
| 155 | 160 | } | |
| @@ -146,10 +146,15 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) { | |||||
| 146 | return wv, nil | 146 | return wv, nil | ||
| 147 | } | 147 | } | ||
| 148 | 148 | ||||
| 149 | + | // createRelatedWarned ensures the session sharing warning is logged only once. | |||
| 150 | + | var createRelatedWarned atomic.Bool | |||
| 151 | + | ||||
| 149 | // CreateRelated creates a WebView that shares session/cookies with the parent. | 152 | // CreateRelated creates a WebView that shares session/cookies with the parent. | ||
| 150 | // TODO(phase2): look up the parent browser by parentID and use the same | 153 | // TODO(phase2): look up the parent browser by parentID and use the same | ||
| 151 | // request context so cookies and session state are shared. | 154 | // request context so cookies and session state are shared. | ||
| 152 | func (f *WebViewFactory) CreateRelated(ctx context.Context, _ port.WebViewID) (port.WebView, error) { | 155 | func (f *WebViewFactory) CreateRelated(ctx context.Context, _ port.WebViewID) (port.WebView, error) { | ||
| 153 | - | logging.FromContext(ctx).Warn().Msg("cef: CreateRelated does not yet support session sharing, creating independent WebView") | 156 | + | if createRelatedWarned.CompareAndSwap(false, true) { |
| 157 | + | logging.FromContext(ctx).Warn().Msg("cef: CreateRelated does not yet support session sharing, creating independent WebView") | |||
| 158 | + | } | |||
| 154 | return f.Create(ctx) | 159 | return f.Create(ctx) | ||
| 155 | } | 160 | } | ||
M internal/infrastructure/cef/input_bridge.go +2 -1
| @@ -219,8 +219,9 @@ func (ib *inputBridge) onMousePress(x, y float64, button, mods uint, clickCount | |||
| 219 | 219 | } | |
| 220 | 220 | ||
| 221 | 221 | // Middle-click on a link opens in a new tab instead of sending to CEF. | |
| 222 | + | // The URI is resolved by the callback closure (factory wiring reads wv.lastHoverURI). | |
| 222 | 223 | if button == 2 && middleCB != nil { | |
| 223 | - | middleCB("") | |
| 224 | + | go middleCB("") | |
| 224 | 225 | return | |
| 225 | 226 | } | |
| 226 | 227 | ||
| @@ -219,8 +219,9 @@ func (ib *inputBridge) onMousePress(x, y float64, button, mods uint, clickCount | |||||
| 219 | } | 219 | } | ||
| 220 | 220 | ||||
| 221 | // Middle-click on a link opens in a new tab instead of sending to CEF. | 221 | // Middle-click on a link opens in a new tab instead of sending to CEF. | ||
| 222 | + | // The URI is resolved by the callback closure (factory wiring reads wv.lastHoverURI). | |||
| 222 | if button == 2 && middleCB != nil { | 223 | if button == 2 && middleCB != nil { | ||
| 223 | - | middleCB("") | 224 | + | go middleCB("") |
| 224 | return | 225 | return | ||
| 225 | } | 226 | } | ||
| 226 | 227 | ||||
M internal/infrastructure/cef/pool.go +2 -1
| @@ -68,7 +68,7 @@ func (p *WebViewPool) Prewarm(count int) { | |||
| 68 | 68 | return | |
| 69 | 69 | } | |
| 70 | 70 | ||
| 71 | - | ctx := context.Background() | |
| 71 | + | ctx := p.factory.engine.ctx | |
| 72 | 72 | log := logging.FromContext(ctx) | |
| 73 | 73 | views := make([]*WebView, 0, count) | |
| 74 | 74 | for range count { | |
| @@ -80,6 +80,7 @@ func (p *WebViewPool) Prewarm(count int) { | |||
| 80 | 80 | cefWV, ok := wv.(*WebView) | |
| 81 | 81 | if !ok { | |
| 82 | 82 | log.Warn().Str("concrete_type", fmt.Sprintf("%T", wv)).Msg("cef: pool prewarm got unexpected WebView type") | |
| 83 | + | wv.Destroy() | |
| 83 | 84 | continue | |
| 84 | 85 | } | |
| 85 | 86 | views = append(views, cefWV) | |
| @@ -68,7 +68,7 @@ func (p *WebViewPool) Prewarm(count int) { | |||||
| 68 | return | 68 | return | ||
| 69 | } | 69 | } | ||
| 70 | 70 | ||||
| 71 | - | ctx := context.Background() | 71 | + | ctx := p.factory.engine.ctx |
| 72 | log := logging.FromContext(ctx) | 72 | log := logging.FromContext(ctx) | ||
| 73 | views := make([]*WebView, 0, count) | 73 | views := make([]*WebView, 0, count) | ||
| 74 | for range count { | 74 | for range count { | ||
| @@ -80,6 +80,7 @@ func (p *WebViewPool) Prewarm(count int) { | |||||
| 80 | cefWV, ok := wv.(*WebView) | 80 | cefWV, ok := wv.(*WebView) | ||
| 81 | if !ok { | 81 | if !ok { | ||
| 82 | log.Warn().Str("concrete_type", fmt.Sprintf("%T", wv)).Msg("cef: pool prewarm got unexpected WebView type") | 82 | log.Warn().Str("concrete_type", fmt.Sprintf("%T", wv)).Msg("cef: pool prewarm got unexpected WebView type") | ||
| 83 | + | wv.Destroy() | |||
| 83 | continue | 84 | continue | ||
| 84 | } | 85 | } | ||
| 85 | views = append(views, cefWV) | 86 | views = append(views, cefWV) | ||
M internal/infrastructure/cef/render_pipeline.go +1 -0
| @@ -528,6 +528,7 @@ func (rp *renderPipeline) buildShaderProgram() uint32 { | |||
| 528 | 528 | } | |
| 529 | 529 | ||
| 530 | 530 | // compileShader compiles a single GLSL shader and returns its GL handle. | |
| 531 | + | // TODO: add glGetShaderiv + glGetShaderInfoLog to gl_loader to check compilation status. | |
| 531 | 532 | func compileShader(gl *glLoader, shaderType uint32, source string) uint32 { | |
| 532 | 533 | shader := gl.createShader(shaderType) | |
| 533 | 534 | ||
| @@ -528,6 +528,7 @@ func (rp *renderPipeline) buildShaderProgram() uint32 { | |||||
| 528 | } | 528 | } | ||
| 529 | 529 | ||||
| 530 | // compileShader compiles a single GLSL shader and returns its GL handle. | 530 | // compileShader compiles a single GLSL shader and returns its GL handle. | ||
| 531 | + | // TODO: add glGetShaderiv + glGetShaderInfoLog to gl_loader to check compilation status. | |||
| 531 | func compileShader(gl *glLoader, shaderType uint32, source string) uint32 { | 532 | func compileShader(gl *glLoader, shaderType uint32, source string) uint32 { | ||
| 532 | shader := gl.createShader(shaderType) | 533 | shader := gl.createShader(shaderType) | ||
| 533 | 534 | ||||
M internal/infrastructure/cef/webview.go +15 -7
| @@ -48,7 +48,7 @@ type WebView struct { | |||
| 48 | 48 | ||
| 49 | 49 | // crashCount tracks consecutive renderer crashes to prevent infinite | |
| 50 | 50 | // crash → redirect → crash loops. | |
| 51 | - | crashCount int32 | |
| 51 | + | crashCount atomic.Int32 | |
| 52 | 52 | ||
| 53 | 53 | // Callbacks set by use case layer. | |
| 54 | 54 | mu sync.RWMutex | |
| @@ -108,7 +108,9 @@ func (wv *WebView) LoadURI(_ context.Context, uri string) error { | |||
| 108 | 108 | return nil | |
| 109 | 109 | } | |
| 110 | 110 | wv.mu.Unlock() | |
| 111 | - | browser.GetMainFrame().LoadURL(uri) | |
| 111 | + | if frame := browser.GetMainFrame(); frame != nil { | |
| 112 | + | frame.LoadURL(uri) | |
| 113 | + | } | |
| 112 | 114 | return nil | |
| 113 | 115 | } | |
| 114 | 116 | ||
| @@ -124,7 +126,9 @@ func (wv *WebView) LoadHTML(_ context.Context, content, _ string) error { | |||
| 124 | 126 | return errNoBrowser | |
| 125 | 127 | } | |
| 126 | 128 | dataURL := "data:text/html;base64," + base64.StdEncoding.EncodeToString([]byte(content)) | |
| 127 | - | browser.GetMainFrame().LoadURL(dataURL) | |
| 129 | + | if frame := browser.GetMainFrame(); frame != nil { | |
| 130 | + | frame.LoadURL(dataURL) | |
| 131 | + | } | |
| 128 | 132 | return nil | |
| 129 | 133 | } | |
| 130 | 134 | ||
| @@ -360,7 +364,9 @@ func (wv *WebView) RunJavaScript(_ context.Context, script string) { | |||
| 360 | 364 | if browser == nil { | |
| 361 | 365 | return | |
| 362 | 366 | } | |
| 363 | - | browser.GetMainFrame().ExecuteJavaScript(script, "", 0) | |
| 367 | + | if frame := browser.GetMainFrame(); frame != nil { | |
| 368 | + | frame.ExecuteJavaScript(script, "", 0) | |
| 369 | + | } | |
| 364 | 370 | } | |
| 365 | 371 | ||
| 366 | 372 | // colorScale converts a 0.0–1.0 color component to an 8-bit integer. | |
| @@ -496,8 +502,10 @@ func (wv *WebView) runOnGTK(fn func()) { | |||
| 496 | 502 | return | |
| 497 | 503 | } | |
| 498 | 504 | ||
| 499 | - | cb := glib.SourceOnceFunc(func(_ uintptr) { | |
| 505 | + | // Heap-allocate the callback so it survives until glib invokes it. | |
| 506 | + | cb := new(glib.SourceOnceFunc) | |
| 507 | + | *cb = func(_ uintptr) { | |
| 500 | 508 | fn() | |
| 501 | - | }) | |
| 502 | - | glib.IdleAddOnce(&cb, 0) | |
| 509 | + | } | |
| 510 | + | glib.IdleAddOnce(cb, 0) | |
| 503 | 511 | } | |
| @@ -48,7 +48,7 @@ type WebView struct { | |||||
| 48 | 48 | ||||
| 49 | // crashCount tracks consecutive renderer crashes to prevent infinite | 49 | // crashCount tracks consecutive renderer crashes to prevent infinite | ||
| 50 | // crash → redirect → crash loops. | 50 | // crash → redirect → crash loops. | ||
| 51 | - | crashCount int32 | 51 | + | crashCount atomic.Int32 |
| 52 | 52 | ||||
| 53 | // Callbacks set by use case layer. | 53 | // Callbacks set by use case layer. | ||
| 54 | mu sync.RWMutex | 54 | mu sync.RWMutex | ||
| @@ -108,7 +108,9 @@ func (wv *WebView) LoadURI(_ context.Context, uri string) error { | |||||
| 108 | return nil | 108 | return nil | ||
| 109 | } | 109 | } | ||
| 110 | wv.mu.Unlock() | 110 | wv.mu.Unlock() | ||
| 111 | - | browser.GetMainFrame().LoadURL(uri) | 111 | + | if frame := browser.GetMainFrame(); frame != nil { |
| 112 | + | frame.LoadURL(uri) | |||
| 113 | + | } | |||
| 112 | return nil | 114 | return nil | ||
| 113 | } | 115 | } | ||
| 114 | 116 | ||||
| @@ -124,7 +126,9 @@ func (wv *WebView) LoadHTML(_ context.Context, content, _ string) error { | |||||
| 124 | return errNoBrowser | 126 | return errNoBrowser | ||
| 125 | } | 127 | } | ||
| 126 | dataURL := "data:text/html;base64," + base64.StdEncoding.EncodeToString([]byte(content)) | 128 | dataURL := "data:text/html;base64," + base64.StdEncoding.EncodeToString([]byte(content)) | ||
| 127 | - | browser.GetMainFrame().LoadURL(dataURL) | 129 | + | if frame := browser.GetMainFrame(); frame != nil { |
| 130 | + | frame.LoadURL(dataURL) | |||
| 131 | + | } | |||
| 128 | return nil | 132 | return nil | ||
| 129 | } | 133 | } | ||
| 130 | 134 | ||||
| @@ -360,7 +364,9 @@ func (wv *WebView) RunJavaScript(_ context.Context, script string) { | |||||
| 360 | if browser == nil { | 364 | if browser == nil { | ||
| 361 | return | 365 | return | ||
| 362 | } | 366 | } | ||
| 363 | - | browser.GetMainFrame().ExecuteJavaScript(script, "", 0) | 367 | + | if frame := browser.GetMainFrame(); frame != nil { |
| 368 | + | frame.ExecuteJavaScript(script, "", 0) | |||
| 369 | + | } | |||
| 364 | } | 370 | } | ||
| 365 | 371 | ||||
| 366 | // colorScale converts a 0.0–1.0 color component to an 8-bit integer. | 372 | // colorScale converts a 0.0–1.0 color component to an 8-bit integer. | ||
| @@ -496,8 +502,10 @@ func (wv *WebView) runOnGTK(fn func()) { | |||||
| 496 | return | 502 | return | ||
| 497 | } | 503 | } | ||
| 498 | 504 | ||||
| 499 | - | cb := glib.SourceOnceFunc(func(_ uintptr) { | 505 | + | // Heap-allocate the callback so it survives until glib invokes it. |
| 506 | + | cb := new(glib.SourceOnceFunc) | |||
| 507 | + | *cb = func(_ uintptr) { | |||
| 500 | fn() | 508 | fn() | ||
| 501 | - | }) | 509 | + | } |
| 502 | - | glib.IdleAddOnce(&cb, 0) | 510 | + | glib.IdleAddOnce(cb, 0) |
| 503 | } | 511 | } | ||
M internal/infrastructure/cef/webview_handlers.go +9 -8
| @@ -304,7 +304,7 @@ func (h *handlerSet) OnLoadEnd(_ purecef.Browser, frame purecef.Frame, httpStatu | |||
| 304 | 304 | return | |
| 305 | 305 | } | |
| 306 | 306 | // Successful load — reset the consecutive crash counter. | |
| 307 | - | h.wv.crashCount = 0 | |
| 307 | + | h.wv.crashCount.Store(0) | |
| 308 | 308 | ||
| 309 | 309 | h.wv.mu.RLock() | |
| 310 | 310 | cb := h.wv.callbacks | |
| @@ -355,12 +355,12 @@ func (h *handlerSet) OnBeforeDevToolsPopup( | |||
| 355 | 355 | // OnAfterCreated stores the browser and host references and enables input. | |
| 356 | 356 | func (h *handlerSet) OnAfterCreated(browser purecef.Browser) { | |
| 357 | 357 | log := logging.FromContext(h.wv.ctx) | |
| 358 | - | browserID := int32(0) | |
| 359 | - | if browser != nil { | |
| 360 | - | browserID = browser.GetIdentifier() | |
| 358 | + | if browser == nil { | |
| 359 | + | log.Warn().Msg("cef: OnAfterCreated called with nil browser") | |
| 360 | + | return | |
| 361 | 361 | } | |
| 362 | + | browserID := browser.GetIdentifier() | |
| 362 | 363 | log.Debug(). | |
| 363 | - | Bool("browser_nil", browser == nil). | |
| 364 | 364 | Int32("browser_id", browserID). | |
| 365 | 365 | Msg("cef: OnAfterCreated") | |
| 366 | 366 | if h.wv.engine != nil { | |
| @@ -377,7 +377,9 @@ func (h *handlerSet) OnAfterCreated(browser purecef.Browser) { | |||
| 377 | 377 | ||
| 378 | 378 | // Replay any navigation that was requested before the browser existed. | |
| 379 | 379 | if uri != "" { | |
| 380 | - | browser.GetMainFrame().LoadURL(uri) | |
| 380 | + | if frame := browser.GetMainFrame(); frame != nil { | |
| 381 | + | frame.LoadURL(uri) | |
| 382 | + | } | |
| 381 | 383 | } | |
| 382 | 384 | h.wv.mu.Unlock() | |
| 383 | 385 | } | |
| @@ -451,8 +453,7 @@ const maxConsecutiveCrashes = 3 | |||
| 451 | 453 | ||
| 452 | 454 | // OnRenderProcessTerminated fires the OnWebProcessTerminated callback with a mapped reason. | |
| 453 | 455 | func (h *handlerSet) OnRenderProcessTerminated(_ purecef.Browser, status purecef.TerminationStatus, _ int32, _ string) { | |
| 454 | - | h.wv.crashCount++ | |
| 455 | - | if h.wv.crashCount > maxConsecutiveCrashes { | |
| 456 | + | if h.wv.crashCount.Add(1) > maxConsecutiveCrashes { | |
| 456 | 457 | return // suppress to break the loop | |
| 457 | 458 | } | |
| 458 | 459 | ||
| @@ -304,7 +304,7 @@ func (h *handlerSet) OnLoadEnd(_ purecef.Browser, frame purecef.Frame, httpStatu | |||||
| 304 | return | 304 | return | ||
| 305 | } | 305 | } | ||
| 306 | // Successful load — reset the consecutive crash counter. | 306 | // Successful load — reset the consecutive crash counter. | ||
| 307 | - | h.wv.crashCount = 0 | 307 | + | h.wv.crashCount.Store(0) |
| 308 | 308 | ||||
| 309 | h.wv.mu.RLock() | 309 | h.wv.mu.RLock() | ||
| 310 | cb := h.wv.callbacks | 310 | cb := h.wv.callbacks | ||
| @@ -355,12 +355,12 @@ func (h *handlerSet) OnBeforeDevToolsPopup( | |||||
| 355 | // OnAfterCreated stores the browser and host references and enables input. | 355 | // OnAfterCreated stores the browser and host references and enables input. | ||
| 356 | func (h *handlerSet) OnAfterCreated(browser purecef.Browser) { | 356 | func (h *handlerSet) OnAfterCreated(browser purecef.Browser) { | ||
| 357 | log := logging.FromContext(h.wv.ctx) | 357 | log := logging.FromContext(h.wv.ctx) | ||
| 358 | - | browserID := int32(0) | 358 | + | if browser == nil { |
| 359 | - | if browser != nil { | 359 | + | log.Warn().Msg("cef: OnAfterCreated called with nil browser") |
| 360 | - | browserID = browser.GetIdentifier() | 360 | + | return |
| 361 | } | 361 | } | ||
| 362 | + | browserID := browser.GetIdentifier() | |||
| 362 | log.Debug(). | 363 | log.Debug(). | ||
| 363 | - | Bool("browser_nil", browser == nil). | |||
| 364 | Int32("browser_id", browserID). | 364 | Int32("browser_id", browserID). | ||
| 365 | Msg("cef: OnAfterCreated") | 365 | Msg("cef: OnAfterCreated") | ||
| 366 | if h.wv.engine != nil { | 366 | if h.wv.engine != nil { | ||
| @@ -377,7 +377,9 @@ func (h *handlerSet) OnAfterCreated(browser purecef.Browser) { | |||||
| 377 | 377 | ||||
| 378 | // Replay any navigation that was requested before the browser existed. | 378 | // Replay any navigation that was requested before the browser existed. | ||
| 379 | if uri != "" { | 379 | if uri != "" { | ||
| 380 | - | browser.GetMainFrame().LoadURL(uri) | 380 | + | if frame := browser.GetMainFrame(); frame != nil { |
| 381 | + | frame.LoadURL(uri) | |||
| 382 | + | } | |||
| 381 | } | 383 | } | ||
| 382 | h.wv.mu.Unlock() | 384 | h.wv.mu.Unlock() | ||
| 383 | } | 385 | } | ||
| @@ -451,8 +453,7 @@ const maxConsecutiveCrashes = 3 | |||||
| 451 | 453 | ||||
| 452 | // OnRenderProcessTerminated fires the OnWebProcessTerminated callback with a mapped reason. | 454 | // OnRenderProcessTerminated fires the OnWebProcessTerminated callback with a mapped reason. | ||
| 453 | func (h *handlerSet) OnRenderProcessTerminated(_ purecef.Browser, status purecef.TerminationStatus, _ int32, _ string) { | 455 | func (h *handlerSet) OnRenderProcessTerminated(_ purecef.Browser, status purecef.TerminationStatus, _ int32, _ string) { | ||
| 454 | - | h.wv.crashCount++ | 456 | + | if h.wv.crashCount.Add(1) > maxConsecutiveCrashes { |
| 455 | - | if h.wv.crashCount > maxConsecutiveCrashes { | |||
| 456 | return // suppress to break the loop | 457 | return // suppress to break the loop | ||
| 457 | } | 458 | } | ||
| 458 | 459 | ||||
M internal/infrastructure/cef/webview_handlers_test.go +29 -39
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "testing" | |
| 5 | 5 | ||
| 6 | 6 | purecef "github.com/bnema/purego-cef/cef" | |
| 7 | + | "github.com/stretchr/testify/require" | |
| 7 | 8 | ||
| 8 | 9 | "github.com/bnema/dumber/internal/application/port" | |
| 9 | 10 | ) | |
| @@ -13,39 +14,34 @@ type stubFrame struct { | |||
| 13 | 14 | url string | |
| 14 | 15 | } | |
| 15 | 16 | ||
| 16 | - | func (f stubFrame) IsValid() bool { return true } | |
| 17 | - | func (f stubFrame) Undo() {} | |
| 18 | - | func (f stubFrame) Redo() {} | |
| 19 | - | func (f stubFrame) Cut() {} | |
| 20 | - | func (f stubFrame) Copy() {} | |
| 21 | - | func (f stubFrame) Paste() {} | |
| 22 | - | func (f stubFrame) PasteAndMatchStyle() { | |
| 23 | - | } | |
| 24 | - | func (f stubFrame) Del() {} | |
| 25 | - | func (f stubFrame) SelectAll() {} | |
| 26 | - | func (f stubFrame) ViewSource() {} | |
| 27 | - | func (f stubFrame) GetSource(purecef.StringVisitor) { | |
| 28 | - | } | |
| 29 | - | func (f stubFrame) GetText(purecef.StringVisitor) { | |
| 30 | - | } | |
| 31 | - | func (f stubFrame) LoadRequest(purecef.Request) {} | |
| 32 | - | func (f stubFrame) LoadURL(string) {} | |
| 33 | - | func (f stubFrame) ExecuteJavaScript(string, string, int32) { | |
| 34 | - | } | |
| 35 | - | func (f stubFrame) IsMain() bool { return f.main } | |
| 36 | - | func (f stubFrame) IsFocused() bool { return false } | |
| 37 | - | func (f stubFrame) GetName() string { return "" } | |
| 38 | - | func (f stubFrame) GetIdentifier() string { return "" } | |
| 39 | - | func (f stubFrame) GetParent() purecef.Frame { return nil } | |
| 40 | - | func (f stubFrame) GetURL() string { return f.url } | |
| 41 | - | func (f stubFrame) GetBrowser() purecef.Browser { return nil } | |
| 42 | - | func (f stubFrame) GetV8Context() purecef.V8Context { return nil } | |
| 43 | - | func (f stubFrame) VisitDom(purecef.Domvisitor) {} | |
| 17 | + | func (f stubFrame) IsValid() bool { return true } | |
| 18 | + | func (f stubFrame) Undo() {} | |
| 19 | + | func (f stubFrame) Redo() {} | |
| 20 | + | func (f stubFrame) Cut() {} | |
| 21 | + | func (f stubFrame) Copy() {} | |
| 22 | + | func (f stubFrame) Paste() {} | |
| 23 | + | func (f stubFrame) PasteAndMatchStyle() {} | |
| 24 | + | func (f stubFrame) Del() {} | |
| 25 | + | func (f stubFrame) SelectAll() {} | |
| 26 | + | func (f stubFrame) ViewSource() {} | |
| 27 | + | func (f stubFrame) GetSource(purecef.StringVisitor) {} | |
| 28 | + | func (f stubFrame) GetText(purecef.StringVisitor) {} | |
| 29 | + | func (f stubFrame) LoadRequest(purecef.Request) {} | |
| 30 | + | func (f stubFrame) LoadURL(string) {} | |
| 31 | + | func (f stubFrame) ExecuteJavaScript(string, string, int32) {} | |
| 32 | + | func (f stubFrame) IsMain() bool { return f.main } | |
| 33 | + | func (f stubFrame) IsFocused() bool { return false } | |
| 34 | + | func (f stubFrame) GetName() string { return "" } | |
| 35 | + | func (f stubFrame) GetIdentifier() string { return "" } | |
| 36 | + | func (f stubFrame) GetParent() purecef.Frame { return nil } | |
| 37 | + | func (f stubFrame) GetURL() string { return f.url } | |
| 38 | + | func (f stubFrame) GetBrowser() purecef.Browser { return nil } | |
| 39 | + | func (f stubFrame) GetV8Context() purecef.V8Context { return nil } | |
| 40 | + | func (f stubFrame) VisitDom(purecef.Domvisitor) {} | |
| 41 | + | func (f stubFrame) SendProcessMessage(purecef.ProcessID, purecef.ProcessMessage) {} | |
| 44 | 42 | func (f stubFrame) CreateUrlrequest(purecef.Request, purecef.UrlrequestClient) purecef.Urlrequest { | |
| 45 | 43 | return nil | |
| 46 | 44 | } | |
| 47 | - | func (f stubFrame) SendProcessMessage(purecef.ProcessID, purecef.ProcessMessage) { | |
| 48 | - | } | |
| 49 | 45 | ||
| 50 | 46 | func TestOnLoadStartFiresCommittedAndUpdatesURI(t *testing.T) { | |
| 51 | 47 | wv := &WebView{} | |
| @@ -59,13 +55,7 @@ func TestOnLoadStartFiresCommittedAndUpdatesURI(t *testing.T) { | |||
| 59 | 55 | h := &handlerSet{wv: wv} | |
| 60 | 56 | h.OnLoadStart(nil, stubFrame{main: true, url: "https://google.com"}, 0) | |
| 61 | 57 | ||
| 62 | - | if len(gotEvents) != 1 { | |
| 63 | - | t.Fatalf("expected 1 load event, got %d", len(gotEvents)) | |
| 64 | - | } | |
| 65 | - | if gotEvents[0] != port.LoadCommitted { | |
| 66 | - | t.Fatalf("expected %v, got %v", port.LoadCommitted, gotEvents[0]) | |
| 67 | - | } | |
| 68 | - | if gotURI := wv.URI(); gotURI != "https://google.com" { | |
| 69 | - | t.Fatalf("expected URI to update from frame on load commit, got %q", gotURI) | |
| 70 | - | } | |
| 58 | + | require.Len(t, gotEvents, 1) | |
| 59 | + | require.Equal(t, port.LoadCommitted, gotEvents[0]) | |
| 60 | + | require.Equal(t, "https://google.com", wv.URI()) | |
| 71 | 61 | } | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "testing" | 4 | "testing" | ||
| 5 | 5 | ||||
| 6 | purecef "github.com/bnema/purego-cef/cef" | 6 | purecef "github.com/bnema/purego-cef/cef" | ||
| 7 | + | "github.com/stretchr/testify/require" | |||
| 7 | 8 | ||||
| 8 | "github.com/bnema/dumber/internal/application/port" | 9 | "github.com/bnema/dumber/internal/application/port" | ||
| 9 | ) | 10 | ) | ||
| @@ -13,39 +14,34 @@ type stubFrame struct { | |||||
| 13 | url string | 14 | url string | ||
| 14 | } | 15 | } | ||
| 15 | 16 | ||||
| 16 | - | func (f stubFrame) IsValid() bool { return true } | 17 | + | func (f stubFrame) IsValid() bool { return true } |
| 17 | - | func (f stubFrame) Undo() {} | 18 | + | func (f stubFrame) Undo() {} |
| 18 | - | func (f stubFrame) Redo() {} | 19 | + | func (f stubFrame) Redo() {} |
| 19 | - | func (f stubFrame) Cut() {} | 20 | + | func (f stubFrame) Cut() {} |
| 20 | - | func (f stubFrame) Copy() {} | 21 | + | func (f stubFrame) Copy() {} |
| 21 | - | func (f stubFrame) Paste() {} | 22 | + | func (f stubFrame) Paste() {} |
| 22 | - | func (f stubFrame) PasteAndMatchStyle() { | 23 | + | func (f stubFrame) PasteAndMatchStyle() {} |
| 23 | - | } | 24 | + | func (f stubFrame) Del() {} |
| 24 | - | func (f stubFrame) Del() {} | 25 | + | func (f stubFrame) SelectAll() {} |
| 25 | - | func (f stubFrame) SelectAll() {} | 26 | + | func (f stubFrame) ViewSource() {} |
| 26 | - | func (f stubFrame) ViewSource() {} | 27 | + | func (f stubFrame) GetSource(purecef.StringVisitor) {} |
| 27 | - | func (f stubFrame) GetSource(purecef.StringVisitor) { | 28 | + | func (f stubFrame) GetText(purecef.StringVisitor) {} |
| 28 | - | } | 29 | + | func (f stubFrame) LoadRequest(purecef.Request) {} |
| 29 | - | func (f stubFrame) GetText(purecef.StringVisitor) { | 30 | + | func (f stubFrame) LoadURL(string) {} |
| 30 | - | } | 31 | + | func (f stubFrame) ExecuteJavaScript(string, string, int32) {} |
| 31 | - | func (f stubFrame) LoadRequest(purecef.Request) {} | 32 | + | func (f stubFrame) IsMain() bool { return f.main } |
| 32 | - | func (f stubFrame) LoadURL(string) {} | 33 | + | func (f stubFrame) IsFocused() bool { return false } |
| 33 | - | func (f stubFrame) ExecuteJavaScript(string, string, int32) { | 34 | + | func (f stubFrame) GetName() string { return "" } |
| 34 | - | } | 35 | + | func (f stubFrame) GetIdentifier() string { return "" } |
| 35 | - | func (f stubFrame) IsMain() bool { return f.main } | 36 | + | func (f stubFrame) GetParent() purecef.Frame { return nil } |
| 36 | - | func (f stubFrame) IsFocused() bool { return false } | 37 | + | func (f stubFrame) GetURL() string { return f.url } |
| 37 | - | func (f stubFrame) GetName() string { return "" } | 38 | + | func (f stubFrame) GetBrowser() purecef.Browser { return nil } |
| 38 | - | func (f stubFrame) GetIdentifier() string { return "" } | 39 | + | func (f stubFrame) GetV8Context() purecef.V8Context { return nil } |
| 39 | - | func (f stubFrame) GetParent() purecef.Frame { return nil } | 40 | + | func (f stubFrame) VisitDom(purecef.Domvisitor) {} |
| 40 | - | func (f stubFrame) GetURL() string { return f.url } | 41 | + | func (f stubFrame) SendProcessMessage(purecef.ProcessID, purecef.ProcessMessage) {} |
| 41 | - | func (f stubFrame) GetBrowser() purecef.Browser { return nil } | |||
| 42 | - | func (f stubFrame) GetV8Context() purecef.V8Context { return nil } | |||
| 43 | - | func (f stubFrame) VisitDom(purecef.Domvisitor) {} | |||
| 44 | func (f stubFrame) CreateUrlrequest(purecef.Request, purecef.UrlrequestClient) purecef.Urlrequest { | 42 | func (f stubFrame) CreateUrlrequest(purecef.Request, purecef.UrlrequestClient) purecef.Urlrequest { | ||
| 45 | return nil | 43 | return nil | ||
| 46 | } | 44 | } | ||
| 47 | - | func (f stubFrame) SendProcessMessage(purecef.ProcessID, purecef.ProcessMessage) { | |||
| 48 | - | } | |||
| 49 | 45 | ||||
| 50 | func TestOnLoadStartFiresCommittedAndUpdatesURI(t *testing.T) { | 46 | func TestOnLoadStartFiresCommittedAndUpdatesURI(t *testing.T) { | ||
| 51 | wv := &WebView{} | 47 | wv := &WebView{} | ||
| @@ -59,13 +55,7 @@ func TestOnLoadStartFiresCommittedAndUpdatesURI(t *testing.T) { | |||||
| 59 | h := &handlerSet{wv: wv} | 55 | h := &handlerSet{wv: wv} | ||
| 60 | h.OnLoadStart(nil, stubFrame{main: true, url: "https://google.com"}, 0) | 56 | h.OnLoadStart(nil, stubFrame{main: true, url: "https://google.com"}, 0) | ||
| 61 | 57 | ||||
| 62 | - | if len(gotEvents) != 1 { | 58 | + | require.Len(t, gotEvents, 1) |
| 63 | - | t.Fatalf("expected 1 load event, got %d", len(gotEvents)) | 59 | + | require.Equal(t, port.LoadCommitted, gotEvents[0]) |
| 64 | - | } | 60 | + | require.Equal(t, "https://google.com", wv.URI()) |
| 65 | - | if gotEvents[0] != port.LoadCommitted { | |||
| 66 | - | t.Fatalf("expected %v, got %v", port.LoadCommitted, gotEvents[0]) | |||
| 67 | - | } | |||
| 68 | - | if gotURI := wv.URI(); gotURI != "https://google.com" { | |||
| 69 | - | t.Fatalf("expected URI to update from frame on load commit, got %q", gotURI) | |||
| 70 | - | } | |||
| 71 | } | 61 | } | ||