// Package stacktrace contains code borrowed from the github.com/pkg/errors package stacktrace import ( "bytes" "fmt" "io" "os" "path" "runtime" "strconv" "strings" ) const unknown = "unknown" var ( dunno = []byte("???") centerDot = []byte("·") dot = []byte(".") slash = []byte("/") ) // Frame represents a program counter inside a stack frame. // For historical reasons if Frame is interpreted as an uintptr // its value represents the program counter + 1. type Frame uintptr // pc returns the program counter for this frame; // multiple frames may have the same PC value. func (f Frame) pc() uintptr { return uintptr(f) - 1 } // file returns the full path to the file that contains the // function for this Frame's pc. func (f Frame) file() string { fn := runtime.FuncForPC(f.pc()) if fn == nil { return unknown } file, _ := fn.FileLine(f.pc()) return file } // line returns the line number of source code of the // function for this Frame's pc. func (f Frame) line() int { fn := runtime.FuncForPC(f.pc()) if fn == nil { return 0 } _, line := fn.FileLine(f.pc()) return line } // name returns the name of this function, if known. func (f Frame) name() string { fn := runtime.FuncForPC(f.pc()) if fn == nil { return unknown } return fn.Name() } // Format formats the frame according to the fmt.Formatter interface. // // %s source file // %d source line // %n function name // %v equivalent to %s:%d // // Format accepts flags that alter the printing of some verbs, as follows: // // %+s function name and path of source file relative to the compile time // GOPATH separated by \n\t (\n\t) // %+v equivalent to %+s:%d func (f Frame) Format(s fmt.State, verb rune) { switch verb { case 's': switch { case s.Flag('+'): _, _ = io.WriteString(s, f.name()) _, _ = io.WriteString(s, "\n\t") _, _ = io.WriteString(s, f.file()) default: _, _ = io.WriteString(s, path.Base(f.file())) } case 'd': _, _ = io.WriteString(s, strconv.Itoa(f.line())) case 'n': _, _ = io.WriteString(s, funcname(f.name())) case 'v': f.Format(s, 's') _, _ = io.WriteString(s, ":") f.Format(s, 'd') } } // MarshalText formats a stacktrace Frame as a text string. The output is the // same as that of fmt.Sprintf("%+v", f), but without newlines or tabs. func (f Frame) MarshalText() ([]byte, error) { name := f.name() if name == unknown { return []byte(name), nil } return []byte(fmt.Sprintf("%s %s:%d", name, f.file(), f.line())), nil } // StackTrace is stack of Frames from innermost (newest) to outermost (oldest). type StackTrace []Frame // Format formats the stack of Frames according to the fmt.Formatter interface. // // %s lists source files for each Frame in the stack // %v lists the source file and line number for each Frame in the stack // // Format accepts flags that alter the printing of some verbs, as follows: // // %+v Prints filename, function, and line number for each Frame in the stack. func (st StackTrace) Format(s fmt.State, verb rune) { switch verb { case 'v': switch { case s.Flag('+'): for _, f := range st { _, _ = io.WriteString(s, "\n") f.Format(s, verb) } case s.Flag('#'): _, _ = fmt.Fprintf(s, "%#v", []Frame(st)) default: st.formatSlice(s, verb) } case 's': st.formatSlice(s, verb) } } // formatSlice will format this StackTrace into the given buffer as a slice of // Frame, only valid when called with '%s' or '%v'. func (st StackTrace) formatSlice(s fmt.State, verb rune) { _, _ = io.WriteString(s, "[") for i, f := range st { if i > 0 { _, _ = io.WriteString(s, " ") } f.Format(s, verb) } _, _ = io.WriteString(s, "]") } // stack represents a stack of program counters. type stack []uintptr type StackTraced interface { StackTrace() StackTrace } func (s *stack) Format(st fmt.State, verb rune) { if verb == 'v' && st.Flag('+') { for _, pc := range *s { f := Frame(pc) _, _ = fmt.Fprintf(st, "\n%+v", f) } } } func (s *stack) StackTrace() StackTrace { f := make([]Frame, len(*s)) for i := 0; i < len(f); i++ { f[i] = Frame((*s)[i]) } return f } // FormattedStack returns a nicely formatted stack frame, skipping skip frames. func FormattedStack(skip int, prefix string) []byte { buf := new(bytes.Buffer) // the returned data // As we loop, we open files and read them. These variables record the currently // loaded file. var lines [][]byte var lastFile string for i := skip; ; i++ { // Skip the expected number of frames pc, file, line, ok := runtime.Caller(i) if !ok { break } // Print this much at least. If we can't find the source, it won't show. _, _ = fmt.Fprintf(buf, "%s%s:%d (0x%x)\n", prefix, file, line, pc) if file != lastFile { data, err := os.ReadFile(file) if err != nil { continue } lines = bytes.Split(data, []byte{'\n'}) lastFile = file } _, _ = fmt.Fprintf(buf, "%s\t%s: %s\n", prefix, function(pc), source(lines, line)) } return buf.Bytes() } func callers(skip int) *stack { const depth = 32 var pcs [depth]uintptr n := runtime.Callers(skip, pcs[:]) var st stack = pcs[0:n] return &st } // function returns, if possible, the name of the function containing the PC. func function(pc uintptr) []byte { fn := runtime.FuncForPC(pc) if fn == nil { return dunno } name := []byte(fn.Name()) // The name includes the path name to the package, which is unnecessary // since the file name is already included. Plus, it has center dots. // That is, we see // runtime/debug.*T·ptrmethod // and want // *T.ptrmethod // Also the package path might contains dot (e.g. code.google.com/...), // so first eliminate the path prefix if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 { name = name[lastSlash+1:] } if period := bytes.Index(name, dot); period >= 0 { name = name[period+1:] } name = bytes.ReplaceAll(name, centerDot, dot) return name } // source returns a space-trimmed slice of the n'th line. func source(lines [][]byte, n int) []byte { n-- // in stack trace, lines are 1-indexed but our array is 0-indexed if n < 0 || n >= len(lines) { return dunno } return bytes.TrimSpace(lines[n]) } // funcname removes the path prefix component of a function's name reported by func.Name(). func funcname(name string) string { i := strings.LastIndex(name, "/") name = name[i+1:] i = strings.Index(name, ".") return name[i+1:] }