commit d9c0c27

Hubert Hirtz  ·  2021-03-03 17:30:25 +0000 UTC
parent fce8215
Better connection management

- Send keepalives
- Reconnect when the connection is lost
- Set timeout for write operations
2 files changed,  +170, -69
M app.go
M app.go
+158, -61
  1@@ -13,10 +13,25 @@ import (
  2 	"github.com/gdamore/tcell/v2"
  3 )
  4 
  5+const eventChanSize = 64
  6+
  7+type source int
  8+
  9+const (
 10+	uiEvent source = iota
 11+	ircEvent
 12+)
 13+
 14+type event struct {
 15+	src     source
 16+	content interface{}
 17+}
 18+
 19 type App struct {
 20 	win     *ui.UI
 21 	s       *irc.Session
 22 	pasting bool
 23+	events  chan event
 24 
 25 	cfg        Config
 26 	highlights []string
 27@@ -26,7 +41,8 @@ type App struct {
 28 
 29 func NewApp(cfg Config) (app *App, err error) {
 30 	app = &App{
 31-		cfg: cfg,
 32+		cfg:    cfg,
 33+		events: make(chan event, eventChanSize),
 34 	}
 35 
 36 	if cfg.Highlights != nil {
 37@@ -50,87 +66,168 @@ func NewApp(cfg Config) (app *App, err error) {
 38 
 39 	app.initWindow()
 40 
 41-	var conn net.Conn
 42-	app.addLineNow(Home, ui.Line{
 43-		Head: "--",
 44-		Body: fmt.Sprintf("Connecting to %s...", cfg.Addr),
 45-	})
 46-	if cfg.NoTLS {
 47-		conn, err = net.Dial("tcp", cfg.Addr)
 48-	} else {
 49-		conn, err = tls.Dial("tcp", cfg.Addr, nil)
 50+	return
 51+}
 52+
 53+func (app *App) Close() {
 54+	app.win.Close()
 55+	if app.s != nil {
 56+		app.s.Stop()
 57 	}
 58-	if err != nil {
 59+}
 60+
 61+func (app *App) Run() {
 62+	go app.uiLoop()
 63+	go app.ircLoop()
 64+	app.eventLoop()
 65+}
 66+
 67+// eventLoop retrieves events (in batches) from the event channel and handle
 68+// them, then draws the interface after each batch is handled.
 69+func (app *App) eventLoop() {
 70+	evs := make([]event, 0, eventChanSize)
 71+	for !app.win.ShouldExit() {
 72+		ev := <-app.events
 73+		evs = evs[:0]
 74+		evs = append(evs, ev)
 75+	Batch:
 76+		for i := 0; i < eventChanSize; i++ {
 77+			select {
 78+			case ev := <-app.events:
 79+				evs = append(evs, ev)
 80+			default:
 81+				break Batch
 82+			}
 83+		}
 84+
 85+		app.handleEvents(evs)
 86+		if !app.pasting {
 87+			app.draw()
 88+		}
 89+	}
 90+}
 91+
 92+// handleEvents handles a batch of events.
 93+func (app *App) handleEvents(evs []event) {
 94+	for _, ev := range evs {
 95+		switch ev.src {
 96+		case uiEvent:
 97+			app.handleUIEvent(ev.content.(tcell.Event))
 98+		case ircEvent:
 99+			app.handleIRCEvent(ev.content.(irc.Event))
100+		default:
101+			panic("unreachable")
102+		}
103+	}
104+}
105+
106+// ircLoop maintains a connection to the IRC server by connecting and then
107+// forwarding IRC events to app.events repeatedly.
108+func (app *App) ircLoop() {
109+	for !app.win.ShouldExit() {
110+		app.connect()
111+		for ev := range app.s.Poll() {
112+			app.events <- event{
113+				src:     ircEvent,
114+				content: ev,
115+			}
116+		}
117 		app.addLineNow(Home, ui.Line{
118 			Head:      "!!",
119 			HeadColor: ui.ColorRed,
120-			Body:      fmt.Sprintf("Connection failed: %v", err),
121+			Body:      "Connection lost",
122 		})
123-		err = nil
124-		return
125 	}
126+}
127 
128-	var auth irc.SASLClient
129-	if cfg.Password != nil {
130-		auth = &irc.SASLPlain{Username: cfg.User, Password: *cfg.Password}
131-	}
132-	app.s, err = irc.NewSession(conn, irc.SessionParams{
133-		Nickname: cfg.Nick,
134-		Username: cfg.User,
135-		RealName: cfg.Real,
136-		Auth:     auth,
137-		Debug:    cfg.Debug,
138-	})
139-	if err != nil {
140+func (app *App) connect() {
141+	for {
142+		app.addLineNow(Home, ui.Line{
143+			Head: "--",
144+			Body: fmt.Sprintf("Connecting to %s...", app.cfg.Addr),
145+		})
146+		err := app.tryConnect()
147+		if err == nil {
148+			break
149+		}
150 		app.addLineNow(Home, ui.Line{
151 			Head:      "!!",
152 			HeadColor: ui.ColorRed,
153 			Body:      fmt.Sprintf("Connection failed: %v", err),
154 		})
155+		time.Sleep(1 * time.Minute)
156 	}
157-
158-	return
159 }
160 
161-func (app *App) Close() {
162-	app.win.Close()
163-	if app.s != nil {
164-		app.s.Stop()
165+func (app *App) tryConnect() (err error) {
166+	addr := app.cfg.Addr
167+	serverName := app.cfg.Addr
168+	if i := strings.IndexByte(app.cfg.Addr, ':'); i >= 0 {
169+		serverName = app.cfg.Addr[:i]
170+	} else {
171+		addr = app.cfg.Addr + ":6697"
172 	}
173-}
174 
175-func (app *App) Run() {
176-	for !app.win.ShouldExit() {
177-		if app.s != nil {
178-			select {
179-			case ev := <-app.s.Poll():
180-				evs := []irc.Event{ev}
181-			Batch:
182-				for i := 0; i < 64; i++ {
183-					select {
184-					case ev := <-app.s.Poll():
185-						evs = append(evs, ev)
186-					default:
187-						break Batch
188-					}
189-				}
190-				app.handleIRCEvents(evs)
191-			case ev := <-app.win.Events:
192-				app.handleUIEvent(ev)
193-			}
194-		} else {
195-			ev := <-app.win.Events
196-			app.handleUIEvent(ev)
197+	peerAddr, err := net.ResolveTCPAddr("tcp", addr)
198+	if err != nil {
199+		return
200+	}
201+
202+	tcpConn, err := net.DialTCP("tcp", nil, peerAddr)
203+	if err != nil {
204+		tcpConn.Close()
205+		return
206+	}
207+	if err = tcpConn.SetKeepAlivePeriod(1 * time.Minute); err != nil {
208+		tcpConn.Close()
209+		return
210+	}
211+	if err = tcpConn.SetKeepAlive(true); err != nil {
212+		tcpConn.Close()
213+		return
214+	}
215+
216+	var conn net.Conn = tcpConn
217+	if !app.cfg.NoTLS {
218+		conn = tls.Client(conn, &tls.Config{
219+			ServerName: serverName,
220+		})
221+	}
222+
223+	var auth irc.SASLClient
224+	if app.cfg.Password != nil {
225+		auth = &irc.SASLPlain{
226+			Username: app.cfg.User,
227+			Password: *app.cfg.Password,
228 		}
229 	}
230+	app.s, err = irc.NewSession(conn, irc.SessionParams{
231+		Nickname: app.cfg.Nick,
232+		Username: app.cfg.User,
233+		RealName: app.cfg.Real,
234+		Auth:     auth,
235+		Debug:    app.cfg.Debug,
236+	})
237+	if err != nil {
238+		conn.Close()
239+		return
240+	}
241+
242+	return
243 }
244 
245-func (app *App) handleIRCEvents(evs []irc.Event) {
246-	for _, ev := range evs {
247-		app.handleIRCEvent(ev)
248-	}
249-	if !app.pasting {
250-		app.draw()
251+// uiLoop retrieves events from the UI and forwards them to app.events for
252+// handling in app.eventLoop().
253+func (app *App) uiLoop() {
254+	for {
255+		ev, ok := <-app.win.Events
256+		if !ok {
257+			break
258+		}
259+		app.events <- event{
260+			src:     uiEvent,
261+			content: ev,
262+		}
263 	}
264 }
265 
+12, -8
 1@@ -6,13 +6,15 @@ import (
 2 	"encoding/base64"
 3 	"errors"
 4 	"fmt"
 5-	"io"
 6+	"net"
 7 	"strconv"
 8 	"strings"
 9 	"sync/atomic"
10 	"time"
11 )
12 
13+const writeDeadline = 10 * time.Second
14+
15 type SASLClient interface {
16 	Handshake() (mech string)
17 	Respond(challenge string) (res string, err error)
18@@ -149,7 +151,7 @@ type SessionParams struct {
19 
20 // Session is an IRC session/connection/whatever.
21 type Session struct {
22-	conn io.ReadWriteCloser
23+	conn net.Conn
24 	msgs chan Message // incoming messages.
25 	acts chan action  // user actions.
26 	evts chan Event   // events sent to the user.
27@@ -183,7 +185,7 @@ type Session struct {
28 //
29 // It returns an error when the paramaters are invalid, or when it cannot write
30 // to the connection.
31-func NewSession(conn io.ReadWriteCloser, params SessionParams) (*Session, error) {
32+func NewSession(conn net.Conn, params SessionParams) (*Session, error) {
33 	s := &Session{
34 		conn:          conn,
35 		msgs:          make(chan Message, 64),
36@@ -207,11 +209,6 @@ func NewSession(conn io.ReadWriteCloser, params SessionParams) (*Session, error)
37 
38 	s.running.Store(true)
39 
40-	err := s.send("CAP LS 302\r\nNICK %s\r\nUSER %s 0 * :%s\r\n", s.nick, s.user, s.real)
41-	if err != nil {
42-		return nil, err
43-	}
44-
45 	go func() {
46 		r := bufio.NewScanner(conn)
47 
48@@ -233,6 +230,11 @@ func NewSession(conn io.ReadWriteCloser, params SessionParams) (*Session, error)
49 		s.Stop()
50 	}()
51 
52+	err := s.send("CAP LS 302\r\nNICK %s\r\nUSER %s 0 * :%s\r\n", s.nick, s.user, s.real)
53+	if err != nil {
54+		return nil, err
55+	}
56+
57 	go s.run()
58 
59 	return s, nil
60@@ -1081,6 +1083,8 @@ func (s *Session) updateFeatures(features []string) {
61 
62 func (s *Session) send(format string, args ...interface{}) (err error) {
63 	msg := fmt.Sprintf(format, args...)
64+
65+	s.conn.SetWriteDeadline(time.Now().Add(writeDeadline))
66 	_, err = s.conn.Write([]byte(msg))
67 
68 	if s.debug {