detached 9c26b13

fix(runtime): harden dev browser launch sockets

commit
9c26b134f47546e03d408de37c07c8ce86cb64bc
author
brice · · 2026-06-26 13:33 UTC
parent
f150f41
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +173 -13
M internal/bootstrap/runtime_profile_test.go +6 -1
@@ -1,6 +1,7 @@
11 package bootstrap
22
33 import (
4+ "os"
45 "path/filepath"
56 "strings"
67 "testing"
@@ -53,7 +54,11 @@ func TestResolveRuntimeProfile_DevUsesEngineSpecificNamespaces(t *testing.T) {
5354 wd := t.TempDir()
5455 t.Chdir(wd)
5556
56- runtimeDir := t.TempDir()
57+ runtimeDir, err := os.MkdirTemp("", "dbr-")
58+ require.NoError(t, err)
59+ t.Cleanup(func() {
60+ _ = os.RemoveAll(runtimeDir)
61+ })
5762 t.Setenv("ENV", "dev")
5863 t.Setenv("XDG_RUNTIME_DIR", runtimeDir)
5964 profile, err := ResolveRuntimeProfile(cfg)
@@ -1,6 +1,7 @@
1 package bootstrap1 package bootstrap
2 2
3 import (3 import (
4+ "os"
4 "path/filepath"5 "path/filepath"
5 "strings"6 "strings"
6 "testing"7 "testing"
@@ -53,7 +54,11 @@ func TestResolveRuntimeProfile_DevUsesEngineSpecificNamespaces(t *testing.T) {
53 wd := t.TempDir()54 wd := t.TempDir()
54 t.Chdir(wd)55 t.Chdir(wd)
55 56
56- runtimeDir := t.TempDir()57+ runtimeDir, err := os.MkdirTemp("", "dbr-")
58+ require.NoError(t, err)
59+ t.Cleanup(func() {
60+ _ = os.RemoveAll(runtimeDir)
61+ })
57 t.Setenv("ENV", "dev")62 t.Setenv("ENV", "dev")
58 t.Setenv("XDG_RUNTIME_DIR", runtimeDir)63 t.Setenv("XDG_RUNTIME_DIR", runtimeDir)
59 profile, err := ResolveRuntimeProfile(cfg)64 profile, err := ResolveRuntimeProfile(cfg)
M internal/infrastructure/cef/engine_init_test.go +19 -2
@@ -181,12 +181,18 @@ func TestCreateAudioOutputFactory_ReturnsFactory(t *testing.T) {
181181 func testCEFDevProfile(t *testing.T) runtimeprofile.Profile {
182182 t.Helper()
183183 cwd := t.TempDir()
184+ runtimeDir := shortRuntimeDir(t)
185+ t.Setenv("XDG_RUNTIME_DIR", runtimeDir)
184186 profile, err := runtimeprofile.Resolve(runtimeprofile.ResolveInput{
185187 Env: func(key string) string {
186- if key == "ENV" {
188+ switch key {
189+ case "ENV":
187190 return "dev"
191+ case "XDG_RUNTIME_DIR":
192+ return runtimeDir
193+ default:
194+ return ""
188195 }
189- return ""
190196 },
191197 Engine: "cef",
192198 CWD: func() (string, error) { return cwd, nil },
@@ -195,6 +201,17 @@ func testCEFDevProfile(t *testing.T) runtimeprofile.Profile {
195201 return profile
196202 }
197203
204+func shortRuntimeDir(t *testing.T) string {
205+ t.Helper()
206+
207+ dir, err := os.MkdirTemp("", "dbr-")
208+ require.NoError(t, err)
209+ t.Cleanup(func() {
210+ _ = os.RemoveAll(dir)
211+ })
212+ return dir
213+}
214+
198215 func testCEFProdProfile(t *testing.T) runtimeprofile.Profile {
199216 t.Helper()
200217 root := t.TempDir()
@@ -181,12 +181,18 @@ func TestCreateAudioOutputFactory_ReturnsFactory(t *testing.T) {
181 func testCEFDevProfile(t *testing.T) runtimeprofile.Profile {181 func testCEFDevProfile(t *testing.T) runtimeprofile.Profile {
182 t.Helper()182 t.Helper()
183 cwd := t.TempDir()183 cwd := t.TempDir()
184+ runtimeDir := shortRuntimeDir(t)
185+ t.Setenv("XDG_RUNTIME_DIR", runtimeDir)
184 profile, err := runtimeprofile.Resolve(runtimeprofile.ResolveInput{186 profile, err := runtimeprofile.Resolve(runtimeprofile.ResolveInput{
185 Env: func(key string) string {187 Env: func(key string) string {
186- if key == "ENV" {188+ switch key {
189+ case "ENV":
187 return "dev"190 return "dev"
191+ case "XDG_RUNTIME_DIR":
192+ return runtimeDir
193+ default:
194+ return ""
188 }195 }
189- return ""
190 },196 },
191 Engine: "cef",197 Engine: "cef",
192 CWD: func() (string, error) { return cwd, nil },198 CWD: func() (string, error) { return cwd, nil },
@@ -195,6 +201,17 @@ func testCEFDevProfile(t *testing.T) runtimeprofile.Profile {
195 return profile201 return profile
196 }202 }
197 203
204+func shortRuntimeDir(t *testing.T) string {
205+ t.Helper()
206+
207+ dir, err := os.MkdirTemp("", "dbr-")
208+ require.NoError(t, err)
209+ t.Cleanup(func() {
210+ _ = os.RemoveAll(dir)
211+ })
212+ return dir
213+}
214+
198 func testCEFProdProfile(t *testing.T) runtimeprofile.Profile {215 func testCEFProdProfile(t *testing.T) runtimeprofile.Profile {
199 t.Helper()216 t.Helper()
200 root := t.TempDir()217 root := t.TempDir()
M internal/infrastructure/desktop/browser_launch_relay.go +32 -0
@@ -186,6 +186,31 @@ func setBrowserLaunchConnDeadline(ctx context.Context, conn net.Conn) error {
186186 return conn.SetDeadline(deadline)
187187 }
188188
189+func validateBrowserLaunchSocketDirOwned(socketPath string, expectedUID uint32) error {
190+ for dir := filepath.Dir(socketPath); ; dir = filepath.Dir(dir) {
191+ info, err := os.Stat(dir)
192+ if err != nil {
193+ return fmt.Errorf("stat browser launch dir: %w", err)
194+ }
195+ stat, ok := info.Sys().(*syscall.Stat_t)
196+ if !ok {
197+ return nil
198+ }
199+ if stat.Uid != expectedUID {
200+ if stat.Uid == 0 {
201+ return nil
202+ }
203+ return fmt.Errorf("browser launch dir owner mismatch: %s owned by uid %d, want uid %d", dir, stat.Uid, expectedUID)
204+ }
205+ if info.Mode().Perm()&0o022 != 0 {
206+ return fmt.Errorf("browser launch dir permissions too broad: %s has mode %04o", dir, info.Mode().Perm())
207+ }
208+ if parent := filepath.Dir(dir); parent == dir {
209+ return nil
210+ }
211+ }
212+}
213+
189214 func (r *browserLaunchRelay) Listen(ctx context.Context, opener port.BrowserWindowOpener) (io.Closer, error) {
190215 socketPath, err := r.socketPath()
191216 if err != nil {
@@ -195,6 +220,9 @@ func (r *browserLaunchRelay) Listen(ctx context.Context, opener port.BrowserWind
195220 if mkdirErr := os.MkdirAll(filepath.Dir(socketPath), browserLaunchDirPerm); mkdirErr != nil {
196221 return nil, fmt.Errorf("create browser launch dir: %w", mkdirErr)
197222 }
223+ if ownerErr := validateBrowserLaunchSocketDirOwned(socketPath, uint32(os.Geteuid())); ownerErr != nil {
224+ return nil, ownerErr
225+ }
198226 listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: socketPath, Net: "unix"})
199227 if err != nil {
200228 if !errors.Is(err, syscall.EADDRINUSE) {
@@ -209,6 +237,10 @@ func (r *browserLaunchRelay) Listen(ctx context.Context, opener port.BrowserWind
209237 return nil, errors.New("browser launch relay already running")
210238 }
211239
240+ if ownerErr := validateBrowserLaunchSocketDirOwned(socketPath, uint32(os.Geteuid())); ownerErr != nil {
241+ return nil, ownerErr
242+ }
243+
212244 if removeErr := os.Remove(socketPath); removeErr != nil && !os.IsNotExist(removeErr) {
213245 return nil, fmt.Errorf("remove stale browser launch socket: %w", removeErr)
214246 }
@@ -186,6 +186,31 @@ func setBrowserLaunchConnDeadline(ctx context.Context, conn net.Conn) error {
186 return conn.SetDeadline(deadline)186 return conn.SetDeadline(deadline)
187 }187 }
188 188
189+func validateBrowserLaunchSocketDirOwned(socketPath string, expectedUID uint32) error {
190+ for dir := filepath.Dir(socketPath); ; dir = filepath.Dir(dir) {
191+ info, err := os.Stat(dir)
192+ if err != nil {
193+ return fmt.Errorf("stat browser launch dir: %w", err)
194+ }
195+ stat, ok := info.Sys().(*syscall.Stat_t)
196+ if !ok {
197+ return nil
198+ }
199+ if stat.Uid != expectedUID {
200+ if stat.Uid == 0 {
201+ return nil
202+ }
203+ return fmt.Errorf("browser launch dir owner mismatch: %s owned by uid %d, want uid %d", dir, stat.Uid, expectedUID)
204+ }
205+ if info.Mode().Perm()&0o022 != 0 {
206+ return fmt.Errorf("browser launch dir permissions too broad: %s has mode %04o", dir, info.Mode().Perm())
207+ }
208+ if parent := filepath.Dir(dir); parent == dir {
209+ return nil
210+ }
211+ }
212+}
213+
189 func (r *browserLaunchRelay) Listen(ctx context.Context, opener port.BrowserWindowOpener) (io.Closer, error) {214 func (r *browserLaunchRelay) Listen(ctx context.Context, opener port.BrowserWindowOpener) (io.Closer, error) {
190 socketPath, err := r.socketPath()215 socketPath, err := r.socketPath()
191 if err != nil {216 if err != nil {
@@ -195,6 +220,9 @@ func (r *browserLaunchRelay) Listen(ctx context.Context, opener port.BrowserWind
195 if mkdirErr := os.MkdirAll(filepath.Dir(socketPath), browserLaunchDirPerm); mkdirErr != nil {220 if mkdirErr := os.MkdirAll(filepath.Dir(socketPath), browserLaunchDirPerm); mkdirErr != nil {
196 return nil, fmt.Errorf("create browser launch dir: %w", mkdirErr)221 return nil, fmt.Errorf("create browser launch dir: %w", mkdirErr)
197 }222 }
223+ if ownerErr := validateBrowserLaunchSocketDirOwned(socketPath, uint32(os.Geteuid())); ownerErr != nil {
224+ return nil, ownerErr
225+ }
198 listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: socketPath, Net: "unix"})226 listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: socketPath, Net: "unix"})
199 if err != nil {227 if err != nil {
200 if !errors.Is(err, syscall.EADDRINUSE) {228 if !errors.Is(err, syscall.EADDRINUSE) {
@@ -209,6 +237,10 @@ func (r *browserLaunchRelay) Listen(ctx context.Context, opener port.BrowserWind
209 return nil, errors.New("browser launch relay already running")237 return nil, errors.New("browser launch relay already running")
210 }238 }
211 239
240+ if ownerErr := validateBrowserLaunchSocketDirOwned(socketPath, uint32(os.Geteuid())); ownerErr != nil {
241+ return nil, ownerErr
242+ }
243+
212 if removeErr := os.Remove(socketPath); removeErr != nil && !os.IsNotExist(removeErr) {244 if removeErr := os.Remove(socketPath); removeErr != nil && !os.IsNotExist(removeErr) {
213 return nil, fmt.Errorf("remove stale browser launch socket: %w", removeErr)245 return nil, fmt.Errorf("remove stale browser launch socket: %w", removeErr)
214 }246 }
M internal/infrastructure/desktop/browser_launch_relay_test.go +38 -0
@@ -51,6 +51,44 @@ func shortTempDir(t *testing.T) string {
5151 return dir
5252 }
5353
54+func TestValidateBrowserLaunchSocketDirOwnedAcceptsCurrentUser(t *testing.T) {
55+ ipc := testIPC(shortTempDir(t))
56+ require.NoError(t, os.MkdirAll(filepath.Dir(ipc.BrowserLaunchSocket), 0o700))
57+
58+ require.NoError(t, validateBrowserLaunchSocketDirOwned(ipc.BrowserLaunchSocket, uint32(os.Geteuid())))
59+}
60+
61+func TestValidateBrowserLaunchSocketDirOwnedRejectsUnexpectedOwner(t *testing.T) {
62+ ipc := testIPC(shortTempDir(t))
63+ require.NoError(t, os.MkdirAll(filepath.Dir(ipc.BrowserLaunchSocket), 0o700))
64+
65+ unexpectedUID := uint32(os.Geteuid() + 1)
66+ if unexpectedUID == uint32(os.Geteuid()) {
67+ unexpectedUID++
68+ }
69+
70+ err := validateBrowserLaunchSocketDirOwned(ipc.BrowserLaunchSocket, unexpectedUID)
71+ require.Error(t, err)
72+ require.Contains(t, err.Error(), "browser launch dir owner")
73+}
74+
75+func TestValidateBrowserLaunchSocketDirOwnedRejectsWritableUserParent(t *testing.T) {
76+ root := shortTempDir(t)
77+ ipc := runtimeprofile.IPCPaths{
78+ RuntimeDir: filepath.Join(root, "dumber", "dev-abc123", "cef"),
79+ BrowserLaunchSocket: filepath.Join(root, "dumber", "dev-abc123", "cef", browserLaunchSocketName),
80+ }
81+ require.NoError(t, os.MkdirAll(filepath.Dir(ipc.BrowserLaunchSocket), 0o700))
82+ require.NoError(t, os.Chmod(filepath.Join(root, "dumber"), 0o777))
83+ t.Cleanup(func() {
84+ _ = os.Chmod(filepath.Join(root, "dumber"), 0o700)
85+ })
86+
87+ err := validateBrowserLaunchSocketDirOwned(ipc.BrowserLaunchSocket, uint32(os.Geteuid()))
88+ require.Error(t, err)
89+ require.Contains(t, err.Error(), "browser launch dir permissions")
90+}
91+
5492 func testIPC(root string) runtimeprofile.IPCPaths {
5593 runtimeDir := filepath.Join(root, "runtime")
5694 return runtimeprofile.IPCPaths{
@@ -51,6 +51,44 @@ func shortTempDir(t *testing.T) string {
51 return dir51 return dir
52 }52 }
53 53
54+func TestValidateBrowserLaunchSocketDirOwnedAcceptsCurrentUser(t *testing.T) {
55+ ipc := testIPC(shortTempDir(t))
56+ require.NoError(t, os.MkdirAll(filepath.Dir(ipc.BrowserLaunchSocket), 0o700))
57+
58+ require.NoError(t, validateBrowserLaunchSocketDirOwned(ipc.BrowserLaunchSocket, uint32(os.Geteuid())))
59+}
60+
61+func TestValidateBrowserLaunchSocketDirOwnedRejectsUnexpectedOwner(t *testing.T) {
62+ ipc := testIPC(shortTempDir(t))
63+ require.NoError(t, os.MkdirAll(filepath.Dir(ipc.BrowserLaunchSocket), 0o700))
64+
65+ unexpectedUID := uint32(os.Geteuid() + 1)
66+ if unexpectedUID == uint32(os.Geteuid()) {
67+ unexpectedUID++
68+ }
69+
70+ err := validateBrowserLaunchSocketDirOwned(ipc.BrowserLaunchSocket, unexpectedUID)
71+ require.Error(t, err)
72+ require.Contains(t, err.Error(), "browser launch dir owner")
73+}
74+
75+func TestValidateBrowserLaunchSocketDirOwnedRejectsWritableUserParent(t *testing.T) {
76+ root := shortTempDir(t)
77+ ipc := runtimeprofile.IPCPaths{
78+ RuntimeDir: filepath.Join(root, "dumber", "dev-abc123", "cef"),
79+ BrowserLaunchSocket: filepath.Join(root, "dumber", "dev-abc123", "cef", browserLaunchSocketName),
80+ }
81+ require.NoError(t, os.MkdirAll(filepath.Dir(ipc.BrowserLaunchSocket), 0o700))
82+ require.NoError(t, os.Chmod(filepath.Join(root, "dumber"), 0o777))
83+ t.Cleanup(func() {
84+ _ = os.Chmod(filepath.Join(root, "dumber"), 0o700)
85+ })
86+
87+ err := validateBrowserLaunchSocketDirOwned(ipc.BrowserLaunchSocket, uint32(os.Geteuid()))
88+ require.Error(t, err)
89+ require.Contains(t, err.Error(), "browser launch dir permissions")
90+}
91+
54 func testIPC(root string) runtimeprofile.IPCPaths {92 func testIPC(root string) runtimeprofile.IPCPaths {
55 runtimeDir := filepath.Join(root, "runtime")93 runtimeDir := filepath.Join(root, "runtime")
56 return runtimeprofile.IPCPaths{94 return runtimeprofile.IPCPaths{
M internal/infrastructure/runtimeprofile/profile.go +32 -8
@@ -10,6 +10,7 @@ import (
1010
1111 const (
1212 browserLaunchSocketName = "browser-launch.sock"
13+ devIPCSocketPathLimit = 104
1314 engineWebKit = "webkit"
1415 engineCEF = "cef"
1516 )
@@ -84,7 +85,10 @@ func Resolve(input ResolveInput) (Profile, error) {
8485 }
8586 root := filepath.Join(cwd, ".dev", "dumber")
8687 engineRoot := filepath.Join(root, "engines", engine)
87- ipcRoot := devIPCRoot(input.Env, root, engine)
88+ ipcRoot, err := devIPCRoot(input.Env, root, engine)
89+ if err != nil {
90+ return Profile{}, err
91+ }
8892 return Profile{
8993 Mode: ModeDev,
9094 Engine: engine,
@@ -135,16 +139,36 @@ func Resolve(input ResolveInput) (Profile, error) {
135139 }, nil
136140 }
137141
138-func devIPCRoot(env func(string) string, root, engine string) string {
139- base := envValue(env, "XDG_RUNTIME_DIR")
140- if base == "" {
141- base = envValue(env, "TMPDIR")
142+func devIPCRoot(env func(string) string, root, engine string) (string, error) {
143+ rootHash := devIPCRootHash(root)
144+ for _, base := range []string{envValue(env, "XDG_RUNTIME_DIR"), envValue(env, "TMPDIR")} {
145+ if base == "" {
146+ continue
147+ }
148+ ipcRoot := devIPCRootFromBase(base, rootHash, engine)
149+ if len(filepath.Join(ipcRoot, browserLaunchSocketName)) < devIPCSocketPathLimit {
150+ return ipcRoot, nil
151+ }
142152 }
143- if base == "" {
144- base = "/tmp"
153+
154+ ipcRoot := filepath.Join(root, "runtime", engine)
155+ socketPath := filepath.Join(ipcRoot, browserLaunchSocketName)
156+ if len(socketPath) < devIPCSocketPathLimit {
157+ return ipcRoot, nil
145158 }
159+ return "", fmt.Errorf(
160+ "browser launch socket path too long: %d bytes; set XDG_RUNTIME_DIR or TMPDIR to a shorter runtime directory",
161+ len(socketPath),
162+ )
163+}
164+
165+func devIPCRootFromBase(base, rootHash, engine string) string {
166+ return filepath.Join(base, "dumber", "dev-"+rootHash, engine)
167+}
168+
169+func devIPCRootHash(root string) string {
146170 sum := sha256.Sum256([]byte(root))
147- return filepath.Join(base, "dumber", "dev-"+hex.EncodeToString(sum[:])[:12], engine)
171+ return hex.EncodeToString(sum[:])[:12]
148172 }
149173
150174 // normalizeEngine trims and lowercases the runtime engine name.
@@ -10,6 +10,7 @@ import (
10 10
11 const (11 const (
12 browserLaunchSocketName = "browser-launch.sock"12 browserLaunchSocketName = "browser-launch.sock"
13+ devIPCSocketPathLimit = 104
13 engineWebKit = "webkit"14 engineWebKit = "webkit"
14 engineCEF = "cef"15 engineCEF = "cef"
15 )16 )
@@ -84,7 +85,10 @@ func Resolve(input ResolveInput) (Profile, error) {
84 }85 }
85 root := filepath.Join(cwd, ".dev", "dumber")86 root := filepath.Join(cwd, ".dev", "dumber")
86 engineRoot := filepath.Join(root, "engines", engine)87 engineRoot := filepath.Join(root, "engines", engine)
87- ipcRoot := devIPCRoot(input.Env, root, engine)88+ ipcRoot, err := devIPCRoot(input.Env, root, engine)
89+ if err != nil {
90+ return Profile{}, err
91+ }
88 return Profile{92 return Profile{
89 Mode: ModeDev,93 Mode: ModeDev,
90 Engine: engine,94 Engine: engine,
@@ -135,16 +139,36 @@ func Resolve(input ResolveInput) (Profile, error) {
135 }, nil139 }, nil
136 }140 }
137 141
138-func devIPCRoot(env func(string) string, root, engine string) string {142+func devIPCRoot(env func(string) string, root, engine string) (string, error) {
139- base := envValue(env, "XDG_RUNTIME_DIR")143+ rootHash := devIPCRootHash(root)
140- if base == "" {144+ for _, base := range []string{envValue(env, "XDG_RUNTIME_DIR"), envValue(env, "TMPDIR")} {
141- base = envValue(env, "TMPDIR")145+ if base == "" {
146+ continue
147+ }
148+ ipcRoot := devIPCRootFromBase(base, rootHash, engine)
149+ if len(filepath.Join(ipcRoot, browserLaunchSocketName)) < devIPCSocketPathLimit {
150+ return ipcRoot, nil
151+ }
142 }152 }
143- if base == "" {153+
144- base = "/tmp"154+ ipcRoot := filepath.Join(root, "runtime", engine)
155+ socketPath := filepath.Join(ipcRoot, browserLaunchSocketName)
156+ if len(socketPath) < devIPCSocketPathLimit {
157+ return ipcRoot, nil
145 }158 }
159+ return "", fmt.Errorf(
160+ "browser launch socket path too long: %d bytes; set XDG_RUNTIME_DIR or TMPDIR to a shorter runtime directory",
161+ len(socketPath),
162+ )
163+}
164+
165+func devIPCRootFromBase(base, rootHash, engine string) string {
166+ return filepath.Join(base, "dumber", "dev-"+rootHash, engine)
167+}
168+
169+func devIPCRootHash(root string) string {
146 sum := sha256.Sum256([]byte(root))170 sum := sha256.Sum256([]byte(root))
147- return filepath.Join(base, "dumber", "dev-"+hex.EncodeToString(sum[:])[:12], engine)171+ return hex.EncodeToString(sum[:])[:12]
148 }172 }
149 173
150 // normalizeEngine trims and lowercases the runtime engine name.174 // normalizeEngine trims and lowercases the runtime engine name.
M internal/infrastructure/runtimeprofile/profile_test.go +46 -2
@@ -48,8 +48,7 @@ func TestResolve_DevCEF_SeparatesSharedEngineAndIPC(t *testing.T) {
4848 require.Equal(t, "/repo/.dev/dumber/config", profile.Shared.ConfigDir)
4949 require.Equal(t, "/repo/.dev/dumber/data", profile.Shared.DataDir)
5050 require.Equal(t, "/repo/.dev/dumber/engines/cef/data", profile.CEFUserDataDir())
51- require.True(t, strings.HasPrefix(profile.IPC.BrowserLaunchSocket, "/tmp/dumber/dev-"))
52- require.Contains(t, profile.IPC.BrowserLaunchSocket, "/cef/browser-launch.sock")
51+ require.Equal(t, "/repo/.dev/dumber/runtime/cef/browser-launch.sock", profile.IPC.BrowserLaunchSocket)
5352 }
5453
5554 func TestResolve_DevIPCPathStaysShortForLongWorktrees(t *testing.T) {
@@ -76,6 +75,51 @@ func TestResolve_DevIPCPathStaysShortForLongWorktrees(t *testing.T) {
7675 require.Contains(t, profile.IPC.BrowserLaunchSocket, "/cef/browser-launch.sock")
7776 }
7877
78+func TestResolve_DevIPCPathFallsBackToTMPDIRWhenRuntimeDirWouldExceedSocketLimit(t *testing.T) {
79+ longRuntimeDir := "/tmp/" + strings.Repeat("long-runtime-dir-name-", 5) + "run"
80+
81+ profile, err := Resolve(ResolveInput{
82+ Env: func(key string) string {
83+ switch key {
84+ case "ENV":
85+ return "dev"
86+ case "XDG_RUNTIME_DIR":
87+ return longRuntimeDir
88+ case "TMPDIR":
89+ return "/run/user/1000"
90+ default:
91+ return ""
92+ }
93+ },
94+ Engine: "cef",
95+ CWD: func() (string, error) { return "/repo", nil },
96+ })
97+
98+ require.NoError(t, err)
99+ require.Less(t, len(profile.IPC.BrowserLaunchSocket), 104)
100+ require.NotContains(t, profile.IPC.BrowserLaunchSocket, longRuntimeDir)
101+ require.True(t, strings.HasPrefix(profile.IPC.BrowserLaunchSocket, "/run/user/1000/dumber/dev-"))
102+ require.Contains(t, profile.IPC.BrowserLaunchSocket, "/cef/browser-launch.sock")
103+}
104+
105+func TestResolve_DevIPCPathErrorsWhenNoShortBaseExists(t *testing.T) {
106+ longWorktree := "/tmp/" + strings.Repeat("long-worktree-name-", 8) + "project"
107+
108+ _, err := Resolve(ResolveInput{
109+ Env: func(key string) string {
110+ if key == "ENV" {
111+ return "dev"
112+ }
113+ return ""
114+ },
115+ Engine: "cef",
116+ CWD: func() (string, error) { return longWorktree, nil },
117+ })
118+
119+ require.Error(t, err)
120+ require.Contains(t, err.Error(), "browser launch socket path too long")
121+}
122+
79123 func TestResolve_DevWebKitAndCEF_HaveDifferentTechnicalNamespaces(t *testing.T) {
80124 devEnv := func(key string) string {
81125 if key == "ENV" {
@@ -48,8 +48,7 @@ func TestResolve_DevCEF_SeparatesSharedEngineAndIPC(t *testing.T) {
48 require.Equal(t, "/repo/.dev/dumber/config", profile.Shared.ConfigDir)48 require.Equal(t, "/repo/.dev/dumber/config", profile.Shared.ConfigDir)
49 require.Equal(t, "/repo/.dev/dumber/data", profile.Shared.DataDir)49 require.Equal(t, "/repo/.dev/dumber/data", profile.Shared.DataDir)
50 require.Equal(t, "/repo/.dev/dumber/engines/cef/data", profile.CEFUserDataDir())50 require.Equal(t, "/repo/.dev/dumber/engines/cef/data", profile.CEFUserDataDir())
51- require.True(t, strings.HasPrefix(profile.IPC.BrowserLaunchSocket, "/tmp/dumber/dev-"))51+ require.Equal(t, "/repo/.dev/dumber/runtime/cef/browser-launch.sock", profile.IPC.BrowserLaunchSocket)
52- require.Contains(t, profile.IPC.BrowserLaunchSocket, "/cef/browser-launch.sock")
53 }52 }
54 53
55 func TestResolve_DevIPCPathStaysShortForLongWorktrees(t *testing.T) {54 func TestResolve_DevIPCPathStaysShortForLongWorktrees(t *testing.T) {
@@ -76,6 +75,51 @@ func TestResolve_DevIPCPathStaysShortForLongWorktrees(t *testing.T) {
76 require.Contains(t, profile.IPC.BrowserLaunchSocket, "/cef/browser-launch.sock")75 require.Contains(t, profile.IPC.BrowserLaunchSocket, "/cef/browser-launch.sock")
77 }76 }
78 77
78+func TestResolve_DevIPCPathFallsBackToTMPDIRWhenRuntimeDirWouldExceedSocketLimit(t *testing.T) {
79+ longRuntimeDir := "/tmp/" + strings.Repeat("long-runtime-dir-name-", 5) + "run"
80+
81+ profile, err := Resolve(ResolveInput{
82+ Env: func(key string) string {
83+ switch key {
84+ case "ENV":
85+ return "dev"
86+ case "XDG_RUNTIME_DIR":
87+ return longRuntimeDir
88+ case "TMPDIR":
89+ return "/run/user/1000"
90+ default:
91+ return ""
92+ }
93+ },
94+ Engine: "cef",
95+ CWD: func() (string, error) { return "/repo", nil },
96+ })
97+
98+ require.NoError(t, err)
99+ require.Less(t, len(profile.IPC.BrowserLaunchSocket), 104)
100+ require.NotContains(t, profile.IPC.BrowserLaunchSocket, longRuntimeDir)
101+ require.True(t, strings.HasPrefix(profile.IPC.BrowserLaunchSocket, "/run/user/1000/dumber/dev-"))
102+ require.Contains(t, profile.IPC.BrowserLaunchSocket, "/cef/browser-launch.sock")
103+}
104+
105+func TestResolve_DevIPCPathErrorsWhenNoShortBaseExists(t *testing.T) {
106+ longWorktree := "/tmp/" + strings.Repeat("long-worktree-name-", 8) + "project"
107+
108+ _, err := Resolve(ResolveInput{
109+ Env: func(key string) string {
110+ if key == "ENV" {
111+ return "dev"
112+ }
113+ return ""
114+ },
115+ Engine: "cef",
116+ CWD: func() (string, error) { return longWorktree, nil },
117+ })
118+
119+ require.Error(t, err)
120+ require.Contains(t, err.Error(), "browser launch socket path too long")
121+}
122+
79 func TestResolve_DevWebKitAndCEF_HaveDifferentTechnicalNamespaces(t *testing.T) {123 func TestResolve_DevWebKitAndCEF_HaveDifferentTechnicalNamespaces(t *testing.T) {
80 devEnv := func(key string) string {124 devEnv := func(key string) string {
81 if key == "ENV" {125 if key == "ENV" {

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