diff --git a/TCPClient.go b/TCPClient.go index 5c1e76b..ec27a8d 100644 --- a/TCPClient.go +++ b/TCPClient.go @@ -19,6 +19,21 @@ var spotRe *regexp.Regexp = regexp.MustCompile(`DX\sde\s([\w\d]+).*:\s+(\d+.\d)\ var defaultLoginRe *regexp.Regexp = regexp.MustCompile("[\\w\\d-_]+ login:") var defaultPasswordRe *regexp.Regexp = regexp.MustCompile("Password:") +const ( + // Reconnection settings + MaxReconnectAttempts = 10 + BaseReconnectDelay = 1 * time.Second + MaxReconnectDelay = 60 * time.Second + + // Timeout settings + ConnectionTimeout = 10 * time.Second + LoginTimeout = 30 * time.Second + ReadTimeout = 5 * time.Minute + + // Channel buffer sizes + SpotChannelBuffer = 100 +) + type TCPClient struct { Login string Password string @@ -62,15 +77,15 @@ func NewTCPClient(TCPServer *TCPServer, Countries Countries, contactRepo *Log4OM MsgChan: TCPServer.MsgChan, CmdChan: TCPServer.CmdChan, SpotChanToHTTPServer: spotChanToHTTPServer, - SpotChanToFlex: make(chan TelnetSpot, 100), + SpotChanToFlex: make(chan TelnetSpot, SpotChannelBuffer), TCPServer: *TCPServer, Countries: Countries, ContactRepo: contactRepo, ctx: ctx, cancel: cancel, - maxReconnectAttempts: 10, // Max 10 tentatives avant abandon - baseReconnectDelay: 1 * time.Second, // Délai initial - maxReconnectDelay: 60 * time.Second, // Max 1 minute entre tentatives + maxReconnectAttempts: MaxReconnectAttempts, + baseReconnectDelay: BaseReconnectDelay, + maxReconnectDelay: MaxReconnectDelay, } } @@ -108,7 +123,7 @@ func (c *TCPClient) connect() error { Log.Debugf("Attempting to connect to %s (attempt %d/%d)", addr, c.reconnectAttempts+1, c.maxReconnectAttempts) - conn, err := net.DialTimeout("tcp", addr, 10*time.Second) + conn, err := net.DialTimeout("tcp", addr, ConnectionTimeout) if err != nil { return fmt.Errorf("failed to connect to %s: %w", addr, err) } @@ -267,12 +282,19 @@ func (c *TCPClient) ReadLine() { } if c.LoggedIn { + // Check for cancellation before reading + select { + case <-c.ctx.Done(): + return + default: + } + // Lecture avec timeout pour détecter les connexions mortes - c.Conn.SetReadDeadline(time.Now().Add(5 * time.Minute)) + c.Conn.SetReadDeadline(time.Now().Add(ReadTimeout)) message, err := c.Reader.ReadBytes('\n') if err != nil { Log.Errorf("Error reading message: %s", err) - return // ✅ Retour au lieu de récursion - la boucle principale va reconnecter + return } c.Conn.SetReadDeadline(time.Time{}) // Reset deadline diff --git a/TCPServer.go b/TCPServer.go index c178c26..b6bad56 100644 --- a/TCPServer.go +++ b/TCPServer.go @@ -2,7 +2,6 @@ package main import ( "bufio" - "fmt" "net" "os" "strings" @@ -78,37 +77,45 @@ func (s *TCPServer) StartServer() { } func (s *TCPServer) handleConnection() { - s.Conn.Write([]byte("Welcome to the FlexDXCluster telnet server! Type 'bye' to exit.\n")) + // Store the connection locally to avoid race conditions + conn := s.Conn - s.Reader = bufio.NewReader(s.Conn) - s.Writer = bufio.NewWriter(s.Conn) + conn.Write([]byte("Welcome to the FlexDXCluster telnet server! Type 'bye' to exit.\n")) + + reader := bufio.NewReader(conn) + + defer func() { + s.Mutex.Lock() + delete(s.Clients, conn) + s.Mutex.Unlock() + conn.Close() + Log.Infof("Client %s disconnected", conn.RemoteAddr().String()) + }() for { - - message, err := s.Reader.ReadString('\n') + message, err := reader.ReadString('\n') if err != nil { - s.Mutex.Lock() - delete(s.Clients, s.Conn) - s.Mutex.Unlock() + Log.Debugf("Error reading from client %s: %v", conn.RemoteAddr().String(), err) return } message = strings.TrimSpace(message) - // if message is by then disconnect + // if message is bye then disconnect if message == "bye" { - s.Mutex.Lock() - delete(s.Clients, s.Conn) - s.Mutex.Unlock() - s.Conn.Close() - Log.Infof("client %s disconnected", s.Conn.RemoteAddr().String()) + Log.Infof("Client %s sent bye command", conn.RemoteAddr().String()) + return } if strings.Contains(message, "DX") || strings.Contains(message, "SH/DX") || strings.Contains(message, "set") || strings.Contains(message, "SET") { // send DX spot to the client - s.CmdChan <- message + select { + case s.CmdChan <- message: + Log.Debugf("Command from client %s: %s", conn.RemoteAddr().String(), message) + default: + Log.Warn("Command channel is full, dropping command") + } } - } } @@ -123,17 +130,31 @@ func (s *TCPServer) Write(message string) (n int, err error) { func (s *TCPServer) broadcastMessage(message string) { s.Mutex.Lock() defer s.Mutex.Unlock() - if len(s.Clients) > 0 { - if s.MessageSent == 0 { - time.Sleep(3 * time.Second) - s.MessageSent += 1 - } - for client := range s.Clients { - _, err := client.Write([]byte(message + "\r\n")) - s.MessageSent += 1 - if err != nil { - fmt.Println("Error while sending message to clients:", client.RemoteAddr()) - } + + if len(s.Clients) == 0 { + return + } + + if s.MessageSent == 0 { + time.Sleep(3 * time.Second) + s.MessageSent = 1 + } + + // Collect failed clients + var failedClients []net.Conn + + for client := range s.Clients { + _, err := client.Write([]byte(message + "\r\n")) + s.MessageSent++ + if err != nil { + Log.Warnf("Error sending to client %s: %v", client.RemoteAddr(), err) + failedClients = append(failedClients, client) } } + + // Remove failed clients + for _, client := range failedClients { + delete(s.Clients, client) + client.Close() + } } diff --git a/config default.yml b/config default.yml index d56e9f2..5be988b 100644 --- a/config default.yml +++ b/config default.yml @@ -5,7 +5,7 @@ general: log_level: INFO # INFO or DEBUG or WARN telnetserver: true # not in use for now flexradiospot: true - send_freq_log: true # if not using a Flex then turn this on to send Freq and Mode to Log4OM which should in turn change the freq on the radio + sendFreqModeToLog4OM: true # if not using a Flex then turn this on to send Freq and Mode to Log4OM which should in turn change the freq on the radio # Spot colors, if empty then default, colors in HEX AARRGGBB format spot_color_new_entity: background_color_new_entity: diff --git a/config.go b/config.go index f73dd66..8c256ee 100644 --- a/config.go +++ b/config.go @@ -18,7 +18,7 @@ type Config struct { LogLevel string `yaml:"log_level"` TelnetServer bool `yaml:"telnetserver"` FlexRadioSpot bool `yaml:"flexradiospot"` - SendFreqModeToLog bool `yaml:"send_freq_log"` + SendFreqModeToLog bool `yaml:"sendFreqModeToLog4OM"` SpotColorNewEntity string `yaml:"spot_color_new_entity"` BackgroundColorNewEntity string `yaml:"background_color_new_entity"` SpotColorNewBand string `yaml:"spot_color_new_band"` diff --git a/database.go b/database.go index 2f6bae6..8c5d5c6 100644 --- a/database.go +++ b/database.go @@ -24,11 +24,6 @@ type Contact struct { Country string } -type Spotter struct { - Spotter string - NumberofSpots string -} - type QSO struct { Callsign string `json:"callsign"` Band string `json:"band"` @@ -65,6 +60,11 @@ func NewLog4OMContactsRepository(filePath string) *Log4OMContactsRepository { Log.Errorf("Cannot open db", err) } + // Configure connection pool + db.SetMaxOpenConns(25) + db.SetMaxIdleConns(5) + db.SetConnMaxLifetime(5 * time.Minute) + return &Log4OMContactsRepository{ db: db, Log: Log} @@ -74,6 +74,11 @@ func NewLog4OMContactsRepository(filePath string) *Log4OMContactsRepository { if err != nil { Log.Errorf("Cannot open db", err) } + + // Configure connection pool for SQLite + db.SetMaxOpenConns(1) // SQLite works best with single connection for writes + db.SetMaxIdleConns(1) + _, err = db.Exec("PRAGMA journal_mode=WAL") if err != nil { panic(err) @@ -91,11 +96,14 @@ func NewFlexDXDatabase(filePath string) *FlexDXClusterRepository { db, err := sql.Open("sqlite3", filePath) if err != nil { - fmt.Println("Cannot open db", err) + Log.Errorf("Cannot open db: %v", err) } Log.Debugln("Opening SQLite database") + db.SetMaxOpenConns(1) + db.SetMaxIdleConns(1) + _, err = db.ExecContext( context.Background(), `CREATE TABLE IF NOT EXISTS "spots" ( @@ -204,7 +212,7 @@ func (r *Log4OMContactsRepository) ListByCountryMode(countryID string, mode stri for rows.Next() { c := Contact{} if err := rows.Scan(&c.Callsign, &c.Band, &c.Mode, &c.DXCC, &c.StationCallsign, &c.Country); err != nil { - fmt.Println(err) + r.Log.Println(err) } contacts = append(contacts, c) @@ -241,7 +249,7 @@ func (r *Log4OMContactsRepository) ListByCountryModeBand(countryID string, band rows, err := r.db.Query("SELECT callsign, band, mode, dxcc, stationcallsign, country FROM log WHERE dxcc = ? AND mode = ? AND band = ?", countryID, mode, band) if err != nil { - log.Error("could not query the database", err) + r.Log.Error("could not query the database", err) } defer rows.Close() @@ -250,7 +258,7 @@ func (r *Log4OMContactsRepository) ListByCountryModeBand(countryID string, band for rows.Next() { c := Contact{} if err := rows.Scan(&c.Callsign, &c.Band, &c.Mode, &c.DXCC, &c.StationCallsign, &c.Country); err != nil { - fmt.Println(err) + r.Log.Error(err) } contacts = append(contacts, c) @@ -264,7 +272,7 @@ func (r *Log4OMContactsRepository) ListByCountryBand(countryID string, band stri defer wg.Done() rows, err := r.db.Query("SELECT callsign, band, mode, dxcc, stationcallsign, country FROM log WHERE dxcc = ? AND band = ?", countryID, band) if err != nil { - fmt.Println(err) + r.Log.Error(err) } defer rows.Close() @@ -273,7 +281,7 @@ func (r *Log4OMContactsRepository) ListByCountryBand(countryID string, band stri for rows.Next() { c := Contact{} if err := rows.Scan(&c.Callsign, &c.Band, &c.Mode, &c.DXCC, &c.StationCallsign, &c.Country); err != nil { - fmt.Println(err) + r.Log.Error(err) } contacts = append(contacts, c) @@ -285,7 +293,7 @@ func (r *Log4OMContactsRepository) ListByCallSign(callSign string, band string, defer wg.Done() rows, err := r.db.Query("SELECT callsign, band, mode, dxcc, stationcallsign, country FROM log WHERE callsign = ? AND band = ? AND mode = ?", callSign, band, mode) if err != nil { - fmt.Println(err) + r.Log.Error(err) } defer rows.Close() @@ -294,7 +302,7 @@ func (r *Log4OMContactsRepository) ListByCallSign(callSign string, band string, for rows.Next() { c := Contact{} if err := rows.Scan(&c.Callsign, &c.Band, &c.Mode, &c.DXCC, &c.StationCallsign, &c.Country); err != nil { - fmt.Println(err) + r.Log.Error(err) } contacts = append(contacts, c) @@ -461,32 +469,6 @@ func (r *FlexDXClusterRepository) GetAllSpots(limit string) []FlexSpot { return Spots } -func (r *FlexDXClusterRepository) GetSpotters() []Spotter { - - sList := []Spotter{} - - rows, err := r.db.Query("select spotter, count(*) as occurences from spots group by spotter order by occurences desc, spotter limit 15") - - if err != nil { - r.Log.Error(err) - return nil - } - - defer rows.Close() - - s := Spotter{} - for rows.Next() { - if err := rows.Scan(&s.Spotter, &s.NumberofSpots); err != nil { - fmt.Println(err) - return nil - } - - sList = append(sList, s) - } - - return sList -} - func (r *FlexDXClusterRepository) FindDXSameBand(spot FlexSpot) (*FlexSpot, error) { rows, err := r.db.Query("SELECT * from spots WHERE dx = ? AND band = ?", spot.DX, spot.Band) if err != nil { @@ -500,7 +482,7 @@ func (r *FlexDXClusterRepository) FindDXSameBand(spot FlexSpot) (*FlexSpot, erro for rows.Next() { if err := rows.Scan(&s.ID, &s.CommandNumber, &s.FlexSpotNumber, &s.DX, &s.FrequencyMhz, &s.FrequencyHz, &s.Band, &s.Mode, &s.SpotterCallsign, &s.FlexMode, &s.Source, &s.UTCTime, &s.TimeStamp, &s.LifeTime, &s.Priority, &s.Comment, &s.Color, &s.BackgroundColor, &s.CountryName, &s.DXCC, &s.NewDXCC, &s.NewBand, &s.NewMode, &s.NewSlot, &s.Worked); err != nil { - fmt.Println(err) + r.Log.Error(err) return nil, err } } @@ -534,7 +516,7 @@ func (r *FlexDXClusterRepository) UpdateSpotSameBand(spot FlexSpot) error { func (r *FlexDXClusterRepository) FindSpotByCommandNumber(commandNumber string) (*FlexSpot, error) { rows, err := r.db.Query("SELECT * from spots WHERE commandNumber = ?", commandNumber) if err != nil { - fmt.Println(err) + r.Log.Error(err) return nil, err } @@ -544,7 +526,7 @@ func (r *FlexDXClusterRepository) FindSpotByCommandNumber(commandNumber string) for rows.Next() { if err := rows.Scan(&s.ID, &s.CommandNumber, &s.FlexSpotNumber, &s.DX, &s.FrequencyMhz, &s.FrequencyHz, &s.Band, &s.Mode, &s.SpotterCallsign, &s.FlexMode, &s.Source, &s.UTCTime, &s.TimeStamp, &s.LifeTime, &s.Priority, &s.Comment, &s.Color, &s.BackgroundColor, &s.CountryName, &s.DXCC, &s.NewDXCC, &s.NewBand, &s.NewMode, &s.NewSlot, &s.Worked); err != nil { - fmt.Println(err) + r.Log.Error(err) return nil, err } } @@ -554,7 +536,7 @@ func (r *FlexDXClusterRepository) FindSpotByCommandNumber(commandNumber string) func (r *FlexDXClusterRepository) FindSpotByFlexSpotNumber(spotNumber string) (*FlexSpot, error) { rows, err := r.db.Query("SELECT * from spots WHERE flexSpotNumber = ?", spotNumber) if err != nil { - fmt.Println(err) + r.Log.Error(err) return nil, err } @@ -564,7 +546,7 @@ func (r *FlexDXClusterRepository) FindSpotByFlexSpotNumber(spotNumber string) (* for rows.Next() { if err := rows.Scan(&s.ID, &s.CommandNumber, &s.FlexSpotNumber, &s.DX, &s.FrequencyMhz, &s.FrequencyHz, &s.Band, &s.Mode, &s.SpotterCallsign, &s.FlexMode, &s.Source, &s.UTCTime, &s.TimeStamp, &s.LifeTime, &s.Priority, &s.Comment, &s.Color, &s.BackgroundColor, &s.CountryName, &s.DXCC, &s.NewDXCC, &s.NewBand, &s.NewMode, &s.NewSlot, &s.Worked); err != nil { - fmt.Println(err) + r.Log.Error(err) return nil, err } } @@ -584,7 +566,7 @@ func (r *FlexDXClusterRepository) UpdateFlexSpotNumberByID(flexSpotNumber string for rows.Next() { if err := rows.Scan(&s.ID, &s.CommandNumber, &s.FlexSpotNumber, &s.DX, &s.FrequencyMhz, &s.FrequencyHz, &s.Band, &s.Mode, &s.SpotterCallsign, &s.FlexMode, &s.Source, &s.UTCTime, &s.TimeStamp, &s.LifeTime, &s.Priority, &s.Comment, &s.Color, &s.BackgroundColor, &s.CountryName, &s.DXCC, &s.NewDXCC, &s.NewBand, &s.NewMode, &s.NewSlot, &s.Worked); err != nil { - fmt.Println(err) + r.Log.Error(err) return nil, err } } diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index d32af5d..7b214f3 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -1,5 +1,6 @@
@@ -59,14 +65,15 @@
-
DX
-
Country
-
Freq
-
Band
-
Mode
-
Spotter
-
Time
-
Status
+
DX
+
Country
+
Freq
+
Band
+
Mode
+
Spotter
+
Time
+
Comment
+
Status
@@ -74,7 +81,7 @@
-
+
-
+
{item.CountryName || 'N/A'}
-
{item.FrequencyMhz}
-
+
{item.FrequencyMhz}
+
{item.Band}
-
+
{item.Mode}
-
+
{item.SpotterCallsign}
-
{item.UTCTime}
-
+
{item.UTCTime}
+
+ {getCleanComment(item)} +
+
{#if getStatusLabel(item)} {getStatusLabel(item)} diff --git a/frontend/src/components/StatsCards.svelte b/frontend/src/components/StatsCards.svelte index 76829e5..99c4779 100644 --- a/frontend/src/components/StatsCards.svelte +++ b/frontend/src/components/StatsCards.svelte @@ -10,7 +10,8 @@ } -
+
+
@@ -21,6 +22,7 @@

Total Spots

+
@@ -31,71 +33,75 @@

New DXCC

-
-
- - - -
{stats.activeSpotters}
-
-

Spotters

-
- +
- +
{stats.connectedClients}

Clients

-
-
-