Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package main

import (
"errors"
"log"
"math"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)

const (
DEFAULT_LISTENER_PORT uint16 = 43383
DEFAULT_STATS_FILENAME string = "./stats.json"
DEFAULT_HEARTBEAT_INTERVAL time.Duration = 30 * time.Second
DEFAULT_INACTIVITY_INTERVAL time.Duration = 5 * time.Minute

ENVVAR_PREFIX string = "ANCHOR"

LISTENER_ADDRESS_ENVVAR string = "LISTENER_ADDRESS"
LISTENER_PORT_ENVVAR string = "LISTENER_PORT"
STATS_FILENAME_ENVVAR string = "STATS_FILENAME"
HEARTBEAT_INTERVAL_S_ENVVAR string = "HEARTBEAT_INTERVAL_S"
INACTIVITY_INTERVAL_S_ENVVAR string = "INACTIVITY_INTERVAL_S"
)

type Configuration struct {
ListenerAddress net.IP
ListenerPort uint16
StatsFilename string
HeartbeatInterval time.Duration
InactivityInterval time.Duration
}

func passthroughString(value string) (string, error) {
return value, nil
}

func toIP(value string) (net.IP, error) {
return net.ParseIP(value), nil
}

func toUint16(value string) (uint16, error) {
intValue, err := strconv.ParseUint(value, 10, 16)

if err != nil {
return 0, err
}

if intValue > math.MaxUint16 {
return 0, errors.New("value is greater than 65355")
}

return uint16(intValue), nil
}

func toDurationSeconds(value string) (time.Duration, error) {
seconds, err := strconv.ParseUint(value, 10, 64)

if err != nil {
return 0, err
}

return time.Duration(seconds) * time.Second, nil
}

func resolveConfigurationValue[T any](envvarKey string, envvarValueAdapter func(string) (T, error), fallbackValue T) T {
fullEnvvarKey := strings.Join([]string{ENVVAR_PREFIX, envvarKey}, "_")
envvarValue, isSet := os.LookupEnv(fullEnvvarKey)

if isSet {
value, err := envvarValueAdapter(envvarValue)

if err == nil {
return value
}

log.Printf("Falling back to default value due to failed parse of value for the environment variable '%s': %e", fullEnvvarKey, err)
}

return fallbackValue
}

func NewConfiguration() (*Configuration, error) {
statsFilename, err := filepath.Abs(
resolveConfigurationValue(STATS_FILENAME_ENVVAR, passthroughString, DEFAULT_STATS_FILENAME),
)

if err != nil {
return nil, err
}

return &Configuration{
ListenerAddress: resolveConfigurationValue(LISTENER_ADDRESS_ENVVAR, toIP, net.IPv6unspecified), // Listen on all IPv4 and IPv6 by default
ListenerPort: resolveConfigurationValue(LISTENER_PORT_ENVVAR, toUint16, DEFAULT_LISTENER_PORT),
StatsFilename: statsFilename,
HeartbeatInterval: resolveConfigurationValue(HEARTBEAT_INTERVAL_S_ENVVAR, toDurationSeconds, DEFAULT_HEARTBEAT_INTERVAL),
InactivityInterval: resolveConfigurationValue(INACTIVITY_INTERVAL_S_ENVVAR, toDurationSeconds, DEFAULT_INACTIVITY_INTERVAL),
}, nil
}

func (c *Configuration) Print() {
log.Println("Server configuration:")
log.Printf(" - Listener Address: %v", c.ListenerAddress)
log.Printf(" - Listener Port: %d", c.ListenerPort)
log.Printf(" - Stats Filename: %s", c.StatsFilename)
log.Printf(" - Heartbeat Interval: %v", c.HeartbeatInterval)
log.Printf(" - Inactivity Interval: %v", c.InactivityInterval)
}

