diff --git a/builtin.go b/builtin.go index 5143dc0..686f9ac 100644 --- a/builtin.go +++ b/builtin.go @@ -12,17 +12,10 @@ import ( "github.com/araddon/dateparse" ) -const ( - stateUnlocked uint32 = iota - stateLocked -) - // registerBuiltin sets up built-in handlers, based on client // configuration. func (c *Client) registerBuiltins() { c.debug.Print("registering built-in handlers") - c.Handlers.mu.Lock() - defer c.Handlers.mu.Unlock() // Built-in things that should always be supported. c.Handlers.register(true, true, RPL_WELCOME, HandlerFunc(handleConnect)) @@ -107,10 +100,19 @@ func handleConnect(c *Client, e Event) { c.state.nick.Store(e.Params[0]) c.state.notify(c, UPDATE_GENERAL) split := strings.Split(e.Params[1], " ") - if strings.HasPrefix(e.Params[1], "Welcome to the") && len(split) > 3 { - if len(split[3]) > 0 { - c.state.network = split[3] - c.IRCd.Network = split[3] + search: + for i, artifact := range split { + switch strings.ToLower(artifact) { + case "welcome", "to": + continue + case "the": + if len(split) < i { + break search + } + c.IRCd.Network = split[i+1] + break search + default: + break search } } } @@ -221,9 +223,7 @@ func handlePART(c *Client, e Event) { defer c.state.notify(c, UPDATE_STATE) if e.Source.ID() == c.GetID() { - c.state.deleteChannel(channel) - return } @@ -376,7 +376,6 @@ func handleNICK(c *Client, e Event) { if len(e.Params) >= 1 { c.state.renameUser(e.Source.ID(), e.Last()) } - c.state.notify(c, UPDATE_STATE) } @@ -404,7 +403,6 @@ func handleGLOBALUSERS(c *Client, e Event) { if err != nil { return } - c.IRCd.UserCount = cusers c.IRCd.MaxUserCount = musers } @@ -418,7 +416,6 @@ func handleLOCALUSERS(c *Client, e Event) { if err != nil { return } - c.IRCd.LocalUserCount = cusers c.IRCd.LocalMaxUserCount = musers } @@ -428,7 +425,6 @@ func handleLUSERCHANNELS(c *Client, e Event) { if err != nil { return } - c.IRCd.ChannelCount = ccount } @@ -437,7 +433,6 @@ func handleLUSEROP(c *Client, e Event) { if err != nil { return } - c.IRCd.OperCount = ocount } @@ -462,9 +457,7 @@ func handleCREATED(c *Client, e Event) { if err != nil { return } - c.IRCd.Compiled = compiled - c.state.notify(c, UPDATE_GENERAL) } @@ -484,10 +477,8 @@ func handleYOURHOST(c *Client, e Event) { if len(host)+len(ver) == 0 { return } - c.IRCd.Host = host c.IRCd.Version = ver - c.state.notify(c, UPDATE_GENERAL) } @@ -533,13 +524,11 @@ func handleISUPPORT(c *Client, e Event) { // handleMOTD handles incoming MOTD messages and buffers them up for use with // Client.ServerMOTD(). func handleMOTD(c *Client, e Event) { - defer c.state.notify(c, UPDATE_GENERAL) // Beginning of the MOTD. if e.Command == RPL_MOTDSTART { c.state.motd = "" - return } @@ -548,7 +537,6 @@ func handleMOTD(c *Client, e Event) { c.state.motd += "\n" } c.state.motd += e.Last() - } // handleNAMES handles incoming NAMES queries, of which lists all users in @@ -608,7 +596,6 @@ func handleNAMES(c *Client, e Event) { perms.set(modes, false) user.Perms.set(channel.Name, perms) } - c.state.notify(c, UPDATE_STATE) } diff --git a/cap.go b/cap.go index c47b0d3..906fd11 100644 --- a/cap.go +++ b/cap.go @@ -9,6 +9,8 @@ import ( "strconv" "strings" "time" + + cmap "github.com/orcaman/concurrent-map" ) // Something not in the list? Depending on the type of capability, you can @@ -118,12 +120,11 @@ func parseCap(raw string) map[string]map[string]string { // This will lock further registration until we have acknowledged (or denied) // the capabilities. func handleCAP(c *Client, e Event) { - if len(e.Params) >= 2 && e.Params[1] == CAP_DEL { caps := parseCap(e.Last()) for capab := range caps { // TODO: test the deletion. - delete(c.state.enabledCap, capab) + c.state.enabledCap.Remove(capab) } return } @@ -146,7 +147,7 @@ func handleCAP(c *Client, e Event) { } if len(possible[capName]) == 0 || len(caps[capName]) == 0 { - c.state.tmpCap[capName] = caps[capName] + c.state.tmpCap.Set(capName, caps[capName]) continue } @@ -167,7 +168,7 @@ func handleCAP(c *Client, e Event) { continue } - c.state.tmpCap[capName] = caps[capName] + c.state.tmpCap.Set(capName, caps[capName]) } // Indicates if this is a multi-line LS. (3 args means it's the @@ -180,10 +181,11 @@ func handleCAP(c *Client, e Event) { } // Let them know which ones we'd like to enable. - reqKeys := make([]string, len(c.state.tmpCap)) + reqKeys := make([]string, len(c.state.tmpCap.Keys())) i := 0 - for k := range c.state.tmpCap { - reqKeys[i] = k + for k := range c.state.tmpCap.IterBuffered() { + kv := k.Val.(string) + reqKeys[i] = kv i++ } c.write(&Event{Command: CAP, Params: []string{CAP_REQ, strings.Join(reqKeys, " ")}}) @@ -193,10 +195,12 @@ func handleCAP(c *Client, e Event) { if len(e.Params) == 3 && e.Params[1] == CAP_ACK { enabled := strings.Split(e.Last(), " ") for _, capab := range enabled { - if val, ok := c.state.tmpCap[capab]; ok { - c.state.enabledCap[capab] = val + val, ok := c.state.tmpCap.Get(capab) + if ok { + val = val.(map[string]string) + c.state.enabledCap.Set(capab, val) } else { - c.state.enabledCap[capab] = nil + c.state.enabledCap.Set(capab, nil) } } @@ -205,9 +209,10 @@ func handleCAP(c *Client, e Event) { // Handle STS, and only if it's something specifically we enabled (client // may choose to disable girc automatic STS, and do it themselves). - if sts, sok := c.state.enabledCap["sts"]; sok && !c.Config.DisableSTS { + stsi, sok := c.state.enabledCap.Get("sts") + if sok && !c.Config.DisableSTS { var isError bool - + sts := stsi.(map[string]string) // Some things are updated in the policy depending on if the current // connection is over tls or not. var hasTLSConnection bool @@ -284,9 +289,10 @@ func handleCAP(c *Client, e Event) { // Re-initialize the tmpCap, so if we get multiple 'CAP LS' requests // due to cap-notify, we can re-evaluate what we can support. - c.state.tmpCap = make(map[string]map[string]string) + c.state.tmpCap = cmap.New() - if _, ok := c.state.enabledCap["sasl"]; ok && c.Config.SASL != nil { + _, ok := c.state.enabledCap.Get("sasl") + if ok && c.Config.SASL != nil { c.write(&Event{Command: AUTHENTICATE, Params: []string{c.Config.SASL.Method()}}) // Don't "CAP END", since we want to authenticate. return @@ -342,11 +348,9 @@ func handleACCOUNT(c *Client, e Event) { account = "" } - c.state.Lock() user := c.state.lookupUser(e.Source.Name) if user != nil { user.Extras.Account = account } - c.state.Unlock() c.state.notify(c, UPDATE_STATE) } diff --git a/cap_test.go b/cap_test.go index 3b5126d..b9e1139 100644 --- a/cap_test.go +++ b/cap_test.go @@ -5,6 +5,7 @@ package girc import ( + "os" "reflect" "testing" ) @@ -16,7 +17,7 @@ func TestCapSupported(t *testing.T) { User: "user", SASL: &SASLPlain{User: "test", Pass: "example"}, SupportedCaps: map[string][]string{"example": nil}, - Debug: newDebugWriter(t), + Debug: os.Stdout, }) var ok bool diff --git a/client.go b/client.go index 1700c83..c3e0f06 100644 --- a/client.go +++ b/client.go @@ -19,7 +19,6 @@ import ( "strconv" "strings" "sync" - "sync/atomic" "time" cmap "github.com/orcaman/concurrent-map" @@ -297,7 +296,6 @@ func New(config Config) *Client { tx: make(chan *Event, 25), CTCP: newCTCP(), initTime: time.Now(), - atom: stateUnlocked, } c.IRCd = Server{ @@ -341,6 +339,7 @@ func New(config Config) *Client { // Give ourselves a new state. c.state = &state{} + c.state.RWMutex = &sync.RWMutex{} c.state.reset(true) c.state.client = c @@ -596,9 +595,12 @@ func (c *Client) GetHost() (host string) { func (c *Client) ChannelList() []string { c.panicIfNotTracking() - channels := make([]string, 0, len(c.state.channels)) + channels := make([]string, 0, len(c.state.channels.Keys())) for channel := range c.state.channels.IterBuffered() { chn := channel.Val.(*Channel) + if !chn.UserIn(c.GetNick()) { + continue + } channels = append(channels, chn.Name) } @@ -729,7 +731,7 @@ func (c *Client) NetworkName() (name string) { var ok bool if len(c.state.network) > 0 { - return + return c.state.network } name, ok = c.GetServerOption("NETWORK") @@ -788,12 +790,7 @@ func (c *Client) HasCapability(name string) (has bool) { name = strings.ToLower(name) - for atomic.CompareAndSwapUint32(&c.atom, stateUnlocked, stateLocked) { - randSleep() - } - defer atomic.StoreUint32(&c.atom, stateUnlocked) - - for key := range c.state.enabledCap { + for _, key := range c.state.enabledCap.Keys() { key = strings.ToLower(key) if key == name { has = true diff --git a/client_test.go b/client_test.go index 749a0fb..4fef11f 100644 --- a/client_test.go +++ b/client_test.go @@ -5,24 +5,12 @@ package girc import ( + "os" "strings" "testing" "time" ) -type debugWriter struct { - t *testing.T -} - -func newDebugWriter(t *testing.T) debugWriter { - return debugWriter{t: t} -} - -func (d debugWriter) Write(p []byte) (n int, err error) { - go d.t.Logf("%v", string(p)) - return len(p), nil -} - func TestDisableTracking(t *testing.T) { client := New(Config{ Server: "dummy.int", @@ -30,7 +18,7 @@ func TestDisableTracking(t *testing.T) { Nick: "test", User: "test", Name: "Testing123", - Debug: newDebugWriter(t), + Debug: os.Stdout, }) if client.Handlers.internal.len() < 1 { @@ -96,7 +84,7 @@ func TestClientLifetime(t *testing.T) { Nick: "test", User: "test", Name: "Testing123", - Debug: newDebugWriter(t), + Debug: os.Stdout, }) tm := client.Lifetime() @@ -107,7 +95,7 @@ func TestClientLifetime(t *testing.T) { } func TestClientUptime(t *testing.T) { - c, conn, server := genMockConn(t) + c, conn, server := genMockConn() defer conn.Close() defer server.Close() go mockReadBuffer(conn) @@ -152,7 +140,7 @@ func TestClientUptime(t *testing.T) { } func TestClientGet(t *testing.T) { - c, conn, server := genMockConn(t) + c, conn, server := genMockConn() defer conn.Close() defer server.Close() go mockReadBuffer(conn) @@ -183,7 +171,7 @@ func TestClientGet(t *testing.T) { } func TestClientClose(t *testing.T) { - c, conn, server := genMockConn(t) + c, conn, server := genMockConn() defer server.Close() defer conn.Close() go mockReadBuffer(conn) diff --git a/cmdhandler/cmd.go b/cmdhandler/cmd.go deleted file mode 100644 index 706534f..0000000 --- a/cmdhandler/cmd.go +++ /dev/null @@ -1,191 +0,0 @@ -package cmdhandler - -import ( - "errors" - "fmt" - "regexp" - "strings" - - "github.com/yunginnanet/girc-atomic" -) - -// Input is a wrapper for events, based around private messages. -type Input struct { - Origin *girc.Event - Args []string - RawArgs string -} - -// Command is an IRC command, supporting aliases, help documentation and easy -// wrapping for message inputs. -type Command struct { - // Name of command, e.g. "search" or "ping". - Name string - // Aliases for the above command, e.g. "s" for search, or "p" for "ping". - Aliases []string - // Help documentation. Should be in the format " [arg] -- - // something useful here" - Help string - // MinArgs is the minimum required arguments for the command. Defaults to - // 0, which means multiple, or no arguments can be supplied. If set - // above 0, this means that the command handler will throw an error asking - // the person to check "help " for more info. - MinArgs int - // Fn is the function which is executed when the command is ran from a - // private message, or channel. - Fn func(*girc.Client, *Input) -} - -func (c *Command) genHelp(prefix string) string { - out := "{b}" + prefix + c.Name + "{b}" - - if c.Aliases != nil && len(c.Aliases) > 0 { - out += " ({b}" + prefix + strings.Join(c.Aliases, "{b}, {b}"+prefix) + "{b})" - } - - out += " :: " + c.Help - - return out -} - -// CmdHandler is an irc command parser and execution format which you could -// use as an example for building your own version/bot. -// -// An example of how you would register this with girc: -// -// ch, err := cmdhandler.New("!") -// if err != nil { -// panic(err) -// } -// -// ch.Add(&cmdhandler.Command{ -// Name: "ping", -// Help: "Sends a pong reply back to the original user.", -// Fn: func(c *girc.Client, input *cmdhandler.Input) { -// c.Commands.ReplyTo(*input.Origin, "pong!") -// }, -// }) -// -// client.Handlers.AddHandler(girc.PRIVMSG, ch) -type CmdHandler struct { - prefix string - re *regexp.Regexp - - cmds map[string]*Command -} - -var cmdMatch = `^%s([a-z0-9-_]{1,20})(?: (.*))?$` - -// New returns a new CmdHandler based on the specified command prefix. A good -// prefix is a single character, and easy to remember/use. E.g. "!", or ".". -func New(prefix string) (*CmdHandler, error) { - re, err := regexp.Compile(fmt.Sprintf(cmdMatch, regexp.QuoteMeta(prefix))) - if err != nil { - return nil, err - } - - return &CmdHandler{prefix: prefix, re: re, cmds: make(map[string]*Command)}, nil -} - -var validName = regexp.MustCompile(`^[a-z0-9-_]{1,20}$`) - -// Add registers a new command to the handler. Note that you cannot remove -// commands once added, unless you add another CmdHandler to the client. -func (ch *CmdHandler) Add(cmd *Command) error { - if cmd == nil { - return errors.New("nil command provided to CmdHandler") - } - - cmd.Name = strings.ToLower(cmd.Name) - if !validName.MatchString(cmd.Name) { - return fmt.Errorf("invalid command name: %q (req: %q)", cmd.Name, validName.String()) - } - - if cmd.Aliases != nil { - for i := 0; i < len(cmd.Aliases); i++ { - cmd.Aliases[i] = strings.ToLower(cmd.Aliases[i]) - if !validName.MatchString(cmd.Aliases[i]) { - return fmt.Errorf("invalid command name: %q (req: %q)", cmd.Aliases[i], validName.String()) - } - } - } - - if cmd.MinArgs < 0 { - cmd.MinArgs = 0 - } - - if _, ok := ch.cmds[cmd.Name]; ok { - return fmt.Errorf("command already registered: %s", cmd.Name) - } - - ch.cmds[cmd.Name] = cmd - - // Since we'd be storing pointers, duplicates do not matter. - for i := 0; i < len(cmd.Aliases); i++ { - if _, ok := ch.cmds[cmd.Aliases[i]]; ok { - return fmt.Errorf("alias already registered: %s", cmd.Aliases[i]) - } - - ch.cmds[cmd.Aliases[i]] = cmd - } - - return nil -} - -// Execute satisfies the girc.Handler interface. -func (ch *CmdHandler) Execute(client *girc.Client, event girc.Event) { - if event.Source == nil || event.Command != girc.PRIVMSG { - return - } - - parsed := ch.re.FindStringSubmatch(event.Last()) - if len(parsed) != 3 { - return - } - - invCmd := strings.ToLower(parsed[1]) - args := strings.Split(parsed[2], " ") - if len(args) == 1 && args[0] == "" { - args = []string{} - } - - if invCmd == "help" { - if len(args) == 0 { - client.Cmd.ReplyTo(event, girc.Fmt("type '{b}!help {blue}{c}{b}' to optionally get more info about a specific command.")) - return - } - - args[0] = strings.ToLower(args[0]) - - if _, ok := ch.cmds[args[0]]; !ok { - client.Cmd.ReplyTof(event, girc.Fmt("unknown command {b}%q{b}."), args[0]) - return - } - - if ch.cmds[args[0]].Help == "" { - client.Cmd.ReplyTof(event, girc.Fmt("there is no help documentation for {b}%q{b}"), args[0]) - return - } - - client.Cmd.ReplyTo(event, girc.Fmt(ch.cmds[args[0]].genHelp(ch.prefix))) - return - } - - cmd, ok := ch.cmds[invCmd] - if !ok { - return - } - - if len(args) < cmd.MinArgs { - client.Cmd.ReplyTof(event, girc.Fmt("not enough arguments supplied for {b}%q{b}. try '{b}%shelp %s{b}'?"), invCmd, ch.prefix, invCmd) - return - } - - in := &Input{ - Origin: &event, - Args: args, - RawArgs: parsed[2], - } - - go cmd.Fn(client, in) -} diff --git a/conn.go b/conn.go index 88e9362..a6a6cef 100644 --- a/conn.go +++ b/conn.go @@ -441,11 +441,6 @@ func (c *Client) Send(event *Event) { event.Network = c.NetworkName() - for atomic.CompareAndSwapUint32(&c.atom, stateUnlocked, stateLocked) { - randSleep() - } - defer atomic.StoreUint32(&c.atom, stateUnlocked) - if !c.Config.AllowFlood { // Drop the event early as we're disconnected, this way we don't have to wait // the (potentially long) rate limit delay before dropping. @@ -515,7 +510,7 @@ func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGro // var in bool for i := 0; i < len(c.state.enabledCap); i++ { - if _, ok := c.state.enabledCap["message-tags"]; ok { + if _, ok := c.state.enabledCap.Get("message-tags"); ok { in = true break } @@ -580,9 +575,9 @@ type ErrTimedOut struct { func (ErrTimedOut) Error() string { return "timed out waiting for a requested PING response" } func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) { - defer wg.Done() // Don't run the pingLoop if they want to disable it. if c.Config.PingDelay <= 0 { + wg.Done() return } @@ -621,6 +616,7 @@ func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGro Delay: c.Config.PingDelay, } + wg.Done() return } @@ -628,6 +624,7 @@ func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGro c.Cmd.Ping(fmt.Sprintf("%d", time.Now().UnixNano())) case <-ctx.Done(): + wg.Done() return } } diff --git a/conn_test.go b/conn_test.go index 3477545..e6a8a48 100644 --- a/conn_test.go +++ b/conn_test.go @@ -8,6 +8,7 @@ import ( "bufio" "bytes" "net" + "os" "sync/atomic" "testing" "time" @@ -93,14 +94,14 @@ func TestRate(t *testing.T) { return } -func genMockConn(t *testing.T) (client *Client, clientConn net.Conn, serverConn net.Conn) { +func genMockConn() (client *Client, clientConn net.Conn, serverConn net.Conn) { client = New(Config{ Server: "dummy.int", Port: 6667, Nick: "test", User: "test", Name: "Testing123", - Debug: newDebugWriter(t), + Debug: os.Stdout, }) conn1, conn2 := net.Pipe() @@ -108,19 +109,14 @@ func genMockConn(t *testing.T) (client *Client, clientConn net.Conn, serverConn return client, conn1, conn2 } -func mockReadBuffer(conn net.Conn) error { +func mockReadBuffer(conn net.Conn) { // Accept all outgoing writes from the client. b := bufio.NewReader(conn) for { - err := conn.SetReadDeadline(time.Now().Add(10 * time.Second)) + conn.SetReadDeadline(time.Now().Add(10 * time.Second)) + _, err := b.ReadString(byte('\n')) if err != nil { - return err - } - var str string - str, err = b.ReadString(byte('\n')) - println(str) - if err != nil { - return err + return } } } diff --git a/handler.go b/handler.go index 335b7e7..74d4751 100644 --- a/handler.go +++ b/handler.go @@ -167,6 +167,7 @@ func (c *Caller) Len() int { // Count is much like Caller.Len(), however it counts the number of // registered handlers for a given command. func (c *Caller) Count(cmd string) int { + cmd = strings.ToUpper(cmd) return c.external.lenFor(cmd) } @@ -198,63 +199,91 @@ func (c *Caller) cuidToID(input string) (cmd, uid string) { return input[:i], input[i+1:] } +type execStack struct { + Handler + cuid string +} + // exec executes all handlers pertaining to specified event. Internal first, // then external. // // Please note that there is no specific order/priority for which the handlers // are executed. func (c *Caller) exec(command string, bg bool, client *Client, event *Event) { - handle := func(wgr *sync.WaitGroup, h handlerTuple) { - c.debug.Printf("(%s) exec %s => %s", c.parent.Config.Nick, command, h.cuid) - start := time.Now() + // Build a stack of handlers which can be executed concurrently. + var stack []execStack - if bg { - go func() { - defer wgr.Done() - if client.Config.RecoverFunc != nil { - defer recoverHandlerPanic(client, event, h.cuid, 3) - } - h.handler.Execute(client, *event) - c.debug.Printf("(%s) done %s == %s", c.parent.Config.Nick, - h.cuid, time.Since(start)) - }() - return + // Get internal handlers first. + ihm, iok := c.internal.cm.Get(command) + if iok { + hmap := ihm.(cmap.ConcurrentMap) + for _, cuid := range hmap.Keys() { + if (strings.HasSuffix(cuid, ":bg") && !bg) || (!strings.HasSuffix(cuid, ":bg") && bg) { + continue + } + hi, _ := hmap.Get(cuid) + hndlr, ok := hi.(Handler) + if !ok { + continue + } + stack = append(stack, execStack{hndlr, cuid}) } - - if client.Config.RecoverFunc != nil { - defer recoverHandlerPanic(client, event, h.cuid, 3) + } + // Then external handlers. + ehm, eok := c.external.cm.Get(command) + if eok { + hmap := ehm.(cmap.ConcurrentMap) + for _, cuid := range hmap.Keys() { + if (strings.HasSuffix(cuid, ":bg") && !bg) || (!strings.HasSuffix(cuid, ":bg") && bg) { + continue + } + hi, _ := hmap.Get(cuid) + hndlr, ok := hi.(Handler) + if !ok { + continue + } + stack = append(stack, execStack{hndlr, cuid}) } - - h.handler.Execute(client, *event) - c.debug.Printf("(%s) done %s == %s", c.parent.Config.Nick, h.cuid, time.Since(start)) - wgr.Done() } // Run all handlers concurrently across the same event. This should // still help prevent mis-ordered events, while speeding up the // execution speed. var wg sync.WaitGroup + wg.Add(len(stack)) + for i := 0; i < len(stack); i++ { + go func(index int) { + c.debug.Printf("(%s) [%d/%d] exec %s => %s", c.parent.Config.Nick, + index+1, len(stack), stack[index].cuid, command) + start := time.Now() - internals, iok := c.internal.getAllHandlersFor(command) - if iok { - for h := range internals { - wg.Add(1) - go handle(&wg, h) - } - } - externals, eok := c.external.getAllHandlersFor(command) - if eok { - for h := range externals { - wg.Add(1) - go handle(&wg, h) - } - } + if bg { + go func() { + defer wg.Done() + if client.Config.RecoverFunc != nil { + defer recoverHandlerPanic(client, event, stack[index].cuid, 3) + } + stack[index].Handler.Execute(client, *event) + c.debug.Printf("(%s) done %s == %s", c.parent.Config.Nick, + stack[index].cuid, time.Since(start)) + }() + return + } + defer wg.Done() - // Wait for all of the handlers to complete. Not doing this may cause - // new events from becoming ahead of older handlers. - c.debug.Printf("(%s) wg.Wait()", c.parent.Config.Nick) - wg.Wait() + if client.Config.RecoverFunc != nil { + defer recoverHandlerPanic(client, event, stack[index].cuid, 3) + } + + stack[index].Handler.Execute(client, *event) + c.debug.Printf("(%s) done %s == %s", c.parent.Config.Nick, stack[index].cuid, time.Since(start)) + }(i) + + // new events from becoming ahead of ol1 handlers. + c.debug.Printf("(%s) wg.Wait()", c.parent.Config.Nick) + wg.Wait() + } } // ClearAll clears all external handlers currently setup within the client. @@ -283,11 +312,8 @@ func (c *Caller) Clear(cmd string) { // indicates that it existed, and has been removed. If not success, it // wasn't a registered handler. func (c *Caller) Remove(cuid string) (success bool) { - c.mu.Lock() - success = c.remove(cuid) - c.mu.Unlock() - - return success + c.remove(cuid) + return true } // remove is much like Remove, however is NOT concurrency safe. Lock Caller.mu @@ -359,12 +385,12 @@ func (c *Caller) register(internal, bg bool, cmd string, handler Handler) (cuid } else { chandlers = cmap.New() } - parent.cm.SetIfAbsent(cmd, chandlers) chandlers.Set(uid, handler) - _, file, line, _ := runtime.Caller(2) + parent.cm.Set(cmd, chandlers) + _, file, line, _ := runtime.Caller(2) c.debug.Printf("reg %q => %s [int:%t bg:%t] %s:%d", uid, cmd, internal, bg, file, line) return cuid diff --git a/modes.go b/modes.go index 306ad33..a7ca482 100644 --- a/modes.go +++ b/modes.go @@ -404,13 +404,9 @@ func (p *UserPerms) Copy() (perms *UserPerms) { np := &UserPerms{ channels: make(map[string]Perms), } - - p.mu.RLock() for key := range p.channels { np.channels[key] = p.channels[key] } - p.mu.RUnlock() - return np } @@ -426,9 +422,7 @@ func (p *UserPerms) MarshalJSON() ([]byte, error) { // Lookup looks up the users permissions for a given channel. ok is false // if the user is not in the given channel. func (p *UserPerms) Lookup(channel string) (perms Perms, ok bool) { - p.mu.RLock() perms, ok = p.channels[ToRFC1459(channel)] - p.mu.RUnlock() return perms, ok } diff --git a/state.go b/state.go index 54af6d9..786e3f6 100644 --- a/state.go +++ b/state.go @@ -28,11 +28,11 @@ type state struct { // users map[string]*User users cmap.ConcurrentMap // enabledCap are the capabilities which are enabled for this connection. - enabledCap map[string]map[string]string + enabledCap cmap.ConcurrentMap // tmpCap are the capabilties which we share with the server during the // last capability check. These will get sent once we have received the // last capability list command from the server. - tmpCap map[string]map[string]string + tmpCap cmap.ConcurrentMap // serverOptions are the standard capabilities and configurations // supported by the server at connection time. This also includes // RPL_ISUPPORT entries. @@ -69,8 +69,8 @@ func (s *state) reset(initial bool) { } } - s.enabledCap = make(map[string]map[string]string) - s.tmpCap = make(map[string]map[string]string) + s.enabledCap = cmap.New() + s.tmpCap = cmap.New() s.motd = "" if initial { @@ -482,6 +482,7 @@ func (s *state) createUser(src *Source) (u *User, ok bool) { func (s *state) deleteUser(channelName, nick string) { user := s.lookupUser(nick) if user == nil { + s.client.debug.Printf(nick + ": was not found when trying to deleteUser from " + channelName) return } diff --git a/state_test.go b/state_test.go index f94e3c9..fb22bca 100644 --- a/state_test.go +++ b/state_test.go @@ -5,7 +5,6 @@ package girc import ( - "log" "reflect" "testing" "time" @@ -22,6 +21,7 @@ func debounce(delay time.Duration, done chan bool, f func()) { f() return } + default: } } } @@ -57,8 +57,7 @@ const mockConnEndState = `:nick2!nick2@other.int QUIT :example reason ` func TestState(t *testing.T) { - c, conn, server := genMockConn(t) - + c, conn, server := genMockConn() defer c.Close() go mockReadBuffer(conn) @@ -73,57 +72,50 @@ func TestState(t *testing.T) { finishStart := make(chan bool, 1) go debounce(250*time.Millisecond, bounceStart, func() { if motd := c.ServerMOTD(); motd != "example motd" { - t.Errorf("Client.ServerMOTD() returned invalid MOTD: %q", motd) + t.Fatalf("Client.ServerMOTD() returned invalid MOTD: %q", motd) } if network := c.NetworkName(); network != "DummyIRC" && network != "DUMMY" { - t.Errorf("User.Network == %q, want \"DummyIRC\" or \"DUMMY\"", network) + t.Fatalf("User.Network == %q, want \"DummyIRC\" or \"DUMMY\"", network) } if caseExample, ok := c.GetServerOption("NICKLEN"); !ok || caseExample != "20" { - t.Errorf("Client.GetServerOptions returned invalid ISUPPORT variable: %q", caseExample) + t.Fatalf("Client.GetServerOptions returned invalid ISUPPORT variable: %q", caseExample) } - t.Logf("getting user list") users := c.UserList() - t.Logf("getting channel list") channels := c.ChannelList() if !reflect.DeepEqual(users, []string{"fhjones", "nick2"}) { // This could fail too, if sorting isn't occurring. - t.Errorf("got state users %#v, wanted: %#v", users, []string{"fhjones", "nick2"}) + t.Fatalf("got state users %#v, wanted: %#v", users, []string{"fhjones", "nick2"}) } if !reflect.DeepEqual(channels, []string{"#channel", "#channel2"}) { // This could fail too, if sorting isn't occurring. - t.Errorf("got state channels %#v, wanted: %#v", channels, []string{"#channel", "#channel2"}) + t.Fatalf("got state channels %#v, wanted: %#v", channels, []string{"#channel", "#channel2"}) } fullChannels := c.Channels() for i := 0; i < len(fullChannels); i++ { if fullChannels[i].Name != channels[i] { - t.Errorf("fullChannels name doesn't map to same name in ChannelsList: %q :: %#v", fullChannels[i].Name, channels) + t.Fatalf("fullChannels name doesn't map to same name in ChannelsList: %q :: %#v", fullChannels[i].Name, channels) } } fullUsers := c.Users() for i := 0; i < len(fullUsers); i++ { if fullUsers[i].Nick != users[i] { - t.Errorf("fullUsers nick doesn't map to same nick in UsersList: %q :: %#v", fullUsers[i].Nick, users) + t.Fatalf("fullUsers nick doesn't map to same nick in UsersList: %q :: %#v", fullUsers[i].Nick, users) } } ch := c.LookupChannel("#channel") if ch == nil { - t.Error("Client.LookupChannel returned nil on existing channel") - return + t.Fatal("Client.LookupChannel returned nil on existing channel") } adm := ch.Admins(c) - if adm == nil { - t.Errorf("admin list is nil") - t.Fail() - } admList := []string{} for i := 0; i < len(adm); i++ { admList = append(admList, adm[i].Nick) @@ -135,78 +127,76 @@ func TestState(t *testing.T) { } if !reflect.DeepEqual(admList, []string{"nick2"}) { - t.Errorf("got Channel.Admins() == %#v, wanted %#v", admList, []string{"nick2"}) + t.Fatalf("got Channel.Admins() == %#v, wanted %#v", admList, []string{"nick2"}) } if !reflect.DeepEqual(trustedList, []string{"nick2"}) { - t.Errorf("got Channel.Trusted() == %#v, wanted %#v", trustedList, []string{"nick2"}) + t.Fatalf("got Channel.Trusted() == %#v, wanted %#v", trustedList, []string{"nick2"}) } if topic := ch.Topic; topic != "example topic" { - t.Errorf("Channel.Topic == %q, want \"example topic\"", topic) + t.Fatalf("Channel.Topic == %q, want \"example topic\"", topic) } if ch.Network != "DummyIRC" && ch.Network != "DUMMY" { - t.Errorf("Channel.Network == %q, want \"DummyIRC\" or \"DUMMY\"", ch.Network) + t.Fatalf("Channel.Network == %q, want \"DummyIRC\" or \"DUMMY\"", ch.Network) } if in := ch.UserIn("fhjones"); !in { - t.Errorf("Channel.UserIn == %t, want %t", in, true) + t.Fatalf("Channel.UserIn == %t, want %t", in, true) } if users := ch.Users(c); len(users) != 2 { - t.Errorf("Channel.Users == %#v, wanted length of 2", users) + t.Fatalf("Channel.Users == %#v, wanted length of 2", users) } if h := c.GetHost(); h != "local.int" { - t.Errorf("Client.GetHost() == %q, want local.int", h) + t.Fatalf("Client.GetHost() == %q, want local.int", h) } if nick := c.GetNick(); nick != "fhjones" { - t.Errorf("Client.GetNick() == %q, want nick", nick) + t.Fatalf("Client.GetNick() == %q, want nick", nick) } if ident := c.GetIdent(); ident != "~user" { - t.Errorf("Client.GetIdent() == %q, want ~user", ident) + t.Fatalf("Client.GetIdent() == %q, want ~user", ident) } user := c.LookupUser("fhjones") if user == nil { - t.Errorf("Client.LookupUser() returned nil on existing user") - return + t.Fatal("Client.LookupUser() returned nil on existing user") } if !reflect.DeepEqual(user.ChannelList, []string{"#channel", "#channel2"}) { - t.Errorf("User.ChannelList == %#v, wanted %#v", user.ChannelList, []string{"#channel", "#channel2"}) + t.Fatalf("User.ChannelList == %#v, wanted %#v", user.ChannelList, []string{"#channel", "#channel2"}) } if count := len(user.Channels(c)); count != 2 { - t.Errorf("len(User.Channels) == %d, want 2", count) + t.Fatalf("len(User.Channels) == %d, want 2", count) } if user.Nick != "fhjones" { - t.Errorf("User.Nick == %q, wanted \"nick\"", user.Nick) + t.Fatalf("User.Nick == %q, wanted \"nick\"", user.Nick) } if user.Extras.Name != "realname" { - t.Errorf("User.Extras.Name == %q, wanted \"realname\"", user.Extras.Name) + t.Fatalf("User.Extras.Name == %q, wanted \"realname\"", user.Extras.Name) } if user.Host != "local.int" { - t.Errorf("User.Host == %q, wanted \"local.int\"", user.Host) + t.Fatalf("User.Host == %q, wanted \"local.int\"", user.Host) } if user.Ident != "~user" { - t.Errorf("User.Ident == %q, wanted \"~user\"", user.Ident) + t.Fatalf("User.Ident == %q, wanted \"~user\"", user.Ident) } if user.Network != "DummyIRC" && user.Network != "DUMMY" { - t.Errorf("User.Network == %q, want \"DummyIRC\" or \"DUMMY\"", user.Network) + t.Fatalf("User.Network == %q, want \"DummyIRC\" or \"DUMMY\"", user.Network) } if !user.InChannel("#channel2") { - t.Error("User.InChannel() returned false for existing channel") - return + t.Fatal("User.InChannel() returned false for existing channel") } finishStart <- true @@ -217,11 +207,8 @@ func TestState(t *testing.T) { bounceStart <- true }) - err := conn.SetDeadline(time.Now().Add(5 * time.Second)) - if err != nil { - log.Fatalf(err.Error()) - } - _, err = conn.Write([]byte(mockConnStartState)) + conn.SetDeadline(time.Now().Add(5 * time.Second)) + _, err := conn.Write([]byte(mockConnStartState)) if err != nil { panic(err) } @@ -237,11 +224,11 @@ func TestState(t *testing.T) { finishEnd := make(chan bool, 1) go debounce(250*time.Millisecond, bounceEnd, func() { if !reflect.DeepEqual(c.ChannelList(), []string{"#channel"}) { - t.Errorf("Client.ChannelList() == %#v, wanted %#v", c.ChannelList(), []string{"#channel"}) + t.Fatalf("Client.ChannelList() == %#v, wanted %#v", c.ChannelList(), []string{"#channel"}) } if !reflect.DeepEqual(c.UserList(), []string{"notjones"}) { - t.Errorf("Client.UserList() == %#v, wanted %#v", c.UserList(), []string{"notjones"}) + t.Fatalf("Client.UserList() == %#v, wanted %#v", c.UserList(), []string{"notjones"}) } user := c.LookupUser("notjones") @@ -250,19 +237,18 @@ func TestState(t *testing.T) { } if !reflect.DeepEqual(user.ChannelList, []string{"#channel"}) { - t.Errorf("user.ChannelList == %q, wanted %q", user.ChannelList, []string{"#channel"}) + t.Fatalf("user.ChannelList == %q, wanted %q", user.ChannelList, []string{"#channel"}) } channel := c.LookupChannel("#channel") if channel == nil { - t.Error("Client.LookupChannel() returned nil for existing channel") + t.Fatal("Client.LookupChannel() returned nil for existing channel") } if !reflect.DeepEqual(channel.UserList, []string{"notjones"}) { - t.Errorf("channel.UserList == %q, wanted %q", channel.UserList, []string{"notjones"}) + t.Fatalf("channel.UserList == %q, wanted %q", channel.UserList, []string{"notjones"}) } - t.Logf(c.String()) finishEnd <- true })