master sewn/kohai / irc / channel.go
  1package irc
  2
  3import (
  4	"bufio"
  5	"fmt"
  6	"net"
  7	"strconv"
  8	"strings"
  9	"sync/atomic"
 10	"time"
 11	"unicode"
 12)
 13
 14// special internal Message commands to propagate labeled response status to the writing goroutine
 15var labelEnableCommand = ":enable_labeled_response"
 16var labelDisableCommand = ":disable_labeled_response"
 17
 18const chanCapacity = 64
 19
 20func ChanInOut(conn net.Conn) (in <-chan Message, out chan<- Message) {
 21	in_ := make(chan Message, chanCapacity)
 22	out_ := make(chan Message, chanCapacity)
 23
 24	const keepAlive = 30 * time.Second
 25	const maxRTT = 10 * time.Second
 26	var last atomic.Value
 27	last.Store(time.Now())
 28
 29	go func() {
 30		r := bufio.NewScanner(conn)
 31		for r.Scan() {
 32			line := r.Text()
 33			line = strings.ToValidUTF8(line, string([]rune{unicode.ReplacementChar}))
 34			// Workaround for https://bugs.unrealircd.org/view.php?id=6598
 35			line = strings.TrimSuffix(line, "\r")
 36			msg, err := ParseMessage(line)
 37			if err != nil {
 38				continue
 39			}
 40			now := time.Now()
 41			last.Store(now)
 42			conn.SetReadDeadline(now.Add(keepAlive + maxRTT))
 43			in_ <- msg
 44		}
 45		close(in_)
 46	}()
 47
 48	go func() {
 49		t := time.NewTicker(time.Second)
 50		defer t.Stop()
 51		labelOff := 1
 52		labeledResponse := false
 53	outer:
 54		for {
 55			select {
 56			case msg, ok := <-out_:
 57				if !ok {
 58					break outer
 59				}
 60				if msg.Command == labelEnableCommand {
 61					labeledResponse = true
 62					continue
 63				}
 64				if msg.Command == labelDisableCommand {
 65					labeledResponse = false
 66					continue
 67				}
 68				if labeledResponse {
 69					label := strconv.Itoa(labelOff)
 70					labelOff++
 71					if msg.Tags == nil {
 72						msg.Tags = map[string]string{"label": label}
 73					} else {
 74						msg.Tags["label"] = label
 75					}
 76				}
 77
 78				last.Store(time.Now())
 79				// TODO send messages by batches
 80				_, err := fmt.Fprintf(conn, "%s\r\n", msg.String())
 81				if err != nil {
 82					break outer
 83				}
 84			case <-t.C:
 85				now := time.Now()
 86				if last.Load().(time.Time).Add(keepAlive).After(now) {
 87					continue
 88				}
 89				if last.Load().(time.Time).Add(keepAlive + maxRTT).Before(now) {
 90					// probably out of sleep, reset connection
 91					conn.Close()
 92					continue
 93				}
 94				last.Store(now)
 95				_, err := fmt.Fprint(conn, "PING _\r\n")
 96				if err != nil {
 97					break outer
 98				}
 99			}
100		}
101		_ = conn.Close()
102	}()
103
104	return in_, out_
105}