From e657ffd2ab2cd5fae8c1d19e39b2268fa758153e Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Sun, 11 Oct 2015 11:12:55 +0300 Subject: [PATCH] Code refactoring * Less memory allocations * Daemon instance replaced with global variables * Code simplification * Asynchronously send messages to clients --- client.go | 121 ++++++++++----- client_test.go | 79 +--------- common_test.go | 93 ++++++++++++ daemon.go | 389 +++++++++++++++++++++++-------------------------- daemon_test.go | 32 ++-- events.go | 9 +- goircd.go | 31 ++-- room.go | 136 ++++++++--------- room_test.go | 70 +++++---- 9 files changed, 513 insertions(+), 447 deletions(-) create mode 100644 common_test.go diff --git a/client.go b/client.go index 9bcaf30..a43cc1e 100644 --- a/client.go +++ b/client.go @@ -23,11 +23,13 @@ import ( "log" "net" "strings" + "sync" "time" ) const ( - BufSize = 1500 + BufSize = 1500 + MaxOutBuf = 1 << 12 ) var ( @@ -35,35 +37,60 @@ var ( ) type Client struct { - hostname *string - conn net.Conn - registered bool - nickname string - username string - realname string - password string - away *string + conn net.Conn + registered bool + nickname *string + username *string + realname *string + password *string + away *string + recvTimestamp time.Time + sendTimestamp time.Time + outBuf chan string + alive bool + sync.Mutex } -type ClientAlivenessState struct { - pingSent bool - timestamp time.Time +func (c Client) String() string { + return *c.nickname + "!" + *c.username + "@" + c.conn.RemoteAddr().String() } -func (client Client) String() string { - return client.nickname + "!" + client.username + "@" + client.conn.RemoteAddr().String() +func NewClient(conn net.Conn) *Client { + nickname := "*" + username := "" + c := Client{ + conn: conn, + nickname: &nickname, + username: &username, + recvTimestamp: time.Now(), + sendTimestamp: time.Now(), + alive: true, + outBuf: make(chan string, MaxOutBuf), + } + go c.MsgSender() + return &c } -func NewClient(hostname *string, conn net.Conn) *Client { - return &Client{hostname: hostname, conn: conn, nickname: "*", password: ""} +func (c *Client) SetDead() { + close(c.outBuf) + c.alive = false +} + +func (c *Client) Close() { + c.Lock() + c.conn.Close() + if c.alive { + c.SetDead() + } + c.Unlock() } // Client processor blockingly reads everything remote client sends, // splits messages by CRLF and send them to Daemon gorouting for processing // it futher. Also it can signalize that client is unavailable (disconnected). -func (client *Client) Processor(sink chan<- ClientEvent) { - sink <- ClientEvent{client, EventNew, ""} - log.Println(client, "New client") +func (c *Client) Processor(sink chan ClientEvent) { + sink <- ClientEvent{c, EventNew, ""} + log.Println(c, "New client") buf := make([]byte, BufSize*2) var n int var prev int @@ -71,14 +98,11 @@ func (client *Client) Processor(sink chan<- ClientEvent) { var err error for { if prev == BufSize { - log.Println(client, "buffer size exceeded, kicking him") - sink <- ClientEvent{client, EventDel, ""} - client.conn.Close() + log.Println(c, "input buffer size exceeded, kicking him") break } - n, err = client.conn.Read(buf[prev:]) + n, err = c.conn.Read(buf[prev:]) if err != nil { - sink <- ClientEvent{client, EventDel, ""} break } prev += n @@ -87,50 +111,69 @@ func (client *Client) Processor(sink chan<- ClientEvent) { if i == -1 { continue } - sink <- ClientEvent{client, EventMsg, string(buf[:i])} + sink <- ClientEvent{c, EventMsg, string(buf[:i])} copy(buf, buf[i+2:prev]) prev -= (i + 2) goto CheckMore } + c.Close() + sink <- ClientEvent{c, EventDel, ""} +} + +func (c *Client) MsgSender() { + for msg := range c.outBuf { + c.conn.Write(append([]byte(msg), CRLF...)) + } } // Send message as is with CRLF appended. -func (client *Client) Msg(text string) { - client.conn.Write(append([]byte(text), CRLF...)) +func (c *Client) Msg(text string) { + c.Lock() + defer c.Unlock() + if !c.alive { + return + } + if len(c.outBuf) == MaxOutBuf { + log.Println(c, "output buffer size exceeded, kicking him") + go c.Close() + c.SetDead() + return + } + c.outBuf <- text } // Send message from server. It has ": servername" prefix. -func (client *Client) Reply(text string) { - client.Msg(":" + *client.hostname + " " + text) +func (c *Client) Reply(text string) { + c.Msg(":" + *hostname + " " + text) } // Send server message, concatenating all provided text parts and // prefix the last one with ":". -func (client *Client) ReplyParts(code string, text ...string) { +func (c *Client) ReplyParts(code string, text ...string) { parts := []string{code} for _, t := range text { parts = append(parts, t) } parts[len(parts)-1] = ":" + parts[len(parts)-1] - client.Reply(strings.Join(parts, " ")) + c.Reply(strings.Join(parts, " ")) } // Send nicknamed server message. After servername it always has target // client's nickname. The last part is prefixed with ":". -func (client *Client) ReplyNicknamed(code string, text ...string) { - client.ReplyParts(code, append([]string{client.nickname}, text...)...) +func (c *Client) ReplyNicknamed(code string, text ...string) { + c.ReplyParts(code, append([]string{*c.nickname}, text...)...) } // Reply "461 not enough parameters" error for given command. -func (client *Client) ReplyNotEnoughParameters(command string) { - client.ReplyNicknamed("461", command, "Not enough parameters") +func (c *Client) ReplyNotEnoughParameters(command string) { + c.ReplyNicknamed("461", command, "Not enough parameters") } // Reply "403 no such channel" error for specified channel. -func (client *Client) ReplyNoChannel(channel string) { - client.ReplyNicknamed("403", channel, "No such channel") +func (c *Client) ReplyNoChannel(channel string) { + c.ReplyNicknamed("403", channel, "No such channel") } -func (client *Client) ReplyNoNickChan(channel string) { - client.ReplyNicknamed("401", channel, "No such nick/channel") +func (c *Client) ReplyNoNickChan(channel string) { + c.ReplyNicknamed("401", channel, "No such nick/channel") } diff --git a/client_test.go b/client_test.go index 3735e1f..4c9682e 100644 --- a/client_test.go +++ b/client_test.go @@ -19,86 +19,17 @@ along with this program. If not, see . package main import ( - "net" "testing" - "time" ) -// Testing network connection that satisfies net.Conn interface -// Can send predefined messages and store all written ones -type TestingConn struct { - inbound chan string - outbound chan string - closed bool -} - -func NewTestingConn() *TestingConn { - inbound := make(chan string, 8) - outbound := make(chan string, 8) - return &TestingConn{inbound: inbound, outbound: outbound} -} - -func (conn TestingConn) Error() string { - return "i am finished" -} - -func (conn *TestingConn) Read(b []byte) (n int, err error) { - msg := <-conn.inbound - if msg == "" { - return 0, conn - } - for n, bt := range append([]byte(msg), CRLF...) { - b[n] = bt - } - return len(msg)+2, nil -} - -type MyAddr struct{} - -func (a MyAddr) String() string { - return "someclient" -} -func (a MyAddr) Network() string { - return "somenet" -} - -func (conn *TestingConn) Write(b []byte) (n int, err error) { - conn.outbound <- string(b) - return len(b), nil -} - -func (conn *TestingConn) Close() error { - conn.closed = true - return nil -} - -func (conn TestingConn) LocalAddr() net.Addr { - return nil -} - -func (conn TestingConn) RemoteAddr() net.Addr { - return MyAddr{} -} - -func (conn TestingConn) SetDeadline(t time.Time) error { - return nil -} - -func (conn TestingConn) SetReadDeadline(t time.Time) error { - return nil -} - -func (conn TestingConn) SetWriteDeadline(t time.Time) error { - return nil -} - // New client creation test. It must send an event about new client, // two predefined messages from it and deletion one func TestNewClient(t *testing.T) { conn := NewTestingConn() sink := make(chan ClientEvent) host := "foohost" - client := NewClient(&host, conn) + hostname = &host + client := NewClient(conn) go client.Processor(sink) event := <-sink @@ -126,8 +57,10 @@ func TestNewClient(t *testing.T) { func TestClientReplies(t *testing.T) { conn := NewTestingConn() host := "foohost" - client := NewClient(&host, conn) - client.nickname = "мойник" + hostname = &host + client := NewClient(conn) + nickname := "мойник" + client.nickname = &nickname client.Reply("hello") if r := <-conn.outbound; r != ":foohost hello\r\n" { diff --git a/common_test.go b/common_test.go new file mode 100644 index 0000000..9c4cb05 --- /dev/null +++ b/common_test.go @@ -0,0 +1,93 @@ +/* +goircd -- minimalistic simple Internet Relay Chat (IRC) server +Copyright (C) 2014-2015 Sergey Matveev + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package main + +import ( + "net" + "time" +) + +// Testing network connection that satisfies net.Conn interface +// Can send predefined messages and store all written ones +type TestingConn struct { + inbound chan string + outbound chan string + closed bool +} + +func NewTestingConn() *TestingConn { + inbound := make(chan string, 8) + outbound := make(chan string, 8) + return &TestingConn{inbound: inbound, outbound: outbound} +} + +func (conn TestingConn) Error() string { + return "i am finished" +} + +func (conn *TestingConn) Read(b []byte) (n int, err error) { + msg := <-conn.inbound + if msg == "" { + return 0, conn + } + for n, bt := range append([]byte(msg), CRLF...) { + b[n] = bt + } + return len(msg) + 2, nil +} + +type MyAddr struct{} + +func (a MyAddr) String() string { + return "someclient" +} +func (a MyAddr) Network() string { + return "somenet" +} + +func (conn *TestingConn) Write(b []byte) (n int, err error) { + conn.outbound <- string(b) + return len(b), nil +} + +func (conn *TestingConn) Close() error { + conn.closed = true + close(conn.outbound) + return nil +} + +func (conn TestingConn) LocalAddr() net.Addr { + return nil +} + +func (conn TestingConn) RemoteAddr() net.Addr { + return MyAddr{} +} + +func (conn TestingConn) SetDeadline(t time.Time) error { + return nil +} + +func (conn TestingConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (conn TestingConn) SetWriteDeadline(t time.Time) error { + return nil +} diff --git a/daemon.go b/daemon.go index 9145ba0..80a3229 100644 --- a/daemon.go +++ b/daemon.go @@ -26,56 +26,28 @@ import ( "regexp" "sort" "strings" + "sync" "time" ) const ( - // Max time deadline for client's unresponsiveness + // Max deadline time of client's unresponsiveness PingTimeout = time.Second * 180 // Max idle client's time before PING are sent PingThreshold = time.Second * 90 - // Client's aliveness check period - AlivenessCheck = time.Second * 10 ) var ( RENickname = regexp.MustCompile("^[a-zA-Z0-9-]{1,24}$") -) -type Daemon struct { - Verbose bool - version string - hostname *string - motd *string - passwords *string - clients map[*Client]struct{} - clientAliveness map[*Client]*ClientAlivenessState - rooms map[string]*Room - roomSinks map[*Room]chan ClientEvent - lastAlivenessCheck time.Time - logSink chan<- LogEvent - stateSink chan<- StateEvent -} + roomsGroup sync.WaitGroup -func NewDaemon(version string, hostname, motd, passwords *string, logSink chan<- LogEvent, stateSink chan<- StateEvent) *Daemon { - daemon := Daemon{ - version: version, - hostname: hostname, - motd: motd, - passwords: passwords, - } - daemon.clients = make(map[*Client]struct{}) - daemon.clientAliveness = make(map[*Client]*ClientAlivenessState) - daemon.rooms = make(map[string]*Room) - daemon.roomSinks = make(map[*Room]chan ClientEvent) - daemon.logSink = logSink - daemon.stateSink = stateSink - return &daemon -} + clients map[*Client]struct{} = make(map[*Client]struct{}) +) -func (daemon *Daemon) SendLusers(client *Client) { +func SendLusers(client *Client) { lusers := 0 - for client := range daemon.clients { + for client := range clients { if client.registered { lusers++ } @@ -83,79 +55,87 @@ func (daemon *Daemon) SendLusers(client *Client) { client.ReplyNicknamed("251", fmt.Sprintf("There are %d users and 0 invisible on 1 servers", lusers)) } -func (daemon *Daemon) SendMotd(client *Client) { - if daemon.motd == nil || *daemon.motd == "" { +func SendMotd(client *Client) { + if motd == nil { client.ReplyNicknamed("422", "MOTD File is missing") return } - - motd, err := ioutil.ReadFile(*daemon.motd) + motdText, err := ioutil.ReadFile(*motd) if err != nil { - log.Printf("Can not read motd file %s: %v", *daemon.motd, err) + log.Printf("Can not read motd file %s: %v", *motd, err) client.ReplyNicknamed("422", "Error reading MOTD File") return } - - client.ReplyNicknamed("375", "- "+*daemon.hostname+" Message of the day -") - for _, s := range strings.Split(strings.Trim(string(motd), "\n"), "\n") { - client.ReplyNicknamed("372", "- "+string(s)) + client.ReplyNicknamed("375", "- "+*hostname+" Message of the day -") + for _, s := range strings.Split(strings.Trim(string(motdText), "\n"), "\n") { + client.ReplyNicknamed("372", "- "+s) } client.ReplyNicknamed("376", "End of /MOTD command") } -func (daemon *Daemon) SendWhois(client *Client, nicknames []string) { +func SendWhois(client *Client, nicknames []string) { + var c *Client + var hostPort string + var err error + var subscriptions []string + var room *Room + var subscriber *Client for _, nickname := range nicknames { nickname = strings.ToLower(nickname) - found := false - for c := range daemon.clients { - if strings.ToLower(c.nickname) != nickname { - continue - } - found = true - h := c.conn.RemoteAddr().String() - h, _, err := net.SplitHostPort(h) - if err != nil { - log.Printf("Can't parse RemoteAddr %q: %v", h, err) - h = "Unknown" - } - client.ReplyNicknamed("311", c.nickname, c.username, h, "*", c.realname) - client.ReplyNicknamed("312", c.nickname, *daemon.hostname, *daemon.hostname) - if c.away != nil { - client.ReplyNicknamed("301", c.nickname, *c.away) + for c = range clients { + if strings.ToLower(*c.nickname) == nickname { + goto Found } - subscriptions := []string{} - for _, room := range daemon.rooms { - for subscriber := range room.members { - if subscriber.nickname == nickname { - subscriptions = append(subscriptions, room.name) - } + } + client.ReplyNoNickChan(nickname) + continue + Found: + hostPort, _, err = net.SplitHostPort(c.conn.RemoteAddr().String()) + if err != nil { + log.Printf("Can't parse RemoteAddr %q: %v", hostPort, err) + hostPort = "Unknown" + } + client.ReplyNicknamed("311", *c.nickname, *c.username, hostPort, "*", *c.realname) + client.ReplyNicknamed("312", *c.nickname, *hostname, *hostname) + if c.away != nil { + client.ReplyNicknamed("301", *c.nickname, *c.away) + } + subscriptions = make([]string, 0) + for _, room = range rooms { + for subscriber = range room.members { + if *subscriber.nickname == nickname { + subscriptions = append(subscriptions, *room.name) } } - sort.Strings(subscriptions) - client.ReplyNicknamed("319", c.nickname, strings.Join(subscriptions, " ")) - client.ReplyNicknamed("318", c.nickname, "End of /WHOIS list") - } - if !found { - client.ReplyNoNickChan(nickname) } + sort.Strings(subscriptions) + client.ReplyNicknamed("319", *c.nickname, strings.Join(subscriptions, " ")) + client.ReplyNicknamed("318", *c.nickname, "End of /WHOIS list") } } -func (daemon *Daemon) SendList(client *Client, cols []string) { - var rooms []string +func SendList(client *Client, cols []string) { + var rs []string + var r string if (len(cols) > 1) && (cols[1] != "") { - rooms = strings.Split(strings.Split(cols[1], " ")[0], ",") + rs = strings.Split(strings.Split(cols[1], " ")[0], ",") } else { - rooms = []string{} - for room := range daemon.rooms { - rooms = append(rooms, room) + rs = make([]string, 0) + for r = range rooms { + rs = append(rs, r) } } - sort.Strings(rooms) - for _, room := range rooms { - r, found := daemon.rooms[room] - if found { - client.ReplyNicknamed("322", room, fmt.Sprintf("%d", len(r.members)), r.topic) + sort.Strings(rs) + var room *Room + var found bool + for _, r = range rs { + if room, found = rooms[r]; found { + client.ReplyNicknamed( + "322", + r, + fmt.Sprintf("%d", len(room.members)), + *room.topic, + ) } } client.ReplyNicknamed("323", "End of /LIST") @@ -166,14 +146,14 @@ func (daemon *Daemon) SendList(client *Client, cols []string) { // * only QUIT, NICK and USER commands are processed // * other commands are quietly ignored // When client finishes NICK/USER workflow, then MOTD and LUSERS are send to him. -func (daemon *Daemon) ClientRegister(client *Client, command string, cols []string) { - switch command { +func ClientRegister(client *Client, cmd string, cols []string) { + switch cmd { case "PASS": if len(cols) == 1 || len(cols[1]) < 1 { client.ReplyNotEnoughParameters("PASS") return } - client.password = cols[1] + client.password = &cols[1] case "NICK": if len(cols) == 1 || len(cols[1]) < 1 { client.ReplyParts("431", "No nickname given") @@ -182,8 +162,8 @@ func (daemon *Daemon) ClientRegister(client *Client, command string, cols []stri nickname := cols[1] // Compatibility with some clients prepending colons to nickname nickname = strings.TrimPrefix(nickname, ":") - for existingClient := range daemon.clients { - if existingClient.nickname == nickname { + for existingClient := range clients { + if *existingClient.nickname == nickname { client.ReplyParts("433", "*", nickname, "Nickname is already in use") return } @@ -192,7 +172,7 @@ func (daemon *Daemon) ClientRegister(client *Client, command string, cols []stri client.ReplyParts("432", "*", cols[1], "Erroneous nickname") return } - client.nickname = nickname + client.nickname = &nickname case "USER": if len(cols) == 1 { client.ReplyNotEnoughParameters("USER") @@ -203,66 +183,69 @@ func (daemon *Daemon) ClientRegister(client *Client, command string, cols []stri client.ReplyNotEnoughParameters("USER") return } - client.username = args[0] - client.realname = strings.TrimLeft(args[3], ":") + client.username = &args[0] + realname := strings.TrimLeft(args[3], ":") + client.realname = &realname } - if client.nickname != "*" && client.username != "" { - if daemon.passwords != nil && *daemon.passwords != "" { - if client.password == "" { + if *client.nickname != "*" && *client.username != "" { + if passwords != nil && *passwords != "" { + if client.password == nil { client.ReplyParts("462", "You may not register") - client.conn.Close() + client.Close() return } - contents, err := ioutil.ReadFile(*daemon.passwords) + contents, err := ioutil.ReadFile(*passwords) if err != nil { - log.Fatalf("Can no read passwords file %s: %s", *daemon.passwords, err) + log.Fatalf("Can no read passwords file %s: %s", *passwords, err) return } for _, entry := range strings.Split(string(contents), "\n") { if entry == "" { continue } - if lp := strings.Split(entry, ":"); lp[0] == client.nickname && lp[1] != client.password { + if lp := strings.Split(entry, ":"); lp[0] == *client.nickname && lp[1] != *client.password { client.ReplyParts("462", "You may not register") - client.conn.Close() + client.Close() return } } } - client.registered = true client.ReplyNicknamed("001", "Hi, welcome to IRC") - client.ReplyNicknamed("002", "Your host is "+*daemon.hostname+", running goircd "+daemon.version) + client.ReplyNicknamed("002", "Your host is "+*hostname+", running goircd "+version) client.ReplyNicknamed("003", "This server was created sometime") - client.ReplyNicknamed("004", *daemon.hostname+" goircd o o") - daemon.SendLusers(client) - daemon.SendMotd(client) + client.ReplyNicknamed("004", *hostname+" goircd o o") + SendLusers(client) + SendMotd(client) log.Println(client, "logged in") } } // Register new room in Daemon. Create an object, events sink, save pointers // to corresponding daemon's places and start room's processor goroutine. -func (daemon *Daemon) RoomRegister(name string) (*Room, chan<- ClientEvent) { - roomNew := NewRoom(daemon.hostname, name, daemon.logSink, daemon.stateSink) - roomNew.Verbose = daemon.Verbose +func RoomRegister(name string) (*Room, chan ClientEvent) { + roomNew := NewRoom(name) roomSink := make(chan ClientEvent) - daemon.rooms[name] = roomNew - daemon.roomSinks[roomNew] = roomSink + rooms[name] = roomNew + roomSinks[roomNew] = roomSink go roomNew.Processor(roomSink) + roomsGroup.Add(1) return roomNew, roomSink } -func (daemon *Daemon) HandlerJoin(client *Client, cmd string) { +func HandlerJoin(client *Client, cmd string) { args := strings.Split(cmd, " ") - rooms := strings.Split(args[0], ",") + rs := strings.Split(args[0], ",") var keys []string if len(args) > 1 { keys = strings.Split(args[1], ",") } else { - keys = []string{} + keys = make([]string, 0) } - for n, room := range rooms { + var roomExisting *Room + var roomSink chan ClientEvent + var roomNew *Room + for n, room := range rs { if !RoomNameValid(room) { client.ReplyNoChannel(room) continue @@ -273,97 +256,88 @@ func (daemon *Daemon) HandlerJoin(client *Client, cmd string) { } else { key = "" } - denied := false - joined := false - for roomExisting, roomSink := range daemon.roomSinks { - if room == roomExisting.name { - if (roomExisting.key != "") && (roomExisting.key != key) { - denied = true - } else { - roomSink <- ClientEvent{client, EventNew, ""} - joined = true + for roomExisting, roomSink = range roomSinks { + if room == *roomExisting.name { + if (roomExisting.key != nil) && (*roomExisting.key != key) { + goto Denied } - break + roomSink <- ClientEvent{client, EventNew, ""} + goto Joined } } - if denied { - client.ReplyNicknamed("475", room, "Cannot join channel (+k) - bad key") - } - if denied || joined { - continue - } - roomNew, roomSink := daemon.RoomRegister(room) + roomNew, roomSink = RoomRegister(room) log.Println("Room", roomNew, "created") if key != "" { - roomNew.key = key + roomNew.key = &key roomNew.StateSave() } roomSink <- ClientEvent{client, EventNew, ""} + continue + Denied: + client.ReplyNicknamed("475", room, "Cannot join channel (+k) - bad key") + Joined: } } -func (daemon *Daemon) Processor(events <-chan ClientEvent) { +func Processor(events chan ClientEvent, finished chan struct{}) { var now time.Time + go func() { + for { + time.Sleep(10 * time.Second) + events <- ClientEvent{eventType: EventTick} + } + }() for event := range events { now = time.Now() client := event.client - - // Check for clients aliveness - if daemon.lastAlivenessCheck.Add(AlivenessCheck).Before(now) { - for c := range daemon.clients { - aliveness, alive := daemon.clientAliveness[c] - if !alive { - continue - } - if aliveness.timestamp.Add(PingTimeout).Before(now) { + switch event.eventType { + case EventTick: + for c := range clients { + if c.recvTimestamp.Add(PingTimeout).Before(now) { log.Println(c, "ping timeout") - c.conn.Close() + c.Close() continue } - if !aliveness.pingSent && aliveness.timestamp.Add(PingThreshold).Before(now) { + if c.sendTimestamp.Add(PingThreshold).Before(now) { if c.registered { - c.Msg("PING :" + *daemon.hostname) - aliveness.pingSent = true + c.Msg("PING :" + *hostname) + c.sendTimestamp = time.Now() } else { log.Println(c, "ping timeout") - c.conn.Close() + c.Close() } } } - daemon.lastAlivenessCheck = now - } - - switch event.eventType { - case EventNew: - daemon.clients[client] = struct{}{} - daemon.clientAliveness[client] = &ClientAlivenessState{ - pingSent: false, - timestamp: now, + case EventTerm: + for _, sink := range roomSinks { + sink <- ClientEvent{eventType: EventTerm} } + roomsGroup.Wait() + close(finished) + return + case EventNew: + clients[client] = struct{}{} case EventDel: - delete(daemon.clients, client) - delete(daemon.clientAliveness, client) - for _, roomSink := range daemon.roomSinks { + delete(clients, client) + for _, roomSink := range roomSinks { roomSink <- event } case EventMsg: cols := strings.SplitN(event.text, " ", 2) - command := strings.ToUpper(cols[0]) - if daemon.Verbose { - log.Println(client, "command", command) + cmd := strings.ToUpper(cols[0]) + if *verbose { + log.Println(client, "command", cmd) } - if command == "QUIT" { + if cmd == "QUIT" { log.Println(client, "quit") - delete(daemon.clients, client) - delete(daemon.clientAliveness, client) - client.conn.Close() + client.Close() continue } if !client.registered { - daemon.ClientRegister(client, command, cols) + ClientRegister(client, cmd, cols) continue } - switch command { + switch cmd { case "AWAY": if len(cols) == 1 { client.away = nil @@ -378,63 +352,63 @@ func (daemon *Daemon) Processor(events <-chan ClientEvent) { client.ReplyNotEnoughParameters("JOIN") continue } - daemon.HandlerJoin(client, cols[1]) + HandlerJoin(client, cols[1]) case "LIST": - daemon.SendList(client, cols) + SendList(client, cols) case "LUSERS": - daemon.SendLusers(client) + SendLusers(client) case "MODE": if len(cols) == 1 || len(cols[1]) < 1 { client.ReplyNotEnoughParameters("MODE") continue } cols = strings.SplitN(cols[1], " ", 2) - if cols[0] == client.username { + if cols[0] == *client.username { if len(cols) == 1 { - client.Msg("221 " + client.nickname + " +") + client.Msg("221 " + *client.nickname + " +") } else { client.ReplyNicknamed("501", "Unknown MODE flag") } continue } room := cols[0] - r, found := daemon.rooms[room] + r, found := rooms[room] if !found { client.ReplyNoChannel(room) continue } if len(cols) == 1 { - daemon.roomSinks[r] <- ClientEvent{client, EventMode, ""} + roomSinks[r] <- ClientEvent{client, EventMode, ""} } else { - daemon.roomSinks[r] <- ClientEvent{client, EventMode, cols[1]} + roomSinks[r] <- ClientEvent{client, EventMode, cols[1]} } case "MOTD": - go daemon.SendMotd(client) + SendMotd(client) case "PART": if len(cols) == 1 || len(cols[1]) < 1 { client.ReplyNotEnoughParameters("PART") continue } - rooms := strings.Split(cols[1], " ")[0] - for _, room := range strings.Split(rooms, ",") { - r, found := daemon.rooms[room] - if !found { + rs := strings.Split(cols[1], " ")[0] + for _, room := range strings.Split(rs, ",") { + if r, found := rooms[room]; found { + roomSinks[r] <- ClientEvent{client, EventDel, ""} + } else { client.ReplyNoChannel(room) continue } - daemon.roomSinks[r] <- ClientEvent{client, EventDel, ""} } case "PING": if len(cols) == 1 { client.ReplyNicknamed("409", "No origin specified") continue } - client.Reply(fmt.Sprintf("PONG %s :%s", *daemon.hostname, cols[1])) + client.Reply(fmt.Sprintf("PONG %s :%s", *hostname, cols[1])) case "PONG": continue case "NOTICE", "PRIVMSG": if len(cols) == 1 { - client.ReplyNicknamed("411", "No recipient given ("+command+")") + client.ReplyNicknamed("411", "No recipient given ("+cmd+")") continue } cols = strings.SplitN(cols[1], " ", 2) @@ -444,12 +418,12 @@ func (daemon *Daemon) Processor(events <-chan ClientEvent) { } msg := "" target := strings.ToLower(cols[0]) - for c := range daemon.clients { - if c.nickname == target { - msg = fmt.Sprintf(":%s %s %s %s", client, command, c.nickname, cols[1]) + for c := range clients { + if *c.nickname == target { + msg = fmt.Sprintf(":%s %s %s %s", client, cmd, *c.nickname, cols[1]) c.Msg(msg) if c.away != nil { - client.ReplyNicknamed("301", c.nickname, *c.away) + client.ReplyNicknamed("301", *c.nickname, *c.away) } break } @@ -457,15 +431,14 @@ func (daemon *Daemon) Processor(events <-chan ClientEvent) { if msg != "" { continue } - r, found := daemon.rooms[target] - if !found { + if r, found := rooms[target]; found { + roomSinks[r] <- ClientEvent{ + client, + EventMsg, + cmd + " " + strings.TrimLeft(cols[1], ":"), + } + } else { client.ReplyNoNickChan(target) - continue - } - daemon.roomSinks[r] <- ClientEvent{ - client, - EventMsg, - command + " " + strings.TrimLeft(cols[1], ":"), } case "TOPIC": if len(cols) == 1 { @@ -473,7 +446,7 @@ func (daemon *Daemon) Processor(events <-chan ClientEvent) { continue } cols = strings.SplitN(cols[1], " ", 2) - r, found := daemon.rooms[cols[0]] + r, found := rooms[cols[0]] if !found { client.ReplyNoChannel(cols[0]) continue @@ -484,19 +457,18 @@ func (daemon *Daemon) Processor(events <-chan ClientEvent) { } else { change = "" } - daemon.roomSinks[r] <- ClientEvent{client, EventTopic, change} + roomSinks[r] <- ClientEvent{client, EventTopic, change} case "WHO": if len(cols) == 1 || len(cols[1]) < 1 { client.ReplyNotEnoughParameters("WHO") continue } room := strings.Split(cols[1], " ")[0] - r, found := daemon.rooms[room] - if !found { + if r, found := rooms[room]; found { + roomSinks[r] <- ClientEvent{client, EventWho, ""} + } else { client.ReplyNoChannel(room) - continue } - daemon.roomSinks[r] <- ClientEvent{client, EventWho, ""} case "WHOIS": if len(cols) == 1 || len(cols[1]) < 1 { client.ReplyNotEnoughParameters("WHOIS") @@ -504,22 +476,21 @@ func (daemon *Daemon) Processor(events <-chan ClientEvent) { } cols := strings.Split(cols[1], " ") nicknames := strings.Split(cols[len(cols)-1], ",") - daemon.SendWhois(client, nicknames) + SendWhois(client, nicknames) case "VERSION": var debug string - if daemon.Verbose { + if *verbose { debug = "debug" } else { debug = "" } - client.ReplyNicknamed("351", fmt.Sprintf("%s.%s %s :", daemon.version, debug, *daemon.hostname)) + client.ReplyNicknamed("351", fmt.Sprintf("%s.%s %s :", version, debug, *hostname)) default: - client.ReplyNicknamed("421", command, "Unknown command") + client.ReplyNicknamed("421", cmd, "Unknown command") } } - if aliveness, alive := daemon.clientAliveness[client]; alive { - aliveness.timestamp = now - aliveness.pingSent = false + if client != nil { + client.recvTimestamp = now } } } diff --git a/daemon_test.go b/daemon_test.go index efdb39f..e48d22d 100644 --- a/daemon_test.go +++ b/daemon_test.go @@ -27,21 +27,24 @@ import ( func TestRegistrationWorkflow(t *testing.T) { host := "foohost" - daemon := NewDaemon("ver1", &host, nil, nil, nil, nil) + hostname = &host events := make(chan ClientEvent) - go daemon.Processor(events) + defer func() { + events <- ClientEvent{eventType: EventTerm} + }() + go Processor(events, make(chan struct{})) conn := NewTestingConn() - client := NewClient(&host, conn) + client := NewClient(conn) go client.Processor(events) - conn.inbound <- "UNEXISTENT CMD" // should recieve nothing on this + conn.inbound <- "UNEXISTENT CMD" // should receive nothing on this conn.inbound <- "NICK" if r := <-conn.outbound; r != ":foohost 431 :No nickname given\r\n" { t.Fatal("431 for NICK", r) } - for _, n := range []string{"привет", " foo", "longlonglong", "#foo", "mein nick", "foo_bar"} { + for _, n := range []string{"привет", " foo", "#foo", "mein nick", "foo_bar"} { conn.inbound <- "NICK " + n if r := <-conn.outbound; r != ":foohost 432 * "+n+" :Erroneous nickname\r\n" { t.Fatal("nickname validation", r) @@ -52,7 +55,7 @@ func TestRegistrationWorkflow(t *testing.T) { if r := <-conn.outbound; r != ":foohost 461 meinick USER :Not enough parameters\r\n" { t.Fatal("461 for USER", r) } - if (client.nickname != "meinick") || client.registered { + if (*client.nickname != "meinick") || client.registered { t.Fatal("NICK saved") } @@ -61,7 +64,7 @@ func TestRegistrationWorkflow(t *testing.T) { t.Fatal("461 again for USER", r) } - daemon.SendLusers(client) + SendLusers(client) if r := <-conn.outbound; !strings.Contains(r, "There are 0 users") { t.Fatal("LUSERS", r) } @@ -85,7 +88,7 @@ func TestRegistrationWorkflow(t *testing.T) { if r := <-conn.outbound; !strings.Contains(r, ":foohost 422") { t.Fatal("422 after registration", r) } - if (client.username != "1") || (client.realname != "4 5") || !client.registered { + if (*client.username != "1") || (*client.realname != "4 5") || !client.registered { t.Fatal("client register") } @@ -96,7 +99,7 @@ func TestRegistrationWorkflow(t *testing.T) { t.Fatal("reply for unexistent command", r) } - daemon.SendLusers(client) + SendLusers(client) if r := <-conn.outbound; !strings.Contains(r, "There are 1 users") { t.Fatal("1 users logged in", r) } @@ -107,10 +110,6 @@ func TestRegistrationWorkflow(t *testing.T) { } conn.inbound <- "QUIT\r\nUNEXISTENT CMD" - <-conn.outbound - if !conn.closed { - t.Fatal("closed connection on QUIT") - } } func TestMotd(t *testing.T) { @@ -123,11 +122,12 @@ func TestMotd(t *testing.T) { conn := NewTestingConn() host := "foohost" - client := NewClient(&host, conn) + hostname = &host + client := NewClient(conn) motdName := fd.Name() - daemon := NewDaemon("ver1", &host, &motdName, nil, nil, nil) + motd = &motdName - daemon.SendMotd(client) + SendMotd(client) if r := <-conn.outbound; !strings.HasPrefix(r, ":foohost 375") { t.Fatal("MOTD start", r) } diff --git a/events.go b/events.go index bc659a6..9f3673b 100644 --- a/events.go +++ b/events.go @@ -34,10 +34,17 @@ const ( EventTopic = iota EventWho = iota EventMode = iota + EventTerm = iota + EventTick = iota FormatMsg = "[%s] <%s> %s\n" FormatMeta = "[%s] * %s %s\n" ) +var ( + logSink chan LogEvent = make(chan LogEvent) + stateSink chan StateEvent = make(chan StateEvent) +) + // Client events going from each of client // They can be either NEW, DEL or unparsed MSG type ClientEvent struct { @@ -70,7 +77,7 @@ func Logger(logdir string, events <-chan LogEvent) { var fd *os.File var err error for event := range events { - logfile = path.Join(logdir, event.where + ".log") + logfile = path.Join(logdir, event.where+".log") fd, err = os.OpenFile(logfile, mode, perm) if err != nil { log.Println("Can not open logfile", logfile, err) diff --git a/goircd.go b/goircd.go index 95b463a..8bc5e3e 100644 --- a/goircd.go +++ b/goircd.go @@ -37,21 +37,19 @@ var ( logdir = flag.String("logdir", "", "Absolute path to directory for logs") statedir = flag.String("statedir", "", "Absolute path to directory for states") passwords = flag.String("passwords", "", "Optional path to passwords file") - - tlsBind = flag.String("tlsbind", "", "TLS address to bind to") - tlsPEM = flag.String("tlspem", "", "Path to TLS certificat+key PEM file") - - verbose = flag.Bool("v", false, "Enable verbose logging.") + tlsBind = flag.String("tlsbind", "", "TLS address to bind to") + tlsPEM = flag.String("tlspem", "", "Path to TLS certificat+key PEM file") + verbose = flag.Bool("v", false, "Enable verbose logging.") ) -func listenerLoop(sock net.Listener, events chan<- ClientEvent) { +func listenerLoop(sock net.Listener, events chan ClientEvent) { for { conn, err := sock.Accept() if err != nil { log.Println("Error during accepting connection", err) continue } - client := NewClient(hostname, conn) + client := NewClient(conn) go client.Processor(events) } } @@ -60,7 +58,6 @@ func Run() { events := make(chan ClientEvent) log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile) - logSink := make(chan LogEvent) if *logdir == "" { // Dummy logger go func() { @@ -75,10 +72,7 @@ func Run() { log.Println(*logdir, "logger initialized") } - stateSink := make(chan StateEvent) - daemon := NewDaemon(version, hostname, motd, passwords, logSink, stateSink) - daemon.Verbose = *verbose - log.Println("goircd " + daemon.version + " is starting") + log.Println("goircd " + version + " is starting") if *statedir == "" { // Dummy statekeeper go func() { @@ -98,14 +92,14 @@ func Run() { if err != nil { log.Fatalf("Can not read state %s: %v", state, err) } - room, _ := daemon.RoomRegister(path.Base(state)) + room, _ := RoomRegister(path.Base(state)) contents := strings.Split(string(buf), "\n") if len(contents) < 2 { - log.Printf("State corrupted for %s: %q", room.name, contents) + log.Printf("State corrupted for %s: %q", *room.name, contents) } else { - room.topic = contents[0] - room.key = contents[1] - log.Println("Loaded state for room", room.name) + room.topic = &contents[0] + room.key = &contents[1] + log.Println("Loaded state for room", *room.name) } } go StateKeeper(*statedir, stateSink) @@ -133,8 +127,7 @@ func Run() { log.Println("TLS listening on", *tlsBind) go listenerLoop(listenerTLS, events) } - - daemon.Processor(events) + Processor(events, make(chan struct{})) } func main() { diff --git a/room.go b/room.go index 4bdb4b2..3da5a3d 100644 --- a/room.go +++ b/room.go @@ -28,6 +28,10 @@ import ( var ( RERoom = regexp.MustCompile("^#[^\x00\x07\x0a\x0d ,:/]{1,200}$") + + rooms map[string]*Room = make(map[string]*Room) + + roomSinks map[*Room]chan ClientEvent = make(map[*Room]chan ClientEvent) ) // Sanitize room's name. It can consist of 1 to 50 ASCII symbols @@ -37,40 +41,34 @@ func RoomNameValid(name string) bool { } type Room struct { - Verbose bool - name string - topic string - key string - members map[*Client]bool - hostname *string - logSink chan<- LogEvent - stateSink chan<- StateEvent + name *string + topic *string + key *string + members map[*Client]struct{} } func (room Room) String() string { - return room.name + return *room.name } -func NewRoom(hostname *string, name string, logSink chan<- LogEvent, stateSink chan<- StateEvent) *Room { - room := Room{name: name} - room.members = make(map[*Client]bool) - room.topic = "" - room.key = "" - room.hostname = hostname - room.logSink = logSink - room.stateSink = stateSink - return &room +func NewRoom(name string) *Room { + topic := "" + return &Room{ + name: &name, + topic: &topic, + members: make(map[*Client]struct{}), + } } func (room *Room) SendTopic(client *Client) { - if room.topic == "" { - client.ReplyNicknamed("331", room.name, "No topic is set") + if *room.topic == "" { + client.ReplyNicknamed("331", *room.name, "No topic is set") } else { - client.ReplyNicknamed("332", room.name, room.topic) + client.ReplyNicknamed("332", *room.name, *room.topic) } } -// Send message to all room's subscribers, possibly excluding someone +// Send message to all room's subscribers, possibly excluding someone. func (room *Room) Broadcast(msg string, clientToIgnore ...*Client) { for member := range room.members { if (len(clientToIgnore) > 0) && member == clientToIgnore[0] { @@ -81,7 +79,11 @@ func (room *Room) Broadcast(msg string, clientToIgnore ...*Client) { } func (room *Room) StateSave() { - room.stateSink <- StateEvent{room.name, room.topic, room.key} + var key string + if room.key != nil { + key = *room.key + } + stateSink <- StateEvent{*room.name, *room.topic, key} } func (room *Room) Processor(events <-chan ClientEvent) { @@ -89,46 +91,50 @@ func (room *Room) Processor(events <-chan ClientEvent) { for event := range events { client = event.client switch event.eventType { + case EventTerm: + roomsGroup.Done() + return case EventNew: - room.members[client] = true - if room.Verbose { + room.members[client] = struct{}{} + if *verbose { log.Println(client, "joined", room.name) } room.SendTopic(client) - room.Broadcast(fmt.Sprintf(":%s JOIN %s", client, room.name)) - room.logSink <- LogEvent{room.name, client.nickname, "joined", true} - nicknames := []string{} + room.Broadcast(fmt.Sprintf(":%s JOIN %s", client, *room.name)) + logSink <- LogEvent{*room.name, *client.nickname, "joined", true} + nicknames := make([]string, 0) for member := range room.members { - nicknames = append(nicknames, member.nickname) + nicknames = append(nicknames, *member.nickname) } sort.Strings(nicknames) - client.ReplyNicknamed("353", "=", room.name, strings.Join(nicknames, " ")) - client.ReplyNicknamed("366", room.name, "End of NAMES list") + client.ReplyNicknamed("353", "=", *room.name, strings.Join(nicknames, " ")) + client.ReplyNicknamed("366", *room.name, "End of NAMES list") case EventDel: if _, subscribed := room.members[client]; !subscribed { - client.ReplyNicknamed("442", room.name, "You are not on that channel") + client.ReplyNicknamed("442", *room.name, "You are not on that channel") continue } delete(room.members, client) - msg := fmt.Sprintf(":%s PART %s :%s", client, room.name, client.nickname) + msg := fmt.Sprintf(":%s PART %s :%s", client, *room.name, *client.nickname) room.Broadcast(msg) - room.logSink <- LogEvent{room.name, client.nickname, "left", true} + logSink <- LogEvent{*room.name, *client.nickname, "left", true} case EventTopic: if _, subscribed := room.members[client]; !subscribed { - client.ReplyParts("442", room.name, "You are not on that channel") + client.ReplyParts("442", *room.name, "You are not on that channel") continue } if event.text == "" { - go room.SendTopic(client) + room.SendTopic(client) continue } - room.topic = strings.TrimLeft(event.text, ":") - msg := fmt.Sprintf(":%s TOPIC %s :%s", client, room.name, room.topic) - go room.Broadcast(msg) - room.logSink <- LogEvent{ - room.name, - client.nickname, - "set topic to " + room.topic, + topic := strings.TrimLeft(event.text, ":") + room.topic = &topic + msg := fmt.Sprintf(":%s TOPIC %s :%s", client, *room.name, *room.topic) + room.Broadcast(msg) + logSink <- LogEvent{ + *room.name, + *client.nickname, + "set topic to " + *room.topic, true, } room.StateSave() @@ -136,32 +142,32 @@ func (room *Room) Processor(events <-chan ClientEvent) { for m := range room.members { client.ReplyNicknamed( "352", - room.name, - m.username, + *room.name, + *m.username, m.conn.RemoteAddr().String(), - *room.hostname, - m.nickname, + *hostname, + *m.nickname, "H", - "0 "+m.realname, + "0 "+*m.realname, ) } - client.ReplyNicknamed("315", room.name, "End of /WHO list") + client.ReplyNicknamed("315", *room.name, "End of /WHO list") case EventMode: if event.text == "" { mode := "+" - if room.key != "" { + if room.key != nil { mode = mode + "k" } - client.Msg(fmt.Sprintf("324 %s %s %s", client.nickname, room.name, mode)) + client.Msg(fmt.Sprintf("324 %s %s %s", *client.nickname, *room.name, mode)) continue } if strings.HasPrefix(event.text, "b") { - client.ReplyNicknamed("368", room.name, "End of channel ban list") + client.ReplyNicknamed("368", *room.name, "End of channel ban list") continue } if strings.HasPrefix(event.text, "-k") || strings.HasPrefix(event.text, "+k") { if _, subscribed := room.members[client]; !subscribed { - client.ReplyParts("442", room.name, "You are not on that channel") + client.ReplyParts("442", *room.name, "You are not on that channel") continue } } else { @@ -176,16 +182,16 @@ func (room *Room) Processor(events <-chan ClientEvent) { client.ReplyNotEnoughParameters("MODE") continue } - room.key = cols[1] - msg = fmt.Sprintf(":%s MODE %s +k %s", client, room.name, room.key) - msgLog = "set channel key to " + room.key - } else if strings.HasPrefix(event.text, "-k") { - room.key = "" - msg = fmt.Sprintf(":%s MODE %s -k", client, room.name) + room.key = &cols[1] + msg = fmt.Sprintf(":%s MODE %s +k %s", client, *room.name, *room.key) + msgLog = "set channel key to " + *room.key + } else { + room.key = nil + msg = fmt.Sprintf(":%s MODE %s -k", client, *room.name) msgLog = "removed channel key" } - go room.Broadcast(msg) - room.logSink <- LogEvent{room.name, client.nickname, msgLog, true} + room.Broadcast(msg) + logSink <- LogEvent{*room.name, *client.nickname, msgLog, true} room.StateSave() case EventMsg: sep := strings.Index(event.text, " ") @@ -193,13 +199,13 @@ func (room *Room) Processor(events <-chan ClientEvent) { ":%s %s %s :%s", client, event.text[:sep], - room.name, + *room.name, event.text[sep+1:]), client, ) - room.logSink <- LogEvent{ - room.name, - client.nickname, + logSink <- LogEvent{ + *room.name, + *client.nickname, event.text[sep+1:], false, } diff --git a/room_test.go b/room_test.go index d94e8d4..342e58c 100644 --- a/room_test.go +++ b/room_test.go @@ -42,17 +42,25 @@ func notEnoughParams(t *testing.T, c *TestingConn) { } func TestTwoUsers(t *testing.T) { - logSink := make(chan LogEvent, 8) - stateSink := make(chan StateEvent, 8) + logSink = make(chan LogEvent, 8) + stateSink = make(chan StateEvent, 8) host := "foohost" - daemon := NewDaemon("ver1", &host, nil, nil, logSink, stateSink) + hostname = &host events := make(chan ClientEvent) - go daemon.Processor(events) + rooms = make(map[string]*Room) + clients = make(map[*Client]struct{}) + roomSinks = make(map[*Room]chan ClientEvent) + finished := make(chan struct{}) + go Processor(events, finished) + defer func() { + events <- ClientEvent{eventType: EventTerm} + <-finished + }() conn1 := NewTestingConn() conn2 := NewTestingConn() - client1 := NewClient(&host, conn1) - client2 := NewClient(&host, conn2) + client1 := NewClient(conn1) + client2 := NewClient(conn2) go client1.Processor(events) go client2.Processor(events) @@ -63,7 +71,7 @@ func TestTwoUsers(t *testing.T) { <-conn2.outbound } - daemon.SendLusers(client1) + SendLusers(client1) if r := <-conn1.outbound; !strings.Contains(r, "There are 2 users") { t.Fatal("LUSERS", r) } @@ -105,27 +113,40 @@ func TestTwoUsers(t *testing.T) { conn1.inbound <- "PRIVMSG nick2 :Hello" conn1.inbound <- "PRIVMSG #foo :world" conn1.inbound <- "NOTICE #foo :world" - <-conn2.outbound - if r := <-conn2.outbound; r != ":nick1!foo1@someclient PRIVMSG nick2 :Hello\r\n" { - t.Fatal("first message", r) + m1 := <-conn2.outbound + m2 := <-conn2.outbound + mNeeded := ":nick1!foo1@someclient PRIVMSG nick2 :Hello\r\n" + if !(m1 == mNeeded || m2 == mNeeded) { + t.Fatal("first message", m1, m2) } - if r := <-conn2.outbound; r != ":nick1!foo1@someclient PRIVMSG #foo :world\r\n" { - t.Fatal("second message", r) + if m2 == mNeeded { + m2 = <-conn2.outbound } - if r := <-conn2.outbound; r != ":nick1!foo1@someclient NOTICE #foo :world\r\n" { - t.Fatal("third message", r) + if m2 != ":nick1!foo1@someclient PRIVMSG #foo :world\r\n" { + t.Fatal("second message", m2) + } + if m2 = <-conn2.outbound; m2 != ":nick1!foo1@someclient NOTICE #foo :world\r\n" { + t.Fatal("third message", m2) } } func TestJoin(t *testing.T) { - logSink := make(chan LogEvent, 8) - stateSink := make(chan StateEvent, 8) + logSink = make(chan LogEvent, 8) + stateSink = make(chan StateEvent, 8) host := "foohost" - daemon := NewDaemon("ver1", &host, nil, nil, logSink, stateSink) + hostname = &host events := make(chan ClientEvent) - go daemon.Processor(events) + rooms = make(map[string]*Room) + clients = make(map[*Client]struct{}) + roomSinks = make(map[*Room]chan ClientEvent) + finished := make(chan struct{}) + go Processor(events, finished) + defer func() { + events <- ClientEvent{eventType: EventTerm} + <-finished + }() conn := NewTestingConn() - client := NewClient(&host, conn) + client := NewClient(conn) go client.Processor(events) conn.inbound <- "NICK nick2\r\nUSER foo2 bar2 baz2 :Long name2" @@ -161,10 +182,10 @@ func TestJoin(t *testing.T) { for i := 0; i < 4*2; i++ { <-conn.outbound } - if _, ok := daemon.rooms["#bar"]; !ok { + if _, ok := rooms["#bar"]; !ok { t.Fatal("#bar does not exist") } - if _, ok := daemon.rooms["#baz"]; !ok { + if _, ok := rooms["#baz"]; !ok { t.Fatal("#baz does not exist") } if r := <-logSink; (r.what != "joined") || (r.where != "#bar") || (r.who != "nick2") || (r.meta != true) { @@ -178,10 +199,10 @@ func TestJoin(t *testing.T) { for i := 0; i < 4*2; i++ { <-conn.outbound } - if daemon.rooms["#barenc"].key != "key1" { + if *rooms["#barenc"].key != "key1" { t.Fatal("no room with key1") } - if daemon.rooms["#bazenc"].key != "key2" { + if *rooms["#bazenc"].key != "key2" { t.Fatal("no room with key2") } if r := <-logSink; (r.what != "joined") || (r.where != "#barenc") || (r.who != "nick2") || (r.meta != true) { @@ -201,7 +222,7 @@ func TestJoin(t *testing.T) { if r := <-conn.outbound; r != ":nick2!foo2@someclient MODE #barenc -k\r\n" { t.Fatal("remove #barenc key", r) } - if daemon.rooms["#barenc"].key != "" { + if rooms["#barenc"].key != nil { t.Fatal("removing key from #barenc") } if r := <-logSink; (r.what != "removed channel key") || (r.where != "#barenc") || (r.who != "nick2") || (r.meta != true) { @@ -253,5 +274,4 @@ func TestJoin(t *testing.T) { if r := <-conn.outbound; r != ":foohost 315 nick2 #barenc :End of /WHO list\r\n" { t.Fatal("end of WHO", r) } - } -- 2.44.0