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 }