package launcher import ( "context" "os" "os/signal" "sync" "syscall" "time" "code.nochebuena.dev/einherjar/contracts/lifecycle" "code.nochebuena.dev/einherjar/contracts/logging" "code.nochebuena.dev/einherjar/contracts/observability" ) // Launcher manages the application lifecycle: init → assemble → start → wait → shutdown. type Launcher interface { // Append adds one or more components. Registered in the order they are appended; // shutdown runs in reverse order. Append(components ...lifecycle.Component) // BeforeStart registers hooks that run after all OnInit calls and before all OnStart // calls. Use for dependency injection wiring. BeforeStart(hooks ...Hook) // Run executes the full application lifecycle. Blocks until an OS shutdown signal is // received or Shutdown is called. Returns an error if any lifecycle step fails. // The caller is responsible for calling os.Exit(1) when needed. Run() error // Shutdown triggers a graceful shutdown and waits for Run to return. // ctx controls the caller-side wait timeout — it does NOT override // Config.ComponentStopTimeout for individual components. // Safe to call multiple times (idempotent). Shutdown(ctx context.Context) error } var _ Launcher = (*launcher)(nil) // New returns a Launcher configured by opts. The zero value of Config is valid. func New(logger logging.Logger, opts ...Config) Launcher { o := Config{ComponentStopTimeout: defaultComponentStopTimeout} if len(opts) > 0 { if opts[0].ComponentStopTimeout > 0 { o.ComponentStopTimeout = opts[0].ComponentStopTimeout } } return &launcher{ logger: logger, opts: o, components: make([]lifecycle.Component, 0), shutdownCh: make(chan struct{}), doneCh: make(chan struct{}), } } type launcher struct { logger logging.Logger opts Config components []lifecycle.Component beforeStart []Hook shutdownCh chan struct{} doneCh chan struct{} shutdownOnce sync.Once } func (l *launcher) Append(components ...lifecycle.Component) { l.components = append(l.components, components...) } func (l *launcher) BeforeStart(hooks ...Hook) { l.beforeStart = append(l.beforeStart, hooks...) } // Run executes the full application lifecycle: // 1. Prints the startup banner (unless EINHERJAR_BANNER=off). // 2. OnInit for all components (in registration order). // 3. BeforeStart hooks (in registration order). // 4. OnStart for all components (in registration order). // 5. Blocks until an OS signal or Shutdown() is called. // 6. stopAll — OnStop for all components (in reverse order). func (l *launcher) Run() error { var ids []observability.Identifiable for _, c := range l.components { if id, ok := c.(observability.Identifiable); ok { ids = append(ids, id) } } printBanner(ids) defer close(l.doneCh) l.logger.Info("launcher: starting init phase (OnInit)") for _, c := range l.components { if err := c.OnInit(); err != nil { return err } } l.logger.Info("launcher: running assembly hooks (BeforeStart)") for _, hook := range l.beforeStart { if err := hook(); err != nil { return err } } l.logger.Info("launcher: starting components (OnStart)") for _, c := range l.components { if err := c.OnStart(); err != nil { l.logger.Error("launcher: OnStart failed, triggering shutdown", err) l.stopAll() return err } } l.logger.Info("launcher: application ready") quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM) defer signal.Stop(quit) select { case s := <-quit: l.logger.Info("launcher: termination signal received", "signal", s.String()) case <-l.shutdownCh: l.logger.Info("launcher: programmatic shutdown requested") } l.stopAll() return nil } func (l *launcher) Shutdown(ctx context.Context) error { l.shutdownOnce.Do(func() { close(l.shutdownCh) }) select { case <-l.doneCh: return nil case <-ctx.Done(): return ctx.Err() } } func (l *launcher) stopAll() { l.logger.Info("launcher: stopping all components") for i := len(l.components) - 1; i >= 0; i-- { done := make(chan struct{}) go func(c lifecycle.Component) { if err := c.OnStop(); err != nil { l.logger.Error("launcher: error during OnStop", err) } close(done) }(l.components[i]) select { case <-done: case <-time.After(l.opts.ComponentStopTimeout): l.logger.Error("launcher: component OnStop timed out", nil) } } l.logger.Info("launcher: all components stopped") }