3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-12-23 03:02:48 +01:00
ergo/irc/idletimer.go
2018-01-30 14:26:29 +10:00

168 lines
4.0 KiB
Go

// Copyright (c) 2017 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// released under the MIT license
package irc
import (
"fmt"
"sync"
"time"
"github.com/oragono/oragono/irc/caps"
)
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
// IdleTimeoutWithResumeCap is how long without traffic before a registered client is considered idle, when they have the resume capability.
IdleTimeoutWithResumeCap = time.Minute*2 + time.Second*30
// QuitTimeout is how long without traffic before an idle client is disconnected
QuitTimeout = time.Minute
)
// 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
TimerDead // client was terminated
)
type IdleTimer struct {
sync.Mutex // tier 1
// immutable after construction
registerTimeout time.Duration
quitTimeout time.Duration
client *Client
// mutable
idleTimeout time.Duration
state TimerState
timer *time.Timer
}
// NewIdleTimer sets up a new IdleTimer using constant timeouts.
func NewIdleTimer(client *Client) *IdleTimer {
it := IdleTimer{
registerTimeout: RegisterTimeout,
idleTimeout: IdleTimeout,
quitTimeout: QuitTimeout,
client: client,
}
return &it
}
// updateIdleDuration updates the idle duration, given the client's caps.
func (it *IdleTimer) updateIdleDuration() {
newIdleTime := IdleTimeout
// if they have the resume cap, wait longer before pinging them out
// to give them a chance to resume their connection
if it.client.capabilities.Has(caps.Resume) {
newIdleTime = IdleTimeoutWithResumeCap
}
it.Lock()
defer it.Unlock()
it.idleTimeout = newIdleTime
}
// Start starts counting idle time; if there is no activity from the client,
// it will eventually be stopped.
func (it *IdleTimer) Start() {
it.Lock()
defer it.Unlock()
it.state = TimerUnregistered
it.resetTimeout()
}
func (it *IdleTimer) Touch() {
// ignore touches from unregistered clients
if !it.client.Registered() {
return
}
it.updateIdleDuration()
it.Lock()
defer it.Unlock()
// a touch transitions TimerUnregistered or TimerIdle into TimerActive
if it.state != TimerDead {
it.state = TimerActive
it.resetTimeout()
}
}
func (it *IdleTimer) processTimeout() {
it.updateIdleDuration()
var previousState TimerState
func() {
it.Lock()
defer it.Unlock()
previousState = it.state
// TimerActive transitions to TimerIdle, all others to TimerDead
if it.state == TimerActive {
// send them a ping, give them time to respond
it.state = TimerIdle
it.resetTimeout()
} else {
it.state = TimerDead
}
}()
if previousState == TimerActive {
it.client.Ping()
} else {
it.client.Quit(it.quitMessage(previousState))
it.client.destroy(false)
}
}
// Stop stops counting idle time.
func (it *IdleTimer) Stop() {
it.Lock()
defer it.Unlock()
it.state = TimerDead
it.resetTimeout()
}
func (it *IdleTimer) resetTimeout() {
if it.timer != nil {
it.timer.Stop()
}
var nextTimeout time.Duration
switch it.state {
case TimerUnregistered:
nextTimeout = it.registerTimeout
case TimerActive:
nextTimeout = it.idleTimeout
case TimerIdle:
nextTimeout = it.quitTimeout
case TimerDead:
return
}
it.timer = time.AfterFunc(nextTimeout, it.processTimeout)
}
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).
it.Lock()
defer it.Unlock()
return fmt.Sprintf("Ping timeout: %v", (it.idleTimeout + it.quitTimeout))
default:
// shouldn't happen
return ""
}
}