stackframe.go

  1package errors
  2
  3import (
  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.
 13type 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.
 27func 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.
 43func (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()
 52func (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.
 64func (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
 79func 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}