parse_panic.go raw

   1  package errors
   2  
   3  import (
   4  	"strconv"
   5  	"strings"
   6  )
   7  
   8  type uncaughtPanic struct{ message string }
   9  
  10  func (p uncaughtPanic) Error() string {
  11  	return p.message
  12  }
  13  
  14  // ParsePanic allows you to get an error object from the output of a go program
  15  // that panicked. This is particularly useful with https://github.com/mitchellh/panicwrap.
  16  func ParsePanic(text string) (*Error, error) {
  17  	lines := strings.Split(text, "\n")
  18  
  19  	state := "start"
  20  
  21  	var message string
  22  	var stack []StackFrame
  23  
  24  	for i := 0; i < len(lines); i++ {
  25  		line := lines[i]
  26  
  27  		if state == "start" {
  28  			if strings.HasPrefix(line, "panic: ") {
  29  				message = strings.TrimPrefix(line, "panic: ")
  30  				state = "seek"
  31  			} else {
  32  				return nil, Errorf("bugsnag.panicParser: Invalid line (no prefix): %s", line)
  33  			}
  34  
  35  		} else if state == "seek" {
  36  			if strings.HasPrefix(line, "goroutine ") && strings.HasSuffix(line, "[running]:") {
  37  				state = "parsing"
  38  			}
  39  
  40  		} else if state == "parsing" {
  41  			if line == "" {
  42  				state = "done"
  43  				break
  44  			}
  45  			createdBy := false
  46  			if strings.HasPrefix(line, "created by ") {
  47  				line = strings.TrimPrefix(line, "created by ")
  48  				createdBy = true
  49  			}
  50  
  51  			i++
  52  
  53  			if i >= len(lines) {
  54  				return nil, Errorf("bugsnag.panicParser: Invalid line (unpaired): %s", line)
  55  			}
  56  
  57  			frame, err := parsePanicFrame(line, lines[i], createdBy)
  58  			if err != nil {
  59  				return nil, err
  60  			}
  61  
  62  			stack = append(stack, *frame)
  63  			if createdBy {
  64  				state = "done"
  65  				break
  66  			}
  67  		}
  68  	}
  69  
  70  	if state == "done" || state == "parsing" {
  71  		return &Error{Err: uncaughtPanic{message}, frames: stack}, nil
  72  	}
  73  	return nil, Errorf("could not parse panic: %v", text)
  74  }
  75  
  76  // The lines we're passing look like this:
  77  //
  78  //     main.(*foo).destruct(0xc208067e98)
  79  //             /0/go/src/github.com/bugsnag/bugsnag-go/pan/main.go:22 +0x151
  80  func parsePanicFrame(name string, line string, createdBy bool) (*StackFrame, error) {
  81  	idx := strings.LastIndex(name, "(")
  82  	if idx == -1 && !createdBy {
  83  		return nil, Errorf("bugsnag.panicParser: Invalid line (no call): %s", name)
  84  	}
  85  	if idx != -1 {
  86  		name = name[:idx]
  87  	}
  88  	pkg := ""
  89  
  90  	if lastslash := strings.LastIndex(name, "/"); lastslash >= 0 {
  91  		pkg += name[:lastslash] + "/"
  92  		name = name[lastslash+1:]
  93  	}
  94  	if period := strings.Index(name, "."); period >= 0 {
  95  		pkg += name[:period]
  96  		name = name[period+1:]
  97  	}
  98  
  99  	name = strings.Replace(name, "ยท", ".", -1)
 100  
 101  	if !strings.HasPrefix(line, "\t") {
 102  		return nil, Errorf("bugsnag.panicParser: Invalid line (no tab): %s", line)
 103  	}
 104  
 105  	idx = strings.LastIndex(line, ":")
 106  	if idx == -1 {
 107  		return nil, Errorf("bugsnag.panicParser: Invalid line (no line number): %s", line)
 108  	}
 109  	file := line[1:idx]
 110  
 111  	number := line[idx+1:]
 112  	if idx = strings.Index(number, " +"); idx > -1 {
 113  		number = number[:idx]
 114  	}
 115  
 116  	lno, err := strconv.ParseInt(number, 10, 32)
 117  	if err != nil {
 118  		return nil, Errorf("bugsnag.panicParser: Invalid line (bad line number): %s", line)
 119  	}
 120  
 121  	return &StackFrame{
 122  		File:       file,
 123  		LineNumber: int(lno),
 124  		Package:    pkg,
 125  		Name:       name,
 126  	}, nil
 127  }
 128