package application import ( "context" "errors" "fmt" "log/slog" "os" "strings" "time" ) var errApplication = errors.New("application") // Application is a lifecycle controlling structure for application. type Application struct { shutdownChan chan struct{} ctx context.Context cancelFunc context.CancelFunc baseLogger *slog.Logger appLogger *slog.Logger services []Service } // New creates new instance of lifecycle controlling structure. func New() *Application { appl := &Application{} appl.initialize() return appl } func (a *Application) configure() error { // First iteration - core services. for _, service := range a.services { if !strings.Contains(service.Name(), "core/") { continue } a.appLogger.Debug("Launching configuration procedure for service", "service", service.Name()) if err := service.Configure(); err != nil { return fmt.Errorf("configure service '%s': %w", service.Name(), err) } } // Second iteration - rest of the services. for _, service := range a.services { if strings.Contains(service.Name(), "core/") { continue } a.appLogger.Debug("Launching configuration procedure for service", "service", service.Name()) if err := service.Configure(); err != nil { return fmt.Errorf("configure service '%s': %w", service.Name(), err) } } return nil } func (a *Application) connectDependencies() error { // First iteration - core services. for _, service := range a.services { if !strings.Contains(service.Name(), "core/") { continue } a.appLogger.Debug("Connecting dependencies for service.", "service", service.Name()) if err := service.ConnectDependencies(); err != nil { return fmt.Errorf("connect dependencies for service '%s': %w", service.Name(), err) } } // Second iteration - rest of the services. for _, service := range a.services { if strings.Contains(service.Name(), "core/") { continue } a.appLogger.Debug("Connecting dependencies for service.", "service", service.Name()) if err := service.ConnectDependencies(); err != nil { return fmt.Errorf("connect dependencies for service '%s': %w", service.Name(), err) } } return nil } // ContextWithTimeout returns context.Context with requested timeout. func (a *Application) ContextWithTimeout(timeout time.Duration) context.Context { ctx, cancelFunc := context.WithTimeout(a.ctx, timeout) // As we do not need to call cancelFunc - make linter happy. // This probably will lead to context leak, so it should be investigated. go func(_ context.CancelFunc) {}(cancelFunc) return ctx } func (a *Application) initialize() { a.ctx, a.cancelFunc = context.WithCancel(context.Background()) a.initializeLogger() a.services = make([]Service, 0) } func (a *Application) launchStartupTasks() error { for _, service := range a.services { if strings.Contains(service.Name(), "mainwindow") { continue } if !strings.Contains(service.Name(), "core/") { continue } a.appLogger.Debug("Launching startup tasks for service.", "service", service.Name()) if err := service.LaunchStartupTasks(); err != nil { return fmt.Errorf("launch startup tasks for core/%s: %w", service.Name(), err) } } for _, service := range a.services { if strings.Contains(service.Name(), "core/") { continue } a.appLogger.Debug("Launching startup tasks for service.", "service", service.Name()) if err := service.LaunchStartupTasks(); err != nil { return fmt.Errorf("launch startup tasks for core/%s: %w", service.Name(), err) } } return nil } // Shutdown stops application. func (a *Application) Shutdown() error { a.appLogger.Info("Stopping pztrn's Bunker...") for _, service := range a.services { if !strings.Contains(service.Name(), "features/") { continue } a.appLogger.Debug("Shutting down service.", "service", service.Name()) if err := service.Shutdown(); err != nil { return fmt.Errorf("shutting down service '%s': %w", service.Name(), err) } } for _, service := range a.services { if !strings.Contains(service.Name(), "core/") { continue } a.appLogger.Debug("Shutting down service.", "service", service.Name()) if err := service.Shutdown(); err != nil { return fmt.Errorf("shutting down service '%s': %w", service.Name(), err) } } os.Exit(0) return nil } // Start starts application. // Server application will start a goroutine that monitors SIGTERM and sends empty struct to channel. func (a *Application) Start() error { if err := a.connectDependencies(); err != nil { return fmt.Errorf("%w: %w", errApplication, err) } if err := a.configure(); err != nil { return fmt.Errorf("%w: %w", errApplication, err) } if err := a.launchStartupTasks(); err != nil { return fmt.Errorf("%w: %w", errApplication, err) } a.startServer() return nil }