package main import ( "encoding/xml" "fmt" "net/http" "os" "os/exec" "os/signal" "strings" "syscall" "time" "github.com/tarm/serial" log "github.com/sirupsen/logrus" yaml "gopkg.in/yaml.v3" "gitea.futuresens.co.uk/futuresens/hardlink/dispenser" "gitea.futuresens.co.uk/futuresens/hardlink/handlers" "gitea.futuresens.co.uk/futuresens/hardlink/lockserver" "gitea.futuresens.co.uk/futuresens/hardlink/printer" ) const ( buildVersion = "1.0.27" serviceName = "hardlink" ) // configRec holds values from config.yml. type configRec struct { Port int `yaml:"port"` LockserverUrl string `yaml:"lockservUrl"` LockType string `yaml:"lockType"` EncoderAddress string `yaml:"encoderAddr"` Cert string `yaml:"cert"` DispenserPort string `yaml:"dispensPort"` DispenserAdrr string `yaml:"dispensAddr"` PrinterName string `yaml:"printerName"` LogDir string `yaml:"logdir"` IsPayment bool `yaml:"isPayment"` TestMode bool `yaml:"testMode"` } func main() { // Load config config := readConfig() printer.Layout = readTicketLayout() printer.PrinterName = config.PrinterName lockserver.Cert = config.Cert lockserver.LockServerURL = config.LockserverUrl dispHandle := &serial.Port{} // Setup logging and get file handle logFile, err := setupLogging(config.LogDir) if err != nil { log.Printf("Failed to set up logging: %v\n", err) } defer logFile.Close() // Initialize dispenser if !config.TestMode { dispenser.SerialPort = config.DispenserPort dispenser.Address = []byte(config.DispenserAdrr) dispHandle, err = dispenser.InitializeDispenser() if err != nil { handlers.FatalError(err) } defer dispHandle.Close() status, err := dispenser.CheckDispenserStatus(dispHandle) if err != nil { if len(status) == 0 { err = fmt.Errorf("%s; wrong dispenser address: %s", err, config.DispenserAdrr) handlers.FatalError(err) } else { fmt.Println(status) fmt.Println(err.Error()) } } log.Infof("Dispenser initialized on port %s, %s", config.DispenserPort, status) } // Test lock-server connection switch strings.ToLower(config.LockType) { case lockserver.TLJ: default: lockConn, err := lockserver.InitializeServerConnection(config.LockserverUrl) if err != nil { fmt.Println(err.Error()) log.Errorf(err.Error()) } else { fmt.Printf("Connected to the lock server successfuly at %s\n", config.LockserverUrl) log.Infof("Connected to the lock server successfuly at %s", config.LockserverUrl) lockConn.Close() } } if config.IsPayment { fmt.Println("Payment processing is enabled") log.Info("Payment processing is enabled") startChipDnaClient() } else { fmt.Println("Payment processing is disabled") log.Info("Payment processing is disabled") } // Create App and wire routes app := handlers.NewApp(dispHandle, config.LockType, config.EncoderAddress, config.IsPayment) mux := http.NewServeMux() app.RegisterRoutes(mux) addr := fmt.Sprintf(":%d", config.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 { handlers.FatalError(err) } } // setupLogging ensures log directory, opens log file, and configures logrus. // Returns the *os.File so caller can defer its Close(). func setupLogging(logDir string) (*os.File, error) { fileName := logDir + serviceName + ".log" f, err := os.OpenFile(fileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) if err != nil { return nil, fmt.Errorf("open log file: %w", err) } log.SetOutput(f) log.SetFormatter(&log.JSONFormatter{ TimestampFormat: time.RFC3339, }) log.SetLevel(log.InfoLevel) log.WithFields(log.Fields{ "buildVersion": buildVersion, }).Info("Logging initialized") return f, nil } // readConfig reads config.yml and applies defaults. func readConfig() configRec { var cfg configRec const configName = "config.yml" defaultPort := 9091 sep := string(os.PathSeparator) data, err := os.ReadFile(configName) if err != nil { log.Warnf("ReadConfig %s: %v", configName, err) } else if err := yaml.Unmarshal(data, &cfg); err != nil { log.Warnf("Unmarshal config: %v", err) } if cfg.Port == 0 { cfg.Port = defaultPort } if cfg.LockType == "" { err = fmt.Errorf("LockType is required in %s", configName) handlers.FatalError(err) } cfg.LockType = strings.ToLower(cfg.LockType) if cfg.LogDir == "" { cfg.LogDir = "./logs" + sep } else if !strings.HasSuffix(cfg.LogDir, sep) { cfg.LogDir += sep } return cfg } func readTicketLayout() printer.LayoutOptions { const layoutName = "TicketLayout.xml" var layout printer.LayoutOptions // 1) Read the file data, err := os.ReadFile(layoutName) if err != nil { handlers.FatalError(fmt.Errorf("failed to read %s: %v", layoutName, err)) } // 2) Unmarshal into your struct if err := xml.Unmarshal(data, &layout); err != nil { handlers.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 { handlers.FatalError(err) } // Restart loop go func() { for { err := cmd.Wait() if err != nil { log.Errorf("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") } } }() // 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) }() }