package main import ( "context" "log/slog" "net/http" "os" "os/signal" "strconv" "sync" "syscall" "time" "github.com/CubeCraft-Creations/Extrudex/backend/internal/clients" "github.com/CubeCraft-Creations/Extrudex/backend/internal/config" "github.com/CubeCraft-Creations/Extrudex/backend/internal/db" "github.com/CubeCraft-Creations/Extrudex/backend/internal/models" "github.com/CubeCraft-Creations/Extrudex/backend/internal/router" "github.com/CubeCraft-Creations/Extrudex/backend/internal/sse" "github.com/CubeCraft-Creations/Extrudex/backend/internal/workers" "github.com/jackc/pgx/v5/pgxpool" ) func main() { slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelInfo, }))) cfg, err := config.Load() if err != nil { slog.Error("failed to load config", "error", err) os.Exit(1) } dbPool, err := db.NewPool(cfg.DatabaseURL) if err != nil { slog.Error("failed to connect to database", "error", err) os.Exit(1) } defer db.ClosePool(dbPool) sseBC := sse.NewBroadcaster(128) sseBC.Start() defer sseBC.Stop() r := router.New(cfg, dbPool, sseBC) // ── Workers ───────────────────────────────────────────────────────── var wg sync.WaitGroup workersCtx, cancelWorkers := context.WithCancel(context.Background()) defer cancelWorkers() pollInterval, _ := time.ParseDuration(cfg.MoonrakerPollInterval) if pollInterval <= 0 { pollInterval = 10 * time.Second } activePrinters := listActivePrinters(workersCtx, dbPool) for _, p := range activePrinters { startWorkerForPrinter(workersCtx, &wg, cfg, dbPool, p, pollInterval) } // ── HTTP server ───────────────────────────────────────────────────── server := &http.Server{ Addr: ":" + cfg.Port, Handler: r, ReadTimeout: 15 * time.Second, WriteTimeout: 0, IdleTimeout: 60 * time.Second, } go func() { slog.Info("server starting", "addr", server.Addr) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { slog.Error("server error", "error", err) os.Exit(1) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit slog.Info("server shutting down") cancelWorkers() shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) defer shutdownCancel() if err := server.Shutdown(shutdownCtx); err != nil { slog.Error("server shutdown error", "error", err) } done := make(chan struct{}) go func() { wg.Wait() close(done) }() select { case <-done: slog.Info("all workers stopped") case <-time.After(15 * time.Second): slog.Warn("timed out waiting for workers to stop") } slog.Info("server stopped") } func listActivePrinters(ctx context.Context, pool *pgxpool.Pool) []models.Printer { rows, err := pool.Query(ctx, ` SELECT id, name, printer_type_id, manufacturer, model, moonraker_url, moonraker_api_key, mqtt_broker_host, mqtt_topic_prefix, mqtt_tls_enabled, is_active, created_at, updated_at FROM printers WHERE is_active = TRUE ORDER BY name `) if err != nil { slog.Warn("failed to query active printers", "error", err) return nil } defer rows.Close() var printers []models.Printer for rows.Next() { var p models.Printer if err := rows.Scan( &p.ID, &p.Name, &p.PrinterTypeID, &p.Manufacturer, &p.Model, &p.MoonrakerURL, &p.MoonrakerAPIKey, &p.MQTTBrokerHost, &p.MQTTTopicPrefix, &p.MQTTTLSEnabled, &p.IsActive, &p.CreatedAt, &p.UpdatedAt, ); err != nil { slog.Warn("failed to scan printer row", "error", err) continue } printers = append(printers, p) } return printers } func startWorkerForPrinter( ctx context.Context, wg *sync.WaitGroup, cfg *config.Config, pool *pgxpool.Pool, printer models.Printer, pollInterval time.Duration, ) { if printer.MoonrakerURL != nil && *printer.MoonrakerURL != "" { mc := clients.NewMoonrakerClient(*printer.MoonrakerURL) poller := workers.NewMoonrakerPoller(workers.MoonrakerPollerConfig{ Client: mc, Pool: pool, PollInterval: pollInterval, PrinterID: printer.ID, PrinterName: printer.Name, }) wg.Add(1) go func() { defer wg.Done() poller.Run(ctx) }() } if printer.MQTTBrokerHost != nil && *printer.MQTTBrokerHost != "" { topicPrefix := cfg.MQTTTopicPrefix if printer.MQTTTopicPrefix != nil && *printer.MQTTTopicPrefix != "" { topicPrefix = *printer.MQTTTopicPrefix } sub := workers.NewMQTTSubscriber(workers.MQTTSubscriberConfig{ Pool: pool, PrinterID: printer.ID, PrinterName: printer.Name, }) mqttClient := clients.NewMQTTClient(clients.MQTTConfig{ Broker: *printer.MQTTBrokerHost, ClientID: cfg.MQTTClientID + "-p" + strconv.Itoa(printer.ID), TopicPrefix: topicPrefix, TLSCert: cfg.MQTTTLSCert, TLSKey: cfg.MQTTTLSKey, Handler: sub.HandleBambuReport, }) sub.Client = mqttClient wg.Add(1) go func() { defer wg.Done() if err := sub.Run(ctx); err != nil { slog.Error("mqtt subscriber error", "printer_id", printer.ID, "error", err) } }() } }