source: code/trunk/conn.go@ 311

Last change on this file since 311 was 280, checked in by contact, 5 years ago

Use a lock to protect conn.{closed,outgoing}

Unfortunately, I don't think there's a good way to implement net.Conn
semantics on top of channels. The Close and SendMessage methods should
gracefully fail without panicking if the connection is already closed.
Using only channels leads to race conditions.

We could remove the lock if Close and SendMessage are only called from a
single goroutine. However that's not the case right now.

Closes: https://todo.sr.ht/~emersion/soju/55

File size: 2.2 KB
Line 
1package soju
2
3import (
4 "fmt"
5 "net"
6 "sync"
7 "time"
8
9 "gopkg.in/irc.v3"
10)
11
12func setKeepAlive(c net.Conn) error {
13 tcpConn, ok := c.(*net.TCPConn)
14 if !ok {
15 return fmt.Errorf("cannot enable keep-alive on a non-TCP connection")
16 }
17 if err := tcpConn.SetKeepAlive(true); err != nil {
18 return err
19 }
20 return tcpConn.SetKeepAlivePeriod(keepAlivePeriod)
21}
22
23type conn struct {
24 net net.Conn
25 irc *irc.Conn
26 srv *Server
27 logger Logger
28
29 lock sync.Mutex
30 outgoing chan<- *irc.Message
31 closed bool
32}
33
34func newConn(srv *Server, netConn net.Conn, logger Logger) *conn {
35 setKeepAlive(netConn)
36
37 outgoing := make(chan *irc.Message, 64)
38 c := &conn{
39 net: netConn,
40 irc: irc.NewConn(netConn),
41 srv: srv,
42 outgoing: outgoing,
43 logger: logger,
44 }
45
46 go func() {
47 for msg := range outgoing {
48 if c.srv.Debug {
49 c.logger.Printf("sent: %v", msg)
50 }
51 c.net.SetWriteDeadline(time.Now().Add(writeTimeout))
52 if err := c.irc.WriteMessage(msg); err != nil {
53 c.logger.Printf("failed to write message: %v", err)
54 break
55 }
56 }
57 if err := c.net.Close(); err != nil {
58 c.logger.Printf("failed to close connection: %v", err)
59 } else {
60 c.logger.Printf("connection closed")
61 }
62 // Drain the outgoing channel to prevent SendMessage from blocking
63 for range outgoing {
64 // This space is intentionally left blank
65 }
66 }()
67
68 c.logger.Printf("new connection")
69 return c
70}
71
72func (c *conn) isClosed() bool {
73 c.lock.Lock()
74 defer c.lock.Unlock()
75 return c.closed
76}
77
78// Close closes the connection. It is safe to call from any goroutine.
79func (c *conn) Close() error {
80 c.lock.Lock()
81 defer c.lock.Unlock()
82
83 if c.closed {
84 return fmt.Errorf("connection already closed")
85 }
86
87 c.closed = true
88 close(c.outgoing)
89 return nil
90}
91
92func (c *conn) ReadMessage() (*irc.Message, error) {
93 msg, err := c.irc.ReadMessage()
94 if err != nil {
95 return nil, err
96 }
97
98 if c.srv.Debug {
99 c.logger.Printf("received: %v", msg)
100 }
101
102 return msg, nil
103}
104
105// SendMessage queues a new outgoing message. It is safe to call from any
106// goroutine.
107//
108// If the connection is closed before the message is sent, SendMessage silently
109// drops the message.
110func (c *conn) SendMessage(msg *irc.Message) {
111 c.lock.Lock()
112 defer c.lock.Unlock()
113
114 if c.closed {
115 return
116 }
117 c.outgoing <- msg
118}
Note: See TracBrowser for help on using the repository browser.