package main import ( "context" "encoding/xml" "fmt" "net/http" "os" "os/exec" "os/signal" "strings" "syscall" "time" "github.com/tarm/serial" log "github.com/sirupsen/logrus" "gitea.futuresens.co.uk/futuresens/hardlink/bootstrap" "gitea.futuresens.co.uk/futuresens/hardlink/config" "gitea.futuresens.co.uk/futuresens/hardlink/dispenser" "gitea.futuresens.co.uk/futuresens/hardlink/errorhandlers" "gitea.futuresens.co.uk/futuresens/hardlink/handlers" "gitea.futuresens.co.uk/futuresens/hardlink/lockserver" "gitea.futuresens.co.uk/futuresens/hardlink/logging" "gitea.futuresens.co.uk/futuresens/hardlink/mail" "gitea.futuresens.co.uk/futuresens/hardlink/payment" "gitea.futuresens.co.uk/futuresens/hardlink/printer" ) const ( buildVersion = "1.2.5" serviceName = "hardlink" pollingFrequency = 8 * time.Second ) func main() { // Load config cfg := config.ReadHardlinkConfig() printer.Layout = readTicketLayout() printer.PrinterName = cfg.PrinterName lockserver.Cert = cfg.Cert lockserver.LockServerURL = cfg.LockserverUrl mail.SendErrorEmails = cfg.SendErrorEmails // Root context for background goroutines // rootCtx, rootCancel := context.WithCancel(context.Background()) // defer rootCancel() var ( dispPort *serial.Port disp *dispenser.Client cardWellStatus string ) // Setup logging and get file handle logFile, err := logging.SetupLogging(cfg.LogDir, serviceName, buildVersion) if err != nil { log.Printf("Failed to set up logging: %v\n", err) } if logFile != nil { defer logFile.Close() } // Initialize dispenser if !cfg.TestMode { dispenser.SerialPort = cfg.DispenserPort dispenser.Address = []byte(cfg.DispenserAdrr) dispPort, err = dispenser.InitializeDispenser() if err != nil { mail.SendEmailOnError(cfg.Hotel, cfg.Kiosk, "Dispenser Initialization Error", fmt.Sprintf("Failed to initialize dispenser: %v", err)) errorhandlers.FatalError(err) } defer dispPort.Close() disp = dispenser.NewClient(dispPort, 32) defer disp.Close() ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() cardWellStatus, err = disp.DispenserPrepare(ctx) if err != nil { err = fmt.Errorf("%s; wrong dispenser address: %s", err, cfg.DispenserAdrr) mail.SendEmailOnError(cfg.Hotel, cfg.Kiosk, "Dispenser Preparation Error", err.Error()) errorhandlers.FatalError(err) } fmt.Println(cardWellStatus) } // Test lock-server connection switch strings.ToLower(cfg.LockType) { case lockserver.TLJ: // TLJ uses HTTP - skip TCP probe here default: lockConn, err := lockserver.InitializeServerConnection(cfg.LockserverUrl) if err != nil { fmt.Println(err.Error()) log.Errorf(err.Error()) mail.SendEmailOnError(cfg.Hotel, cfg.Kiosk, "Lock Server Connection Error", err.Error()) } else { fmt.Printf("Connected to the lock server successfuly at %s\n", cfg.LockserverUrl) log.Infof("Connected to the lock server successfuly at %s", cfg.LockserverUrl) lockConn.Close() } } database, err := bootstrap.OpenDB(&cfg) if err != nil { log.Warnf("DB init failed: %v", err) } if database != nil { defer database.Close() } if cfg.IsPayment { fmt.Println("Payment processing is enabled") log.Info("Payment processing is enabled") startChipDnaClient() // check ChipDNA and PDQ status and log any errors, but continue running even if it fails go func() { time.Sleep(30 * time.Second) // give ChipDNA client a moment to start pdqstatus, err := payment.ReadPdqStatus(cfg.Hotel, cfg.Kiosk) if err != nil { mail.SendEmailOnError(cfg.Hotel, cfg.Kiosk, "PDQ Status Read Error", err.Error()) } else { fmt.Printf("\nPDQ availabile: %v\n", pdqstatus.IsAvailable) log.Infof("PDQ availabile: %v", pdqstatus.IsAvailable) } }() } else { fmt.Println("Payment processing is disabled") log.Info("Payment processing is disabled") } // Create App and wire routes app := handlers.NewApp(disp, cfg.LockType, cfg.EncoderAddress, cardWellStatus, database, &cfg) // Update cardWellStatus when dispenser status changes if !cfg.TestMode && disp != nil { // Set initial cardWellStatus app.SetCardWellStatus(cardWellStatus) // Set up callback to update cardWellStatus when dispenser status changes disp.OnStockUpdate(func(stock string) { app.SetCardWellStatus(stock) }) // Start polling for dispenser status every 10 seconds disp.StartPolling(pollingFrequency) } mux := http.NewServeMux() app.RegisterRoutes(mux) addr := fmt.Sprintf(":%d", cfg.Port) log.Infof("Starting HTTP server on http://localhost%s", addr) fmt.Printf("Starting HTTP server on http://localhost%s", addr) if err := http.ListenAndServe(addr, mux); err != nil { errorhandlers.FatalError(err) } } func readTicketLayout() printer.LayoutOptions { const layoutName = "TicketLayout.xml" var layout printer.LayoutOptions // 1) Read the file data, err := os.ReadFile(layoutName) if err != nil { errorhandlers.FatalError(fmt.Errorf("failed to read %s: %v", layoutName, err)) } // 2) Unmarshal into your struct if err := xml.Unmarshal(data, &layout); err != nil { errorhandlers.FatalError(fmt.Errorf("failed to parse %s: %v", layoutName, err)) } return layout } func startChipDnaClient() { startClient := func() (*exec.Cmd, error) { cmd := exec.Command("./ChipDNAClient/ChipDnaClient.exe") err := cmd.Start() if err != nil { return nil, fmt.Errorf("failed to start ChipDnaClient: %v", err) } log.Infof("ChipDnaClient started with PID %d", cmd.Process.Pid) return cmd, nil } cmd, err := startClient() if err != nil { errorhandlers.FatalError(err) } // Restart loop go func() { for { err := cmd.Wait() if err != nil { log.Errorf("ChipDnaClient exited unexpectedly: %v", err) fmt.Printf("ChipDnaClient exited unexpectedly: %v", err) time.Sleep(2 * time.Second) cmd, err = startClient() if err != nil { log.Errorf("Restart failed: %v", err) return } log.Info("ChipDnaClient restarted successfully") fmt.Printf("ChipDnaClient restarted successfully") } } }() // Handle shutdown signals sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) go func() { <-sigs log.Info("Shutting down...") if cmd.Process != nil { log.Info("Sending SIGTERM to ChipDnaClient...") _ = cmd.Process.Signal(syscall.SIGTERM) // wait up to 5s for graceful shutdown done := make(chan error, 1) go func() { done <- cmd.Wait() }() select { case <-time.After(5 * time.Second): log.Warn("ChipDnaClient did not exit in time, killing...") _ = cmd.Process.Kill() case err := <-done: log.Infof("ChipDnaClient exited cleanly: %v", err) } } os.Exit(0) }() }