package main import ( "context" "database/sql" "errors" "log" "net/http" "os" "os/signal" "syscall" "time" "cmroubao/backend-api/internal/config" "cmroubao/backend-api/internal/platform/assetstore" "cmroubao/backend-api/internal/platform/database" "cmroubao/backend-api/internal/platform/migration" repository "cmroubao/backend-api/internal/repository/sqlite" "cmroubao/backend-api/internal/transport/httpapi" "cmroubao/backend-api/internal/transport/webui" "cmroubao/backend-api/internal/usecase" ) func main() { if err := run(); err != nil { log.Printf("cmroubao API stopped: %v", err) os.Exit(1) } } func run() error { cfg, err := config.Load(os.LookupEnv) if err != nil { return err } startupContext, cancelStartup := context.WithTimeout( context.Background(), cfg.ReadTimeout, ) defer cancelStartup() db, err := database.Open(startupContext, cfg.DatabasePath) if err != nil { return errors.New("database startup failed") } defer func() { if err := db.Close(); err != nil { log.Print("database close failed") } }() router, err := buildRouter(startupContext, cfg, db) if err != nil { if errors.Is(err, errPendingMigrations) { return err } return errors.New("HTTP router setup failed") } server := httpapi.NewServer(cfg, router) signalContext, stopSignals := signal.NotifyContext( context.Background(), os.Interrupt, syscall.SIGTERM, ) defer stopSignals() serverErrors := make(chan error, 1) go func() { serverErrors <- server.ListenAndServe() }() select { case err := <-serverErrors: if errors.Is(err, http.ErrServerClosed) { return nil } return errors.New("HTTP server failed") case <-signalContext.Done(): return shutdownServer(server, serverErrors, cfg.ShutdownTimeout) } } func shutdownServer( server *http.Server, serverErrors <-chan error, timeout time.Duration, ) error { shutdownContext, cancelShutdown := context.WithTimeout( context.Background(), timeout, ) defer cancelShutdown() shutdownErr := server.Shutdown(shutdownContext) if shutdownErr != nil { if err := server.Close(); err != nil { return errors.New("HTTP server force close failed") } } waitTimeout := timeout if waitTimeout > time.Second { waitTimeout = time.Second } timer := time.NewTimer(waitTimeout) defer timer.Stop() select { case err := <-serverErrors: if err != nil && !errors.Is(err, http.ErrServerClosed) { return errors.New("HTTP server failed during shutdown") } case <-timer.C: _ = server.Close() return errors.New("HTTP server did not stop") } if shutdownErr != nil { return errors.New("HTTP server graceful shutdown timed out") } return nil } type migrationStatusReader interface { Status(context.Context) ([]migration.Status, error) } var errPendingMigrations = errors.New( "database migrations are pending; run migrate up", ) func buildRouter( ctx context.Context, cfg config.Config, db *sql.DB, ) (http.Handler, error) { runner, err := migration.New(db) if err != nil { return nil, err } if err := requireCurrentMigrations(ctx, runner); err != nil { return nil, err } store, err := repository.New(db) if err != nil { return nil, err } files, err := assetstore.New(cfg.AssetDirectory) if err != nil { return nil, err } clock := usecase.SystemClock{} ids := usecase.UUIDGenerator{} assets, err := usecase.NewAssetService(store, files, clock, ids) if err != nil { return nil, err } tasks, err := usecase.NewTaskService(store, clock, ids) if err != nil { return nil, err } webService, err := webui.NewUsecaseAdapter(tasks, assets) if err != nil { return nil, err } renderer, err := webui.NewRenderer() if err != nil { return nil, err } webHandler, err := webui.NewHandler(webService, renderer) if err != nil { return nil, err } registerAdminRoutes, err := httpapi.NewAdminRouteRegistrar( httpapi.AdminServices{ Assets: assets, Tasks: tasks, }, webHandler, ) if err != nil { return nil, err } return httpapi.NewRouter(httpapi.RouterDependencies{ Database: db, RegisterAdminRoutes: registerAdminRoutes, LogEvent: func(event string) { log.Print(event) }, }) } func requireCurrentMigrations( ctx context.Context, reader migrationStatusReader, ) error { statuses, err := reader.Status(ctx) if err != nil { return errors.New("database migration status failed") } if len(statuses) == 0 { return errors.New("database has no known migrations") } for _, status := range statuses { if !status.Applied { return errPendingMigrations } } return nil }