summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/lrstanley/girc/handler.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/lrstanley/girc/handler.go')
-rw-r--r--vendor/github.com/lrstanley/girc/handler.go484
1 files changed, 484 insertions, 0 deletions
diff --git a/vendor/github.com/lrstanley/girc/handler.go b/vendor/github.com/lrstanley/girc/handler.go
new file mode 100644
index 00000000..f0c737f2
--- /dev/null
+++ b/vendor/github.com/lrstanley/girc/handler.go
@@ -0,0 +1,484 @@
+// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
+// of this source code is governed by the MIT license that can be found in
+// the LICENSE file.
+
+package girc
+
+import (
+ "fmt"
+ "log"
+ "math/rand"
+ "runtime"
+ "runtime/debug"
+ "strings"
+ "sync"
+ "time"
+)
+
+// RunHandlers manually runs handlers for a given event.
+func (c *Client) RunHandlers(event *Event) {
+ if event == nil {
+ return
+ }
+
+ // Log the event.
+ c.debug.Print("< " + StripRaw(event.String()))
+ if c.Config.Out != nil {
+ if pretty, ok := event.Pretty(); ok {
+ fmt.Fprintln(c.Config.Out, StripRaw(pretty))
+ }
+ }
+
+ // Regular wildcard handlers.
+ c.Handlers.exec(ALL_EVENTS, c, event.Copy())
+
+ // Then regular handlers.
+ c.Handlers.exec(event.Command, c, event.Copy())
+
+ // Check if it's a CTCP.
+ if ctcp := decodeCTCP(event.Copy()); ctcp != nil {
+ // Execute it.
+ c.CTCP.call(c, ctcp)
+ }
+}
+
+// Handler is lower level implementation of a handler. See
+// Caller.AddHandler()
+type Handler interface {
+ Execute(*Client, Event)
+}
+
+// HandlerFunc is a type that represents the function necessary to
+// implement Handler.
+type HandlerFunc func(client *Client, event Event)
+
+// Execute calls the HandlerFunc with the sender and irc message.
+func (f HandlerFunc) Execute(client *Client, event Event) {
+ f(client, event)
+}
+
+// Caller manages internal and external (user facing) handlers.
+type Caller struct {
+ // mu is the mutex that should be used when accessing handlers.
+ mu sync.RWMutex
+
+ // external/internal keys are of structure:
+ // map[COMMAND][CUID]Handler
+ // Also of note: "COMMAND" should always be uppercase for normalization.
+
+ // external is a map of user facing handlers.
+ external map[string]map[string]Handler
+ // internal is a map of internally used handlers for the client.
+ internal map[string]map[string]Handler
+ // debug is the clients logger used for debugging.
+ debug *log.Logger
+}
+
+// newCaller creates and initializes a new handler.
+func newCaller(debugOut *log.Logger) *Caller {
+ c := &Caller{
+ external: map[string]map[string]Handler{},
+ internal: map[string]map[string]Handler{},
+ debug: debugOut,
+ }
+
+ return c
+}
+
+// Len returns the total amount of user-entered registered handlers.
+func (c *Caller) Len() int {
+ var total int
+
+ c.mu.RLock()
+ for command := range c.external {
+ total += len(c.external[command])
+ }
+ c.mu.RUnlock()
+
+ return total
+}
+
+// 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 {
+ var total int
+
+ cmd = strings.ToUpper(cmd)
+
+ c.mu.RLock()
+ for command := range c.external {
+ if command == cmd {
+ total += len(c.external[command])
+ }
+ }
+ c.mu.RUnlock()
+
+ return total
+}
+
+func (c *Caller) String() string {
+ var total int
+
+ c.mu.RLock()
+ for cmd := range c.internal {
+ total += len(c.internal[cmd])
+ }
+ c.mu.RUnlock()
+
+ return fmt.Sprintf("<Caller external:%d internal:%d>", c.Len(), total)
+}
+
+const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+// cuid generates a unique UID string for each handler for ease of removal.
+func (c *Caller) cuid(cmd string, n int) (cuid, uid string) {
+ b := make([]byte, n)
+
+ for i := range b {
+ b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
+ }
+
+ return cmd + ":" + string(b), string(b)
+}
+
+// cuidToID allows easy mapping between a generated cuid and the caller
+// external/internal handler maps.
+func (c *Caller) cuidToID(input string) (cmd, uid string) {
+ i := strings.IndexByte(input, 0x3A)
+ if i < 0 {
+ return "", ""
+ }
+
+ 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
+// handler types themselves or the handlers are executed.
+func (c *Caller) exec(command string, client *Client, event *Event) {
+ // Build a stack of handlers which can be executed concurrently.
+ var stack []execStack
+
+ c.mu.RLock()
+ // Get internal handlers first.
+ if _, ok := c.internal[command]; ok {
+ for cuid := range c.internal[command] {
+ stack = append(stack, execStack{c.internal[command][cuid], cuid})
+ }
+ }
+
+ // Aaand then external handlers.
+ if _, ok := c.external[command]; ok {
+ for cuid := range c.external[command] {
+ stack = append(stack, execStack{c.external[command][cuid], cuid})
+ }
+ }
+ c.mu.RUnlock()
+
+ // 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("executing handler %s for event %s (%d of %d)", stack[index].cuid, command, index+1, len(stack))
+ start := time.Now()
+
+ // If they want to catch any panics, add to defer stack.
+ if client.Config.RecoverFunc != nil {
+ defer recoverHandlerPanic(client, event, stack[index].cuid, 3)
+ }
+
+ stack[index].Execute(client, *event)
+
+ c.debug.Printf("execution of %s took %s (%d of %d)", stack[index].cuid, time.Since(start), index+1, len(stack))
+ wg.Done()
+ }(i)
+ }
+
+ // Wait for all of the handlers to complete. Not doing this may cause
+ // new events from becoming ahead of older handlers.
+ wg.Wait()
+}
+
+// ClearAll clears all external handlers currently setup within the client.
+// This ignores internal handlers.
+func (c *Caller) ClearAll() {
+ c.mu.Lock()
+ c.external = map[string]map[string]Handler{}
+ c.mu.Unlock()
+
+ c.debug.Print("cleared all external handlers")
+}
+
+// clearInternal clears all internal handlers currently setup within the
+// client.
+func (c *Caller) clearInternal() {
+ c.mu.Lock()
+ c.internal = map[string]map[string]Handler{}
+ c.mu.Unlock()
+
+ c.debug.Print("cleared all internal handlers")
+}
+
+// Clear clears all of the handlers for the given event.
+// This ignores internal handlers.
+func (c *Caller) Clear(cmd string) {
+ cmd = strings.ToUpper(cmd)
+
+ c.mu.Lock()
+ if _, ok := c.external[cmd]; ok {
+ delete(c.external, cmd)
+ }
+ c.mu.Unlock()
+
+ c.debug.Printf("cleared external handlers for %s", cmd)
+}
+
+// Remove removes the handler with cuid from the handler stack. success
+// 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
+}
+
+// remove is much like Remove, however is NOT concurrency safe. Lock Caller.mu
+// on your own.
+func (c *Caller) remove(cuid string) (success bool) {
+ cmd, uid := c.cuidToID(cuid)
+ if len(cmd) == 0 || len(uid) == 0 {
+ return false
+ }
+
+ // Check if the irc command/event has any handlers on it.
+ if _, ok := c.external[cmd]; !ok {
+ return false
+ }
+
+ // Check to see if it's actually a registered handler.
+ if _, ok := c.external[cmd][uid]; !ok {
+ return false
+ }
+
+ delete(c.external[cmd], uid)
+ c.debug.Printf("removed handler %s", cuid)
+
+ // Assume success.
+ return true
+}
+
+// sregister is much like Caller.register(), except that it safely locks
+// the Caller mutex.
+func (c *Caller) sregister(internal bool, cmd string, handler Handler) (cuid string) {
+ c.mu.Lock()
+ cuid = c.register(internal, cmd, handler)
+ c.mu.Unlock()
+
+ return cuid
+}
+
+// register will register a handler in the internal tracker. Unsafe (you
+// must lock c.mu yourself!)
+func (c *Caller) register(internal bool, cmd string, handler Handler) (cuid string) {
+ var uid string
+
+ cmd = strings.ToUpper(cmd)
+
+ if internal {
+ if _, ok := c.internal[cmd]; !ok {
+ c.internal[cmd] = map[string]Handler{}
+ }
+
+ cuid, uid = c.cuid(cmd, 20)
+ c.internal[cmd][uid] = handler
+ } else {
+ if _, ok := c.external[cmd]; !ok {
+ c.external[cmd] = map[string]Handler{}
+ }
+
+ cuid, uid = c.cuid(cmd, 20)
+ c.external[cmd][uid] = handler
+ }
+
+ _, file, line, _ := runtime.Caller(3)
+
+ c.debug.Printf("registering handler for %q with cuid %q (internal: %t) from: %s:%d", cmd, cuid, internal, file, line)
+
+ return cuid
+}
+
+// AddHandler registers a handler (matching the handler interface) for the
+// given event. cuid is the handler uid which can be used to remove the
+// handler with Caller.Remove().
+func (c *Caller) AddHandler(cmd string, handler Handler) (cuid string) {
+ return c.sregister(false, cmd, handler)
+}
+
+// Add registers the handler function for the given event. cuid is the
+// handler uid which can be used to remove the handler with Caller.Remove().
+func (c *Caller) Add(cmd string, handler func(client *Client, event Event)) (cuid string) {
+ return c.sregister(false, cmd, HandlerFunc(handler))
+}
+
+// AddBg registers the handler function for the given event and executes it
+// in a go-routine. cuid is the handler uid which can be used to remove the
+// handler with Caller.Remove().
+func (c *Caller) AddBg(cmd string, handler func(client *Client, event Event)) (cuid string) {
+ return c.sregister(false, cmd, HandlerFunc(func(client *Client, event Event) {
+ // Setting up background-based handlers this way allows us to get
+ // clean call stacks for use with panic recovery.
+ go func() {
+ // If they want to catch any panics, add to defer stack.
+ if client.Config.RecoverFunc != nil {
+ defer recoverHandlerPanic(client, &event, "goroutine", 3)
+ }
+
+ handler(client, event)
+ }()
+ }))
+}
+
+// AddTmp adds a "temporary" handler, which is good for one-time or few-time
+// uses. This supports a deadline and/or manual removal, as this differs
+// much from how normal handlers work. An example of a good use for this
+// would be to capture the entire output of a multi-response query to the
+// server. (e.g. LIST, WHOIS, etc)
+//
+// The supplied handler is able to return a boolean, which if true, will
+// remove the handler from the handler stack.
+//
+// Additionally, AddTmp has a useful option, deadline. When set to greater
+// than 0, deadline will be the amount of time that passes before the handler
+// is removed from the stack, regardless if the handler returns true or not.
+// This is useful in that it ensures that the handler is cleaned up if the
+// server does not respond appropriately, or takes too long to respond.
+//
+// Note that handlers supplied with AddTmp are executed in a goroutine to
+// ensure that they are not blocking other handlers. Additionally, use cuid
+// with Caller.Remove() to prematurely remove the handler from the stack,
+// bypassing the timeout or waiting for the handler to return that it wants
+// to be removed from the stack.
+func (c *Caller) AddTmp(cmd string, deadline time.Duration, handler func(client *Client, event Event) bool) (cuid string, done chan struct{}) {
+ var uid string
+ cuid, uid = c.cuid(cmd, 20)
+
+ done = make(chan struct{})
+
+ c.mu.Lock()
+ if _, ok := c.external[cmd]; !ok {
+ c.external[cmd] = map[string]Handler{}
+ }
+ c.external[cmd][uid] = HandlerFunc(func(client *Client, event Event) {
+ // Setting up background-based handlers this way allows us to get
+ // clean call stacks for use with panic recovery.
+ go func() {
+ // If they want to catch any panics, add to defer stack.
+ if client.Config.RecoverFunc != nil {
+ defer recoverHandlerPanic(client, &event, "tmp-goroutine", 3)
+ }
+
+ remove := handler(client, event)
+ if remove {
+ if ok := c.Remove(cuid); ok {
+ close(done)
+ }
+ }
+ }()
+ })
+ c.mu.Unlock()
+
+ if deadline > 0 {
+ go func() {
+ <-time.After(deadline)
+ if ok := c.Remove(cuid); ok {
+ close(done)
+ }
+ }()
+ }
+
+ return cuid, done
+}
+
+// recoverHandlerPanic is used to catch all handler panics, and re-route
+// them if necessary.
+func recoverHandlerPanic(client *Client, event *Event, id string, skip int) {
+ perr := recover()
+ if perr == nil {
+ return
+ }
+
+ var file string
+ var line int
+ var ok bool
+
+ _, file, line, ok = runtime.Caller(skip)
+
+ err := &HandlerError{
+ Event: *event,
+ ID: id,
+ File: file,
+ Line: line,
+ Panic: perr,
+ Stack: debug.Stack(),
+ callOk: ok,
+ }
+
+ client.Config.RecoverFunc(client, err)
+ return
+}
+
+// HandlerError is the error returned when a panic is intentionally recovered
+// from. It contains useful information like the handler identifier (if
+// applicable), filename, line in file where panic occurred, the call
+// trace, and original event.
+type HandlerError struct {
+ Event Event // Event is the event that caused the error.
+ ID string // ID is the CUID of the handler.
+ File string // File is the file from where the panic originated.
+ Line int // Line number where panic originated.
+ Panic interface{} // Panic is the error that was passed to panic().
+ Stack []byte // Stack is the call stack. Note you may have to skip 1 or 2 due to debug functions.
+ callOk bool
+}
+
+// Error returns a prettified version of HandlerError, containing ID, file,
+// line, and basic error string.
+func (e *HandlerError) Error() string {
+ if e.callOk {
+ return fmt.Sprintf("panic during handler [%s] execution in %s:%d: %s", e.ID, e.File, e.Line, e.Panic)
+ }
+
+ return fmt.Sprintf("panic during handler [%s] execution in unknown: %s", e.ID, e.Panic)
+}
+
+// String returns the error that panic returned, as well as the entire call
+// trace of where it originated.
+func (e *HandlerError) String() string {
+ return fmt.Sprintf("panic: %s\n\n%s", e.Panic, string(e.Stack))
+}
+
+// DefaultRecoverHandler can be used with Config.RecoverFunc as a default
+// catch-all for panics. This will log the error, and the call trace to the
+// debug log (see Config.Debug), or os.Stdout if Config.Debug is unset.
+func DefaultRecoverHandler(client *Client, err *HandlerError) {
+ if client.Config.Debug == nil {
+ fmt.Println(err.Error())
+ fmt.Println(err.String())
+ return
+ }
+
+ client.debug.Println(err.Error())
+ client.debug.Println(err.String())
+}