Ever cook bacon on your CPU cooler?

This commit is contained in:
kayos@tcp.direct 2021-10-01 06:15:34 -07:00
parent 771323f162
commit 01ce96b07a
11 changed files with 150 additions and 172 deletions

View File

@ -1,10 +1,10 @@
<p align="center"><a href="https://godoc.org/github.com/lrstanley/girc"><img width="270" src="http://i.imgur.com/DEnyrdB.png"></a></p>
<p align="center"><a href="https://godoc.org/git.tcp.direct/kayos/girc-atomic"><img width="270" src="http://i.imgur.com/DEnyrdB.png"></a></p>
<p align="center">girc, a flexible IRC library for Go</p>
<p align="center">
<a href="https://travis-ci.org/lrstanley/girc"><img src="https://travis-ci.org/lrstanley/girc.svg?branch=master" alt="Build Status"></a>
<a href="https://codecov.io/gh/lrstanley/girc"><img src="https://codecov.io/gh/lrstanley/girc/branch/master/graph/badge.svg" alt="Coverage Status"></a>
<a href="https://godoc.org/github.com/lrstanley/girc"><img src="https://godoc.org/github.com/lrstanley/girc?status.png" alt="GoDoc"></a>
<a href="https://goreportcard.com/report/github.com/lrstanley/girc"><img src="https://goreportcard.com/badge/github.com/lrstanley/girc" alt="Go Report Card"></a>
<a href="https://godoc.org/git.tcp.direct/kayos/girc-atomic"><img src="https://godoc.org/git.tcp.direct/kayos/girc-atomic?status.png" alt="GoDoc"></a>
<a href="https://goreportcard.com/report/git.tcp.direct/kayos/girc-atomic"><img src="https://goreportcard.com/badge/git.tcp.direct/kayos/girc-atomic" alt="Go Report Card"></a>
<a href="https://byteirc.org/channel/%23%2Fdev%2Fnull"><img src="https://img.shields.io/badge/ByteIRC-%23%2Fdev%2Fnull-blue.svg" alt="IRC Chat"></a>
</p>
@ -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:

View File

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

3
cap.go
View File

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

View File

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

View File

@ -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 "<user>, <message>". 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

104
conn.go
View File

@ -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():

13
ctcp.go
View File

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

View File

@ -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
}

2
go.mod
View File

@ -1,3 +1,3 @@
module github.com/lrstanley/girc
module git.tcp.direct/kayos/girc-atomic
go 1.12

View File

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

View File

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