Scott Baker | 1fe7273 | 2019-10-21 10:58:51 -0700 | [diff] [blame] | 1 | package errors |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "fmt" |
| 6 | "io/ioutil" |
| 7 | "runtime" |
| 8 | "strings" |
| 9 | ) |
| 10 | |
| 11 | // A StackFrame contains all necessary information about to generate a line |
| 12 | // in a callstack. |
| 13 | type StackFrame struct { |
| 14 | // The path to the file containing this ProgramCounter |
| 15 | File string |
| 16 | // The LineNumber in that file |
| 17 | LineNumber int |
| 18 | // The Name of the function that contains this ProgramCounter |
| 19 | Name string |
| 20 | // The Package that contains this function |
| 21 | Package string |
| 22 | // The underlying ProgramCounter |
| 23 | ProgramCounter uintptr |
| 24 | } |
| 25 | |
| 26 | // NewStackFrame popoulates a stack frame object from the program counter. |
| 27 | func NewStackFrame(pc uintptr) (frame StackFrame) { |
| 28 | |
| 29 | frame = StackFrame{ProgramCounter: pc} |
| 30 | if frame.Func() == nil { |
| 31 | return |
| 32 | } |
| 33 | frame.Package, frame.Name = packageAndName(frame.Func()) |
| 34 | |
| 35 | // pc -1 because the program counters we use are usually return addresses, |
| 36 | // and we want to show the line that corresponds to the function call |
| 37 | frame.File, frame.LineNumber = frame.Func().FileLine(pc - 1) |
| 38 | return |
| 39 | |
| 40 | } |
| 41 | |
| 42 | // Func returns the function that contained this frame. |
| 43 | func (frame *StackFrame) Func() *runtime.Func { |
| 44 | if frame.ProgramCounter == 0 { |
| 45 | return nil |
| 46 | } |
| 47 | return runtime.FuncForPC(frame.ProgramCounter) |
| 48 | } |
| 49 | |
| 50 | // String returns the stackframe formatted in the same way as go does |
| 51 | // in runtime/debug.Stack() |
| 52 | func (frame *StackFrame) String() string { |
| 53 | str := fmt.Sprintf("%s:%d (0x%x)\n", frame.File, frame.LineNumber, frame.ProgramCounter) |
| 54 | |
| 55 | source, err := frame.SourceLine() |
| 56 | if err != nil { |
| 57 | return str |
| 58 | } |
| 59 | |
| 60 | return str + fmt.Sprintf("\t%s: %s\n", frame.Name, source) |
| 61 | } |
| 62 | |
| 63 | // SourceLine gets the line of code (from File and Line) of the original source if possible. |
| 64 | func (frame *StackFrame) SourceLine() (string, error) { |
| 65 | data, err := ioutil.ReadFile(frame.File) |
| 66 | |
| 67 | if err != nil { |
| 68 | return "", New(err) |
| 69 | } |
| 70 | |
| 71 | lines := bytes.Split(data, []byte{'\n'}) |
| 72 | if frame.LineNumber <= 0 || frame.LineNumber >= len(lines) { |
| 73 | return "???", nil |
| 74 | } |
| 75 | // -1 because line-numbers are 1 based, but our array is 0 based |
| 76 | return string(bytes.Trim(lines[frame.LineNumber-1], " \t")), nil |
| 77 | } |
| 78 | |
| 79 | func packageAndName(fn *runtime.Func) (string, string) { |
| 80 | name := fn.Name() |
| 81 | pkg := "" |
| 82 | |
| 83 | // The name includes the path name to the package, which is unnecessary |
| 84 | // since the file name is already included. Plus, it has center dots. |
| 85 | // That is, we see |
| 86 | // runtime/debug.*T·ptrmethod |
| 87 | // and want |
| 88 | // *T.ptrmethod |
| 89 | // Since the package path might contains dots (e.g. code.google.com/...), |
| 90 | // we first remove the path prefix if there is one. |
| 91 | if lastslash := strings.LastIndex(name, "/"); lastslash >= 0 { |
| 92 | pkg += name[:lastslash] + "/" |
| 93 | name = name[lastslash+1:] |
| 94 | } |
| 95 | if period := strings.Index(name, "."); period >= 0 { |
| 96 | pkg += name[:period] |
| 97 | name = name[period+1:] |
| 98 | } |
| 99 | |
| 100 | name = strings.Replace(name, "·", ".", -1) |
| 101 | return pkg, name |
| 102 | } |