blob: cc37052d786022b19fe8c0a39242b79dfa097ebf [file] [log] [blame]
Scott Baker1fe72732019-10-21 10:58:51 -07001package errors
2
3import (
4 "strconv"
5 "strings"
6)
7
8type uncaughtPanic struct{ message string }
9
10func (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.
16func 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
80func 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}