fix: handle empty cef relaunch browse URLs
- commit
- 1f0bfbdfbb786a315c2e8b0f3105b5c3e86cff86
- author
- brice · · 2026-04-14 16:25 UTC
- parent
- 74e124a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +87 -16
M cmd/dumber/main.go +2 -3
| @@ -18,7 +18,6 @@ import ( | |||
| 18 | 18 | "github.com/bnema/dumber/internal/domain/entity" | |
| 19 | 19 | "github.com/bnema/dumber/internal/domain/repository" | |
| 20 | 20 | "github.com/bnema/dumber/internal/infrastructure/cache" | |
| 21 | - | cefinfra "github.com/bnema/dumber/internal/infrastructure/cef" | |
| 22 | 21 | "github.com/bnema/dumber/internal/infrastructure/clipboard" | |
| 23 | 22 | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 24 | 23 | "github.com/bnema/dumber/internal/infrastructure/deps" | |
| @@ -206,8 +205,8 @@ func runGUI() int { | |||
| 206 | 205 | log.Error().Err(err).Msg("failed to create application") | |
| 207 | 206 | return 1 | |
| 208 | 207 | } | |
| 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) { | |
| 211 | 210 | if err := app.OpenFreshWindow(ctx, url); err != nil { | |
| 212 | 211 | log.Warn().Err(err).Str("url", url).Msg("failed to open relaunch browser window") | |
| 213 | 212 | } | |
| @@ -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 1 | 206 | 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 { | |||
| 18 | 18 | DeliverOpenFreshWindow(ctx context.Context, url string) (bool, error) | |
| 19 | 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 | + | } | |
| @@ -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) | |||
| 127 | 127 | } | |
| 128 | 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()) | |
| 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 | |
| 139 | 139 | } | |
| 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) | |
| 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 | 194 | func (h *dumberBPH) OnAlreadyRunningAppRelaunch(commandLine purecef.CommandLine, _ string) int32 { | |
| 150 | 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 | 197 | h.engine.alreadyRunningAppRelaunchHandler(browseURL) | |
| 153 | 198 | } | |
| 154 | 199 | } | |
| @@ -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 | - | continue | 134 | + | 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 | |||
| 101 | 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 | + | } | |
| @@ -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 ( | |||
| 14 | 14 | ||
| 15 | 15 | // Compile-time interface check. | |
| 16 | 16 | var _ port.Engine = (*Engine)(nil) | |
| 17 | + | var _ port.AlreadyRunningAppRelaunchHandlerSetter = (*Engine)(nil) | |
| 17 | 18 | ||
| 18 | 19 | // Engine implements port.Engine for the CEF browser backend. | |
| 19 | 20 | // 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. | ||