1// Copyright 2019 The Gitea Authors. All rights reserved.
2// SPDX-License-Identifier: MIT
3
4package log
5
6import (
7 "bytes"
8 "fmt"
9 "os"
10 "runtime"
11)
12
13var unknown = []byte("???")
14
15// Stack will skip back the provided number of frames and return a stack trace with source code.
16// Although we could just use debug.Stack(), this routine will return the source code and
17// skip back the provided number of frames - i.e. allowing us to ignore preceding function calls.
18// A skip of 0 returns the stack trace for the calling function, not including this call.
19// If the problem is a lack of memory of course all this is not going to work...
20func Stack(skip int) string {
21 buf := new(bytes.Buffer)
22
23 // Store the last file we opened as its probable that the preceding stack frame
24 // will be in the same file
25 var lines [][]byte
26 var lastFilename string
27 for i := skip + 1; ; i++ { // Skip over frames
28 programCounter, filename, lineNumber, ok := runtime.Caller(i)
29 // If we can't retrieve the information break - basically we're into go internals at this point.
30 if !ok {
31 break
32 }
33
34 // Print equivalent of debug.Stack()
35 _, _ = fmt.Fprintf(buf, "%s:%d (0x%x)\n", filename, lineNumber, programCounter)
36 // Now try to print the offending line
37 if filename != lastFilename {
38 data, err := os.ReadFile(filename)
39 if err != nil {
40 // can't read this source file
41 // likely we don't have the sourcecode available
42 continue
43 }
44 lines = bytes.Split(data, []byte{'\n'})
45 lastFilename = filename
46 }
47 _, _ = fmt.Fprintf(buf, "\t%s: %s\n", functionName(programCounter), source(lines, lineNumber))
48 }
49 return buf.String()
50}
51
52// functionName converts the provided programCounter into a function name
53func functionName(programCounter uintptr) []byte {
54 function := runtime.FuncForPC(programCounter)
55 if function == nil {
56 return unknown
57 }
58 name := []byte(function.Name())
59
60 // Because we provide the filename we can drop the preceding package name.
61 if lastslash := bytes.LastIndex(name, []byte("/")); lastslash >= 0 {
62 name = name[lastslash+1:]
63 }
64 // And the current package name.
65 if period := bytes.Index(name, []byte(".")); period >= 0 {
66 name = name[period+1:]
67 }
68 // And we should just replace the interpunct with a dot
69 name = bytes.ReplaceAll(name, []byte("·"), []byte("."))
70 return name
71}
72
73// source returns a space-trimmed slice of the n'th line.
74func source(lines [][]byte, n int) []byte {
75 n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
76 if n < 0 || n >= len(lines) {
77 return unknown
78 }
79 return bytes.TrimSpace(lines[n])
80}