2017-10-15 18:24:28 +02:00
|
|
|
// Copyright (c) 2017 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
|
|
|
// released under the MIT license
|
|
|
|
|
|
|
|
package irc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
2017-10-16 04:37:36 +02:00
|
|
|
const (
|
|
|
|
// RegisterTimeout is how long clients have to register before we disconnect them
|
|
|
|
RegisterTimeout = time.Minute
|
|
|
|
// IdleTimeout is how long without traffic before a registered client is considered idle.
|
|
|
|
IdleTimeout = time.Minute + time.Second*30
|
|
|
|
// QuitTimeout is how long without traffic before an idle client is disconnected
|
|
|
|
QuitTimeout = time.Minute
|
|
|
|
)
|
|
|
|
|
2017-10-15 18:24:28 +02:00
|
|
|
// client idleness state machine
|
|
|
|
|
|
|
|
type TimerState uint
|
|
|
|
|
|
|
|
const (
|
|
|
|
TimerUnregistered TimerState = iota // client is unregistered
|
|
|
|
TimerActive // client is actively sending commands
|
|
|
|
TimerIdle // client is idle, we sent PING and are waiting for PONG
|
|
|
|
)
|
|
|
|
|
|
|
|
type IdleTimer struct {
|
|
|
|
sync.Mutex
|
|
|
|
|
|
|
|
// immutable after construction
|
|
|
|
registerTimeout time.Duration
|
|
|
|
idleTimeout time.Duration
|
|
|
|
quitTimeout time.Duration
|
|
|
|
|
|
|
|
// mutable
|
|
|
|
client *Client
|
|
|
|
state TimerState
|
|
|
|
lastSeen time.Time
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewIdleTimer sets up a new IdleTimer using constant timeouts.
|
|
|
|
func NewIdleTimer(client *Client) *IdleTimer {
|
|
|
|
it := IdleTimer{
|
|
|
|
registerTimeout: RegisterTimeout,
|
|
|
|
idleTimeout: IdleTimeout,
|
|
|
|
quitTimeout: QuitTimeout,
|
|
|
|
client: client,
|
|
|
|
state: TimerUnregistered,
|
|
|
|
}
|
|
|
|
return &it
|
|
|
|
}
|
|
|
|
|
|
|
|
// Start starts counting idle time; if there is no activity from the client,
|
|
|
|
// it will eventually be stopped.
|
|
|
|
func (it *IdleTimer) Start() {
|
|
|
|
it.Lock()
|
2017-10-16 04:37:36 +02:00
|
|
|
it.state = TimerUnregistered
|
2017-10-15 18:24:28 +02:00
|
|
|
it.lastSeen = time.Now()
|
|
|
|
it.Unlock()
|
|
|
|
go it.mainLoop()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (it *IdleTimer) mainLoop() {
|
|
|
|
for {
|
|
|
|
it.Lock()
|
|
|
|
client := it.client
|
|
|
|
state := it.state
|
|
|
|
lastSeen := it.lastSeen
|
|
|
|
it.Unlock()
|
|
|
|
|
|
|
|
if client == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-10-16 04:37:36 +02:00
|
|
|
idleTime := time.Now().Sub(lastSeen)
|
|
|
|
var nextSleep time.Duration
|
2017-10-15 18:24:28 +02:00
|
|
|
|
2017-10-16 04:37:36 +02:00
|
|
|
if state == TimerUnregistered {
|
|
|
|
if client.Registered() {
|
|
|
|
// transition to active, process new deadlines below
|
|
|
|
state = TimerActive
|
|
|
|
} else {
|
|
|
|
nextSleep = it.registerTimeout - idleTime
|
2017-10-15 18:24:28 +02:00
|
|
|
}
|
2017-10-16 04:37:36 +02:00
|
|
|
} else if state == TimerIdle {
|
|
|
|
if idleTime < it.quitTimeout {
|
|
|
|
// new ping came in after we transitioned to TimerIdle,
|
|
|
|
// transition back to active and process deadlines below
|
|
|
|
state = TimerActive
|
|
|
|
} else {
|
|
|
|
nextSleep = 0
|
2017-10-15 18:24:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-16 04:37:36 +02:00
|
|
|
if state == TimerActive {
|
2017-10-15 18:24:28 +02:00
|
|
|
nextSleep = it.idleTimeout - idleTime
|
2017-10-16 04:37:36 +02:00
|
|
|
if nextSleep <= 0 {
|
|
|
|
state = TimerIdle
|
|
|
|
client.Ping()
|
|
|
|
// grant the client at least quitTimeout to respond
|
|
|
|
nextSleep = it.quitTimeout
|
|
|
|
}
|
2017-10-15 18:24:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if nextSleep <= 0 {
|
|
|
|
// ran out of time, hang them up
|
2017-10-16 04:37:36 +02:00
|
|
|
client.Quit(it.quitMessage(state))
|
2017-10-15 18:24:28 +02:00
|
|
|
client.destroy()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-10-16 04:37:36 +02:00
|
|
|
it.Lock()
|
|
|
|
it.state = state
|
|
|
|
it.Unlock()
|
|
|
|
|
2017-10-15 18:24:28 +02:00
|
|
|
time.Sleep(nextSleep)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Touch registers activity (e.g., sending a command) from an client.
|
|
|
|
func (it *IdleTimer) Touch() {
|
|
|
|
it.Lock()
|
|
|
|
client := it.client
|
|
|
|
it.Unlock()
|
|
|
|
|
|
|
|
// ignore touches for unregistered clients
|
|
|
|
if client != nil && !client.Registered() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
it.Lock()
|
|
|
|
it.lastSeen = time.Now()
|
|
|
|
it.Unlock()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Stop stops counting idle time.
|
|
|
|
func (it *IdleTimer) Stop() {
|
|
|
|
it.Lock()
|
|
|
|
defer it.Unlock()
|
|
|
|
// no need to stop the goroutine, it'll clean itself up in a few minutes;
|
|
|
|
// just ensure the Client object is collectable
|
|
|
|
it.client = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (it *IdleTimer) quitMessage(state TimerState) string {
|
|
|
|
switch state {
|
|
|
|
case TimerUnregistered:
|
|
|
|
return fmt.Sprintf("Registration timeout: %v", it.registerTimeout)
|
|
|
|
case TimerIdle:
|
|
|
|
// how many seconds before registered clients are timed out (IdleTimeout plus QuitTimeout).
|
|
|
|
return fmt.Sprintf("Ping timeout: %v", (it.idleTimeout + it.quitTimeout))
|
|
|
|
default:
|
|
|
|
// shouldn't happen
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
}
|