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
+88
View File
@@ -9,6 +9,7 @@ import (
"net"
"os"
"runtime"
"slices"
"strings"
"testing"
"time"
@@ -174,6 +175,42 @@ func (env *testEnv) dial(t *testing.T) *testClient {
}
}
// The ping interval and PONG deadline for dialWithPingTimes.
// They are equal, so that three answered PINGs take longer
// than a client that answers none stays connected.
const (
testPingInterval = 300 * time.Millisecond
testPongDeadline = 300 * time.Millisecond
)
// dialWithPingTimes connects a client to the test server
// whose connection is sent a PING after testPingInterval with
// nothing read, and closed when nothing is read within
// testPongDeadline after that.
func (env *testEnv) dialWithPingTimes(t *testing.T) *testClient {
t.Helper()
serverSide, clientSide := net.Pipe()
t.Cleanup(func() { _ = clientSide.Close() })
env.srv.ServeConn(
serverSide, testPingInterval, testPongDeadline,
)
return &testClient{
t: t,
conn: clientSide,
scanner: bufio.NewScanner(clientSide),
}
}
// isServerPing reports whether line is the PING the test
// server sends a quiet client.
func isServerPing(line string) bool {
return line == "PING :"+testServerName
}
// testClient wraps a raw TCP connection with helpers.
type testClient struct {
t *testing.T
@@ -388,6 +425,57 @@ func TestPingPong(t *testing.T) {
assertContains(t, lines, "PONG", "PONG response")
}
// TestQuietClientAnsweringPingStaysConnected checks that a
// registered client that sends nothing is sent a PING each
// time the ping interval passes, and stays connected as long
// as it answers each one.
func TestQuietClientAnsweringPingStaysConnected(t *testing.T) {
t.Parallel()
env := newTestEnv(t)
client := env.dialWithPingTimes(t)
client.register("answers")
for range 3 {
client.readUntil(isServerPing)
client.send("PONG :" + testServerName)
}
client.sendAndExpect("PING :still here", "PONG")
}
// TestQuietClientNotAnsweringPingIsClosed checks that a
// registered client that does not answer the PING is
// disconnected.
func TestQuietClientNotAnsweringPingIsClosed(t *testing.T) {
t.Parallel()
env := newTestEnv(t)
client := env.dialWithPingTimes(t)
client.register("silent")
lines := client.readUntilClosed()
if !slices.ContainsFunc(lines, isServerPing) {
t.Errorf("closed without a PING, got lines %q", lines)
}
}
// TestUnregisteredClientAnswersPing checks that a PONG before
// registration, the answer to the PING unregistered clients
// are sent too, gets no ERR_NOTREGISTERED.
func TestUnregisteredClientAnswersPing(t *testing.T) {
t.Parallel()
env := newTestEnv(t)
client := env.dialWithPingTimes(t)
client.readUntil(isServerPing)
client.send("PONG :" + testServerName)
lines := client.sendAndExpect("PING :after", "PONG")
assertNotContains(t, lines, " 451 ", "ERR_NOTREGISTERED")
}
// TestIdleCleanupKeepsActiveUser checks that a user who
// sends commands is kept by the idle cleanup, and a user who
// sends nothing is not.