detached 1f0bfbd

fix: handle empty cef relaunch browse URLs

commit
1f0bfbdfbb786a315c2e8b0f3105b5c3e86cff86
author
brice · · 2026-04-14 16:25 UTC
parent
74e124a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +87 -16
M cmd/dumber/main.go +2 -3
@@ -18,7 +18,6 @@ import (
1818 "github.com/bnema/dumber/internal/domain/entity"
1919 "github.com/bnema/dumber/internal/domain/repository"
2020 "github.com/bnema/dumber/internal/infrastructure/cache"
21- cefinfra "github.com/bnema/dumber/internal/infrastructure/cef"
2221 "github.com/bnema/dumber/internal/infrastructure/clipboard"
2322 "github.com/bnema/dumber/internal/infrastructure/config"
2423 "github.com/bnema/dumber/internal/infrastructure/deps"
@@ -206,8 +205,8 @@ func runGUI() int {
206205 log.Error().Err(err).Msg("failed to create application")
207206 return 1
208207 }
209- if cefEngine, ok := engine.(*cefinfra.Engine); ok {
210- cefEngine.SetAlreadyRunningAppRelaunchHandler(func(url string) {
208+ if relaunchSetter, ok := engine.(port.AlreadyRunningAppRelaunchHandlerSetter); ok {
209+ relaunchSetter.SetAlreadyRunningAppRelaunchHandler(func(url string) {
211210 if err := app.OpenFreshWindow(ctx, url); err != nil {
212211 log.Warn().Err(err).Str("url", url).Msg("failed to open relaunch browser window")
213212 }
@@ -18,7 +18,6 @@ import (
18 "github.com/bnema/dumber/internal/domain/entity"18 "github.com/bnema/dumber/internal/domain/entity"
19 "github.com/bnema/dumber/internal/domain/repository"19 "github.com/bnema/dumber/internal/domain/repository"
20 "github.com/bnema/dumber/internal/infrastructure/cache"20 "github.com/bnema/dumber/internal/infrastructure/cache"
21- cefinfra "github.com/bnema/dumber/internal/infrastructure/cef"
22 "github.com/bnema/dumber/internal/infrastructure/clipboard"21 "github.com/bnema/dumber/internal/infrastructure/clipboard"
23 "github.com/bnema/dumber/internal/infrastructure/config"22 "github.com/bnema/dumber/internal/infrastructure/config"
24 "github.com/bnema/dumber/internal/infrastructure/deps"23 "github.com/bnema/dumber/internal/infrastructure/deps"
@@ -206,8 +205,8 @@ func runGUI() int {
206 log.Error().Err(err).Msg("failed to create application")205 log.Error().Err(err).Msg("failed to create application")
207 return 1206 return 1
208 }207 }
209- if cefEngine, ok := engine.(*cefinfra.Engine); ok {208+ if relaunchSetter, ok := engine.(port.AlreadyRunningAppRelaunchHandlerSetter); ok {
210- cefEngine.SetAlreadyRunningAppRelaunchHandler(func(url string) {209+ relaunchSetter.SetAlreadyRunningAppRelaunchHandler(func(url string) {
211 if err := app.OpenFreshWindow(ctx, url); err != nil {210 if err := app.OpenFreshWindow(ctx, url); err != nil {
212 log.Warn().Err(err).Str("url", url).Msg("failed to open relaunch browser window")211 log.Warn().Err(err).Str("url", url).Msg("failed to open relaunch browser window")
213 }212 }
M internal/application/port/browser_launch.go +5 -0
@@ -18,3 +18,8 @@ type BrowserLaunchRelay interface {
1818 DeliverOpenFreshWindow(ctx context.Context, url string) (bool, error)
1919 Listen(ctx context.Context, opener BrowserWindowOpener) (io.Closer, error)
2020 }
21+
22+// AlreadyRunningAppRelaunchHandlerSetter configures a handler for CEF relaunches.
23+type AlreadyRunningAppRelaunchHandlerSetter interface {
24+ SetAlreadyRunningAppRelaunchHandler(func(string))
25+}
@@ -18,3 +18,8 @@ type BrowserLaunchRelay interface {
18 DeliverOpenFreshWindow(ctx context.Context, url string) (bool, error)18 DeliverOpenFreshWindow(ctx context.Context, url string) (bool, error)
19 Listen(ctx context.Context, opener BrowserWindowOpener) (io.Closer, error)19 Listen(ctx context.Context, opener BrowserWindowOpener) (io.Closer, error)
20 }20 }
21+
22+// AlreadyRunningAppRelaunchHandlerSetter configures a handler for CEF relaunches.
23+type AlreadyRunningAppRelaunchHandlerSetter interface {
24+ SetAlreadyRunningAppRelaunchHandler(func(string))
25+}
M internal/infrastructure/cef/app.go +58 -13
@@ -127,28 +127,73 @@ func (h *dumberBPH) OnBeforeChildProcessLaunch(commandLine purecef.CommandLine)
127127 }
128128 h.engine.recordChildProcessLaunch(processType, useAngle, ozonePlatform, commandLineString)
129129 }
130-func parseBrowseURLFromRelaunchCommandLine(commandLine purecef.CommandLine) string {
131- if commandLine == nil {
132- return ""
133- }
134130
135- fields := strings.Fields(commandLine.GetCommandLineString())
136- for i, field := range fields {
137- if field != "browse" {
138- continue
131+func parseRelaunchCommandLineArgs(commandLine string) []string {
132+ args := make([]string, 0, 4)
133+ var current strings.Builder
134+ var quote rune
135+ escaped := false
136+ flush := func() {
137+ if current.Len() == 0 {
138+ return
139139 }
140- if i+1 < len(fields) {
141- return fields[i+1]
140+ args = append(args, current.String())
141+ current.Reset()
142+ }
143+
144+ for _, r := range commandLine {
145+ switch {
146+ case escaped:
147+ current.WriteRune(r)
148+ escaped = false
149+ case r == '\\':
150+ escaped = true
151+ case quote != 0:
152+ if r == quote {
153+ quote = 0
154+ continue
155+ }
156+ current.WriteRune(r)
157+ case r == '"' || r == '\'':
158+ quote = r
159+ case r == ' ' || r == '\t' || r == '\n' || r == '\r':
160+ flush()
161+ default:
162+ current.WriteRune(r)
142163 }
143- return ""
144164 }
145165
146- return ""
166+ if escaped {
167+ current.WriteRune('\\')
168+ }
169+ flush()
170+
171+ return args
172+}
173+
174+func parseBrowseRelaunchCommandLine(commandLine purecef.CommandLine) (string, bool) {
175+ if commandLine == nil {
176+ return "", false
177+ }
178+
179+ args := parseRelaunchCommandLineArgs(commandLine.GetCommandLineString())
180+ if len(args) < 2 || args[1] != "browse" {
181+ return "", false
182+ }
183+ if len(args) > 2 {
184+ return args[2], true
185+ }
186+ return "", true
187+}
188+
189+func parseBrowseURLFromRelaunchCommandLine(commandLine purecef.CommandLine) string {
190+ browseURL, _ := parseBrowseRelaunchCommandLine(commandLine)
191+ return browseURL
147192 }
148193
149194 func (h *dumberBPH) OnAlreadyRunningAppRelaunch(commandLine purecef.CommandLine, _ string) int32 {
150195 if h != nil && h.engine != nil {
151- if browseURL := parseBrowseURLFromRelaunchCommandLine(commandLine); browseURL != "" && h.engine.alreadyRunningAppRelaunchHandler != nil {
196+ if browseURL, ok := parseBrowseRelaunchCommandLine(commandLine); ok && h.engine.alreadyRunningAppRelaunchHandler != nil {
152197 h.engine.alreadyRunningAppRelaunchHandler(browseURL)
153198 }
154199 }
@@ -127,28 +127,73 @@ func (h *dumberBPH) OnBeforeChildProcessLaunch(commandLine purecef.CommandLine)
127 }127 }
128 h.engine.recordChildProcessLaunch(processType, useAngle, ozonePlatform, commandLineString)128 h.engine.recordChildProcessLaunch(processType, useAngle, ozonePlatform, commandLineString)
129 }129 }
130-func parseBrowseURLFromRelaunchCommandLine(commandLine purecef.CommandLine) string {
131- if commandLine == nil {
132- return ""
133- }
134 130
135- fields := strings.Fields(commandLine.GetCommandLineString())131+func parseRelaunchCommandLineArgs(commandLine string) []string {
136- for i, field := range fields {132+ args := make([]string, 0, 4)
137- if field != "browse" {133+ var current strings.Builder
138- continue134+ var quote rune
135+ escaped := false
136+ flush := func() {
137+ if current.Len() == 0 {
138+ return
139 }139 }
140- if i+1 < len(fields) {140+ args = append(args, current.String())
141- return fields[i+1]141+ current.Reset()
142+ }
143+
144+ for _, r := range commandLine {
145+ switch {
146+ case escaped:
147+ current.WriteRune(r)
148+ escaped = false
149+ case r == '\\':
150+ escaped = true
151+ case quote != 0:
152+ if r == quote {
153+ quote = 0
154+ continue
155+ }
156+ current.WriteRune(r)
157+ case r == '"' || r == '\'':
158+ quote = r
159+ case r == ' ' || r == '\t' || r == '\n' || r == '\r':
160+ flush()
161+ default:
162+ current.WriteRune(r)
142 }163 }
143- return ""
144 }164 }
145 165
146- return ""166+ if escaped {
167+ current.WriteRune('\\')
168+ }
169+ flush()
170+
171+ return args
172+}
173+
174+func parseBrowseRelaunchCommandLine(commandLine purecef.CommandLine) (string, bool) {
175+ if commandLine == nil {
176+ return "", false
177+ }
178+
179+ args := parseRelaunchCommandLineArgs(commandLine.GetCommandLineString())
180+ if len(args) < 2 || args[1] != "browse" {
181+ return "", false
182+ }
183+ if len(args) > 2 {
184+ return args[2], true
185+ }
186+ return "", true
187+}
188+
189+func parseBrowseURLFromRelaunchCommandLine(commandLine purecef.CommandLine) string {
190+ browseURL, _ := parseBrowseRelaunchCommandLine(commandLine)
191+ return browseURL
147 }192 }
148 193
149 func (h *dumberBPH) OnAlreadyRunningAppRelaunch(commandLine purecef.CommandLine, _ string) int32 {194 func (h *dumberBPH) OnAlreadyRunningAppRelaunch(commandLine purecef.CommandLine, _ string) int32 {
150 if h != nil && h.engine != nil {195 if h != nil && h.engine != nil {
151- if browseURL := parseBrowseURLFromRelaunchCommandLine(commandLine); browseURL != "" && h.engine.alreadyRunningAppRelaunchHandler != nil {196+ if browseURL, ok := parseBrowseRelaunchCommandLine(commandLine); ok && h.engine.alreadyRunningAppRelaunchHandler != nil {
152 h.engine.alreadyRunningAppRelaunchHandler(browseURL)197 h.engine.alreadyRunningAppRelaunchHandler(browseURL)
153 }198 }
154 }199 }
M internal/infrastructure/cef/app_test.go +21 -0
@@ -101,3 +101,24 @@ func TestDumberBPH_OnAlreadyRunningAppRelaunch_ForwardsBrowseURLAndReturns1(t *t
101101 t.Fatalf("forwarded browse url = %q, want %q", gotURL, "https://example.com")
102102 }
103103 }
104+
105+func TestDumberBPH_OnAlreadyRunningAppRelaunch_ForwardsEmptyBrowseURLAndReturns1(t *testing.T) {
106+ t.Parallel()
107+
108+ gotURL := "sentinel"
109+ eng := &Engine{}
110+ eng.SetAlreadyRunningAppRelaunchHandler(func(url string) {
111+ gotURL = url
112+ })
113+
114+ ret := (&dumberBPH{engine: eng}).OnAlreadyRunningAppRelaunch(relaunchCommandLineStub{
115+ commandLineString: "dumber browse",
116+ }, "")
117+
118+ if ret != 1 {
119+ t.Fatalf("OnAlreadyRunningAppRelaunch returned %d, want 1", ret)
120+ }
121+ if gotURL != "" {
122+ t.Fatalf("forwarded browse url = %q, want empty string", gotURL)
123+ }
124+}
@@ -101,3 +101,24 @@ func TestDumberBPH_OnAlreadyRunningAppRelaunch_ForwardsBrowseURLAndReturns1(t *t
101 t.Fatalf("forwarded browse url = %q, want %q", gotURL, "https://example.com")101 t.Fatalf("forwarded browse url = %q, want %q", gotURL, "https://example.com")
102 }102 }
103 }103 }
104+
105+func TestDumberBPH_OnAlreadyRunningAppRelaunch_ForwardsEmptyBrowseURLAndReturns1(t *testing.T) {
106+ t.Parallel()
107+
108+ gotURL := "sentinel"
109+ eng := &Engine{}
110+ eng.SetAlreadyRunningAppRelaunchHandler(func(url string) {
111+ gotURL = url
112+ })
113+
114+ ret := (&dumberBPH{engine: eng}).OnAlreadyRunningAppRelaunch(relaunchCommandLineStub{
115+ commandLineString: "dumber browse",
116+ }, "")
117+
118+ if ret != 1 {
119+ t.Fatalf("OnAlreadyRunningAppRelaunch returned %d, want 1", ret)
120+ }
121+ if gotURL != "" {
122+ t.Fatalf("forwarded browse url = %q, want empty string", gotURL)
123+ }
124+}
M internal/infrastructure/cef/engine.go +1 -0
@@ -14,6 +14,7 @@ import (
1414
1515 // Compile-time interface check.
1616 var _ port.Engine = (*Engine)(nil)
17+var _ port.AlreadyRunningAppRelaunchHandlerSetter = (*Engine)(nil)
1718
1819 // Engine implements port.Engine for the CEF browser backend.
1920 // It manages the CEF lifecycle and provides access to all engine subsystems.
@@ -14,6 +14,7 @@ import (
14 14
15 // Compile-time interface check.15 // Compile-time interface check.
16 var _ port.Engine = (*Engine)(nil)16 var _ port.Engine = (*Engine)(nil)
17+var _ port.AlreadyRunningAppRelaunchHandlerSetter = (*Engine)(nil)
17 18
18 // Engine implements port.Engine for the CEF browser backend.19 // Engine implements port.Engine for the CEF browser backend.
19 // It manages the CEF lifecycle and provides access to all engine subsystems.20 // It manages the CEF lifecycle and provides access to all engine subsystems.

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