diff --git a/client/connection.go b/client/connection.go index e4d474e..a2e3bb3 100644 --- a/client/connection.go +++ b/client/connection.go @@ -47,15 +47,18 @@ type Conn struct { Connected bool // Control channels to goroutines - cSend, cLoop chan bool + cSend, cLoop, cPing chan bool // Misc knobs to tweak client behaviour: // Are we connecting via SSL? Do we care about certificate validity? SSL bool SSLConfig *tls.Config - // Socket timeout, in seconds. Defaulted to 5m in New(). - Timeout int64 + // Client->server ping frequency, in seconds. Defaults to 3m. + PingFreq int64 + + // Socket timeout, in seconds. Default to 5m. + Timeout int64 // Set this to true to disable flood protection and false to re-enable Flood bool @@ -95,8 +98,10 @@ func Client(nick, ident, name string, out: make(chan string, 32), cSend: make(chan bool), cLoop: make(chan bool), + cPing: make(chan bool), SSL: false, SSLConfig: nil, + PingFreq: 180, Timeout: 300, Flood: false, badness: 0, @@ -194,6 +199,7 @@ func (conn *Conn) postConnect() { conn.sock.SetTimeout(conn.Timeout * second) go conn.send() go conn.recv() + go conn.ping() go conn.runLoop() } @@ -236,6 +242,20 @@ func (conn *Conn) recv() { } } +// Repeatedly pings the server every PingFreq seconds (no matter what) +func (conn *Conn) ping() { + tick := time.NewTicker(conn.PingFreq * second) + for { + select { + case <-tick.C: + conn.Raw(fmt.Sprintf("PING :%d", time.Nanoseconds())) + case <-conn.cPing: + tick.Stop() + return + } + } +} + // goroutine to dispatch events for lines received on input channel func (conn *Conn) runLoop() { for { @@ -303,6 +323,7 @@ func (conn *Conn) shutdown() { conn.sock.Close() conn.cSend <- true conn.cLoop <- true + conn.cPing <- true // reinit datastructures ready for next connection // do this here rather than after runLoop()'s for due to race conn.initialise() diff --git a/client/connection_test.go b/client/connection_test.go index b7b4888..1c519ac 100644 --- a/client/connection_test.go +++ b/client/connection_test.go @@ -6,6 +6,7 @@ import ( "github.com/fluffle/golog/logging" "github.com/fluffle/goirc/state" "gomock.googlecode.com/hg/gomock" + "strings" "testing" "time" ) @@ -260,6 +261,7 @@ func TestRecv(t *testing.T) { // channels manually for recv() to be able to call shutdown correctly. <-c.cSend <-c.cLoop + <-c.cPing // Give things time to shake themselves out... <-time.After(1e6) if !exited { @@ -272,6 +274,74 @@ func TestRecv(t *testing.T) { } } +func TestPing(t *testing.T) { + // Passing a second value to setUp inhibits postConnect() + c, s := setUp(t, false) + // We can't use tearDown here, as it will cause a deadlock in shutdown() + // trying to send kill messages down channels to nonexistent goroutines. + defer s.ctrl.Finish() + + // Set a low ping frequency for testing. + // This still increases testing time by a good few seconds :-/ + c.PingFreq = 1 + + // reader is a helper to do a "non-blocking" read of c.out + reader := func() string { + select { + case <-time.After(1e6): + case s := <-c.out: + return s + } + return "" + } + if s := reader(); s != "" { + t.Errorf("Line output before ping started.") + } + + // Start ping loop. + exited := false + go func() { + c.ping() + exited = true + }() + + // The first ping should be after a second, + // so we don't expect anything now on c.in + if s := reader(); s != "" { + t.Errorf("Line output directly after ping started.") + } + + <-time.After(1e9) + if s := reader(); s == "" || !strings.HasPrefix(s, "PING :") { + t.Errorf("Line not output after 1 second.") + } + + <-time.After(1e7) + if s := reader(); s != "" { + t.Errorf("Line output under a second after last ping.") + } + + <-time.After(1e9) + if s := reader(); s == "" || !strings.HasPrefix(s, "PING :") { + t.Errorf("Line not output after another second.") + } + + // Now kill the ping loop. + if exited { + t.Errorf("Exited before signal sent.") + } + + c.cPing <- true + <-time.After(1e9) + if s := reader(); s != "" { + t.Errorf("Line output after ping stopped.") + } + + if !exited { + t.Errorf("Didn't exit after signal.") + } +} + func TestRunLoop(t *testing.T) { // Passing a second value to setUp inhibits postConnect() c, s := setUp(t, false) @@ -364,6 +434,7 @@ func TestWrite(t *testing.T) { go func() { <-c.cSend <-c.cLoop + <-c.cPing }() s.nc.Close()