github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/osutils/stacktrace/stacktrace.go (about)

     1  package stacktrace
     2  
     3  import (
     4  	"fmt"
     5  	"go/build"
     6  	"path/filepath"
     7  	"runtime"
     8  	"strings"
     9  
    10  	"github.com/ActiveState/cli/internal/environment"
    11  	"github.com/ActiveState/cli/internal/rtutils"
    12  )
    13  
    14  // Stacktrace represents a stacktrace
    15  type Stacktrace struct {
    16  	Frames []Frame
    17  }
    18  
    19  // Frame is a single frame in a stacktrace
    20  type Frame struct {
    21  	// Func contains a function name.
    22  	Func string
    23  	// Line contains a line number.
    24  	Line int
    25  	// Path contains a file path.
    26  	Path string
    27  	// Package is the package name for this frame
    28  	Package string
    29  }
    30  
    31  // FrameCap is a default cap for frames array.
    32  // It can be changed to number of expected frames
    33  // for purpose of performance optimisation.
    34  var FrameCap = 20
    35  
    36  var environmentRootPath string
    37  
    38  func init() {
    39  	// Note: ignore any error. It cannot be logged due to logging's dependence on this package.
    40  	environmentRootPath, _ = environment.GetRootPath()
    41  }
    42  
    43  // String returns a string representation of a stacktrace
    44  // For example:
    45  //   ./package/file.go:123:file.func
    46  //   ./another/package.go:456:package.(*Struct).method
    47  //   <go>/src/runtime.s:789:runtime.func
    48  func (t *Stacktrace) String() string {
    49  	result := []string{}
    50  	for _, frame := range t.Frames {
    51  		// Shorten path from its absolute path.
    52  		path := frame.Path
    53  		if strings.HasPrefix(path, build.Default.GOROOT) {
    54  			// Convert "/path/to/go/distribution/file" to "<go>/file".
    55  			path = strings.Replace(path, build.Default.GOROOT, "<go>", 1)
    56  		} else if environmentRootPath != "" {
    57  			// Convert "/path/to/cli/file" to "./file".
    58  			if relPath, err := filepath.Rel(environmentRootPath, path); err == nil {
    59  				path = "./" + relPath
    60  			}
    61  		}
    62  
    63  		// Shorten fully qualified function name to its local package name.
    64  		funcName := frame.Func
    65  		if index := strings.LastIndex(frame.Func, "/"); index > 0 {
    66  			// Convert "example.com/project/package/name.func" to "name.func".
    67  			funcName = frame.Func[index+1:]
    68  		}
    69  
    70  		result = append(result, fmt.Sprintf(`%s:%d:%s`, path, frame.Line, funcName))
    71  	}
    72  	return strings.Join(result, "\n")
    73  }
    74  
    75  // Get returns a stacktrace
    76  func Get() *Stacktrace {
    77  	return GetWithSkip(nil)
    78  }
    79  
    80  func GetWithSkip(skipFiles []string) *Stacktrace {
    81  	stacktrace := &Stacktrace{}
    82  	pc := make([]uintptr, FrameCap)
    83  	n := runtime.Callers(1, pc)
    84  	if n == 0 {
    85  		return stacktrace
    86  	}
    87  
    88  	pc = pc[:n]
    89  	frames := runtime.CallersFrames(pc)
    90  	skipFiles = append(skipFiles, rtutils.CurrentFile()) // Also skip the file we're in
    91  LOOP:
    92  	for {
    93  		frame, more := frames.Next()
    94  		pkg := strings.Split(frame.Func.Name(), ".")[0]
    95  
    96  		for _, skipFile := range skipFiles {
    97  			if frame.File == skipFile {
    98  				continue LOOP
    99  			}
   100  		}
   101  
   102  		stacktrace.Frames = append(stacktrace.Frames, Frame{
   103  			Func:    frame.Func.Name(),
   104  			Line:    frame.Line,
   105  			Path:    frame.File,
   106  			Package: pkg,
   107  		})
   108  
   109  		if !more {
   110  			break
   111  		}
   112  	}
   113  
   114  	return stacktrace
   115  }