implement ability to catch panics in handlers
This commit is contained in:
parent
ca4751aa41
commit
88057bed20
@ -106,8 +106,14 @@ type Config struct {
|
|||||||
AllowFlood bool
|
AllowFlood bool
|
||||||
// Debugger is an optional, user supplied location to log the raw lines
|
// Debugger is an optional, user supplied location to log the raw lines
|
||||||
// sent from the server, or other useful debug logs. Defaults to
|
// 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
|
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
|
// SupportedCaps are the IRCv3 capabilities you would like the client to
|
||||||
// support. Only use this if DisableTracking and DisableCapTracking are
|
// support. Only use this if DisableTracking and DisableCapTracking are
|
||||||
// not enabled, otherwise you will need to handle CAP negotiation yourself.
|
// not enabled, otherwise you will need to handle CAP negotiation yourself.
|
||||||
|
3
conn.go
3
conn.go
@ -141,8 +141,7 @@ func (c *ircConn) setTimeout(timeout time.Duration) {
|
|||||||
// as well as how many characters each event has.
|
// as well as how many characters each event has.
|
||||||
func (c *ircConn) rate(chars int) time.Duration {
|
func (c *ircConn) rate(chars int) time.Duration {
|
||||||
_time := time.Second + ((time.Duration(chars) * time.Second) / 100)
|
_time := time.Second + ((time.Duration(chars) * time.Second) / 100)
|
||||||
elapsed := time.Now().Sub(c.lastWrite)
|
if c.writeDelay += _time - time.Now().Sub(c.lastWrite); c.writeDelay < 0 {
|
||||||
if c.writeDelay += _time - elapsed; c.writeDelay < 0 {
|
|
||||||
c.writeDelay = 0
|
c.writeDelay = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
7
ctcp.go
7
ctcp.go
@ -128,10 +128,15 @@ func newCTCP() *CTCP {
|
|||||||
|
|
||||||
// call executes the necessary CTCP handler for the incoming event/CTCP
|
// call executes the necessary CTCP handler for the incoming event/CTCP
|
||||||
// command.
|
// command.
|
||||||
func (c *CTCP) call(event *CTCPEvent, client *Client) {
|
func (c *CTCP) call(client *Client, event *CTCPEvent) {
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
defer c.mu.RUnlock()
|
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
|
// Support wildcard CTCP event handling. Gets executed first before
|
||||||
// regular event handlers.
|
// regular event handlers.
|
||||||
if _, ok := c.handlers["*"]; ok {
|
if _, ok := c.handlers["*"]; ok {
|
||||||
|
92
handler.go
92
handler.go
@ -8,6 +8,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -31,7 +33,7 @@ func (c *Client) RunHandlers(event *Event) {
|
|||||||
// Check if it's a CTCP.
|
// Check if it's a CTCP.
|
||||||
if ctcp := decodeCTCP(event.Copy()); ctcp != nil {
|
if ctcp := decodeCTCP(event.Copy()); ctcp != nil {
|
||||||
// Execute it.
|
// 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
|
// HandlerFunc is a type that represents the function necessary to
|
||||||
// implement Handler.
|
// 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.
|
// Execute calls the HandlerFunc with the sender and irc message.
|
||||||
func (f HandlerFunc) Execute(c *Client, e Event) {
|
func (f HandlerFunc) Execute(client *Client, event Event) {
|
||||||
f(c, e)
|
f(client, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caller manages internal and external (user facing) handlers.
|
// 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)
|
c.debug.Printf("executing handler %s for event %s", stack[index].cuid, command)
|
||||||
start := time.Now()
|
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)
|
stack[index].Execute(client, *event)
|
||||||
|
|
||||||
c.debug.Printf("execution of %s took %s", stack[index].cuid, time.Since(start))
|
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
|
// 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().
|
// 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))
|
return c.sregister(false, cmd, HandlerFunc(handler))
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddBg registers the handler function for the given event and executes it
|
// 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
|
// in a go-routine. cuid is the handler uid which can be used to remove the
|
||||||
// handler with Caller.Remove().
|
// handler with Caller.Remove().
|
||||||
func (c *Caller) AddBg(cmd string, handler func(c *Client, e Event)) (cuid string) {
|
func (c *Caller) AddBg(cmd string, handler func(client *Client, event Event)) (cuid string) {
|
||||||
return c.sregister(false, cmd, HandlerFunc(func(c *Client, e Event) {
|
return c.sregister(false, cmd, HandlerFunc(func(client *Client, event Event) {
|
||||||
go handler(c, e)
|
// 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))
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user