implement ability to catch panics in handlers

This commit is contained in:
Liam Stanley 2017-02-13 01:15:53 -05:00
parent ca4751aa41
commit 88057bed20
4 changed files with 98 additions and 12 deletions

View File

@ -106,8 +106,14 @@ type Config struct {
AllowFlood bool
// Debugger is an optional, user supplied location to log the raw lines
// sent from the server, or other useful debug logs. Defaults to
// ioutil.Discard.
// ioutil.Discard. For quick debugging, this could be set to os.Stdout.
Debugger io.Writer
// RecoverFunc is called when a handler throws a panic. If RecoverFunc is
// not set, the client will panic. identifier is generally going to be the
// callback ID. The file and line should point to the exact item that
// threw a panic, and stack is the full stack trace of how RecoverFunc
// caught it.
RecoverFunc func(c *Client, e *HandlerError)
// SupportedCaps are the IRCv3 capabilities you would like the client to
// support. Only use this if DisableTracking and DisableCapTracking are
// not enabled, otherwise you will need to handle CAP negotiation yourself.

View File

@ -141,8 +141,7 @@ func (c *ircConn) setTimeout(timeout time.Duration) {
// as well as how many characters each event has.
func (c *ircConn) rate(chars int) time.Duration {
_time := time.Second + ((time.Duration(chars) * time.Second) / 100)
elapsed := time.Now().Sub(c.lastWrite)
if c.writeDelay += _time - elapsed; c.writeDelay < 0 {
if c.writeDelay += _time - time.Now().Sub(c.lastWrite); c.writeDelay < 0 {
c.writeDelay = 0
}

View File

@ -128,10 +128,15 @@ func newCTCP() *CTCP {
// call executes the necessary CTCP handler for the incoming event/CTCP
// command.
func (c *CTCP) call(event *CTCPEvent, client *Client) {
func (c *CTCP) call(client *Client, event *CTCPEvent) {
c.mu.RLock()
defer c.mu.RUnlock()
// If they want to catch any panics, add to defer stack.
if client.Config.RecoverFunc != nil && event.Origin != nil {
defer recoverHandlerPanic(client, event.Origin, "ctcp-"+strings.ToLower(event.Command), 3)
}
// Support wildcard CTCP event handling. Gets executed first before
// regular event handlers.
if _, ok := c.handlers["*"]; ok {

View File

@ -8,6 +8,8 @@ import (
"fmt"
"log"
"math/rand"
"runtime"
"runtime/debug"
"strings"
"sync"
"time"
@ -31,7 +33,7 @@ func (c *Client) RunHandlers(event *Event) {
// Check if it's a CTCP.
if ctcp := decodeCTCP(event.Copy()); ctcp != nil {
// Execute it.
c.CTCP.call(ctcp, c)
c.CTCP.call(c, ctcp)
}
}
@ -43,11 +45,11 @@ type Handler interface {
// HandlerFunc is a type that represents the function necessary to
// implement Handler.
type HandlerFunc func(c *Client, e Event)
type HandlerFunc func(client *Client, event Event)
// Execute calls the HandlerFunc with the sender and irc message.
func (f HandlerFunc) Execute(c *Client, e Event) {
f(c, e)
func (f HandlerFunc) Execute(client *Client, event Event) {
f(client, event)
}
// Caller manages internal and external (user facing) handlers.
@ -183,6 +185,11 @@ func (c *Caller) exec(command string, client *Client, event *Event) {
c.debug.Printf("executing handler %s for event %s", stack[index].cuid, command)
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", stack[index].cuid, time.Since(start))
@ -313,15 +320,84 @@ func (c *Caller) AddHandler(cmd string, handler Handler) (cuid string) {
// 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(c *Client, e Event)) (cuid string) {
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(c *Client, e Event)) (cuid string) {
return c.sregister(false, cmd, HandlerFunc(func(c *Client, e Event) {
go handler(c, e)
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, "unknown-goroutine", 3)
}
handler(client, event)
}()
}))
}
// 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.debug.Println(err.Error())
client.debug.Println(err.String())
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
ID string
File string
Line int
Panic interface{}
Stack []byte
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 (line %d): %s", e.ID, e.File, e.Line, e.Panic)
} else {
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))
}