detached eb16ef1

refactor: inject engine-specific session spawn env

commit
eb16ef1668dda3c2fcc6b71edc50c19539bf0be9
author
brice · · 2026-04-16 04:13 UTC
parent
d1704de
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
9 files · +124 -30
M cmd/dumber/main.go +1 -1
@@ -782,7 +782,7 @@ func buildUIDependencies(
782782 },
783783 CheckUpdateUC: uc.checkUpdate,
784784 ApplyUpdateUC: uc.applyUpdate,
785- SessionSpawner: desktop.NewSessionSpawner(ctx),
785+ SessionSpawner: bootstrap.NewSessionSpawner(ctx, cfg),
786786 FileSystem: filesystem.New(),
787787 AccentFocusProvider: focusProvider,
788788 NewGTKEntryTarget: func(entry *gtk.SearchEntry) port.TextInputTarget {
@@ -782,7 +782,7 @@ func buildUIDependencies(
782 },782 },
783 CheckUpdateUC: uc.checkUpdate,783 CheckUpdateUC: uc.checkUpdate,
784 ApplyUpdateUC: uc.applyUpdate,784 ApplyUpdateUC: uc.applyUpdate,
785- SessionSpawner: desktop.NewSessionSpawner(ctx),785+ SessionSpawner: bootstrap.NewSessionSpawner(ctx, cfg),
786 FileSystem: filesystem.New(),786 FileSystem: filesystem.New(),
787 AccentFocusProvider: focusProvider,787 AccentFocusProvider: focusProvider,
788 NewGTKEntryTarget: func(entry *gtk.SearchEntry) port.TextInputTarget {788 NewGTKEntryTarget: func(entry *gtk.SearchEntry) port.TextInputTarget {
M internal/application/port/session_state.go +12 -0
@@ -21,6 +21,18 @@ type SessionSpawner interface {
2121 SpawnWithSession(sessionID entity.SessionID) error
2222 }
2323
24+// SessionSpawnEnvironment provides engine-specific environment overrides for
25+// spawned restore-session processes. Implementations are optional: engines that
26+// do not need extra launch environment can leave this nil.
27+type SessionSpawnEnvironment interface {
28+ // RootCacheEnvVar returns the environment variable used to override the
29+ // engine's root cache/data directory.
30+ RootCacheEnvVar() string
31+ // SessionRootCachePath returns the engine-specific root cache/data path for
32+ // the provided restored session.
33+ SessionRootCachePath(sessionID entity.SessionID) string
34+}
35+
2436 // SnapshotService manages debounced session-state snapshots.
2537 type SnapshotService interface {
2638 // Start begins the snapshot service with the given context.
@@ -21,6 +21,18 @@ type SessionSpawner interface {
21 SpawnWithSession(sessionID entity.SessionID) error21 SpawnWithSession(sessionID entity.SessionID) error
22 }22 }
23 23
24+// SessionSpawnEnvironment provides engine-specific environment overrides for
25+// spawned restore-session processes. Implementations are optional: engines that
26+// do not need extra launch environment can leave this nil.
27+type SessionSpawnEnvironment interface {
28+ // RootCacheEnvVar returns the environment variable used to override the
29+ // engine's root cache/data directory.
30+ RootCacheEnvVar() string
31+ // SessionRootCachePath returns the engine-specific root cache/data path for
32+ // the provided restored session.
33+ SessionRootCachePath(sessionID entity.SessionID) string
34+}
35+
24 // SnapshotService manages debounced session-state snapshots.36 // SnapshotService manages debounced session-state snapshots.
25 type SnapshotService interface {37 type SnapshotService interface {
26 // Start begins the snapshot service with the given context.38 // Start begins the snapshot service with the given context.
A internal/bootstrap/session_spawner.go +20 -0
@@ -0,0 +1,20 @@
1+package bootstrap
2+
3+import (
4+ "context"
5+
6+ "github.com/bnema/dumber/internal/application/port"
7+ "github.com/bnema/dumber/internal/infrastructure/cef"
8+ "github.com/bnema/dumber/internal/infrastructure/config"
9+ "github.com/bnema/dumber/internal/infrastructure/desktop"
10+)
11+
12+// NewSessionSpawner wires the session spawner with any engine-specific launch
13+// environment required by the current browser backend.
14+func NewSessionSpawner(ctx context.Context, cfg *config.Config) port.SessionSpawner {
15+ var spawnEnv port.SessionSpawnEnvironment
16+ if cfg != nil && cfg.Engine.ResolveEngineType() == config.EngineTypeCEF {
17+ spawnEnv = cef.SessionSpawnEnvironment{}
18+ }
19+ return desktop.NewSessionSpawner(ctx, spawnEnv)
20+}
@@ -0,0 +1,20 @@
1+package bootstrap
2+
3+import (
4+ "context"
5+
6+ "github.com/bnema/dumber/internal/application/port"
7+ "github.com/bnema/dumber/internal/infrastructure/cef"
8+ "github.com/bnema/dumber/internal/infrastructure/config"
9+ "github.com/bnema/dumber/internal/infrastructure/desktop"
10+)
11+
12+// NewSessionSpawner wires the session spawner with any engine-specific launch
13+// environment required by the current browser backend.
14+func NewSessionSpawner(ctx context.Context, cfg *config.Config) port.SessionSpawner {
15+ var spawnEnv port.SessionSpawnEnvironment
16+ if cfg != nil && cfg.Engine.ResolveEngineType() == config.EngineTypeCEF {
17+ spawnEnv = cef.SessionSpawnEnvironment{}
18+ }
19+ return desktop.NewSessionSpawner(ctx, spawnEnv)
20+}
M internal/cli/app.go +3 -0
@@ -10,6 +10,7 @@ import (
1010
1111 "github.com/bnema/dumber/internal/application/port"
1212 "github.com/bnema/dumber/internal/application/usecase"
13+ "github.com/bnema/dumber/internal/bootstrap"
1314 "github.com/bnema/dumber/internal/cli/styles"
1415 "github.com/bnema/dumber/internal/domain/build"
1516 "github.com/bnema/dumber/internal/domain/repository"
@@ -37,6 +38,7 @@ type App struct {
3738
3839 // Services
3940 FaviconService *favicon.Service
41+ SessionSpawner port.SessionSpawner
4042
4143 // Context with logger
4244 ctx context.Context
@@ -131,6 +133,7 @@ func NewApp() (*App, error) {
131133 RestoreUC: restoreUC,
132134 DeleteSessionUC: deleteSessionUC,
133135 FaviconService: faviconService,
136+ SessionSpawner: bootstrap.NewSessionSpawner(ctx, cfg),
134137 ctx: ctx,
135138 logCleanup: logCleanup,
136139 }, nil
@@ -10,6 +10,7 @@ import (
10 10
11 "github.com/bnema/dumber/internal/application/port"11 "github.com/bnema/dumber/internal/application/port"
12 "github.com/bnema/dumber/internal/application/usecase"12 "github.com/bnema/dumber/internal/application/usecase"
13+ "github.com/bnema/dumber/internal/bootstrap"
13 "github.com/bnema/dumber/internal/cli/styles"14 "github.com/bnema/dumber/internal/cli/styles"
14 "github.com/bnema/dumber/internal/domain/build"15 "github.com/bnema/dumber/internal/domain/build"
15 "github.com/bnema/dumber/internal/domain/repository"16 "github.com/bnema/dumber/internal/domain/repository"
@@ -37,6 +38,7 @@ type App struct {
37 38
38 // Services39 // Services
39 FaviconService *favicon.Service40 FaviconService *favicon.Service
41+ SessionSpawner port.SessionSpawner
40 42
41 // Context with logger43 // Context with logger
42 ctx context.Context44 ctx context.Context
@@ -131,6 +133,7 @@ func NewApp() (*App, error) {
131 RestoreUC: restoreUC,133 RestoreUC: restoreUC,
132 DeleteSessionUC: deleteSessionUC,134 DeleteSessionUC: deleteSessionUC,
133 FaviconService: faviconService,135 FaviconService: faviconService,
136+ SessionSpawner: bootstrap.NewSessionSpawner(ctx, cfg),
134 ctx: ctx,137 ctx: ctx,
135 logCleanup: logCleanup,138 logCleanup: logCleanup,
136 }, nil139 }, nil
M internal/cli/cmd/sessions.go +7 -3
@@ -13,7 +13,6 @@ import (
1313 "github.com/bnema/dumber/internal/cli/model"
1414 "github.com/bnema/dumber/internal/cli/styles"
1515 "github.com/bnema/dumber/internal/domain/entity"
16- "github.com/bnema/dumber/internal/infrastructure/desktop"
1716 )
1817
1918 const defaultSessionsLimit = 20
@@ -60,6 +59,7 @@ func runSessions(_ *cobra.Command, _ []string) error {
6059 ListSessionsUC: app.ListSessionsUC,
6160 RestoreUC: app.RestoreUC,
6261 DeleteSessionUC: app.DeleteSessionUC,
62+ SessionSpawner: app.SessionSpawner,
6363 CurrentSession: currentSessionID,
6464 MaxExitedSessions: app.Config.Session.MaxExitedSessions,
6565 })
@@ -181,8 +181,12 @@ func runSessionsRestore(_ *cobra.Command, args []string) error {
181181 }
182182
183183 // Spawn a new dumber instance with the session
184- spawner := desktop.NewSessionSpawner(app.Ctx())
185- if err := spawner.SpawnWithSession(sessionID); err != nil {
184+ if app.SessionSpawner == nil {
185+ err := fmt.Errorf("session spawner not available")
186+ fmt.Fprintln(os.Stderr, renderer.RenderError(err))
187+ return wrapPrintedError(err)
188+ }
189+ if err := app.SessionSpawner.SpawnWithSession(sessionID); err != nil {
186190 wrappedErr := fmt.Errorf("spawn browser: %w", err)
187191 fmt.Fprintln(os.Stderr, renderer.RenderError(wrappedErr))
188192 return wrapPrintedError(wrappedErr)
@@ -13,7 +13,6 @@ import (
13 "github.com/bnema/dumber/internal/cli/model"13 "github.com/bnema/dumber/internal/cli/model"
14 "github.com/bnema/dumber/internal/cli/styles"14 "github.com/bnema/dumber/internal/cli/styles"
15 "github.com/bnema/dumber/internal/domain/entity"15 "github.com/bnema/dumber/internal/domain/entity"
16- "github.com/bnema/dumber/internal/infrastructure/desktop"
17 )16 )
18 17
19 const defaultSessionsLimit = 2018 const defaultSessionsLimit = 20
@@ -60,6 +59,7 @@ func runSessions(_ *cobra.Command, _ []string) error {
60 ListSessionsUC: app.ListSessionsUC,59 ListSessionsUC: app.ListSessionsUC,
61 RestoreUC: app.RestoreUC,60 RestoreUC: app.RestoreUC,
62 DeleteSessionUC: app.DeleteSessionUC,61 DeleteSessionUC: app.DeleteSessionUC,
62+ SessionSpawner: app.SessionSpawner,
63 CurrentSession: currentSessionID,63 CurrentSession: currentSessionID,
64 MaxExitedSessions: app.Config.Session.MaxExitedSessions,64 MaxExitedSessions: app.Config.Session.MaxExitedSessions,
65 })65 })
@@ -181,8 +181,12 @@ func runSessionsRestore(_ *cobra.Command, args []string) error {
181 }181 }
182 182
183 // Spawn a new dumber instance with the session183 // Spawn a new dumber instance with the session
184- spawner := desktop.NewSessionSpawner(app.Ctx())184+ if app.SessionSpawner == nil {
185- if err := spawner.SpawnWithSession(sessionID); err != nil {185+ err := fmt.Errorf("session spawner not available")
186+ fmt.Fprintln(os.Stderr, renderer.RenderError(err))
187+ return wrapPrintedError(err)
188+ }
189+ if err := app.SessionSpawner.SpawnWithSession(sessionID); err != nil {
186 wrappedErr := fmt.Errorf("spawn browser: %w", err)190 wrappedErr := fmt.Errorf("spawn browser: %w", err)
187 fmt.Fprintln(os.Stderr, renderer.RenderError(wrappedErr))191 fmt.Fprintln(os.Stderr, renderer.RenderError(wrappedErr))
188 return wrapPrintedError(wrappedErr)192 return wrapPrintedError(wrappedErr)
M internal/cli/model/sessions.go +8 -3
@@ -11,10 +11,10 @@ import (
1111 tea "github.com/charmbracelet/bubbletea"
1212 "github.com/charmbracelet/lipgloss"
1313
14+ "github.com/bnema/dumber/internal/application/port"
1415 "github.com/bnema/dumber/internal/application/usecase"
1516 "github.com/bnema/dumber/internal/cli/styles"
1617 "github.com/bnema/dumber/internal/domain/entity"
17- "github.com/bnema/dumber/internal/infrastructure/desktop"
1818 "github.com/bnema/dumber/internal/logging"
1919 )
2020
@@ -43,6 +43,7 @@ type SessionsModel struct {
4343 listSessionsUC *usecase.ListSessionsUseCase
4444 restoreUC *usecase.RestoreSessionUseCase
4545 deleteSessionUC *usecase.DeleteSessionUseCase
46+ sessionSpawner port.SessionSpawner
4647 theme *styles.Theme
4748 }
4849
@@ -114,6 +115,7 @@ type SessionsModelConfig struct {
114115 ListSessionsUC *usecase.ListSessionsUseCase
115116 RestoreUC *usecase.RestoreSessionUseCase
116117 DeleteSessionUC *usecase.DeleteSessionUseCase
118+ SessionSpawner port.SessionSpawner
117119 CurrentSession entity.SessionID
118120 MaxExitedSessions int
119121 }
@@ -137,6 +139,7 @@ func NewSessionsModel(ctx context.Context, theme *styles.Theme, cfg SessionsMode
137139 listSessionsUC: cfg.ListSessionsUC,
138140 restoreUC: cfg.RestoreUC,
139141 deleteSessionUC: cfg.DeleteSessionUC,
142+ sessionSpawner: cfg.SessionSpawner,
140143 currentSession: cfg.CurrentSession,
141144 theme: theme,
142145 }
@@ -336,8 +339,10 @@ func (m SessionsModel) restoreSession(sessionID entity.SessionID) tea.Cmd {
336339 }
337340
338341 // Spawn a new dumber instance
339- spawner := desktop.NewSessionSpawner(m.ctx)
340- if err := spawner.SpawnWithSession(sessionID); err != nil {
342+ if m.sessionSpawner == nil {
343+ return sessionRestoredMsg{sessionID: sessionID, err: fmt.Errorf("session spawner not available")}
344+ }
345+ if err := m.sessionSpawner.SpawnWithSession(sessionID); err != nil {
341346 return sessionRestoredMsg{sessionID: sessionID, err: err}
342347 }
343348
@@ -11,10 +11,10 @@ import (
11 tea "github.com/charmbracelet/bubbletea"11 tea "github.com/charmbracelet/bubbletea"
12 "github.com/charmbracelet/lipgloss"12 "github.com/charmbracelet/lipgloss"
13 13
14+ "github.com/bnema/dumber/internal/application/port"
14 "github.com/bnema/dumber/internal/application/usecase"15 "github.com/bnema/dumber/internal/application/usecase"
15 "github.com/bnema/dumber/internal/cli/styles"16 "github.com/bnema/dumber/internal/cli/styles"
16 "github.com/bnema/dumber/internal/domain/entity"17 "github.com/bnema/dumber/internal/domain/entity"
17- "github.com/bnema/dumber/internal/infrastructure/desktop"
18 "github.com/bnema/dumber/internal/logging"18 "github.com/bnema/dumber/internal/logging"
19 )19 )
20 20
@@ -43,6 +43,7 @@ type SessionsModel struct {
43 listSessionsUC *usecase.ListSessionsUseCase43 listSessionsUC *usecase.ListSessionsUseCase
44 restoreUC *usecase.RestoreSessionUseCase44 restoreUC *usecase.RestoreSessionUseCase
45 deleteSessionUC *usecase.DeleteSessionUseCase45 deleteSessionUC *usecase.DeleteSessionUseCase
46+ sessionSpawner port.SessionSpawner
46 theme *styles.Theme47 theme *styles.Theme
47 }48 }
48 49
@@ -114,6 +115,7 @@ type SessionsModelConfig struct {
114 ListSessionsUC *usecase.ListSessionsUseCase115 ListSessionsUC *usecase.ListSessionsUseCase
115 RestoreUC *usecase.RestoreSessionUseCase116 RestoreUC *usecase.RestoreSessionUseCase
116 DeleteSessionUC *usecase.DeleteSessionUseCase117 DeleteSessionUC *usecase.DeleteSessionUseCase
118+ SessionSpawner port.SessionSpawner
117 CurrentSession entity.SessionID119 CurrentSession entity.SessionID
118 MaxExitedSessions int120 MaxExitedSessions int
119 }121 }
@@ -137,6 +139,7 @@ func NewSessionsModel(ctx context.Context, theme *styles.Theme, cfg SessionsMode
137 listSessionsUC: cfg.ListSessionsUC,139 listSessionsUC: cfg.ListSessionsUC,
138 restoreUC: cfg.RestoreUC,140 restoreUC: cfg.RestoreUC,
139 deleteSessionUC: cfg.DeleteSessionUC,141 deleteSessionUC: cfg.DeleteSessionUC,
142+ sessionSpawner: cfg.SessionSpawner,
140 currentSession: cfg.CurrentSession,143 currentSession: cfg.CurrentSession,
141 theme: theme,144 theme: theme,
142 }145 }
@@ -336,8 +339,10 @@ func (m SessionsModel) restoreSession(sessionID entity.SessionID) tea.Cmd {
336 }339 }
337 340
338 // Spawn a new dumber instance341 // Spawn a new dumber instance
339- spawner := desktop.NewSessionSpawner(m.ctx)342+ if m.sessionSpawner == nil {
340- if err := spawner.SpawnWithSession(sessionID); err != nil {343+ return sessionRestoredMsg{sessionID: sessionID, err: fmt.Errorf("session spawner not available")}
344+ }
345+ if err := m.sessionSpawner.SpawnWithSession(sessionID); err != nil {
341 return sessionRestoredMsg{sessionID: sessionID, err: err}346 return sessionRestoredMsg{sessionID: sessionID, err: err}
342 }347 }
343 348
A internal/infrastructure/cef/session_spawn_environment.go +22 -0
@@ -0,0 +1,22 @@
1+package cef
2+
3+import (
4+ "path/filepath"
5+
6+ "github.com/bnema/dumber/internal/application/port"
7+ "github.com/bnema/dumber/internal/domain/entity"
8+)
9+
10+// SessionSpawnEnvironment exposes the engine-specific launch environment needed
11+// when restoring a session into a freshly spawned browser process.
12+type SessionSpawnEnvironment struct{}
13+
14+func (SessionSpawnEnvironment) RootCacheEnvVar() string {
15+ return CEFRootCachePathEnvVar
16+}
17+
18+func (SessionSpawnEnvironment) SessionRootCachePath(sessionID entity.SessionID) string {
19+ return filepath.Join(DefaultCEFUserDataDir(), "sessions", string(sessionID))
20+}
21+
22+var _ port.SessionSpawnEnvironment = SessionSpawnEnvironment{}
@@ -0,0 +1,22 @@
1+package cef
2+
3+import (
4+ "path/filepath"
5+
6+ "github.com/bnema/dumber/internal/application/port"
7+ "github.com/bnema/dumber/internal/domain/entity"
8+)
9+
10+// SessionSpawnEnvironment exposes the engine-specific launch environment needed
11+// when restoring a session into a freshly spawned browser process.
12+type SessionSpawnEnvironment struct{}
13+
14+func (SessionSpawnEnvironment) RootCacheEnvVar() string {
15+ return CEFRootCachePathEnvVar
16+}
17+
18+func (SessionSpawnEnvironment) SessionRootCachePath(sessionID entity.SessionID) string {
19+ return filepath.Join(DefaultCEFUserDataDir(), "sessions", string(sessionID))
20+}
21+
22+var _ port.SessionSpawnEnvironment = SessionSpawnEnvironment{}
M internal/infrastructure/desktop/adapter.go +11 -15
@@ -11,7 +11,6 @@ import (
1111
1212 "github.com/bnema/dumber/internal/application/port"
1313 "github.com/bnema/dumber/internal/domain/entity"
14- "github.com/bnema/dumber/internal/infrastructure/cef"
1514 "github.com/bnema/dumber/internal/logging"
1615 )
1716
@@ -382,12 +381,13 @@ func (a *Adapter) UnsetAsDefaultBrowser(ctx context.Context) error {
382381
383382 // SessionSpawner implements port.SessionSpawner by launching a new dumber process.
384383 type SessionSpawner struct {
385- ctx context.Context
384+ ctx context.Context
385+ spawnEnv port.SessionSpawnEnvironment
386386 }
387387
388388 // NewSessionSpawner creates a new session spawner.
389-func NewSessionSpawner(ctx context.Context) *SessionSpawner {
390- return &SessionSpawner{ctx: ctx}
389+func NewSessionSpawner(ctx context.Context, spawnEnv port.SessionSpawnEnvironment) *SessionSpawner {
390+ return &SessionSpawner{ctx: ctx, spawnEnv: spawnEnv}
391391 }
392392
393393 // RestoreSessionEnvVar is the environment variable used to pass session ID for restoration.
@@ -520,13 +520,13 @@ func (s *SessionSpawner) SpawnWithSession(sessionID entity.SessionID) error {
520520 // Start dumber browse with session ID in environment
521521 cmd := exec.Command(execPath, "browse")
522522
523- // Restore sessions get an isolated CEF state root.
524- sanitizedEnv := withoutEnvKeys(sanitizedChildEnv(os.Environ()), RestoreSessionEnvVar, cef.CEFRootCachePathEnvVar)
525- sanitizedEnv = append(
526- sanitizedEnv,
527- RestoreSessionEnvVar+"="+string(sessionID),
528- cef.CEFRootCachePathEnvVar+"="+sessionCEFRootCachePath(sessionID),
529- )
523+ sanitizedEnv := withoutEnvKeys(sanitizedChildEnv(os.Environ()), RestoreSessionEnvVar)
524+ overrides := []string{RestoreSessionEnvVar + "=" + string(sessionID)}
525+ if s.spawnEnv != nil {
526+ sanitizedEnv = withoutEnvKeys(sanitizedEnv, s.spawnEnv.RootCacheEnvVar())
527+ overrides = append(overrides, s.spawnEnv.RootCacheEnvVar()+"="+s.spawnEnv.SessionRootCachePath(sessionID))
528+ }
529+ sanitizedEnv = append(sanitizedEnv, overrides...)
530530 cmd.Env = sanitizedEnv
531531
532532 // Detach from current process group so the new process survives.
@@ -553,7 +553,3 @@ func (s *SessionSpawner) SpawnWithSession(sessionID entity.SessionID) error {
553553
554554 return nil
555555 }
556-
557-func sessionCEFRootCachePath(sessionID entity.SessionID) string {
558- return filepath.Join(cef.DefaultCEFUserDataDir(), "sessions", string(sessionID))
559-}
@@ -11,7 +11,6 @@ import (
11 11
12 "github.com/bnema/dumber/internal/application/port"12 "github.com/bnema/dumber/internal/application/port"
13 "github.com/bnema/dumber/internal/domain/entity"13 "github.com/bnema/dumber/internal/domain/entity"
14- "github.com/bnema/dumber/internal/infrastructure/cef"
15 "github.com/bnema/dumber/internal/logging"14 "github.com/bnema/dumber/internal/logging"
16 )15 )
17 16
@@ -382,12 +381,13 @@ func (a *Adapter) UnsetAsDefaultBrowser(ctx context.Context) error {
382 381
383 // SessionSpawner implements port.SessionSpawner by launching a new dumber process.382 // SessionSpawner implements port.SessionSpawner by launching a new dumber process.
384 type SessionSpawner struct {383 type SessionSpawner struct {
385- ctx context.Context384+ ctx context.Context
385+ spawnEnv port.SessionSpawnEnvironment
386 }386 }
387 387
388 // NewSessionSpawner creates a new session spawner.388 // NewSessionSpawner creates a new session spawner.
389-func NewSessionSpawner(ctx context.Context) *SessionSpawner {389+func NewSessionSpawner(ctx context.Context, spawnEnv port.SessionSpawnEnvironment) *SessionSpawner {
390- return &SessionSpawner{ctx: ctx}390+ return &SessionSpawner{ctx: ctx, spawnEnv: spawnEnv}
391 }391 }
392 392
393 // RestoreSessionEnvVar is the environment variable used to pass session ID for restoration.393 // RestoreSessionEnvVar is the environment variable used to pass session ID for restoration.
@@ -520,13 +520,13 @@ func (s *SessionSpawner) SpawnWithSession(sessionID entity.SessionID) error {
520 // Start dumber browse with session ID in environment520 // Start dumber browse with session ID in environment
521 cmd := exec.Command(execPath, "browse")521 cmd := exec.Command(execPath, "browse")
522 522
523- // Restore sessions get an isolated CEF state root.523+ sanitizedEnv := withoutEnvKeys(sanitizedChildEnv(os.Environ()), RestoreSessionEnvVar)
524- sanitizedEnv := withoutEnvKeys(sanitizedChildEnv(os.Environ()), RestoreSessionEnvVar, cef.CEFRootCachePathEnvVar)524+ overrides := []string{RestoreSessionEnvVar + "=" + string(sessionID)}
525- sanitizedEnv = append(525+ if s.spawnEnv != nil {
526- sanitizedEnv,526+ sanitizedEnv = withoutEnvKeys(sanitizedEnv, s.spawnEnv.RootCacheEnvVar())
527- RestoreSessionEnvVar+"="+string(sessionID),527+ overrides = append(overrides, s.spawnEnv.RootCacheEnvVar()+"="+s.spawnEnv.SessionRootCachePath(sessionID))
528- cef.CEFRootCachePathEnvVar+"="+sessionCEFRootCachePath(sessionID),528+ }
529- )529+ sanitizedEnv = append(sanitizedEnv, overrides...)
530 cmd.Env = sanitizedEnv530 cmd.Env = sanitizedEnv
531 531
532 // Detach from current process group so the new process survives.532 // Detach from current process group so the new process survives.
@@ -553,7 +553,3 @@ func (s *SessionSpawner) SpawnWithSession(sessionID entity.SessionID) error {
553 553
554 return nil554 return nil
555 }555 }
556-
557-func sessionCEFRootCachePath(sessionID entity.SessionID) string {
558- return filepath.Join(cef.DefaultCEFUserDataDir(), "sessions", string(sessionID))
559-}
M internal/infrastructure/desktop/adapter_test.go +40 -8
@@ -8,11 +8,24 @@ import (
88 "time"
99
1010 "github.com/bnema/dumber/internal/domain/entity"
11- "github.com/bnema/dumber/internal/infrastructure/cef"
1211 )
1312
1413 const testCaptureEnvFile = "DUMBER_TEST_CAPTURE_ENV_FILE"
1514
15+const testRootCacheEnvVar = "DUMBER_TEST_ROOT_CACHE_PATH"
16+
17+type testSessionSpawnEnvironment struct {
18+ rootDir string
19+}
20+
21+func (testSessionSpawnEnvironment) RootCacheEnvVar() string {
22+ return testRootCacheEnvVar
23+}
24+
25+func (e testSessionSpawnEnvironment) SessionRootCachePath(sessionID entity.SessionID) string {
26+ return filepath.Join(e.rootDir, "sessions", string(sessionID))
27+}
28+
1629 func TestMain(m *testing.M) {
1730 if envFile := os.Getenv(testCaptureEnvFile); envFile != "" && len(os.Args) >= 2 && os.Args[1] == "browse" {
1831 if err := os.WriteFile(envFile, []byte(strings.Join(os.Environ(), "\n")), 0o644); err != nil {
@@ -59,16 +72,17 @@ func TestWithoutEnvKeys_RemovesExplicitOverrides(t *testing.T) {
5972 env := withoutEnvKeys([]string{
6073 "PATH=/usr/bin",
6174 RestoreSessionEnvVar + "=old-session",
62- cef.CEFRootCachePathEnvVar + "=/tmp/old-root",
63- }, RestoreSessionEnvVar, cef.CEFRootCachePathEnvVar)
75+ testRootCacheEnvVar + "=/tmp/old-root",
76+ }, RestoreSessionEnvVar, testRootCacheEnvVar)
6477
6578 if len(env) != 1 || env[0] != "PATH=/usr/bin" {
6679 t.Fatalf("expected override keys to be removed, got %#v", env)
6780 }
6881 }
6982
70-func TestSessionSpawner_SpawnWithSession_PassesRestoreSessionAndCEFOverride(t *testing.T) {
71- spawner := NewSessionSpawner(t.Context())
83+func TestSessionSpawner_SpawnWithSession_PassesRestoreSessionAndEngineOverride(t *testing.T) {
84+ rootDir := filepath.Join(t.TempDir(), "cef-root")
85+ spawner := NewSessionSpawner(t.Context(), testSessionSpawnEnvironment{rootDir: rootDir})
7286 childEnvFile := filepath.Join(t.TempDir(), "child-env.txt")
7387 t.Setenv(testCaptureEnvFile, childEnvFile)
7488 t.Setenv("XDG_DATA_HOME", filepath.Join(t.TempDir(), "data"))
@@ -78,12 +92,12 @@ func TestSessionSpawner_SpawnWithSession_PassesRestoreSessionAndCEFOverride(t *t
7892 requireNoError(t, spawner.SpawnWithSession(sessionID))
7993
8094 env := readEnvFile(t, childEnvFile)
81- wantRoot := filepath.Join(cef.DefaultCEFUserDataDir(), "sessions", string(sessionID))
95+ wantRoot := filepath.Join(rootDir, "sessions", string(sessionID))
8296 if got := env[RestoreSessionEnvVar]; got != string(sessionID) {
8397 t.Fatalf("restore session env = %q, want %q", got, string(sessionID))
8498 }
85- if got := env[cef.CEFRootCachePathEnvVar]; got != wantRoot {
86- t.Fatalf("cef state root env = %q, want %q", got, wantRoot)
99+ if got := env[testRootCacheEnvVar]; got != wantRoot {
100+ t.Fatalf("engine state root env = %q, want %q", got, wantRoot)
87101 }
88102 }
89103
@@ -113,6 +127,24 @@ func readEnvFile(t *testing.T, path string) map[string]string {
113127 return nil
114128 }
115129
130+func TestSessionSpawner_SpawnWithSession_OmitsEngineOverrideWhenNotConfigured(t *testing.T) {
131+ spawner := NewSessionSpawner(t.Context(), nil)
132+ childEnvFile := filepath.Join(t.TempDir(), "child-env.txt")
133+ t.Setenv(testCaptureEnvFile, childEnvFile)
134+ t.Setenv("ENV", "")
135+
136+ sessionID := entity.SessionID("session-plain")
137+ requireNoError(t, spawner.SpawnWithSession(sessionID))
138+
139+ env := readEnvFile(t, childEnvFile)
140+ if got := env[RestoreSessionEnvVar]; got != string(sessionID) {
141+ t.Fatalf("restore session env = %q, want %q", got, string(sessionID))
142+ }
143+ if got := env[testRootCacheEnvVar]; got != "" {
144+ t.Fatalf("engine state root env = %q, want empty", got)
145+ }
146+}
147+
116148 func requireNoError(t *testing.T, err error) {
117149 t.Helper()
118150 if err != nil {
@@ -8,11 +8,24 @@ import (
8 "time"8 "time"
9 9
10 "github.com/bnema/dumber/internal/domain/entity"10 "github.com/bnema/dumber/internal/domain/entity"
11- "github.com/bnema/dumber/internal/infrastructure/cef"
12 )11 )
13 12
14 const testCaptureEnvFile = "DUMBER_TEST_CAPTURE_ENV_FILE"13 const testCaptureEnvFile = "DUMBER_TEST_CAPTURE_ENV_FILE"
15 14
15+const testRootCacheEnvVar = "DUMBER_TEST_ROOT_CACHE_PATH"
16+
17+type testSessionSpawnEnvironment struct {
18+ rootDir string
19+}
20+
21+func (testSessionSpawnEnvironment) RootCacheEnvVar() string {
22+ return testRootCacheEnvVar
23+}
24+
25+func (e testSessionSpawnEnvironment) SessionRootCachePath(sessionID entity.SessionID) string {
26+ return filepath.Join(e.rootDir, "sessions", string(sessionID))
27+}
28+
16 func TestMain(m *testing.M) {29 func TestMain(m *testing.M) {
17 if envFile := os.Getenv(testCaptureEnvFile); envFile != "" && len(os.Args) >= 2 && os.Args[1] == "browse" {30 if envFile := os.Getenv(testCaptureEnvFile); envFile != "" && len(os.Args) >= 2 && os.Args[1] == "browse" {
18 if err := os.WriteFile(envFile, []byte(strings.Join(os.Environ(), "\n")), 0o644); err != nil {31 if err := os.WriteFile(envFile, []byte(strings.Join(os.Environ(), "\n")), 0o644); err != nil {
@@ -59,16 +72,17 @@ func TestWithoutEnvKeys_RemovesExplicitOverrides(t *testing.T) {
59 env := withoutEnvKeys([]string{72 env := withoutEnvKeys([]string{
60 "PATH=/usr/bin",73 "PATH=/usr/bin",
61 RestoreSessionEnvVar + "=old-session",74 RestoreSessionEnvVar + "=old-session",
62- cef.CEFRootCachePathEnvVar + "=/tmp/old-root",75+ testRootCacheEnvVar + "=/tmp/old-root",
63- }, RestoreSessionEnvVar, cef.CEFRootCachePathEnvVar)76+ }, RestoreSessionEnvVar, testRootCacheEnvVar)
64 77
65 if len(env) != 1 || env[0] != "PATH=/usr/bin" {78 if len(env) != 1 || env[0] != "PATH=/usr/bin" {
66 t.Fatalf("expected override keys to be removed, got %#v", env)79 t.Fatalf("expected override keys to be removed, got %#v", env)
67 }80 }
68 }81 }
69 82
70-func TestSessionSpawner_SpawnWithSession_PassesRestoreSessionAndCEFOverride(t *testing.T) {83+func TestSessionSpawner_SpawnWithSession_PassesRestoreSessionAndEngineOverride(t *testing.T) {
71- spawner := NewSessionSpawner(t.Context())84+ rootDir := filepath.Join(t.TempDir(), "cef-root")
85+ spawner := NewSessionSpawner(t.Context(), testSessionSpawnEnvironment{rootDir: rootDir})
72 childEnvFile := filepath.Join(t.TempDir(), "child-env.txt")86 childEnvFile := filepath.Join(t.TempDir(), "child-env.txt")
73 t.Setenv(testCaptureEnvFile, childEnvFile)87 t.Setenv(testCaptureEnvFile, childEnvFile)
74 t.Setenv("XDG_DATA_HOME", filepath.Join(t.TempDir(), "data"))88 t.Setenv("XDG_DATA_HOME", filepath.Join(t.TempDir(), "data"))
@@ -78,12 +92,12 @@ func TestSessionSpawner_SpawnWithSession_PassesRestoreSessionAndCEFOverride(t *t
78 requireNoError(t, spawner.SpawnWithSession(sessionID))92 requireNoError(t, spawner.SpawnWithSession(sessionID))
79 93
80 env := readEnvFile(t, childEnvFile)94 env := readEnvFile(t, childEnvFile)
81- wantRoot := filepath.Join(cef.DefaultCEFUserDataDir(), "sessions", string(sessionID))95+ wantRoot := filepath.Join(rootDir, "sessions", string(sessionID))
82 if got := env[RestoreSessionEnvVar]; got != string(sessionID) {96 if got := env[RestoreSessionEnvVar]; got != string(sessionID) {
83 t.Fatalf("restore session env = %q, want %q", got, string(sessionID))97 t.Fatalf("restore session env = %q, want %q", got, string(sessionID))
84 }98 }
85- if got := env[cef.CEFRootCachePathEnvVar]; got != wantRoot {99+ if got := env[testRootCacheEnvVar]; got != wantRoot {
86- t.Fatalf("cef state root env = %q, want %q", got, wantRoot)100+ t.Fatalf("engine state root env = %q, want %q", got, wantRoot)
87 }101 }
88 }102 }
89 103
@@ -113,6 +127,24 @@ func readEnvFile(t *testing.T, path string) map[string]string {
113 return nil127 return nil
114 }128 }
115 129
130+func TestSessionSpawner_SpawnWithSession_OmitsEngineOverrideWhenNotConfigured(t *testing.T) {
131+ spawner := NewSessionSpawner(t.Context(), nil)
132+ childEnvFile := filepath.Join(t.TempDir(), "child-env.txt")
133+ t.Setenv(testCaptureEnvFile, childEnvFile)
134+ t.Setenv("ENV", "")
135+
136+ sessionID := entity.SessionID("session-plain")
137+ requireNoError(t, spawner.SpawnWithSession(sessionID))
138+
139+ env := readEnvFile(t, childEnvFile)
140+ if got := env[RestoreSessionEnvVar]; got != string(sessionID) {
141+ t.Fatalf("restore session env = %q, want %q", got, string(sessionID))
142+ }
143+ if got := env[testRootCacheEnvVar]; got != "" {
144+ t.Fatalf("engine state root env = %q, want empty", got)
145+ }
146+}
147+
116 func requireNoError(t *testing.T, err error) {148 func requireNoError(t *testing.T, err error) {
117 t.Helper()149 t.Helper()
118 if err != nil {150 if err != nil {

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