Add ten minute read/write timeouts

This commit is contained in:
Trevor Slocum 2023-08-26 23:44:41 -07:00
parent 6acce4f905
commit ce59fe7598
8 changed files with 220 additions and 61 deletions

View file

@ -1,5 +1,7 @@
package bgammon
type Client interface {
Terminate(reason string) error
Write(message []byte)
Terminate(reason string)
Terminated() bool
}

View file

@ -3,6 +3,7 @@ package main
import (
"encoding/json"
"fmt"
"log"
"code.rocket9labs.com/tslocum/bgammon"
)
@ -16,7 +17,6 @@ type serverClient struct {
lastActive int64
lastPing int64
commands <-chan []byte
events chan<- []byte
playerNumber int
bgammon.Client
}
@ -25,42 +25,48 @@ func (c *serverClient) sendEvent(e interface{}) {
if c.json {
switch ev := e.(type) {
case *bgammon.EventWelcome:
ev.Type = "welcome"
ev.Type = bgammon.EventTypeWelcome
case *bgammon.EventPing:
ev.Type = bgammon.EventTypePing
case *bgammon.EventNotice:
ev.Type = "notice"
ev.Type = bgammon.EventTypeNotice
case *bgammon.EventSay:
ev.Type = "say"
ev.Type = bgammon.EventTypeSay
case *bgammon.EventList:
ev.Type = "list"
ev.Type = bgammon.EventTypeList
case *bgammon.EventJoined:
ev.Type = "joined"
ev.Type = bgammon.EventTypeJoined
case *bgammon.EventFailedJoin:
ev.Type = "failedjoin"
ev.Type = bgammon.EventTypeFailedJoin
case *bgammon.EventBoard:
ev.Type = "board"
ev.Type = bgammon.EventTypeBoard
case *bgammon.EventRolled:
ev.Type = "rolled"
ev.Type = bgammon.EventTypeRolled
case *bgammon.EventMoved:
ev.Type = "moved"
ev.Type = bgammon.EventTypeMoved
default:
log.Panicf("unknown event type %+v", ev)
}
buf, err := json.Marshal(e)
if err != nil {
panic(err)
}
c.events <- buf
c.Write(buf)
return
}
switch ev := e.(type) {
case *bgammon.EventWelcome:
c.events <- []byte(fmt.Sprintf("welcome %s there are %d clients playing %d games.", ev.PlayerName, ev.Clients, ev.Games))
c.Write([]byte(fmt.Sprintf("welcome %s there are %d clients playing %d games.", ev.PlayerName, ev.Clients, ev.Games)))
case *bgammon.EventPing:
c.Write([]byte(fmt.Sprintf("ping %s", ev.Message)))
case *bgammon.EventNotice:
c.events <- []byte(fmt.Sprintf("notice %s", ev.Message))
c.Write([]byte(fmt.Sprintf("notice %s", ev.Message)))
case *bgammon.EventSay:
c.events <- []byte(fmt.Sprintf("say %s %s", ev.Player, ev.Message))
c.Write([]byte(fmt.Sprintf("say %s %s", ev.Player, ev.Message)))
case *bgammon.EventList:
c.events <- []byte("liststart Games list:")
c.Write([]byte("liststart Games list:"))
for _, g := range ev.Games {
password := 0
if g.Password {
@ -70,17 +76,19 @@ func (c *serverClient) sendEvent(e interface{}) {
if g.Name != "" {
name = g.Name
}
c.events <- []byte(fmt.Sprintf("game %d %d %d %s", g.ID, password, g.Players, name))
c.Write([]byte(fmt.Sprintf("game %d %d %d %s", g.ID, password, g.Players, name)))
}
c.events <- []byte("listend End of games list.")
c.Write([]byte("listend End of games list."))
case *bgammon.EventJoined:
c.events <- []byte(fmt.Sprintf("joined %d %s", ev.GameID, ev.Player))
c.Write([]byte(fmt.Sprintf("joined %d %s", ev.GameID, ev.Player)))
case *bgammon.EventFailedJoin:
c.events <- []byte(fmt.Sprintf("failedjoin %s", ev.Reason))
c.Write([]byte(fmt.Sprintf("failedjoin %s", ev.Reason)))
case *bgammon.EventRolled:
c.events <- []byte(fmt.Sprintf("rolled %s %d %d", ev.Player, ev.Roll1, ev.Roll2))
c.Write([]byte(fmt.Sprintf("rolled %s %d %d", ev.Player, ev.Roll1, ev.Roll2)))
case *bgammon.EventMoved:
c.events <- []byte(fmt.Sprintf("moved %s %s", ev.Player, bgammon.FormatMoves(ev.Moves, c.playerNumber)))
c.Write([]byte(fmt.Sprintf("moved %s %s", ev.Player, bgammon.FormatMoves(ev.Moves, c.playerNumber))))
default:
log.Panicf("unknown event type %+v", ev)
}
}

View file

@ -5,6 +5,8 @@ import (
"fmt"
"log"
"net"
"sync"
"time"
"code.rocket9labs.com/tslocum/bgammon"
)
@ -12,12 +14,14 @@ import (
var _ bgammon.Client = &socketClient{}
type socketClient struct {
conn net.Conn
events <-chan []byte
commands chan<- []byte
conn net.Conn
events chan []byte
commands chan<- []byte
terminated bool
wgEvents sync.WaitGroup
}
func newSocketClient(conn net.Conn, commands chan<- []byte, events <-chan []byte) *socketClient {
func newSocketClient(conn net.Conn, commands chan<- []byte, events chan []byte) *socketClient {
c := &socketClient{
conn: conn,
events: events,
@ -28,29 +32,90 @@ func newSocketClient(conn net.Conn, commands chan<- []byte, events <-chan []byte
return c
}
func (c *socketClient) Write(message []byte) {
if c.terminated {
return
}
c.wgEvents.Add(1)
c.events <- message
}
func (c *socketClient) readCommands() {
setTimeout := func() {
err := c.conn.SetReadDeadline(time.Now().Add(clientTimeout))
if err != nil {
c.Terminate(err.Error())
return
}
}
setTimeout()
var scanner = bufio.NewScanner(c.conn)
for scanner.Scan() {
if c.terminated {
continue // TODO wait group
}
if scanner.Err() != nil {
c.Terminate(scanner.Err().Error())
return
}
buf := make([]byte, len(scanner.Bytes()))
copy(buf, scanner.Bytes())
c.commands <- buf
log.Printf("<- %s", scanner.Bytes())
setTimeout()
}
}
func (c *socketClient) writeEvents() {
setTimeout := func() {
err := c.conn.SetWriteDeadline(time.Now().Add(clientTimeout))
if err != nil {
c.Terminate(err.Error())
return
}
}
var event []byte
for event = range c.events {
c.conn.Write(event)
c.conn.Write([]byte("\n"))
if c.terminated {
c.wgEvents.Done()
continue
}
setTimeout()
_, err := c.conn.Write(append(event, '\n'))
if err != nil {
c.Terminate(err.Error())
c.wgEvents.Done()
continue
}
log.Printf("-> %s", event)
c.wgEvents.Done()
}
}
func (c *socketClient) Terminate(reason string) error {
func (c *socketClient) Terminate(reason string) {
if c.terminated {
return
}
c.terminated = true
c.conn.Write([]byte(fmt.Sprintf("Connection closed: %s\n", reason)))
c.conn.Close()
return nil
go func() {
time.Sleep(5 * time.Second)
c.wgEvents.Wait()
close(c.events)
close(c.commands)
log.Println("FINISHED CLEANUP")
}()
}
func (c *socketClient) Terminated() bool {
return c.terminated
}

View file

@ -84,7 +84,7 @@ func (g *serverGame) sendBoard(client *serverClient) {
scanner := bufio.NewScanner(bytes.NewReader(g.BoardState(playerNumber)))
for scanner.Scan() {
client.events <- append([]byte("notice "), scanner.Bytes()...)
client.Write(append([]byte("notice "), scanner.Bytes()...))
}
}
@ -172,7 +172,7 @@ func (g *serverGame) removeClient(client *serverClient) {
if opponent == nil {
return
}
opponent.events <- []byte(fmt.Sprintf("left %d %s %s", g.id, client.name, g.name))
opponent.Write([]byte(fmt.Sprintf("left %d %s %s", g.id, client.name, g.name)))
if !opponent.json {
g.sendBoard(opponent)
}

View file

@ -14,6 +14,8 @@ import (
"code.rocket9labs.com/tslocum/bgammon"
)
const clientTimeout = 10 * time.Minute
type serverCommand struct {
client *serverClient
command []byte
@ -76,7 +78,6 @@ func (s *server) handleConnection(conn net.Conn) {
connected: now,
lastActive: now,
commands: commands,
events: events,
Client: newSocketClient(conn, commands, events),
}
log.Println("socket client", c)
@ -89,17 +90,25 @@ func (s *server) handleConnection(conn net.Conn) {
func (s *server) handlePingClient(c *serverClient) {
// TODO only ping when there is no recent activity
t := time.NewTicker(time.Minute * 2)
t := time.NewTicker(time.Minute * 4)
for {
<-t.C
if c.Terminated() {
t.Stop()
return
}
if len(c.name) == 0 {
c.Terminate("User did not send login command within 2 minutes.")
t.Stop()
return
}
c.lastPing = time.Now().Unix()
c.events <- []byte(fmt.Sprintf("ping %d", c.lastPing))
c.sendEvent(&bgammon.EventPing{
Message: fmt.Sprintf("%d", c.lastPing),
})
}
}
@ -136,7 +145,7 @@ func (s *server) randomUsername() []byte {
}
func (s *server) sendHello(c *serverClient) {
c.events <- []byte("hello Welcome to bgammon.org! Please log in by sending the 'login' command. You may specify a username, otherwise you will be assigned a random username. If you specify a username, you may also specify a password. Have fun!")
c.Write([]byte("hello Welcome to bgammon.org! Please log in by sending the 'login' command. You may specify a username, otherwise you will be assigned a random username. If you specify a username, you may also specify a password. Have fun!"))
}
func (s *server) sendWelcome(c *serverClient) {
@ -233,12 +242,13 @@ COMMANDS:
switch keyword {
case bgammon.CommandHelp, "h":
// TODO get extended help by specifying a command after help
cmd.client.events <- []byte("helpstart Help text:")
cmd.client.events <- []byte("help Test help text")
cmd.client.events <- []byte("helpend End of help text.")
cmd.client.Write([]byte("helpstart Help text:"))
cmd.client.Write([]byte("help Test help text"))
cmd.client.Write([]byte("helpend End of help text."))
// TODO JSON format
case bgammon.CommandJSON:
sendUsage := func() {
cmd.client.events <- []byte("notice To enable JSON formatted messages, send 'json on'. To disable JSON formatted messages, send 'json off'.")
cmd.client.Write([]byte("notice To enable JSON formatted messages, send 'json on'. To disable JSON formatted messages, send 'json off'."))
}
if len(params) != 1 {
sendUsage()
@ -248,10 +258,10 @@ COMMANDS:
switch paramLower {
case "on":
cmd.client.json = true
cmd.client.events <- []byte("json JSON formatted messages enabled.")
cmd.client.Write([]byte("json JSON formatted messages enabled.")) // TODO send in JSON format
case "off":
cmd.client.json = false
cmd.client.events <- []byte("json JSON formatted messages disabled.")
cmd.client.Write([]byte("json JSON formatted messages disabled."))
default:
sendUsage()
}
@ -260,15 +270,15 @@ COMMANDS:
continue
}
if clientGame == nil {
cmd.client.events <- []byte("notice Message not sent. You are not currently in a game.")
cmd.client.Write([]byte("notice Message not sent. You are not currently in a game."))
continue
}
opponent := clientGame.opponent(cmd.client)
if opponent == nil {
cmd.client.events <- []byte("notice Message not sent. There is no one else in the game.")
cmd.client.Write([]byte("notice Message not sent. There is no one else in the game."))
continue
}
opponent.events <- []byte(fmt.Sprintf("say %s %s", cmd.client.name, bytes.Join(params, []byte(" "))))
opponent.Write([]byte(fmt.Sprintf("say %s %s", cmd.client.name, bytes.Join(params, []byte(" ")))))
case bgammon.CommandList, "ls":
ev := &bgammon.EventList{}
for _, g := range s.games {
@ -282,7 +292,7 @@ COMMANDS:
cmd.client.sendEvent(ev)
case bgammon.CommandCreate, "c":
sendUsage := func() {
cmd.client.events <- []byte("notice To create a public game specify whether it is public or private. When creating a private game, a password must also be provided.")
cmd.client.Write([]byte("notice To create a public game specify whether it is public or private. When creating a private game, a password must also be provided."))
}
if len(params) == 0 {
sendUsage()
@ -354,7 +364,7 @@ COMMANDS:
}
case bgammon.CommandLeave, "l":
if clientGame == nil {
cmd.client.events <- []byte("failedleave You are not currently in a game.")
cmd.client.Write([]byte("failedleave You are not currently in a game."))
continue
}
if clientGame.client1 == cmd.client {
@ -366,19 +376,19 @@ COMMANDS:
// TODO move to .removeClient
leftMessage := []byte(fmt.Sprintf("left %s", cmd.client.name))
cmd.client.events <- leftMessage
cmd.client.Write(leftMessage)
opponent := clientGame.opponent(cmd.client)
if opponent != nil {
opponent.events <- leftMessage
opponent.Write(leftMessage)
}
case bgammon.CommandRoll, "r":
if clientGame == nil {
cmd.client.events <- []byte("notice You are not currently in a game.")
cmd.client.Write([]byte("notice You are not currently in a game."))
continue
}
if !clientGame.roll(clientNumber) {
cmd.client.events <- []byte("notice It is not your turn to roll.")
cmd.client.Write([]byte("notice It is not your turn to roll."))
continue
}
ev := &bgammon.EventRolled{
@ -404,17 +414,17 @@ COMMANDS:
}
case bgammon.CommandMove, "m", "mv":
if clientGame == nil {
cmd.client.events <- []byte("failedmove You are not currently in a game.")
cmd.client.Write([]byte("failedmove You are not currently in a game."))
continue
}
if clientGame.Turn != clientNumber {
cmd.client.events <- []byte("failedmove It is not your turn to move.")
cmd.client.Write([]byte("failedmove It is not your turn to move."))
continue
}
sendUsage := func() {
cmd.client.events <- []byte("failedmove Specify one or more moves in the form FROM/TO. For example: 8/4 6/4")
cmd.client.Write([]byte("failedmove Specify one or more moves in the form FROM/TO. For example: 8/4 6/4"))
}
if len(params) == 0 {
@ -457,7 +467,7 @@ COMMANDS:
}
if !found {
log.Printf("available legal moves: %s", bgammon.FormatMoves(legalMoves, clientNumber))
cmd.client.events <- []byte(fmt.Sprintf("failedmove %d/%d Illegal move.", originalFrom, originalTo))
cmd.client.Write([]byte(fmt.Sprintf("failedmove %d/%d Illegal move.", originalFrom, originalTo)))
continue COMMANDS
}
@ -479,7 +489,7 @@ COMMANDS:
})
case bgammon.CommandOk, "k":
if clientGame == nil {
cmd.client.events <- []byte("notice You are not currently in a game.")
cmd.client.Write([]byte("notice You are not currently in a game."))
continue
}
@ -489,26 +499,28 @@ COMMANDS:
if clientGame.client2 == cmd.client {
playerNumber = 2
}
cmd.client.events <- []byte(fmt.Sprintf("failedok You still have the following legal moves available: %s", bgammon.FormatMoves(legalMoves, playerNumber)))
cmd.client.Write([]byte(fmt.Sprintf("failedok You still have the following legal moves available: %s", bgammon.FormatMoves(legalMoves, playerNumber))))
continue
}
log.Println("legal to pass turn")
case bgammon.CommandBoard, "b":
if clientGame == nil {
cmd.client.events <- []byte("notice You are not currently in a game.")
cmd.client.Write([]byte("notice You are not currently in a game."))
continue
}
scanner := bufio.NewScanner(bytes.NewReader(clientGame.BoardState(clientNumber)))
for scanner.Scan() {
cmd.client.events <- append([]byte("notice "), scanner.Bytes()...)
cmd.client.Write(append([]byte("notice "), scanner.Bytes()...))
}
case bgammon.CommandDisconnect:
if clientGame != nil {
clientGame.removeClient(cmd.client)
}
cmd.client.Terminate("Client disconnected")
case bgammon.CommandPong:
// Do nothing.
default:
log.Printf("unknown command %s", keyword)
}

View file

@ -19,12 +19,21 @@ const (
CommandReset = "reset" // Reset checker movement.
CommandOk = "ok" // Confirm checker movement and pass turn to next player.
CommandBoard = "board" // Print current board state in human-readable form.
CommandPong = "pong" // Response to server ping.
CommandDisconnect = "disconnect" // Disconnect from server.
)
type EventType string
const (
EventTypeWelcome = "welcome"
EventTypeJoined = "joined"
EventTypeWelcome = "welcome"
EventTypePing = "ping"
EventTypeNotice = "notice"
EventTypeSay = "say"
EventTypeList = "list"
EventTypeJoined = "joined"
EventTypeFailedJoin = "failedjoin"
EventTypeBoard = "board"
EventTypeRolled = "rolled"
EventTypeMoved = "moved"
)

View file

@ -19,6 +19,11 @@ type EventWelcome struct {
Games int
}
type EventPing struct {
Event
Message string
}
type EventNotice struct {
Event
Message string
@ -82,6 +87,34 @@ func DecodeEvent(message []byte) (interface{}, error) {
return nil, err
}
return ev, nil
case EventTypePing:
ev := &EventPing{}
err = json.Unmarshal(message, ev)
if err != nil {
return nil, err
}
return ev, nil
case EventTypeNotice:
ev := &EventNotice{}
err = json.Unmarshal(message, ev)
if err != nil {
return nil, err
}
return ev, nil
case EventTypeSay:
ev := &EventSay{}
err = json.Unmarshal(message, ev)
if err != nil {
return nil, err
}
return ev, nil
case EventTypeList:
ev := &EventList{}
err = json.Unmarshal(message, ev)
if err != nil {
return nil, err
}
return ev, nil
case EventTypeJoined:
ev := &EventJoined{}
err = json.Unmarshal(message, ev)
@ -89,6 +122,34 @@ func DecodeEvent(message []byte) (interface{}, error) {
return nil, err
}
return ev, nil
case EventTypeFailedJoin:
ev := &EventFailedJoin{}
err = json.Unmarshal(message, ev)
if err != nil {
return nil, err
}
return ev, nil
case EventTypeBoard:
ev := &EventBoard{}
err = json.Unmarshal(message, ev)
if err != nil {
return nil, err
}
return ev, nil
case EventTypeRolled:
ev := &EventRolled{}
err = json.Unmarshal(message, ev)
if err != nil {
return nil, err
}
return ev, nil
case EventTypeMoved:
ev := &EventMoved{}
err = json.Unmarshal(message, ev)
if err != nil {
return nil, err
}
return ev, nil
default:
return nil, fmt.Errorf("failed to decode event: unknown event type: %s", e.Type)
}

View file

@ -33,11 +33,13 @@ func NewGame() *Game {
func (g *Game) Copy() *Game {
newGame := &Game{
Board: make([]int, len(g.Board)),
Player1: g.Player1,
Player2: g.Player2,
Turn: g.Turn,
Roll1: g.Roll1,
Roll2: g.Roll2,
Moves: make([][]int, len(g.Moves)),
}
copy(newGame.Board, g.Board)
copy(newGame.Moves, g.Moves)