From 01ce96b07a4133cba0d670a19c1772b856a451f3 Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Fri, 1 Oct 2021 06:15:34 -0700 Subject: [PATCH] Ever cook bacon on your CPU cooler? --- README.md | 26 ++++++------- builtin.go | 28 ++++++-------- cap.go | 3 +- client.go | 68 ++++++++++------------------------ commands.go | 33 ++++++++++++++--- conn.go | 104 ++++++++++++++++++++++++---------------------------- ctcp.go | 13 +++---- event.go | 13 ++++--- go.mod | 2 +- handler.go | 19 +++++----- state.go | 13 ++++--- 11 files changed, 150 insertions(+), 172 deletions(-) diff --git a/README.md b/README.md index 35f3d81..3042c30 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -

+

girc, a flexible IRC library for Go

Build Status Coverage Status - GoDoc - Go Report Card + GoDoc + Go Report Card IRC Chat

@@ -17,32 +17,32 @@ you're using won't have breaking changes** ## Features - Focuses on simplicity, yet tries to still be flexible. -- Only requires [standard library packages](https://godoc.org/github.com/lrstanley/girc?imports) -- Event based triggering/responses ([example](https://godoc.org/github.com/lrstanley/girc#ex-package--Commands), and [CTCP too](https://godoc.org/github.com/lrstanley/girc#Commands.SendCTCP)!) -- [Documentation](https://godoc.org/github.com/lrstanley/girc) is _mostly_ complete. +- Only requires [standard library packages](https://godoc.org/git.tcp.direct/kayos/girc-atomic?imports) +- Event based triggering/responses ([example](https://godoc.org/git.tcp.direct/kayos/girc-atomic#ex-package--Commands), and [CTCP too](https://godoc.org/git.tcp.direct/kayos/girc-atomic#Commands.SendCTCP)!) +- [Documentation](https://godoc.org/git.tcp.direct/kayos/girc-atomic) is _mostly_ complete. - Support for almost all of the [IRCv3 spec](http://ircv3.net/software/libraries.html). - SASL Auth (currently only `PLAIN` and `EXTERNAL` is support by default, however you can simply implement `SASLMech` yourself to support additional mechanisms.) - Message tags (things like `account-tag` on by default) - - `account-notify`, `away-notify`, `chghost`, `extended-join`, etc -- all handled seemlessly ([cap.go](https://github.com/lrstanley/girc/blob/master/cap.go) for more info). + - `account-notify`, `away-notify`, `chghost`, `extended-join`, etc -- all handled seemlessly ([cap.go](https://git.tcp.direct/kayos/girc-atomic/blob/master/cap.go) for more info). - Channel and user tracking. Easily find what users are in a channel, if a user is away, or if they are authenticated (if the server supports it!) -- Client state/capability tracking. Easy methods to access capability data ([LookupChannel](https://godoc.org/github.com/lrstanley/girc#Client.LookupChannel), [LookupUser](https://godoc.org/github.com/lrstanley/girc#Client.LookupUser), [GetServerOption (ISUPPORT)](https://godoc.org/github.com/lrstanley/girc#Client.GetServerOption), etc.) +- Client state/capability tracking. Easy methods to access capability data ([LookupChannel](https://godoc.org/git.tcp.direct/kayos/girc-atomic#Client.LookupChannel), [LookupUser](https://godoc.org/git.tcp.direct/kayos/girc-atomic#Client.LookupUser), [GetServerOption (ISUPPORT)](https://godoc.org/git.tcp.direct/kayos/girc-atomic#Client.GetServerOption), etc.) - Built-in support for things you would commonly have to implement yourself. - - Nick collision detection and prevention (also see [Config.HandleNickCollide](https://godoc.org/github.com/lrstanley/girc#Config).) + - Nick collision detection and prevention (also see [Config.HandleNickCollide](https://godoc.org/git.tcp.direct/kayos/girc-atomic#Config).) - Event/message rate limiting. - - Channel, nick, and user validation methods ([IsValidChannel](https://godoc.org/github.com/lrstanley/girc#IsValidChannel), [IsValidNick](https://godoc.org/github.com/lrstanley/girc#IsValidNick), etc.) - - CTCP handling and auto-responses ([CTCP](https://godoc.org/github.com/lrstanley/girc#CTCP)) + - Channel, nick, and user validation methods ([IsValidChannel](https://godoc.org/git.tcp.direct/kayos/girc-atomic#IsValidChannel), [IsValidNick](https://godoc.org/git.tcp.direct/kayos/girc-atomic#IsValidNick), etc.) + - CTCP handling and auto-responses ([CTCP](https://godoc.org/git.tcp.direct/kayos/girc-atomic#CTCP)) - And more! ## Installing - $ go get -u github.com/lrstanley/girc + $ go get -u git.tcp.direct/kayos/girc-atomic ## Examples -See [the examples](https://godoc.org/github.com/lrstanley/girc#example-package--Bare) +See [the examples](https://godoc.org/git.tcp.direct/kayos/girc-atomic#example-package--Bare) within the documentation for real-world usecases. Here are a few real-world usecases/examples/projects which utilize girc: diff --git a/builtin.go b/builtin.go index 778a5c6..634a00b 100644 --- a/builtin.go +++ b/builtin.go @@ -9,6 +9,11 @@ import ( "time" ) +const ( + stateUnlocked uint32 = iota + stateLocked +) + // registerBuiltin sets up built-in handlers, based on client // configuration. func (c *Client) registerBuiltins() { @@ -85,18 +90,14 @@ func handleConnect(c *Client, e Event) { // the one we supplied during connection, but some networks will rename // users on connect. if len(e.Params) > 0 { - c.state.Lock() - c.state.nick = e.Params[0] - c.state.Unlock() + c.state.nick.Store(e.Params[0]) c.state.notify(c, UPDATE_GENERAL) } time.Sleep(2 * time.Second) - c.mu.RLock() server := c.server() - c.mu.RUnlock() c.RunHandlers(&Event{Command: CONNECTED, Params: []string{server}}) } @@ -117,9 +118,7 @@ func handlePING(c *Client, e Event) { } func handlePONG(c *Client, e Event) { - c.conn.mu.Lock() - c.conn.lastPong = time.Now() - c.conn.mu.Unlock() + c.conn.lastPong.Store(time.Now()) } // handleJOIN ensures that the state has updated users and channels. @@ -131,11 +130,11 @@ func handleJOIN(c *Client, e Event) { channelName := e.Params[0] c.state.Lock() + defer c.state.Unlock() channel := c.state.lookupChannel(channelName) if channel == nil { if ok := c.state.createChannel(channelName); !ok { - c.state.Unlock() return } @@ -145,7 +144,6 @@ func handleJOIN(c *Client, e Event) { user := c.state.lookupUser(e.Source.Name) if user == nil { if ok := c.state.createUser(e.Source); !ok { - c.state.Unlock() return } user = c.state.lookupUser(e.Source.Name) @@ -166,7 +164,6 @@ func handleJOIN(c *Client, e Event) { user.Extras.Name = e.Params[2] } } - c.state.Unlock() if e.Source.ID() == c.GetID() { // If it's us, don't just add our user to the list. Run a WHO which @@ -178,10 +175,8 @@ func handleJOIN(c *Client, e Event) { // Update our ident and host too, in state -- since there is no // cleaner method to do this. - c.state.Lock() - c.state.ident = e.Source.Ident - c.state.host = e.Source.Host - c.state.Unlock() + c.state.ident.Store(e.Source.Ident) + c.state.host.Store(e.Source.Host) return } @@ -448,7 +443,6 @@ func handleNAMES(c *Client, e Event) { var modes, nick string var ok bool - s := &Source{} c.state.Lock() for i := 0; i < len(parts); i++ { @@ -457,6 +451,8 @@ func handleNAMES(c *Client, e Event) { continue } + var s *Source = new(Source) + // If userhost-in-names. if strings.Contains(nick, "@") { s = ParseSource(nick) diff --git a/cap.go b/cap.go index 38ff210..7bd7cde 100644 --- a/cap.go +++ b/cap.go @@ -106,7 +106,7 @@ func parseCap(raw string) map[string]map[string]string { if j < 0 { out[parts[i][:val]][option] = "" } else { - out[parts[i][:val]][option[:j]] = option[j+1 : len(option)] + out[parts[i][:val]][option[:j]] = option[j+1:] } } } @@ -316,6 +316,7 @@ func handleCHGHOST(c *Client, e Event) { user.Host = e.Params[1] } c.state.Unlock() + c.state.notify(c, UPDATE_STATE) } diff --git a/client.go b/client.go index f803575..d408e4a 100644 --- a/client.go +++ b/client.go @@ -47,6 +47,9 @@ type Client struct { // so multiple threads aren't trying to connect at the same time, and // vice versa. mu sync.RWMutex + + atom uint32 + // stop is used to communicate with Connect(), letting it know that the // client wishes to cancel/close. stop context.CancelFunc @@ -229,10 +232,10 @@ func (conf *Config) isValid() error { } if !IsValidNick(conf.Nick) { - return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad nickname specified")} + return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad nickname specified: " + conf.Nick)} } if !IsValidUser(conf.User) { - return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad user/ident specified")} + return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad user/ident specified: " + conf.User)} } return nil @@ -250,6 +253,7 @@ func New(config Config) *Client { tx: make(chan *Event, 25), CTCP: newCTCP(), initTime: time.Now(), + atom: stateUnlocked, } c.Cmd = &Commands{c: c} @@ -313,16 +317,11 @@ func (c *Client) String() string { // connection wasn't established using TLS (see ErrConnNotTLS), or if the // client isn't connected. func (c *Client) TLSConnectionState() (*tls.ConnectionState, error) { - c.mu.RLock() - defer c.mu.RUnlock() if c.conn == nil { return nil, ErrNotConnected } - c.conn.mu.RLock() - defer c.conn.mu.RUnlock() - - if !c.conn.connected { + if !c.conn.connected.Load().(bool) { return nil, ErrNotConnected } @@ -442,9 +441,6 @@ func (c *Client) DisableTracking() { // Server returns the string representation of host+port pair for the connection. func (c *Client) Server() string { - c.state.Lock() - defer c.state.Lock() - return c.server() } @@ -465,16 +461,12 @@ func (c *Client) Lifetime() time.Duration { // Uptime is the time at which the client successfully connected to the // server. -func (c *Client) Uptime() (up *time.Time, err error) { +func (c *Client) Uptime() (up time.Time, err error) { if !c.IsConnected() { - return nil, ErrNotConnected + return time.Now(), ErrNotConnected } - c.mu.RLock() - c.conn.mu.RLock() - up = c.conn.connTime - c.conn.mu.RUnlock() - c.mu.RUnlock() + up = c.conn.connTime.Load().(time.Time) return up, nil } @@ -486,29 +478,18 @@ func (c *Client) ConnSince() (since *time.Duration, err error) { return nil, ErrNotConnected } - c.mu.RLock() - c.conn.mu.RLock() - timeSince := time.Since(*c.conn.connTime) - c.conn.mu.RUnlock() - c.mu.RUnlock() + timeSince := time.Since(c.conn.connTime.Load().(time.Time)) return &timeSince, nil } // IsConnected returns true if the client is connected to the server. func (c *Client) IsConnected() bool { - c.mu.RLock() if c.conn == nil { - c.mu.RUnlock() return false } - c.conn.mu.RLock() - connected := c.conn.connected - c.conn.mu.RUnlock() - c.mu.RUnlock() - - return connected + return c.conn.connected.Load().(bool) } // GetNick returns the current nickname of the active connection. Panics if @@ -516,13 +497,10 @@ func (c *Client) IsConnected() bool { func (c *Client) GetNick() string { c.panicIfNotTracking() - c.state.RLock() - defer c.state.RUnlock() - - if c.state.nick == "" { + if c.state.nick.Load().(string) == "" { return c.Config.Nick } - return c.state.nick + return c.state.nick.Load().(string) } // GetID returns an RFC1459 compliant version of the current nickname. Panics @@ -537,13 +515,10 @@ func (c *Client) GetID() string { func (c *Client) GetIdent() string { c.panicIfNotTracking() - c.state.RLock() - defer c.state.RUnlock() - - if c.state.ident == "" { + if c.state.ident.Load().(string) == "" { return c.Config.User } - return c.state.ident + return c.state.ident.Load().(string) } // GetHost returns the current host of the active connection. Panics if @@ -552,9 +527,8 @@ func (c *Client) GetIdent() string { func (c *Client) GetHost() (host string) { c.panicIfNotTracking() - c.state.RLock() - host = c.state.host - c.state.RUnlock() + host = c.state.host.Load().(string) + return host } @@ -714,11 +688,7 @@ func (c *Client) ServerMOTD() (motd string) { // by determining the difference in time between when we ping the server, and // when we receive a pong. func (c *Client) Latency() (delta time.Duration) { - c.mu.RLock() - c.conn.mu.RLock() - delta = c.conn.lastPong.Sub(c.conn.lastPing) - c.conn.mu.RUnlock() - c.mu.RUnlock() + delta = c.conn.lastPong.Load().(time.Time).Sub(c.conn.lastPing.Load().(time.Time)) if delta < 0 { return 0 diff --git a/commands.go b/commands.go index 2ee0a23..c6fe663 100644 --- a/commands.go +++ b/commands.go @@ -5,7 +5,6 @@ package girc import ( - "strconv" "errors" "fmt" ) @@ -112,7 +111,7 @@ func (cmd *Commands) Message(target, message string) { // Messagef sends a formated PRIVMSG to target (either channel, service, or // user). func (cmd *Commands) Messagef(target, format string, a ...interface{}) { - cmd.Message(target, fmt.Sprintf(format, a...)) + cmd.Message(target, fmt.Sprintf(Fmt(format), a...)) } // ErrInvalidSource is returned when a method needs to know the origin of an @@ -136,11 +135,33 @@ func (cmd *Commands) Reply(event Event, message string) { cmd.Message(event.Source.Name, message) } +// ReplyKick kicks the source of the event from the channel where the event originated +func (cmd *Commands) ReplyKick(event Event, reason string) { + if event.Source == nil { + panic(ErrInvalidSource) + } + + if len(event.Params) > 0 && IsValidChannel(event.Params[0]) { + cmd.Kick(event.Params[0], event.Source.Name, reason) + } +} + +// ReplyBan kicks the source of the event from the channel where the event originated +func (cmd *Commands) ReplyBan(event Event, reason string) { + if event.Source == nil { + panic(ErrInvalidSource) + } + + if len(event.Params) > 0 && IsValidChannel(event.Params[0]) { + cmd.Ban(event.Params[0], event.Source.Name) + } +} + // Replyf sends a reply to channel or user with a format string, based on // where the supplied event originated from. See also ReplyTof(). Panics if // the incoming event has no source. func (cmd *Commands) Replyf(event Event, format string, a ...interface{}) { - cmd.Reply(event, fmt.Sprintf(format, a...)) + cmd.Reply(event, fmt.Sprintf(Fmt(format), a...)) } // ReplyTo sends a reply to a channel or user, based on where the supplied @@ -165,7 +186,7 @@ func (cmd *Commands) ReplyTo(event Event, message string) { // from a channel will default to replying with ", ". See // also Replyf(). Panics if the incoming event has no source. func (cmd *Commands) ReplyTof(event Event, format string, a ...interface{}) { - cmd.ReplyTo(event, fmt.Sprintf(format, a...)) + cmd.ReplyTo(event, fmt.Sprintf(Fmt(format), a...)) } // Action sends a PRIVMSG ACTION (/me) to target (either channel, service, @@ -221,7 +242,7 @@ func (cmd *Commands) SendRawf(format string, a ...interface{}) error { // Topic sets the topic of channel to message. Does not verify the length // of the topic. func (cmd *Commands) Topic(channel, message string) { - cmd.c.Send(&Event{Command: TOPIC, Params: []string{channel, message}}) + cmd.c.Send(&Event{Command: TOPIC, Params: []string{channel, Fmt(message)}}) } // Who sends a WHO query to the server, which will attempt WHOX by default. @@ -357,7 +378,7 @@ func (cmd *Commands) List(channels ...string) { // Whowas sends a WHOWAS query to the server. amount is the amount of results // you want back. func (cmd *Commands) Whowas(user string, amount int) { - cmd.c.Send(&Event{Command: WHOWAS, Params: []string{user, strconv.Itoa(amount)}}) + cmd.c.Send(&Event{Command: WHOWAS, Params: []string{user, string(fmt.Sprintf("%d", amount))}}) } // Monitor sends a MONITOR query to the server. The results of the query diff --git a/conn.go b/conn.go index 441c3e7..6e2575a 100644 --- a/conn.go +++ b/conn.go @@ -11,6 +11,7 @@ import ( "fmt" "net" "sync" + "sync/atomic" "time" ) @@ -26,25 +27,24 @@ type ircConn struct { io *bufio.ReadWriter sock net.Conn - mu sync.RWMutex // lastWrite is used to keep track of when we last wrote to the server. - lastWrite time.Time + lastWrite atomic.Value // lastActive is the last time the client was interacting with the server, // excluding a few background commands (PING, PONG, WHO, etc). - lastActive time.Time + lastActive atomic.Value // writeDelay is used to keep track of rate limiting of events sent to // the server. - writeDelay time.Duration + writeDelay atomic.Value // connected is true if we're actively connected to a server. - connected bool + connected atomic.Value // connTime is the time at which the client has connected to a server. - connTime *time.Time + connTime atomic.Value // lastPing is the last time that we pinged the server. - lastPing time.Time + lastPing atomic.Value // lastPong is the last successful time that we pinged the server and // received a successful pong back. - lastPong time.Time - pingDelay time.Duration + lastPong atomic.Value + // pingDelay time.Duration } // Dialer is an interface implementation of net.Dialer. Use this if you would @@ -112,25 +112,27 @@ func newConn(conf Config, dialer Dialer, addr string, sts *strictTransport) (*ir conn = tlsConn } - ctime := time.Now() - c := &ircConn{ sock: conn, - connTime: &ctime, - connected: true, + connTime: atomic.Value{}, + connected: atomic.Value{}, } + c.connTime.Store(time.Now()) + c.connected.Store(true) + c.newReadWriter() return c, nil } func newMockConn(conn net.Conn) *ircConn { - ctime := time.Now() c := &ircConn{ sock: conn, - connTime: &ctime, - connected: true, + connTime: atomic.Value{}, + connected: atomic.Value{}, } + c.connTime.Store(time.Now()) + c.connected.Store(true) c.newReadWriter() return c @@ -156,6 +158,7 @@ func (c *ircConn) decode() (event *Event, err error) { return event, nil } +/* func (c *ircConn) encode(event *Event) error { if _, err := c.io.Write(event.Bytes()); err != nil { return err @@ -166,7 +169,7 @@ func (c *ircConn) encode(event *Event) error { return c.io.Flush() } - +*/ func (c *ircConn) newReadWriter() { c.io = bufio.NewReadWriter(bufio.NewReader(c.sock), bufio.NewWriter(c.sock)) } @@ -262,8 +265,6 @@ func (c *Client) MockConnect(conn net.Conn) error { func (c *Client) internalConnect(mock net.Conn, dialer Dialer) error { startConn: - // We want to be the only one handling connects/disconnects right now. - c.mu.Lock() if c.conn != nil { panic("use of connect more than once") @@ -284,7 +285,6 @@ startConn: c.RunHandlers(&Event{Command: STS_ERR_FALLBACK}) } } - c.mu.Unlock() return err } @@ -295,7 +295,6 @@ startConn: var ctx context.Context ctx, c.stop = context.WithCancel(context.Background()) - c.mu.Unlock() errs := make(chan error, 4) var wg sync.WaitGroup @@ -352,15 +351,13 @@ startConn: } // Make sure that the connection is closed if not already. - c.mu.RLock() + if c.stop != nil { c.stop() } - c.conn.mu.Lock() - c.conn.connected = false + + c.conn.connected.Store(false) _ = c.conn.Close() - c.conn.mu.Unlock() - c.mu.RUnlock() c.RunHandlers(&Event{Command: DISCONNECTED, Params: []string{addr}}) @@ -374,13 +371,11 @@ startConn: // This helps ensure that the end user isn't improperly using the client // more than once. If they want to do this, they should be using multiple // clients, not multiple instances of Connect(). - c.mu.Lock() c.conn = nil if result == nil { if c.state.sts.beginUpgrade { c.state.sts.beginUpgrade = false - c.mu.Unlock() goto startConn } @@ -388,7 +383,6 @@ startConn: c.state.sts.persistenceReceived = time.Now() } } - c.mu.Unlock() return result } @@ -432,21 +426,21 @@ func (c *Client) readLoop(ctx context.Context, errs chan error, wg *sync.WaitGro func (c *Client) Send(event *Event) { var delay time.Duration + for atomic.CompareAndSwapUint32(&c.atom, stateUnlocked, stateLocked) { + randSleep() + } + defer atomic.StoreUint32(&c.atom, stateUnlocked) + if !c.Config.AllowFlood { - c.mu.RLock() // Drop the event early as we're disconnected, this way we don't have to wait // the (potentially long) rate limit delay before dropping. if c.conn == nil { c.debugLogEvent(event, true) - c.mu.RUnlock() return } - c.conn.mu.Lock() delay = c.conn.rate(event.Len()) - c.conn.mu.Unlock() - c.mu.RUnlock() } if c.Config.GlobalFormat && len(event.Params) > 0 && event.Params[len(event.Params)-1] != "" && @@ -477,11 +471,18 @@ func (c *Client) write(event *Event) { func (c *ircConn) rate(chars int) time.Duration { _time := time.Second + ((time.Duration(chars) * time.Second) / 100) - if c.writeDelay += _time - time.Now().Sub(c.lastWrite); c.writeDelay < 0 { - c.writeDelay = 0 + if c.writeDelay.Load() == nil { + c.writeDelay.Store(time.Duration(0)) + } + wdelay := c.writeDelay.Load().(time.Duration) + + lwrite := c.lastWrite.Load().(time.Time) + + if wdelay += _time - time.Since(lwrite); wdelay < 0 { + c.writeDelay.Store(time.Duration(0)) } - if c.writeDelay > (8 * time.Second) { + if c.writeDelay.Load().(time.Duration) > (8 * time.Second) { return _time } @@ -500,7 +501,7 @@ func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGro // Check if tags exist on the event. If they do, and message-tags // isn't a supported capability, remove them from the event. if event.Tags != nil { - c.state.RLock() + // c.state.RLock() var in bool for i := 0; i < len(c.state.enabledCap); i++ { if _, ok := c.state.enabledCap["message-tags"]; ok { @@ -508,7 +509,7 @@ func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGro break } } - c.state.RUnlock() + // c.state.RUnlock() if !in { event.Tags = Tags{} @@ -517,13 +518,11 @@ func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGro c.debugLogEvent(event, false) - c.conn.mu.Lock() - c.conn.lastWrite = time.Now() + c.conn.lastWrite.Store(time.Now()) if event.Command != PING && event.Command != PONG && event.Command != WHO { c.conn.lastActive = c.conn.lastWrite } - c.conn.mu.Unlock() // Write the raw line. _, err = c.conn.io.Write(event.Bytes()) @@ -579,10 +578,8 @@ func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGro c.debug.Print("starting pingLoop") defer c.debug.Print("closing pingLoop") - c.conn.mu.Lock() - c.conn.lastPing = time.Now() - c.conn.lastPong = time.Now() - c.conn.mu.Unlock() + c.conn.lastPing.Store(time.Now()) + c.conn.lastPong.Store(time.Now()) tick := time.NewTicker(c.Config.PingDelay) defer tick.Stop() @@ -603,26 +600,21 @@ func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGro past = true } - c.conn.mu.RLock() - if time.Since(c.conn.lastPong) > c.Config.PingDelay+(60*time.Second) { + if time.Since(c.conn.lastPong.Load().(time.Time)) > c.Config.PingDelay+(120*time.Second) { // It's 60 seconds over what out ping delay is, connection // has probably dropped. errs <- ErrTimedOut{ - TimeSinceSuccess: time.Since(c.conn.lastPong), - LastPong: c.conn.lastPong, - LastPing: c.conn.lastPing, + TimeSinceSuccess: time.Since(c.conn.lastPong.Load().(time.Time)), + LastPong: c.conn.lastPong.Load().(time.Time), + LastPing: c.conn.lastPing.Load().(time.Time), Delay: c.Config.PingDelay, } wg.Done() - c.conn.mu.RUnlock() return } - c.conn.mu.RUnlock() - c.conn.mu.Lock() - c.conn.lastPing = time.Now() - c.conn.mu.Unlock() + c.conn.lastPing.Store(time.Now()) c.Cmd.Ping(fmt.Sprintf("%d", time.Now().UnixNano())) case <-ctx.Done(): diff --git a/ctcp.go b/ctcp.go index 637615e..5787c1a 100644 --- a/ctcp.go +++ b/ctcp.go @@ -193,10 +193,10 @@ func (c *CTCP) Set(cmd string, handler func(client *Client, ctcp CTCPEvent)) { if cmd = c.parseCMD(cmd); cmd == "" { return } - c.mu.Lock() + defer c.mu.Unlock() + c.handlers[cmd] = CTCPHandler(handler) - c.mu.Unlock() } // SetBg is much like Set, however the handler is executed in the background, @@ -270,14 +270,14 @@ func handleCTCPVersion(client *Client, ctcp CTCPEvent) { client.Cmd.SendCTCPReplyf( ctcp.Source.ID(), CTCP_VERSION, - "girc (github.com/lrstanley/girc) using %s (%s, %s)", + "girc (git.tcp.direct/kayos/girc-atomic) using %s (%s, %s)", runtime.Version(), runtime.GOOS, runtime.GOARCH, ) } // handleCTCPSource replies with the public git location of this library. func handleCTCPSource(client *Client, ctcp CTCPEvent) { - client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_SOURCE, "https://github.com/lrstanley/girc") + client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_SOURCE, "https://git.tcp.direct/kayos/girc-atomic") } // handleCTCPTime replies with a RFC 1123 (Z) formatted version of Go's @@ -289,9 +289,6 @@ func handleCTCPTime(client *Client, ctcp CTCPEvent) { // handleCTCPFinger replies with the realname and idle time of the user. This // is obsoleted by improvements to the IRC protocol, however still supported. func handleCTCPFinger(client *Client, ctcp CTCPEvent) { - client.conn.mu.RLock() - active := client.conn.lastActive - client.conn.mu.RUnlock() - + active := client.conn.lastActive.Load().(time.Time) client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_FINGER, fmt.Sprintf("%s -- idle %s", client.Config.Name, time.Since(active))) } diff --git a/event.go b/event.go index 423e452..069f2b1 100644 --- a/event.go +++ b/event.go @@ -13,7 +13,7 @@ import ( const ( eventSpace byte = ' ' // Separator. - maxLength = 510 // Maximum length is 510 (2 for line endings). + maxLength int = 510 // Maximum length is 510 (2 for line endings). ) // cutCRFunc is used to trim CR characters from prefixes/messages. @@ -248,7 +248,7 @@ func (e *Event) Len() (length int) { // If param contains a space or it's empty, it's trailing, so it should be // prefixed with a colon (:). - if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || strings.HasPrefix(e.Params[i], ":") || e.Params[i] == "") { + if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || e.Params[i] == "") { length++ } } @@ -268,7 +268,9 @@ func (e *Event) Bytes() []byte { // Tags. if e.Tags != nil { - e.Tags.writeTo(buffer) + if _, err := e.Tags.writeTo(buffer); err != nil { + return nil + } } // Event prefix. @@ -284,8 +286,9 @@ func (e *Event) Bytes() []byte { // Space separated list of arguments. if len(e.Params) > 0 { // buffer.WriteByte(eventSpace) + for i := 0; i < len(e.Params); i++ { - if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || strings.HasPrefix(e.Params[i], ":") || e.Params[i] == "") { + if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || e.Params[i] == "") { buffer.WriteString(string(eventSpace) + string(messagePrefix) + e.Params[i]) continue } @@ -636,6 +639,4 @@ func (s *Source) writeTo(buffer *bytes.Buffer) { buffer.WriteByte(prefixHost) buffer.WriteString(s.Host) } - - return } diff --git a/go.mod b/go.mod index 5a4a2aa..bf995a8 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/lrstanley/girc +module git.tcp.direct/kayos/girc-atomic go 1.12 diff --git a/handler.go b/handler.go index 4832262..28c941d 100644 --- a/handler.go +++ b/handler.go @@ -99,11 +99,11 @@ func newCaller(debugOut *log.Logger) *Caller { func (c *Caller) Len() int { var total int - c.mu.RLock() + // c.mu.RLock() for command := range c.external { total += len(c.external[command]) } - c.mu.RUnlock() + // c.mu.RUnlock() return total } @@ -115,13 +115,13 @@ func (c *Caller) Count(cmd string) int { cmd = strings.ToUpper(cmd) - c.mu.RLock() + // c.mu.RLock() for command := range c.external { if command == cmd { total += len(c.external[command]) } } - c.mu.RUnlock() + // c.mu.RUnlock() return total } @@ -176,7 +176,7 @@ func (c *Caller) exec(command string, bg bool, client *Client, event *Event) { // Build a stack of handlers which can be executed concurrently. var stack []execStack - c.mu.RLock() + // c.mu.RLock() // Get internal handlers first. if _, ok := c.internal[command]; ok { for cuid := range c.internal[command] { @@ -198,7 +198,7 @@ func (c *Caller) exec(command string, bg bool, client *Client, event *Event) { stack = append(stack, execStack{c.external[command][cuid], cuid}) } } - c.mu.RUnlock() + // c.mu.RUnlock() // Run all handlers concurrently across the same event. This should // still help prevent mis-ordered events, while speeding up the @@ -264,9 +264,9 @@ func (c *Caller) Clear(cmd string) { cmd = strings.ToUpper(cmd) c.mu.Lock() - if _, ok := c.external[cmd]; ok { - delete(c.external, cmd) - } + + delete(c.external, cmd) + c.mu.Unlock() c.debug.Printf("cleared external handlers for %s", cmd) @@ -458,7 +458,6 @@ func recoverHandlerPanic(client *Client, event *Event, id string, skip int) { } client.Config.RecoverFunc(client, err) - return } // HandlerError is the error returned when a panic is intentionally recovered diff --git a/state.go b/state.go index d9e7298..4bd84ac 100644 --- a/state.go +++ b/state.go @@ -8,6 +8,7 @@ import ( "fmt" "sort" "sync" + "sync/atomic" "time" ) @@ -17,7 +18,7 @@ import ( type state struct { sync.RWMutex // nick, ident, and host are the internal trackers for our user. - nick, ident, host string + nick, ident, host atomic.Value // channels represents all channels we're active in. channels map[string]*Channel // users represents all of users that we're tracking. @@ -46,9 +47,9 @@ type state struct { // reset resets the state back to it's original form. func (s *state) reset(initial bool) { s.Lock() - s.nick = "" - s.ident = "" - s.host = "" + s.nick.Store("") + s.ident.Store("") + s.host.Store("") s.channels = make(map[string]*Channel) s.users = make(map[string]*User) s.serverOptions = make(map[string]string) @@ -481,8 +482,8 @@ func (s *state) renameUser(from, to string) { from = ToRFC1459(from) // Update our nickname. - if from == ToRFC1459(s.nick) { - s.nick = to + if from == ToRFC1459(s.nick.Load().(string)) { + s.nick.Store(to) } user := s.lookupUser(from)