package cli import ( "context" "fmt" "os" "os/signal" "syscall" "time" "git.eeqj.de/sneak/vaultik/internal/config" "git.eeqj.de/sneak/vaultik/internal/database" "git.eeqj.de/sneak/vaultik/internal/globals" "git.eeqj.de/sneak/vaultik/internal/log" "go.uber.org/fx" ) // AppOptions contains common options for creating the fx application type AppOptions struct { ConfigPath string LogOptions log.LogOptions Modules []fx.Option Invokes []fx.Option } // setupGlobals sets up the globals with application startup time func setupGlobals(lc fx.Lifecycle, g *globals.Globals) { lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { g.StartTime = time.Now() return nil }, }) } // NewApp creates a new fx application with common modules func NewApp(opts AppOptions) *fx.App { baseModules := []fx.Option{ fx.Supply(config.ConfigPath(opts.ConfigPath)), fx.Supply(opts.LogOptions), fx.Provide(globals.New), fx.Provide(log.New), config.Module, database.Module, log.Module, fx.Invoke(setupGlobals), fx.NopLogger, } allOptions := append(baseModules, opts.Modules...) allOptions = append(allOptions, opts.Invokes...) return fx.New(allOptions...) } // RunApp starts and stops the fx application within the given context func RunApp(ctx context.Context, app *fx.App) error { // Set up signal handling for graceful shutdown sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) // Create a context that will be cancelled on signal ctx, cancel := context.WithCancel(ctx) defer cancel() // Start the app if err := app.Start(ctx); err != nil { return fmt.Errorf("failed to start app: %w", err) } // Handle shutdown shutdownComplete := make(chan struct{}) go func() { defer close(shutdownComplete) <-sigChan log.Notice("Received interrupt signal, shutting down gracefully...") // Create a timeout context for shutdown shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) defer shutdownCancel() if err := app.Stop(shutdownCtx); err != nil { log.Error("Error during shutdown", "error", err) } }() // Wait for either the signal handler to complete shutdown or the app to request shutdown select { case <-shutdownComplete: // Shutdown completed via signal return nil case <-ctx.Done(): // Context cancelled (shouldn't happen in normal operation) if err := app.Stop(context.Background()); err != nil { log.Error("Error stopping app", "error", err) } return ctx.Err() case <-app.Done(): // App finished running (e.g., backup completed) return nil } } // RunWithApp is a helper that creates and runs an fx app with the given options func RunWithApp(ctx context.Context, opts AppOptions) error { app := NewApp(opts) return RunApp(ctx, app) }