github.com/insolar/vanilla@v0.0.0-20201023172447-248fdf805322/throw/stacktrace.go (about)

     1  // Copyright 2020 Insolar Network Ltd.
     2  // All rights reserved.
     3  // This material is licensed under the Insolar License version 1.0,
     4  // available at https://github.com/insolar/assured-ledger/blob/master/LICENSE.md.
     5  
     6  package throw
     7  
     8  import (
     9  	"bytes"
    10  	"io"
    11  	"runtime"
    12  	"runtime/debug"
    13  	"strconv"
    14  	"strings"
    15  )
    16  
    17  const StackTracePrefix = "Stack trace:"
    18  const stackTracePrintPrefix = StackTracePrefix + "\n"
    19  
    20  type StackTrace interface {
    21  	StackTraceAsText() string
    22  	WriteStackTraceTo(writer io.Writer) error
    23  	IsFullStack() bool
    24  }
    25  
    26  // CaptureStack captures whole stack
    27  // When (skipFrames) are more than stack depth then only "created by" entry will be returned
    28  func CaptureStack(skipFrames int) StackTrace {
    29  	return stackTrace{captureStack(skipFrames+1, false), false}
    30  }
    31  
    32  // CaptureStackTop is an optimized version to capture a limited info about top-level stack entry only
    33  // When (skipFrames) are more than stack depth then only "created by" entry will be returned
    34  func CaptureStackTop(skipFrames int) StackTrace {
    35  	return stackTrace{captureStack(skipFrames+1, true), true}
    36  }
    37  
    38  func ExtractStackTop(st StackTrace, skipFrames int) StackTrace {
    39  	var data []byte
    40  	switch vv := st.(type) {
    41  	case nil:
    42  		return nil
    43  	case stackTrace:
    44  		if vv.limit {
    45  			return st
    46  		}
    47  		data = vv.data
    48  	default:
    49  		if !vv.IsFullStack() {
    50  			return st
    51  		}
    52  		data = []byte(vv.StackTraceAsText())
    53  	}
    54  	start := indexOfFrame(data, skipFrames)
    55  	if start < 0 || start == 0 && skipFrames > 0 {
    56  		return stackTrace{data, true}
    57  	}
    58  	start += skipPanicFrame(data)
    59  	end := indexOfFrame(data[start:], 1)
    60  	if end > 0 {
    61  		data = data[start : start+end]
    62  	} else {
    63  		data = data[start:]
    64  	}
    65  	return stackTrace{append([]byte(nil), data...), true}
    66  }
    67  
    68  func IsInSystemPanic(skipFrames int) bool {
    69  	pc := make([]uintptr, 1)
    70  	if runtime.Callers(skipFrames+2, pc) != 1 {
    71  		return false
    72  	}
    73  	n := runtime.FuncForPC(pc[0]).Name()
    74  	return n == "runtime.preprintpanics"
    75  }
    76  
    77  type stackTrace struct {
    78  	data  []byte
    79  	limit bool
    80  }
    81  
    82  func (v stackTrace) IsFullStack() bool {
    83  	return !v.limit
    84  }
    85  
    86  func (v stackTrace) WriteStackTraceTo(w io.Writer) error {
    87  	_, err := w.Write(v.data)
    88  	return err
    89  }
    90  
    91  func (v stackTrace) StackTraceAsText() string {
    92  	return string(v.data)
    93  }
    94  
    95  func (v stackTrace) LogString() string {
    96  	return string(v.data)
    97  }
    98  
    99  func (v stackTrace) String() string {
   100  	return stackTracePrintPrefix + string(v.data)
   101  }
   102  
   103  func captureStack(skipFrames int, limitFrames bool) []byte {
   104  	skipFrames++
   105  	if limitFrames {
   106  		// provides a bit less info, but is 10x times faster
   107  		result := captureStackByCallers(skipFrames, true)
   108  		if len(result) > 0 {
   109  			return result
   110  		}
   111  		// result will be empty at stack top that we can't capture by runtime.Callers() so we will fallback
   112  	}
   113  	return captureStackByDebug(skipFrames, limitFrames)
   114  }
   115  
   116  const topFrameLimit = 1 // MUST be 1, otherwise comparison of stack vs stack top may not work properly
   117  
   118  func captureStackByDebug(skipFrames int, limitFrames bool) []byte {
   119  	stackBytes := debug.Stack()
   120  	capacity := cap(stackBytes)
   121  	if i := bytes.IndexByte(stackBytes, '\n'); i > 0 {
   122  		stackBytes = stackBytes[i+1:]
   123  	} else {
   124  		// strange result, let be safe
   125  		return stackBytes
   126  	}
   127  
   128  	const serviceFrames = 2 //  debug.Stack() + captureStackByDebug()
   129  	if i := indexOfFrame(stackBytes, skipFrames+serviceFrames); i > 0 {
   130  		stackBytes = stackBytes[i:]
   131  
   132  		if limitFrames {
   133  			if i := indexOfFrame(stackBytes, topFrameLimit); i > 0 {
   134  				stackBytes = stackBytes[:i]
   135  			}
   136  		}
   137  	}
   138  
   139  	stackBytes = bytes.TrimSpace(stackBytes)
   140  	return trimCapacity(capacity, stackBytes)
   141  }
   142  
   143  func trimCapacity(actualCapacity int, b []byte) []byte {
   144  	n := len(b) << 1
   145  	if actualCapacity > n || cap(b) > n {
   146  		return append(make([]byte, 0, n), b...)
   147  	}
   148  	return b
   149  }
   150  
   151  func indexOfFrame(stackBytes []byte, skipFrames int) int {
   152  	offset := 0
   153  	for ; skipFrames > 0; skipFrames-- {
   154  		prevOffset := offset
   155  		if i := bytes.Index(stackBytes[offset:], frameFileSep); i > 0 {
   156  			offset += i + len(frameFileSep)
   157  			if j := bytes.IndexByte(stackBytes[offset:], '\n'); j > 0 {
   158  				offset += j + 1
   159  				if offset == len(stackBytes) {
   160  					return prevOffset
   161  				}
   162  				continue
   163  			}
   164  			return prevOffset
   165  		}
   166  		return -1
   167  	}
   168  	return offset
   169  }
   170  
   171  const frameFileSeparator = "\n\t"
   172  
   173  var frameFileSep = []byte(frameFileSeparator)
   174  
   175  func captureStackByCallers(skipFrames int, limitFrames bool) []byte {
   176  	var pcs []uintptr
   177  	if limitFrames {
   178  		pcs = make([]uintptr, topFrameLimit)
   179  	} else {
   180  		pcs = make([]uintptr, 50) // maxStackDepth
   181  	}
   182  	const serviceFrames = 2 //  runtime.Callers() + captureStackByCallers()
   183  	pcs = pcs[:runtime.Callers(skipFrames+serviceFrames, pcs)]
   184  
   185  	result := make([]byte, 0, len(pcs)<<7)
   186  	for i, pc := range pcs {
   187  		fn := runtime.FuncForPC(pc)
   188  		if fn == nil {
   189  			continue
   190  		}
   191  		fName := fn.Name()
   192  		if i == len(pcs)-1 && fName == "runtime.goexit" {
   193  			break
   194  		}
   195  
   196  		// imitation of debug.Stack() format
   197  
   198  		if len(result) > 0 {
   199  			result = append(result, '\n')
   200  		}
   201  
   202  		result = append(result, fName...)
   203  		result = append(result, frameFileSeparator...)
   204  
   205  		fName, line := fn.FileLine(pc)
   206  		result = append(result, fName...)
   207  		result = append(result, ':')
   208  		result = strconv.AppendInt(result, int64(line), 10)
   209  	}
   210  
   211  	return trimCapacity(0, result)
   212  }
   213  
   214  func TrimStackTrace(s string) string {
   215  	if i := strings.Index(s, "\n"+stackTracePrintPrefix); i >= 0 {
   216  		return s[:i]
   217  	}
   218  	return s
   219  }