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 {