feat(infra): add session spawner for restoration
- commit
- 8022d74265249bdff148769b33c0bcac0a1b8dca
- author
- bnema · · 2025-12-24 21:27 UTC
- committer
- brice · · 2025-12-25 05:18 UTC
- parent
- 76add37
- signature
- unsigned
1 file · +45 -0
M internal/infrastructure/desktop/adapter.go +45 -0
| @@ -10,6 +10,7 @@ import ( | |||
| 10 | 10 | "strings" | |
| 11 | 11 | ||
| 12 | 12 | "github.com/bnema/dumber/internal/application/port" | |
| 13 | + | "github.com/bnema/dumber/internal/domain/entity" | |
| 13 | 14 | "github.com/bnema/dumber/internal/logging" | |
| 14 | 15 | ) | |
| 15 | 16 | ||
| @@ -348,3 +349,47 @@ func (a *Adapter) UnsetAsDefaultBrowser(ctx context.Context) error { | |||
| 348 | 349 | log.Warn().Msg("dumber was default browser - user should set a new default") | |
| 349 | 350 | return nil | |
| 350 | 351 | } | |
| 352 | + | ||
| 353 | + | // SessionSpawner implements port.SessionSpawner by launching a new dumber process. | |
| 354 | + | type SessionSpawner struct { | |
| 355 | + | ctx context.Context | |
| 356 | + | } | |
| 357 | + | ||
| 358 | + | // NewSessionSpawner creates a new session spawner. | |
| 359 | + | func NewSessionSpawner(ctx context.Context) *SessionSpawner { | |
| 360 | + | return &SessionSpawner{ctx: ctx} | |
| 361 | + | } | |
| 362 | + | ||
| 363 | + | // SpawnWithSession starts a new dumber instance to restore a session. | |
| 364 | + | func (s *SessionSpawner) SpawnWithSession(sessionID entity.SessionID) error { | |
| 365 | + | log := logging.FromContext(s.ctx) | |
| 366 | + | ||
| 367 | + | execPath, err := getExecutablePath() | |
| 368 | + | if err != nil { | |
| 369 | + | return fmt.Errorf("get executable path: %w", err) | |
| 370 | + | } | |
| 371 | + | ||
| 372 | + | // Start a detached process with the --restore-session flag | |
| 373 | + | cmd := exec.Command(execPath, "browse", "--restore-session", string(sessionID)) | |
| 374 | + | ||
| 375 | + | // Detach from current process group so the new process survives | |
| 376 | + | cmd.Stdin = nil | |
| 377 | + | cmd.Stdout = nil | |
| 378 | + | cmd.Stderr = nil | |
| 379 | + | ||
| 380 | + | if err := cmd.Start(); err != nil { | |
| 381 | + | return fmt.Errorf("spawn session: %w", err) | |
| 382 | + | } | |
| 383 | + | ||
| 384 | + | // Release the process so it continues running after we close | |
| 385 | + | if err := cmd.Process.Release(); err != nil { | |
| 386 | + | log.Warn().Err(err).Msg("failed to release spawned process (non-fatal)") | |
| 387 | + | } | |
| 388 | + | ||
| 389 | + | log.Info(). | |
| 390 | + | Str("session_id", string(sessionID)). | |
| 391 | + | Int("pid", cmd.Process.Pid). | |
| 392 | + | Msg("spawned dumber with session restoration") | |
| 393 | + | ||
| 394 | + | return nil | |
| 395 | + | } | |
| @@ -10,6 +10,7 @@ import ( | |||||
| 10 | "strings" | 10 | "strings" | ||
| 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/logging" | 14 | "github.com/bnema/dumber/internal/logging" | ||
| 14 | ) | 15 | ) | ||
| 15 | 16 | ||||
| @@ -348,3 +349,47 @@ func (a *Adapter) UnsetAsDefaultBrowser(ctx context.Context) error { | |||||
| 348 | log.Warn().Msg("dumber was default browser - user should set a new default") | 349 | log.Warn().Msg("dumber was default browser - user should set a new default") | ||
| 349 | return nil | 350 | return nil | ||
| 350 | } | 351 | } | ||
| 352 | + | ||||
| 353 | + | // SessionSpawner implements port.SessionSpawner by launching a new dumber process. | |||
| 354 | + | type SessionSpawner struct { | |||
| 355 | + | ctx context.Context | |||
| 356 | + | } | |||
| 357 | + | ||||
| 358 | + | // NewSessionSpawner creates a new session spawner. | |||
| 359 | + | func NewSessionSpawner(ctx context.Context) *SessionSpawner { | |||
| 360 | + | return &SessionSpawner{ctx: ctx} | |||
| 361 | + | } | |||
| 362 | + | ||||
| 363 | + | // SpawnWithSession starts a new dumber instance to restore a session. | |||
| 364 | + | func (s *SessionSpawner) SpawnWithSession(sessionID entity.SessionID) error { | |||
| 365 | + | log := logging.FromContext(s.ctx) | |||
| 366 | + | ||||
| 367 | + | execPath, err := getExecutablePath() | |||
| 368 | + | if err != nil { | |||
| 369 | + | return fmt.Errorf("get executable path: %w", err) | |||
| 370 | + | } | |||
| 371 | + | ||||
| 372 | + | // Start a detached process with the --restore-session flag | |||
| 373 | + | cmd := exec.Command(execPath, "browse", "--restore-session", string(sessionID)) | |||
| 374 | + | ||||
| 375 | + | // Detach from current process group so the new process survives | |||
| 376 | + | cmd.Stdin = nil | |||
| 377 | + | cmd.Stdout = nil | |||
| 378 | + | cmd.Stderr = nil | |||
| 379 | + | ||||
| 380 | + | if err := cmd.Start(); err != nil { | |||
| 381 | + | return fmt.Errorf("spawn session: %w", err) | |||
| 382 | + | } | |||
| 383 | + | ||||
| 384 | + | // Release the process so it continues running after we close | |||
| 385 | + | if err := cmd.Process.Release(); err != nil { | |||
| 386 | + | log.Warn().Err(err).Msg("failed to release spawned process (non-fatal)") | |||
| 387 | + | } | |||
| 388 | + | ||||
| 389 | + | log.Info(). | |||
| 390 | + | Str("session_id", string(sessionID)). | |||
| 391 | + | Int("pid", cmd.Process.Pid). | |||
| 392 | + | Msg("spawned dumber with session restoration") | |||
| 393 | + | ||||
| 394 | + | return nil | |||
| 395 | + | } | |||