diff --git a/client.go b/client.go index d136048..6e06e30 100644 --- a/client.go +++ b/client.go @@ -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. diff --git a/conn.go b/conn.go index ec8c0e7..98c74a0 100644 --- a/conn.go +++ b/conn.go @@ -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 } diff --git a/ctcp.go b/ctcp.go index ea11d31..259c6d7 100644 --- a/ctcp.go +++ b/ctcp.go @@ -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 { diff --git a/handler.go b/handler.go index 6187e6e..f23cb35 100644 --- a/handler.go +++ b/handler.go @@ -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)) +}