package net // Close/broadcast race regression: a broadcast racing Session.Close() used // to send on the outbound channel while Close closed the same channel — a // genuine data race (closechan vs chansend) that the old panic-recover only // hid. Closing is now signaled through a separate channel that is only ever // closed, never sent to. import ( stdnet "net" "sync" "testing" "time" ) // newTestSocketPair returns a synchronous, unbuffered connection pair: a // write blocks until the peer reads, which is exactly the stall condition // the echo test has to reproduce. func newTestSocketPair() (stdnet.Conn, stdnet.Conn) { return stdnet.Pipe() } func testTimeout() <-chan time.Time { return time.After(200 * time.Millisecond) } func TestConcurrentWriteCloseIsRaceFree(t *testing.T) { for i := 0; i < 50; i++ { sess := &Session{Conn: &testConn{}} sess.StartWriter() var wg sync.WaitGroup wg.Add(2) start := make(chan struct{}) go func() { defer wg.Done() <-start for j := 0; j < 100; j++ { sess.WriteLine("broadcast") } }() go func() { defer wg.Done() <-start sess.Close() }() close(start) wg.Wait() <-sess.writeDone } } // Echo negotiation regression: login flips telnet echo on the game owner's // goroutine. Writing the negotiation inline made that write a direct socket // I/O, so a client that never read (full receive window, stalled socket) // blocked the entire game loop behind one connection. Queued sessions must // route the negotiation through the outbound writer instead. func TestSetEchoQueuedDoesNotBlockCaller(t *testing.T) { local, peer := newTestSocketPair() defer local.Close() defer peer.Close() sess := &Session{Conn: newTCPConn(local)} sess.StartWriter() defer sess.Close() done := make(chan struct{}) go func() { sess.SetEcho(false) // what the login path calls for password entry close(done) }() select { case <-done: case <-testTimeout(): peer.Close() // unblock the writer so the test can exit <-done t.Fatal("SetEcho blocked the caller instead of queueing the negotiation") } }