Commit 38f7110a authored by ceddral's avatar ceddral
Browse files

player: refactor inputloop, reconnect, use tcpkeepalive instead of manual telnet ping

keepalive takes care of terminating stale connections so we do not have to
implement a ping loop.

inputloop now accesses player reference. no input loop for a player
must be running when that player is being modified
parent ed9023bc
......@@ -78,6 +78,7 @@ func main() {
gs.players = make(map[string]*Player)
gs.players["ceddral"] = &Player{
conn: &FakeConn{},
connClosed: make(chan struct{}, 1),
tnState: NewTelnetState(),
inputEvent: make(chan []byte, 50),
inputKill: make(chan struct{}, 0),
......@@ -86,10 +87,12 @@ func main() {
context: nil,
scr: tg.NewScreen(&FakeConn{}, 80, 24, tg.ColorDefault, tg.ColorDefault),
}
gs.players["ceddral"].connClosed<-struct{}{}
go fakeInputLoop(gs.players["ceddral"].inputKill)
go fakeWindowSizeLoop(gs.players["ceddral"].windowSizeLoopKill)
gs.players["since"] = &Player{
conn: &FakeConn{},
connClosed: make(chan struct{}, 1),
tnState: NewTelnetState(),
inputEvent: make(chan []byte, 50),
inputKill: make(chan struct{}, 0),
......@@ -98,6 +101,7 @@ func main() {
context: nil,
scr: tg.NewScreen(&FakeConn{}, 80, 24, tg.ColorDefault, tg.ColorDefault),
}
gs.players["since"].connClosed<-struct{}{}
go fakeInputLoop(gs.players["since"].inputKill)
go fakeWindowSizeLoop(gs.players["since"].windowSizeLoopKill)
NewGame([]string{"since", "ceddral"}, []string{"base", "ext1"})
......
......@@ -20,6 +20,7 @@ type Context interface {
type Player struct {
tnState *TelnetState
conn net.Conn
connClosed chan struct{}
inputEvent chan []byte
inputKill chan struct{}
windowSizeLoopKill chan struct{}
......@@ -29,14 +30,18 @@ type Player struct {
}
func NewPlayer(conn net.Conn) (p *Player) {
tcpConn := conn.(*net.TCPConn)
tcpConn.SetKeepAlivePeriod(time.Second)
tcpConn.SetKeepAlive(true)
var err error
p = &Player{
conn: conn,
connClosed: make(chan struct{}, 1),
tnState: NewTelnetState(),
inputEvent: make(chan []byte, 50),
inputKill: make(chan struct{}),
}
go inputLoop(conn, p.tnState, p.inputEvent, p.inputKill)
go p.inputLoop()
p.name, err = queryUsername(p.conn, p.tnState)
if err != nil {
log.Println(err)
......@@ -46,8 +51,10 @@ func NewPlayer(conn net.Conn) (p *Player) {
// reconnect with replace
pNew := p
// check if old player is still responsive
timeout := ping(pOld.conn, pOld.tnState)
if !timeout {
select {
case <-pOld.connClosed:
// continue
default:
// old player is responsive, disconnect imposter
pNew.inputKill <- struct{}{}
close(pNew.inputKill)
......@@ -65,14 +72,16 @@ func NewPlayer(conn net.Conn) (p *Player) {
// clean up new player input, was only needed to query username
pNew.inputKill <- struct{}{}
close(pNew.connClosed)
close(pNew.inputKill)
close(pNew.inputEvent)
// assemble reconnected player
p = pOld
// no inputLoop for pOld and pNew may be running during these modifications to p
p.conn = pNew.conn
p.scr.Chown(conn)
go inputLoop(conn, p.tnState, p.inputEvent, p.inputKill)
go p.inputLoop()
} else {
p.windowSizeLoopKill = make(chan struct{})
gs.players[p.name] = p
......@@ -97,20 +106,21 @@ func StopTimer(t *time.Timer) {
}
}
func inputLoop(src net.Conn, tnState *TelnetState, dst chan []byte, kill chan struct{}) {
func (p *Player) inputLoop() {
key := make([]byte, 0, 3)
src.SetReadDeadline(time.Now().Add(time.Second))
p.conn.SetReadDeadline(time.Now().Add(time.Second))
for {
select {
case <-kill:
// kill loop without closing connection in case of reconnect
case <-p.inputKill:
return
default:
}
if 0 == len(key) {
src.SetReadDeadline(time.Now().Add(time.Second))
p.conn.SetReadDeadline(time.Now().Add(time.Second))
}
b, err := readByteTelnet(src, tnState)
b, err := readByteTelnet(p.conn, p.tnState)
if err != nil {
if os.IsTimeout(err) { //errors.Is(err, os.ErrDeadlineExceeded) {
if 0 < len(key) {
......@@ -118,13 +128,14 @@ func inputLoop(src net.Conn, tnState *TelnetState, dst chan []byte, kill chan st
}
continue
}
// await termination
<-kill
p.connClosed <- struct{}{}
// await termination, to unblock a reconnect
<-p.inputKill
return
}
key = append(key, b)
if b == 27 { // ESC
src.SetReadDeadline(time.Now().Add(connectionConfig.ANSIEscapeTimeout * time.Millisecond))
p.conn.SetReadDeadline(time.Now().Add(connectionConfig.ANSIEscapeTimeout * time.Millisecond))
continue
}
if 1 < len(key) {
......@@ -134,7 +145,7 @@ func inputLoop(src net.Conn, tnState *TelnetState, dst chan []byte, kill chan st
if toKey(key) != keyNull {
log.Printf("key %#v\n", key)
select {
case dst <- key:
case p.inputEvent <- key:
default:
// discard inputs if they come too fast
}
......
......@@ -261,10 +261,6 @@ func process3LenTelnetCommand(cmd byte, conn net.Conn, tnState *TelnetState) err
if err != nil {
return err
}
switch b[0] {
case tg.TnTIMINGMARK:
tnState.timingMark = true
}
return nil
}
......@@ -401,25 +397,3 @@ func queryUsername(conn net.Conn, tnState *TelnetState) (string, error) {
}
return <-res, nil
}
func ping(conn net.Conn, tnState *TelnetState) bool {
defer conn.SetWriteDeadline(time.Time{})
res := make(chan bool)
timeoutChan := cond_set_timeout(tnState.lock, tnState.cond, connectionConfig.TelnetQueryTimeout*time.Millisecond)
tnState.Lock()
tnState.timingMark = false
go func() {
for {
timeout := pthread_cond_timedwait_because_fuck_you(tnState.cond, timeoutChan)
if !timeout && !tnState.timingMark {
continue
}
tnState.Unlock()
res <- timeout
return
}
}()
conn.SetWriteDeadline(time.Now().Add(time.Millisecond * connectionConfig.TelnetQueryTimeout))
conn.Write([]byte{tg.TnIAC, tg.TnDO, tg.TnTIMINGMARK})
return <-res
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment