stackframe.go raw

   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  }
 103