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
-
-
+
+
@@ -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)