detached 636c556

fix(cef): recover safely after repeated GPU relaunch

commit
636c5563058b76802f1c200e4f6829f806ef6cb3
author
brice · · 2026-07-14 06:25 UTC
committer
brice · · 2026-07-14 07:06 UTC
parent
a347952
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +100 -0
M internal/infrastructure/cef/app.go +18 -0
@@ -448,12 +448,30 @@ func isBrowseRelaunchCommandLine(commandLine purecef.CommandLine) bool {
448448 return len(args) >= 2 && args[1] == "browse"
449449 }
450450
451+func isGPUProcessRelaunchCommandLine(commandLine purecef.CommandLine) bool {
452+ if commandLine == nil {
453+ return false
454+ }
455+ for _, arg := range parseRelaunchCommandLineArgs(commandLine.GetCommandLineString()) {
456+ if arg == "--type=gpu-process" {
457+ return true
458+ }
459+ }
460+ return false
461+}
462+
451463 func parseBrowseURLFromRelaunchCommandLine(commandLine purecef.CommandLine) string {
452464 browseURL, _ := parseBrowseRelaunchCommandLine(commandLine)
453465 return browseURL
454466 }
455467
456468 func (h *dumberBPH) OnAlreadyRunningAppRelaunch(commandLine purecef.CommandLine, _ string) int32 {
469+ if isGPUProcessRelaunchCommandLine(commandLine) {
470+ if h != nil && h.engine != nil {
471+ h.engine.recordGPURelaunch()
472+ }
473+ return 0
474+ }
457475 if browseURL, ok := parseBrowseRelaunchCommandLine(commandLine); ok {
458476 if h != nil && h.engine != nil {
459477 if handler := h.engine.alreadyRunningAppRelaunchCallback(); handler != nil {
@@ -448,12 +448,30 @@ func isBrowseRelaunchCommandLine(commandLine purecef.CommandLine) bool {
448 return len(args) >= 2 && args[1] == "browse"448 return len(args) >= 2 && args[1] == "browse"
449 }449 }
450 450
451+func isGPUProcessRelaunchCommandLine(commandLine purecef.CommandLine) bool {
452+ if commandLine == nil {
453+ return false
454+ }
455+ for _, arg := range parseRelaunchCommandLineArgs(commandLine.GetCommandLineString()) {
456+ if arg == "--type=gpu-process" {
457+ return true
458+ }
459+ }
460+ return false
461+}
462+
451 func parseBrowseURLFromRelaunchCommandLine(commandLine purecef.CommandLine) string {463 func parseBrowseURLFromRelaunchCommandLine(commandLine purecef.CommandLine) string {
452 browseURL, _ := parseBrowseRelaunchCommandLine(commandLine)464 browseURL, _ := parseBrowseRelaunchCommandLine(commandLine)
453 return browseURL465 return browseURL
454 }466 }
455 467
456 func (h *dumberBPH) OnAlreadyRunningAppRelaunch(commandLine purecef.CommandLine, _ string) int32 {468 func (h *dumberBPH) OnAlreadyRunningAppRelaunch(commandLine purecef.CommandLine, _ string) int32 {
469+ if isGPUProcessRelaunchCommandLine(commandLine) {
470+ if h != nil && h.engine != nil {
471+ h.engine.recordGPURelaunch()
472+ }
473+ return 0
474+ }
457 if browseURL, ok := parseBrowseRelaunchCommandLine(commandLine); ok {475 if browseURL, ok := parseBrowseRelaunchCommandLine(commandLine); ok {
458 if h != nil && h.engine != nil {476 if h != nil && h.engine != nil {
459 if handler := h.engine.alreadyRunningAppRelaunchCallback(); handler != nil {477 if handler := h.engine.alreadyRunningAppRelaunchCallback(); handler != nil {
M internal/infrastructure/cef/engine.go +13 -0
@@ -31,6 +31,7 @@ type Engine struct {
3131 pool *WebViewPool
3232 profileLogDir string
3333 runtimeCEFDir string
34+ stateRoot string
3435 renderStackPlan cef2gtk.RenderStackPlan
3536 applicationScaleMu sync.RWMutex
3637 applicationScale float64
@@ -54,6 +55,7 @@ type Engine struct {
5455 downloadHandler *downloadHandler
5556 alreadyRunningAppRelaunchMu sync.RWMutex
5657 alreadyRunningAppRelaunchHandler func(string)
58+ gpuRelaunchCount atomic.Int32
5759
5860 // activeWebViews tracks all live webviews for CSS broadcast.
5961 activeWebViews sync.Map // map[port.WebViewID]*WebView
@@ -145,6 +147,17 @@ func (e *Engine) alreadyRunningAppRelaunchCallback() func(string) {
145147 return e.alreadyRunningAppRelaunchHandler
146148 }
147149
150+// recordGPURelaunch asks the next engine start to use Chromium's conservative
151+// defaults after two GPU-process relaunch attempts in one running instance.
152+func (e *Engine) recordGPURelaunch() {
153+ if e == nil || e.gpuRelaunchCount.Add(1) != 2 {
154+ return
155+ }
156+ if err := writeNextStartSafetyMarker(e.stateRoot); err != nil {
157+ logging.FromContext(e.ctx).Warn().Err(err).Msg("cef: failed to mark next start safe after repeated GPU relaunch")
158+ }
159+}
160+
148161 // registerWebView adds a webview to the active tracking map.
149162 func (e *Engine) registerWebView(wv *WebView) {
150163 e.activeWebViews.Store(wv.id, wv)
@@ -31,6 +31,7 @@ type Engine struct {
31 pool *WebViewPool31 pool *WebViewPool
32 profileLogDir string32 profileLogDir string
33 runtimeCEFDir string33 runtimeCEFDir string
34+ stateRoot string
34 renderStackPlan cef2gtk.RenderStackPlan35 renderStackPlan cef2gtk.RenderStackPlan
35 applicationScaleMu sync.RWMutex36 applicationScaleMu sync.RWMutex
36 applicationScale float6437 applicationScale float64
@@ -54,6 +55,7 @@ type Engine struct {
54 downloadHandler *downloadHandler55 downloadHandler *downloadHandler
55 alreadyRunningAppRelaunchMu sync.RWMutex56 alreadyRunningAppRelaunchMu sync.RWMutex
56 alreadyRunningAppRelaunchHandler func(string)57 alreadyRunningAppRelaunchHandler func(string)
58+ gpuRelaunchCount atomic.Int32
57 59
58 // activeWebViews tracks all live webviews for CSS broadcast.60 // activeWebViews tracks all live webviews for CSS broadcast.
59 activeWebViews sync.Map // map[port.WebViewID]*WebView61 activeWebViews sync.Map // map[port.WebViewID]*WebView
@@ -145,6 +147,17 @@ func (e *Engine) alreadyRunningAppRelaunchCallback() func(string) {
145 return e.alreadyRunningAppRelaunchHandler147 return e.alreadyRunningAppRelaunchHandler
146 }148 }
147 149
150+// recordGPURelaunch asks the next engine start to use Chromium's conservative
151+// defaults after two GPU-process relaunch attempts in one running instance.
152+func (e *Engine) recordGPURelaunch() {
153+ if e == nil || e.gpuRelaunchCount.Add(1) != 2 {
154+ return
155+ }
156+ if err := writeNextStartSafetyMarker(e.stateRoot); err != nil {
157+ logging.FromContext(e.ctx).Warn().Err(err).Msg("cef: failed to mark next start safe after repeated GPU relaunch")
158+ }
159+}
160+
148 // registerWebView adds a webview to the active tracking map.161 // registerWebView adds a webview to the active tracking map.
149 func (e *Engine) registerWebView(wv *WebView) {162 func (e *Engine) registerWebView(wv *WebView) {
150 e.activeWebViews.Store(wv.id, wv)163 e.activeWebViews.Store(wv.id, wv)
M internal/infrastructure/cef/engine_init.go +5 -0
@@ -45,6 +45,10 @@ func NewEngine(
4545 ) (*Engine, error) {
4646 logger := logging.FromContext(ctx)
4747 stateRoot := resolvedStateRoot(paths.StateRoot, opts)
48+ if consumeNextStartSafetyMarker(stateRoot) {
49+ applyNextStartSafetyEnvironment()
50+ logger.Warn().Str("state_root", stateRoot).Msg("cef: applying next-start GPU safety recovery")
51+ }
4852 cleanStaleSingletonLocks(logger, stateRoot)
4953 windowlessFrameRate := normalizedWindowlessFrameRate(cfg.WindowlessFrameRate)
5054 renderStackPlan, err := resolveCEFRenderStackPlan(cfg.RenderStack)
@@ -67,6 +71,7 @@ func NewEngine(
6771 ctx: ctx,
6872 profileLogDir: paths.ProfileLogDir,
6973 runtimeCEFDir: settings.CEFDir,
74+ stateRoot: stateRoot,
7075 renderStackPlan: renderStackPlan,
7176 applicationScale: normalizedApplicationScale(cfg.ApplicationScale),
7277 registerHandlers: deps.RegisterHandlers,
@@ -45,6 +45,10 @@ func NewEngine(
45 ) (*Engine, error) {45 ) (*Engine, error) {
46 logger := logging.FromContext(ctx)46 logger := logging.FromContext(ctx)
47 stateRoot := resolvedStateRoot(paths.StateRoot, opts)47 stateRoot := resolvedStateRoot(paths.StateRoot, opts)
48+ if consumeNextStartSafetyMarker(stateRoot) {
49+ applyNextStartSafetyEnvironment()
50+ logger.Warn().Str("state_root", stateRoot).Msg("cef: applying next-start GPU safety recovery")
51+ }
48 cleanStaleSingletonLocks(logger, stateRoot)52 cleanStaleSingletonLocks(logger, stateRoot)
49 windowlessFrameRate := normalizedWindowlessFrameRate(cfg.WindowlessFrameRate)53 windowlessFrameRate := normalizedWindowlessFrameRate(cfg.WindowlessFrameRate)
50 renderStackPlan, err := resolveCEFRenderStackPlan(cfg.RenderStack)54 renderStackPlan, err := resolveCEFRenderStackPlan(cfg.RenderStack)
@@ -67,6 +71,7 @@ func NewEngine(
67 ctx: ctx,71 ctx: ctx,
68 profileLogDir: paths.ProfileLogDir,72 profileLogDir: paths.ProfileLogDir,
69 runtimeCEFDir: settings.CEFDir,73 runtimeCEFDir: settings.CEFDir,
74+ stateRoot: stateRoot,
70 renderStackPlan: renderStackPlan,75 renderStackPlan: renderStackPlan,
71 applicationScale: normalizedApplicationScale(cfg.ApplicationScale),76 applicationScale: normalizedApplicationScale(cfg.ApplicationScale),
72 registerHandlers: deps.RegisterHandlers,77 registerHandlers: deps.RegisterHandlers,
A internal/infrastructure/cef/restart_safety.go +64 -0
@@ -0,0 +1,64 @@
1+package cef
2+
3+import (
4+ "errors"
5+ "os"
6+ "path/filepath"
7+)
8+
9+const nextStartSafetyMarkerFile = "cef-next-start-safe"
10+
11+// writeNextStartSafetyMarker records a one-shot recovery request for the next
12+// CEF start. The marker lives beside CEF's root cache so it follows explicit
13+// per-session cache roots as well as the normal profile.
14+func writeNextStartSafetyMarker(stateRoot string) error {
15+ if stateRoot == "" {
16+ return errors.New("CEF state root is empty")
17+ }
18+ if err := os.MkdirAll(stateRoot, 0o700); err != nil {
19+ return err
20+ }
21+
22+ marker := filepath.Join(stateRoot, nextStartSafetyMarkerFile)
23+ temporary, err := os.CreateTemp(stateRoot, ".cef-next-start-safe-*")
24+ if err != nil {
25+ return err
26+ }
27+ temporaryName := temporary.Name()
28+ defer os.Remove(temporaryName)
29+ if err := temporary.Chmod(0o600); err != nil {
30+ _ = temporary.Close()
31+ return err
32+ }
33+ if _, err := temporary.WriteString("safe\n"); err != nil {
34+ _ = temporary.Close()
35+ return err
36+ }
37+ if err := temporary.Close(); err != nil {
38+ return err
39+ }
40+ return os.Rename(temporaryName, marker)
41+}
42+
43+// consumeNextStartSafetyMarker consumes a one-shot marker. It deliberately
44+// does not follow symlinks, so an untrusted cache entry cannot redirect the
45+// recovery check outside the CEF state root.
46+func consumeNextStartSafetyMarker(stateRoot string) bool {
47+ if stateRoot == "" {
48+ return false
49+ }
50+ marker := filepath.Join(stateRoot, nextStartSafetyMarkerFile)
51+ info, err := os.Lstat(marker)
52+ if err != nil || !info.Mode().IsRegular() {
53+ return false
54+ }
55+ return os.Remove(marker) == nil
56+}
57+
58+// applyNextStartSafetyEnvironment leaves the Vulkan render stack intact while
59+// returning Chromium to its hardware-decode safety defaults for one start.
60+func applyNextStartSafetyEnvironment() {
61+ _ = os.Setenv(cefEnableVAAPIEnvVar, "0")
62+ _ = os.Unsetenv(cefChromiumFlagsEnvVar)
63+ _ = os.Unsetenv(cefRenderNodeEnvVar)
64+}
@@ -0,0 +1,64 @@
1+package cef
2+
3+import (
4+ "errors"
5+ "os"
6+ "path/filepath"
7+)
8+
9+const nextStartSafetyMarkerFile = "cef-next-start-safe"
10+
11+// writeNextStartSafetyMarker records a one-shot recovery request for the next
12+// CEF start. The marker lives beside CEF's root cache so it follows explicit
13+// per-session cache roots as well as the normal profile.
14+func writeNextStartSafetyMarker(stateRoot string) error {
15+ if stateRoot == "" {
16+ return errors.New("CEF state root is empty")
17+ }
18+ if err := os.MkdirAll(stateRoot, 0o700); err != nil {
19+ return err
20+ }
21+
22+ marker := filepath.Join(stateRoot, nextStartSafetyMarkerFile)
23+ temporary, err := os.CreateTemp(stateRoot, ".cef-next-start-safe-*")
24+ if err != nil {
25+ return err
26+ }
27+ temporaryName := temporary.Name()
28+ defer os.Remove(temporaryName)
29+ if err := temporary.Chmod(0o600); err != nil {
30+ _ = temporary.Close()
31+ return err
32+ }
33+ if _, err := temporary.WriteString("safe\n"); err != nil {
34+ _ = temporary.Close()
35+ return err
36+ }
37+ if err := temporary.Close(); err != nil {
38+ return err
39+ }
40+ return os.Rename(temporaryName, marker)
41+}
42+
43+// consumeNextStartSafetyMarker consumes a one-shot marker. It deliberately
44+// does not follow symlinks, so an untrusted cache entry cannot redirect the
45+// recovery check outside the CEF state root.
46+func consumeNextStartSafetyMarker(stateRoot string) bool {
47+ if stateRoot == "" {
48+ return false
49+ }
50+ marker := filepath.Join(stateRoot, nextStartSafetyMarkerFile)
51+ info, err := os.Lstat(marker)
52+ if err != nil || !info.Mode().IsRegular() {
53+ return false
54+ }
55+ return os.Remove(marker) == nil
56+}
57+
58+// applyNextStartSafetyEnvironment leaves the Vulkan render stack intact while
59+// returning Chromium to its hardware-decode safety defaults for one start.
60+func applyNextStartSafetyEnvironment() {
61+ _ = os.Setenv(cefEnableVAAPIEnvVar, "0")
62+ _ = os.Unsetenv(cefChromiumFlagsEnvVar)
63+ _ = os.Unsetenv(cefRenderNodeEnvVar)
64+}

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