refactor: inject engine-specific session spawn env
- commit
- eb16ef1668dda3c2fcc6b71edc50c19539bf0be9
- author
- brice · · 2026-04-16 04:13 UTC
- parent
- d1704de
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
9 files · +124 -30
M cmd/dumber/main.go +1 -1
| @@ -782,7 +782,7 @@ func buildUIDependencies( | |||
| 782 | 782 | }, | |
| 783 | 783 | CheckUpdateUC: uc.checkUpdate, | |
| 784 | 784 | ApplyUpdateUC: uc.applyUpdate, | |
| 785 | - | SessionSpawner: desktop.NewSessionSpawner(ctx), | |
| 785 | + | SessionSpawner: bootstrap.NewSessionSpawner(ctx, cfg), | |
| 786 | 786 | FileSystem: filesystem.New(), | |
| 787 | 787 | AccentFocusProvider: focusProvider, | |
| 788 | 788 | 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 { | |||
| 21 | 21 | 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 | 36 | // SnapshotService manages debounced session-state snapshots. | |
| 25 | 37 | type SnapshotService interface { | |
| 26 | 38 | // Start begins the snapshot service with the given context. | |
| @@ -21,6 +21,18 @@ type SessionSpawner interface { | |||||
| 21 | SpawnWithSession(sessionID entity.SessionID) error | 21 | 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 ( | |||
| 10 | 10 | ||
| 11 | 11 | "github.com/bnema/dumber/internal/application/port" | |
| 12 | 12 | "github.com/bnema/dumber/internal/application/usecase" | |
| 13 | + | "github.com/bnema/dumber/internal/bootstrap" | |
| 13 | 14 | "github.com/bnema/dumber/internal/cli/styles" | |
| 14 | 15 | "github.com/bnema/dumber/internal/domain/build" | |
| 15 | 16 | "github.com/bnema/dumber/internal/domain/repository" | |
| @@ -37,6 +38,7 @@ type App struct { | |||
| 37 | 38 | ||
| 38 | 39 | // Services | |
| 39 | 40 | FaviconService *favicon.Service | |
| 41 | + | SessionSpawner port.SessionSpawner | |
| 40 | 42 | ||
| 41 | 43 | // Context with logger | |
| 42 | 44 | ctx context.Context | |
| @@ -131,6 +133,7 @@ func NewApp() (*App, error) { | |||
| 131 | 133 | RestoreUC: restoreUC, | |
| 132 | 134 | DeleteSessionUC: deleteSessionUC, | |
| 133 | 135 | FaviconService: faviconService, | |
| 136 | + | SessionSpawner: bootstrap.NewSessionSpawner(ctx, cfg), | |
| 134 | 137 | ctx: ctx, | |
| 135 | 138 | logCleanup: logCleanup, | |
| 136 | 139 | }, 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 | // Services | 39 | // Services | ||
| 39 | FaviconService *favicon.Service | 40 | FaviconService *favicon.Service | ||
| 41 | + | SessionSpawner port.SessionSpawner | |||
| 40 | 42 | ||||
| 41 | // Context with logger | 43 | // Context with logger | ||
| 42 | ctx context.Context | 44 | 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 | }, nil | 139 | }, nil | ||
M internal/cli/cmd/sessions.go +7 -3
| @@ -13,7 +13,6 @@ import ( | |||
| 13 | 13 | "github.com/bnema/dumber/internal/cli/model" | |
| 14 | 14 | "github.com/bnema/dumber/internal/cli/styles" | |
| 15 | 15 | "github.com/bnema/dumber/internal/domain/entity" | |
| 16 | - | "github.com/bnema/dumber/internal/infrastructure/desktop" | |
| 17 | 16 | ) | |
| 18 | 17 | ||
| 19 | 18 | const defaultSessionsLimit = 20 | |
| @@ -60,6 +59,7 @@ func runSessions(_ *cobra.Command, _ []string) error { | |||
| 60 | 59 | ListSessionsUC: app.ListSessionsUC, | |
| 61 | 60 | RestoreUC: app.RestoreUC, | |
| 62 | 61 | DeleteSessionUC: app.DeleteSessionUC, | |
| 62 | + | SessionSpawner: app.SessionSpawner, | |
| 63 | 63 | CurrentSession: currentSessionID, | |
| 64 | 64 | MaxExitedSessions: app.Config.Session.MaxExitedSessions, | |
| 65 | 65 | }) | |
| @@ -181,8 +181,12 @@ func runSessionsRestore(_ *cobra.Command, args []string) error { | |||
| 181 | 181 | } | |
| 182 | 182 | ||
| 183 | 183 | // 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 { | |
| 186 | 190 | wrappedErr := fmt.Errorf("spawn browser: %w", err) | |
| 187 | 191 | fmt.Fprintln(os.Stderr, renderer.RenderError(wrappedErr)) | |
| 188 | 192 | 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 = 20 | 18 | 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 session | 183 | // 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 ( | |||
| 11 | 11 | tea "github.com/charmbracelet/bubbletea" | |
| 12 | 12 | "github.com/charmbracelet/lipgloss" | |
| 13 | 13 | ||
| 14 | + | "github.com/bnema/dumber/internal/application/port" | |
| 14 | 15 | "github.com/bnema/dumber/internal/application/usecase" | |
| 15 | 16 | "github.com/bnema/dumber/internal/cli/styles" | |
| 16 | 17 | "github.com/bnema/dumber/internal/domain/entity" | |
| 17 | - | "github.com/bnema/dumber/internal/infrastructure/desktop" | |
| 18 | 18 | "github.com/bnema/dumber/internal/logging" | |
| 19 | 19 | ) | |
| 20 | 20 | ||
| @@ -43,6 +43,7 @@ type SessionsModel struct { | |||
| 43 | 43 | listSessionsUC *usecase.ListSessionsUseCase | |
| 44 | 44 | restoreUC *usecase.RestoreSessionUseCase | |
| 45 | 45 | deleteSessionUC *usecase.DeleteSessionUseCase | |
| 46 | + | sessionSpawner port.SessionSpawner | |
| 46 | 47 | theme *styles.Theme | |
| 47 | 48 | } | |
| 48 | 49 | ||
| @@ -114,6 +115,7 @@ type SessionsModelConfig struct { | |||
| 114 | 115 | ListSessionsUC *usecase.ListSessionsUseCase | |
| 115 | 116 | RestoreUC *usecase.RestoreSessionUseCase | |
| 116 | 117 | DeleteSessionUC *usecase.DeleteSessionUseCase | |
| 118 | + | SessionSpawner port.SessionSpawner | |
| 117 | 119 | CurrentSession entity.SessionID | |
| 118 | 120 | MaxExitedSessions int | |
| 119 | 121 | } | |
| @@ -137,6 +139,7 @@ func NewSessionsModel(ctx context.Context, theme *styles.Theme, cfg SessionsMode | |||
| 137 | 139 | listSessionsUC: cfg.ListSessionsUC, | |
| 138 | 140 | restoreUC: cfg.RestoreUC, | |
| 139 | 141 | deleteSessionUC: cfg.DeleteSessionUC, | |
| 142 | + | sessionSpawner: cfg.SessionSpawner, | |
| 140 | 143 | currentSession: cfg.CurrentSession, | |
| 141 | 144 | theme: theme, | |
| 142 | 145 | } | |
| @@ -336,8 +339,10 @@ func (m SessionsModel) restoreSession(sessionID entity.SessionID) tea.Cmd { | |||
| 336 | 339 | } | |
| 337 | 340 | ||
| 338 | 341 | // 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 { | |
| 341 | 346 | return sessionRestoredMsg{sessionID: sessionID, err: err} | |
| 342 | 347 | } | |
| 343 | 348 | ||
| @@ -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.ListSessionsUseCase | 43 | listSessionsUC *usecase.ListSessionsUseCase | ||
| 44 | restoreUC *usecase.RestoreSessionUseCase | 44 | restoreUC *usecase.RestoreSessionUseCase | ||
| 45 | deleteSessionUC *usecase.DeleteSessionUseCase | 45 | deleteSessionUC *usecase.DeleteSessionUseCase | ||
| 46 | + | sessionSpawner port.SessionSpawner | |||
| 46 | theme *styles.Theme | 47 | theme *styles.Theme | ||
| 47 | } | 48 | } | ||
| 48 | 49 | ||||
| @@ -114,6 +115,7 @@ type SessionsModelConfig struct { | |||||
| 114 | ListSessionsUC *usecase.ListSessionsUseCase | 115 | ListSessionsUC *usecase.ListSessionsUseCase | ||
| 115 | RestoreUC *usecase.RestoreSessionUseCase | 116 | RestoreUC *usecase.RestoreSessionUseCase | ||
| 116 | DeleteSessionUC *usecase.DeleteSessionUseCase | 117 | DeleteSessionUC *usecase.DeleteSessionUseCase | ||
| 118 | + | SessionSpawner port.SessionSpawner | |||
| 117 | CurrentSession entity.SessionID | 119 | CurrentSession entity.SessionID | ||
| 118 | MaxExitedSessions int | 120 | 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 instance | 341 | // 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 ( | |||
| 11 | 11 | ||
| 12 | 12 | "github.com/bnema/dumber/internal/application/port" | |
| 13 | 13 | "github.com/bnema/dumber/internal/domain/entity" | |
| 14 | - | "github.com/bnema/dumber/internal/infrastructure/cef" | |
| 15 | 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 | 382 | // SessionSpawner implements port.SessionSpawner by launching a new dumber process. | |
| 384 | 383 | type SessionSpawner struct { | |
| 385 | - | ctx context.Context | |
| 384 | + | ctx context.Context | |
| 385 | + | spawnEnv port.SessionSpawnEnvironment | |
| 386 | 386 | } | |
| 387 | 387 | ||
| 388 | 388 | // 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} | |
| 391 | 391 | } | |
| 392 | 392 | ||
| 393 | 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 | 520 | // Start dumber browse with session ID in environment | |
| 521 | 521 | cmd := exec.Command(execPath, "browse") | |
| 522 | 522 | ||
| 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...) | |
| 530 | 530 | cmd.Env = sanitizedEnv | |
| 531 | 531 | ||
| 532 | 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 | 554 | return nil | |
| 555 | 555 | } | |
| 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.Context | 384 | + | 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 environment | 520 | // 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 = sanitizedEnv | 530 | 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 nil | 554 | 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 ( | |||
| 8 | 8 | "time" | |
| 9 | 9 | ||
| 10 | 10 | "github.com/bnema/dumber/internal/domain/entity" | |
| 11 | - | "github.com/bnema/dumber/internal/infrastructure/cef" | |
| 12 | 11 | ) | |
| 13 | 12 | ||
| 14 | 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 | 29 | func TestMain(m *testing.M) { | |
| 17 | 30 | if envFile := os.Getenv(testCaptureEnvFile); envFile != "" && len(os.Args) >= 2 && os.Args[1] == "browse" { | |
| 18 | 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 | 72 | env := withoutEnvKeys([]string{ | |
| 60 | 73 | "PATH=/usr/bin", | |
| 61 | 74 | RestoreSessionEnvVar + "=old-session", | |
| 62 | - | cef.CEFRootCachePathEnvVar + "=/tmp/old-root", | |
| 63 | - | }, RestoreSessionEnvVar, cef.CEFRootCachePathEnvVar) | |
| 75 | + | testRootCacheEnvVar + "=/tmp/old-root", | |
| 76 | + | }, RestoreSessionEnvVar, testRootCacheEnvVar) | |
| 64 | 77 | ||
| 65 | 78 | if len(env) != 1 || env[0] != "PATH=/usr/bin" { | |
| 66 | 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) { | |
| 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}) | |
| 72 | 86 | childEnvFile := filepath.Join(t.TempDir(), "child-env.txt") | |
| 73 | 87 | t.Setenv(testCaptureEnvFile, childEnvFile) | |
| 74 | 88 | t.Setenv("XDG_DATA_HOME", filepath.Join(t.TempDir(), "data")) | |
| @@ -78,12 +92,12 @@ func TestSessionSpawner_SpawnWithSession_PassesRestoreSessionAndCEFOverride(t *t | |||
| 78 | 92 | requireNoError(t, spawner.SpawnWithSession(sessionID)) | |
| 79 | 93 | ||
| 80 | 94 | env := readEnvFile(t, childEnvFile) | |
| 81 | - | wantRoot := filepath.Join(cef.DefaultCEFUserDataDir(), "sessions", string(sessionID)) | |
| 95 | + | wantRoot := filepath.Join(rootDir, "sessions", string(sessionID)) | |
| 82 | 96 | if got := env[RestoreSessionEnvVar]; got != string(sessionID) { | |
| 83 | 97 | t.Fatalf("restore session env = %q, want %q", got, string(sessionID)) | |
| 84 | 98 | } | |
| 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) | |
| 87 | 101 | } | |
| 88 | 102 | } | |
| 89 | 103 | ||
| @@ -113,6 +127,24 @@ func readEnvFile(t *testing.T, path string) map[string]string { | |||
| 113 | 127 | 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 | 148 | func requireNoError(t *testing.T, err error) { | |
| 117 | 149 | t.Helper() | |
| 118 | 150 | 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 nil | 127 | 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 { | ||