PING a quiet IRC listener client before closing it (closes #155)
check / check (push) Canceled after 0s

serve closed a connection after five minutes with nothing read and never
sent a PING, so a quiet registered client lost its session. It now sends
PING :<server> after pingInterval with nothing read, and closes the
connection when nothing is read within pongDeadline after that. Every
line read starts both waits again. readTimeout is removed.

Unregistered connections are sent the PING too, so a PONG before
registration is now ignored instead of answered with ERR_NOTREGISTERED.

Model: opus-5-5
This commit is contained in:
clawbot
2026-10-20 16:12:36 +00:00
parent 5100b9a665
commit aa56b8b63c
4 changed files with 133 additions and 3 deletions
+25 -3
View File
@@ -20,7 +20,6 @@ import (
const (
maxLineLen = 512
readTimeout = 5 * time.Minute
writeTimeout = 30 * time.Second
killWriteWindow = 2 * time.Second
dnsTimeout = 3 * time.Second
@@ -46,6 +45,11 @@ type Conn struct {
serverSfx string
commands map[string]cmdHandler
// pingInterval and pongDeadline are the constants of the
// same names, except in tests, which shorten them.
pingInterval time.Duration
pongDeadline time.Duration
// writeMu serializes writes to conn, which come from
// serve(), from the relay goroutine and, for KILL, from
// Disconnect.
@@ -96,6 +100,9 @@ func newConn(
serverSfx: srvName,
remoteIP: host,
hostname: resolveHost(ctx, host),
pingInterval: pingInterval,
pongDeadline: pongDeadline,
}
conn.commands = conn.buildCommandMap()
@@ -214,17 +221,28 @@ func resolveHost(ctx context.Context, addr string) string {
// serve is the main loop for a single IRC client connection.
// Cancelling ctx when it returns stops the relay goroutine.
// When nothing has been read for pingInterval the client is
// sent a PING, and when nothing is read within pongDeadline
// after that the connection is closed.
func (c *Conn) serve(ctx context.Context) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
defer c.cleanup(ctx)
ping := time.AfterFunc(c.pingInterval, func() {
c.send("PING :" + c.serverSfx)
})
defer ping.Stop()
scanner := bufio.NewScanner(c.conn)
scanner.Buffer(make([]byte, maxLineLen), maxLineLen)
for {
// Every line read starts both waits again.
ping.Reset(c.pingInterval)
_ = c.conn.SetReadDeadline(
time.Now().Add(readTimeout),
time.Now().Add(c.pingInterval + c.pongDeadline),
)
if !scanner.Scan() {
@@ -366,7 +384,7 @@ func (c *Conn) handleMessage(
msg *Message,
) {
// Before registration, only NICK, USER, PASS, PING,
// QUIT, and CAP are accepted.
// PONG, QUIT, and CAP are accepted.
if !c.registered {
c.handlePreRegistration(ctx, msg)
@@ -452,6 +470,10 @@ func (c *Conn) handlePreRegistration(
case irc.CmdPing:
c.handlePing(msg)
return
case irc.CmdPong:
// The answer to serve's PING, which unregistered
// clients are sent too.
return
case irc.CmdQuit:
c.handleQuit(msg)