package main import ( "context" "embed" "flag" "fmt" "io/fs" "net/http" "os" "os/signal" "syscall" "git.iwork-ai.com/xdc/iqudo-top1/internal/auth" "git.iwork-ai.com/xdc/iqudo-top1/internal/config" "git.iwork-ai.com/xdc/iqudo-top1/internal/database" "git.iwork-ai.com/xdc/iqudo-top1/internal/httpserver" "git.iwork-ai.com/xdc/iqudo-top1/internal/logger" "git.iwork-ai.com/xdc/iqudo-top1/internal/multitable" "go.uber.org/zap" ) //go:embed web/dist/* var frontendFiles embed.FS //go:embed sdk/dist/* var sdkFiles embed.FS func main() { environment := flag.String("env", "", "runtime environment: development, test, prod") configDir := flag.String("config-dir", "configs", "configuration directory") configFile := flag.String("config-file", "", "explicit configuration file; relative paths are resolved from config-dir") flag.Parse() cfg, err := config.Load(config.LoadOptions{Environment: *environment, ConfigDir: *configDir, ConfigFile: *configFile}) if err != nil { fmt.Fprintf(os.Stderr, "load configuration: %v\n", err) os.Exit(1) } log, err := logger.New(cfg.App.Env, cfg.App.Name) if err != nil { fmt.Fprintf(os.Stderr, "create logger: %v\n", err) os.Exit(1) } defer log.Sync() undoGlobalLogger := zap.ReplaceGlobals(log.Named("business")) defer undoGlobalLogger() db, err := database.Open(cfg.Database, log) if err != nil { log.Fatal("connect database", zap.Error(err)) } if err := database.AutoMigrate(db); err != nil { log.Fatal("synchronize database schema", zap.Error(err)) } if err := auth.Seed(db, cfg.Seed); err != nil { log.Fatal("seed administrator", zap.Error(err)) } workerDone := make(chan struct{}) workerCtx, stopWorker := context.WithCancel(context.Background()) if cfg.MultiTable.Enabled { worker := multitable.NewWorker(db, cfg.MultiTable, log) go func() { defer close(workerDone) log.Info("multitable synchronization started", zap.String("worker", worker.String())) worker.Run(workerCtx) }() } else { close(workerDone) } frontend, err := fs.Sub(frontendFiles, "web/dist") if err != nil { log.Fatal("load embedded frontend", zap.Error(err)) } sdk, err := fs.Sub(sdkFiles, "sdk/dist") if err != nil { log.Fatal("load embedded sdk", zap.Error(err)) } authService := auth.NewService(db, cfg.Auth) handler := httpserver.New(db, authService, frontend, sdk, log, cfg.App.Env) server := &http.Server{Addr: cfg.Server.Address(), Handler: handler, ReadTimeout: cfg.Server.ReadTimeout, WriteTimeout: cfg.Server.WriteTimeout} go func() { log.Info("server started", zap.String("address", cfg.Server.Address())) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal("server stopped unexpectedly", zap.Error(err)) } }() stop := make(chan os.Signal, 1) signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) <-stop stopWorker() ctx, cancel := context.WithTimeout(context.Background(), cfg.Server.ShutdownTimeout) defer cancel() if err := server.Shutdown(ctx); err != nil { log.Error("graceful shutdown failed", zap.Error(err)) } select { case <-workerDone: case <-ctx.Done(): log.Warn("multitable worker did not stop before shutdown timeout") } log.Info("server stopped") }