func (c *Configuration) NewTCPAddress() *net.TCPAddr {
return &net.TCPAddr{
IP: c.ListenerAddress,
Port: int(c.ListenerPort),
}
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ module garrettjoecox/anchor
go 1.21.0

require (
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/gjson v1.18.0
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/tidwall/sjson v1.2.5
)
14 changes: 13 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,15 @@ import (
)

func main() {
server := NewServer()
PrintRuntime()
config, err := NewConfiguration()

if err != nil {
log.Fatalf("Failed configuration discovery: %e", err)
}

config.Print()
server := NewServer(config)

errChan := make(chan error)
sigsCa := make(chan os.Signal, 1)
Expand Down Expand Up @@ -45,6 +53,10 @@ func main() {
server.Start(errChan)
}

func PrintRuntime() {
log.Printf("Anchor server running on %s/%s, built using %v", runtime.GOOS, runtime.GOARCH, runtime.Version())
}

func getMessage(input []string) string {
var message bytes.Buffer

Expand Down
28 changes: 16 additions & 12 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ import (
)

const JSON_TEMPLATE = `{"gameCompleteCount":0,"onlineCount":0,"lastStatsHeartbeat":"","uniqueCount":0,"pid":0}`
const INACTIVITY_TIMEOUT = 5 * time.Minute
const HEARTBEAT = 30 * time.Second

type Server struct {
configuration *Configuration
listener net.Listener
quietMode atomic.Bool
onlineClients sync.Map
Expand All @@ -30,8 +29,9 @@ type Server struct {
nextClientId atomic.Uint64
}

func NewServer() *Server {
func NewServer(configuration *Configuration) *Server {
return &Server{
configuration: configuration,
onlineClients: sync.Map{},
quietMode: atomic.Bool{},
rooms: sync.Map{},
Expand All @@ -40,22 +40,26 @@ func NewServer() *Server {
}
}

func (s *Server) Start(errChan chan error) {
listener, err := net.Listen("tcp", ":43383")
func (s *Server) openListener() {
listener, err := net.ListenTCP("tcp", s.configuration.NewTCPAddress())
if err != nil {
log.Fatal(err)
}
s.listener = listener
}

func (s *Server) Start(errChan chan error) {
s.openListener()

go s.cleanupInactiveRooms(errChan)
go s.heartbeat(errChan)
go s.parseStats(errChan)
go s.statsHeartbeat(errChan)

log.Println("Server running on :43383")
log.Printf("Server listening on %v", s.listener.Addr())

for {
conn, err := listener.Accept()
conn, err := s.listener.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
log.Println("Error with listener:", err)
Expand Down Expand Up @@ -114,7 +118,7 @@ func (s *Server) saveStats() {
}

func (s *Server) cleanupInactiveRooms(errChan chan error) {
ticker := time.NewTicker(HEARTBEAT)
ticker := time.NewTicker(s.configuration.HeartbeatInterval)
defer ticker.Stop()
defer func() {
if r := recover(); r != nil {
Expand All @@ -126,7 +130,7 @@ func (s *Server) cleanupInactiveRooms(errChan chan error) {
s.rooms.Range(func(id, value interface{}) bool {
room := value.(*Room)
lastActivity := room.GetLastActivity()
if time.Since(lastActivity) > INACTIVITY_TIMEOUT {
if time.Since(lastActivity) > s.configuration.InactivityInterval {
log.Println("Room", id, "has been inactive for too long, deleting it")
s.rooms.Delete(id)
}
Expand All @@ -136,7 +140,7 @@ func (s *Server) cleanupInactiveRooms(errChan chan error) {
}

func (s *Server) statsHeartbeat(errChan chan error) {
ticker := time.NewTicker(HEARTBEAT)
ticker := time.NewTicker(s.configuration.HeartbeatInterval)
defer ticker.Stop()
defer func() {
if r := recover(); r != nil {
Expand All @@ -150,7 +154,7 @@ func (s *Server) statsHeartbeat(errChan chan error) {
}

func (s *Server) heartbeat(errChan chan error) {
ticker := time.NewTicker(HEARTBEAT)
ticker := time.NewTicker(s.configuration.HeartbeatInterval)
defer ticker.Stop()
defer func() {
if r := recover(); r != nil {
Expand All @@ -165,7 +169,7 @@ func (s *Server) heartbeat(errChan chan error) {

s.onlineClients.Range(func(_, value interface{}) bool {
client := value.(*Client)
if time.Since(client.lastActivity) > HEARTBEAT {
if time.Since(client.lastActivity) > s.configuration.HeartbeatInterval {
go client.sendPacket(`{"type":"HEARTBEAT","quiet":true}`)
}
return true
Expand Down