context.go 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. // Copyright 2018 Marc-Antoine Ruel. All rights reserved.
  2. // Use of this source code is governed under the Apache License, Version 2.0
  3. // that can be found in the LICENSE file.
  4. package stack
  5. import (
  6. "bufio"
  7. "bytes"
  8. "fmt"
  9. "io"
  10. "os"
  11. "os/user"
  12. "path/filepath"
  13. "runtime"
  14. "sort"
  15. "strconv"
  16. "strings"
  17. )
  18. // Context is a parsing context.
  19. //
  20. // It contains the deduced GOROOT and GOPATH, if guesspaths is true.
  21. type Context struct {
  22. // Goroutines is the Goroutines found.
  23. //
  24. // They are in the order that they were printed.
  25. Goroutines []*Goroutine
  26. // GOROOT is the GOROOT as detected in the traceback, not the on the host.
  27. //
  28. // It can be empty if no root was determined, for example the traceback
  29. // contains only non-stdlib source references.
  30. //
  31. // Empty is guesspaths was false.
  32. GOROOT string
  33. // GOPATHs is the GOPATH as detected in the traceback, with the value being
  34. // the corresponding path mapped to the host.
  35. //
  36. // It can be empty if only stdlib code is in the traceback or if no local
  37. // sources were matched up. In the general case there is only one entry in
  38. // the map.
  39. //
  40. // Nil is guesspaths was false.
  41. GOPATHs map[string]string
  42. localgoroot string
  43. localgopaths []string
  44. }
  45. // ParseDump processes the output from runtime.Stack().
  46. //
  47. // Returns nil *Context if no stack trace was detected.
  48. //
  49. // It pipes anything not detected as a panic stack trace from r into out. It
  50. // assumes there is junk before the actual stack trace. The junk is streamed to
  51. // out.
  52. //
  53. // If guesspaths is false, no guessing of GOROOT and GOPATH is done, and Call
  54. // entites do not have LocalSrcPath and IsStdlib filled in.
  55. func ParseDump(r io.Reader, out io.Writer, guesspaths bool) (*Context, error) {
  56. goroutines, err := parseDump(r, out)
  57. if len(goroutines) == 0 {
  58. return nil, err
  59. }
  60. c := &Context{
  61. Goroutines: goroutines,
  62. localgoroot: runtime.GOROOT(),
  63. localgopaths: getGOPATHs(),
  64. }
  65. nameArguments(goroutines)
  66. // Corresponding local values on the host for Context.
  67. if guesspaths {
  68. c.findRoots()
  69. for _, r := range c.Goroutines {
  70. // Note that this is important to call it even if
  71. // c.GOROOT == c.localgoroot.
  72. r.updateLocations(c.GOROOT, c.localgoroot, c.GOPATHs)
  73. }
  74. }
  75. return c, err
  76. }
  77. // Private stuff.
  78. func parseDump(r io.Reader, out io.Writer) ([]*Goroutine, error) {
  79. scanner := bufio.NewScanner(r)
  80. scanner.Split(scanLines)
  81. s := scanningState{}
  82. for scanner.Scan() {
  83. line, err := s.scan(scanner.Text())
  84. if line != "" {
  85. _, _ = io.WriteString(out, line)
  86. }
  87. if err != nil {
  88. return s.goroutines, err
  89. }
  90. }
  91. return s.goroutines, scanner.Err()
  92. }
  93. // scanLines is similar to bufio.ScanLines except that it:
  94. // - doesn't drop '\n'
  95. // - doesn't strip '\r'
  96. // - returns when the data is bufio.MaxScanTokenSize bytes
  97. func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
  98. if atEOF && len(data) == 0 {
  99. return 0, nil, nil
  100. }
  101. if i := bytes.IndexByte(data, '\n'); i >= 0 {
  102. return i + 1, data[0 : i+1], nil
  103. }
  104. if atEOF {
  105. return len(data), data, nil
  106. }
  107. if len(data) >= bufio.MaxScanTokenSize {
  108. // Returns the line even if it is not at EOF nor has a '\n', otherwise the
  109. // scanner will return bufio.ErrTooLong which is definitely not what we
  110. // want.
  111. return len(data), data, nil
  112. }
  113. return 0, nil, nil
  114. }
  115. // scanningState is the state of the scan to detect and process a stack trace.
  116. //
  117. // TODO(maruel): Use a formal state machine. Patterns follows:
  118. // - reRoutineHeader
  119. // Either:
  120. // - reUnavail
  121. // - reFunc + reFile in a loop
  122. // - reElided
  123. // Optionally ends with:
  124. // - reCreated + reFile
  125. type scanningState struct {
  126. goroutines []*Goroutine
  127. goroutine *Goroutine
  128. created bool
  129. firstLine bool // firstLine is the first line after the reRoutineHeader header line.
  130. }
  131. func (s *scanningState) scan(line string) (string, error) {
  132. if line == "\n" || line == "\r\n" {
  133. if s.goroutine != nil {
  134. // goroutines are separated by an empty line.
  135. s.goroutine = nil
  136. return "", nil
  137. }
  138. } else if line[len(line)-1] == '\n' {
  139. if s.goroutine == nil {
  140. if match := reRoutineHeader.FindStringSubmatch(line); match != nil {
  141. if id, err := strconv.Atoi(match[1]); err == nil {
  142. // See runtime/traceback.go.
  143. // "<state>, \d+ minutes, locked to thread"
  144. items := strings.Split(match[2], ", ")
  145. sleep := 0
  146. locked := false
  147. for i := 1; i < len(items); i++ {
  148. if items[i] == lockedToThread {
  149. locked = true
  150. continue
  151. }
  152. // Look for duration, if any.
  153. if match2 := reMinutes.FindStringSubmatch(items[i]); match2 != nil {
  154. sleep, _ = strconv.Atoi(match2[1])
  155. }
  156. }
  157. g := &Goroutine{
  158. Signature: Signature{
  159. State: items[0],
  160. SleepMin: sleep,
  161. SleepMax: sleep,
  162. Locked: locked,
  163. },
  164. ID: id,
  165. First: len(s.goroutines) == 0,
  166. }
  167. s.goroutines = append(s.goroutines, g)
  168. s.goroutine = g
  169. s.firstLine = true
  170. return "", nil
  171. }
  172. }
  173. } else {
  174. if s.firstLine {
  175. s.firstLine = false
  176. if match := reUnavail.FindStringSubmatch(line); match != nil {
  177. // Generate a fake stack entry.
  178. s.goroutine.Stack.Calls = []Call{{SrcPath: "<unavailable>"}}
  179. return "", nil
  180. }
  181. }
  182. if match := reFile.FindStringSubmatch(line); match != nil {
  183. // Triggers after a reFunc or a reCreated.
  184. num, err := strconv.Atoi(match[2])
  185. if err != nil {
  186. return "", fmt.Errorf("failed to parse int on line: %q", strings.TrimSpace(line))
  187. }
  188. if s.created {
  189. s.created = false
  190. s.goroutine.CreatedBy.SrcPath = match[1]
  191. s.goroutine.CreatedBy.Line = num
  192. } else {
  193. i := len(s.goroutine.Stack.Calls) - 1
  194. if i < 0 {
  195. return "", fmt.Errorf("unexpected order on line: %q", strings.TrimSpace(line))
  196. }
  197. s.goroutine.Stack.Calls[i].SrcPath = match[1]
  198. s.goroutine.Stack.Calls[i].Line = num
  199. }
  200. return "", nil
  201. }
  202. if match := reCreated.FindStringSubmatch(line); match != nil {
  203. s.created = true
  204. s.goroutine.CreatedBy.Func.Raw = match[1]
  205. return "", nil
  206. }
  207. if match := reFunc.FindStringSubmatch(line); match != nil {
  208. args := Args{}
  209. for _, a := range strings.Split(match[2], ", ") {
  210. if a == "..." {
  211. args.Elided = true
  212. continue
  213. }
  214. if a == "" {
  215. // Remaining values were dropped.
  216. break
  217. }
  218. v, err := strconv.ParseUint(a, 0, 64)
  219. if err != nil {
  220. return "", fmt.Errorf("failed to parse int on line: %q", strings.TrimSpace(line))
  221. }
  222. args.Values = append(args.Values, Arg{Value: v})
  223. }
  224. s.goroutine.Stack.Calls = append(s.goroutine.Stack.Calls, Call{Func: Func{Raw: match[1]}, Args: args})
  225. return "", nil
  226. }
  227. if match := reElided.FindStringSubmatch(line); match != nil {
  228. s.goroutine.Stack.Elided = true
  229. return "", nil
  230. }
  231. }
  232. }
  233. s.goroutine = nil
  234. return line, nil
  235. }
  236. // hasPathPrefix returns true if any of s is the prefix of p.
  237. func hasPathPrefix(p string, s map[string]string) bool {
  238. for prefix := range s {
  239. if strings.HasPrefix(p, prefix+"/") {
  240. return true
  241. }
  242. }
  243. return false
  244. }
  245. // getFiles returns all the source files deduped and ordered.
  246. func getFiles(goroutines []*Goroutine) []string {
  247. files := map[string]struct{}{}
  248. for _, g := range goroutines {
  249. for _, c := range g.Stack.Calls {
  250. files[c.SrcPath] = struct{}{}
  251. }
  252. }
  253. out := make([]string, 0, len(files))
  254. for f := range files {
  255. out = append(out, f)
  256. }
  257. sort.Strings(out)
  258. return out
  259. }
  260. // splitPath splits a path into its components.
  261. //
  262. // The first item has its initial path separator kept.
  263. func splitPath(p string) []string {
  264. if p == "" {
  265. return nil
  266. }
  267. var out []string
  268. s := ""
  269. for _, c := range p {
  270. if c != '/' || (len(out) == 0 && strings.Count(s, "/") == len(s)) {
  271. s += string(c)
  272. } else if s != "" {
  273. out = append(out, s)
  274. s = ""
  275. }
  276. }
  277. if s != "" {
  278. out = append(out, s)
  279. }
  280. return out
  281. }
  282. // isFile returns true if the path is a valid file.
  283. func isFile(p string) bool {
  284. // TODO(maruel): Is it faster to open the file or to stat it? Worth a perf
  285. // test on Windows.
  286. i, err := os.Stat(p)
  287. return err == nil && !i.IsDir()
  288. }
  289. // rootedIn returns a root if the file split in parts is rooted in root.
  290. func rootedIn(root string, parts []string) string {
  291. //log.Printf("rootIn(%s, %v)", root, parts)
  292. for i := 1; i < len(parts); i++ {
  293. suffix := filepath.Join(parts[i:]...)
  294. if isFile(filepath.Join(root, suffix)) {
  295. return filepath.Join(parts[:i]...)
  296. }
  297. }
  298. return ""
  299. }
  300. // findRoots sets member GOROOT and GOPATHs.
  301. func (c *Context) findRoots() {
  302. c.GOPATHs = map[string]string{}
  303. for _, f := range getFiles(c.Goroutines) {
  304. // TODO(maruel): Could a stack dump have mixed cases? I think it's
  305. // possible, need to confirm and handle.
  306. //log.Printf(" Analyzing %s", f)
  307. if c.GOROOT != "" && strings.HasPrefix(f, c.GOROOT+"/") {
  308. continue
  309. }
  310. if hasPathPrefix(f, c.GOPATHs) {
  311. continue
  312. }
  313. parts := splitPath(f)
  314. if c.GOROOT == "" {
  315. if r := rootedIn(c.localgoroot, parts); r != "" {
  316. c.GOROOT = r
  317. //log.Printf("Found GOROOT=%s", c.GOROOT)
  318. continue
  319. }
  320. }
  321. found := false
  322. for _, l := range c.localgopaths {
  323. if r := rootedIn(l, parts); r != "" {
  324. //log.Printf("Found GOPATH=%s", r)
  325. c.GOPATHs[r] = l
  326. found = true
  327. break
  328. }
  329. }
  330. if !found {
  331. // If the source is not found, just too bad.
  332. //log.Printf("Failed to find locally: %s / %s", f, goroot)
  333. }
  334. }
  335. }
  336. func getGOPATHs() []string {
  337. var out []string
  338. for _, v := range filepath.SplitList(os.Getenv("GOPATH")) {
  339. // Disallow non-absolute paths?
  340. if v != "" {
  341. out = append(out, v)
  342. }
  343. }
  344. if len(out) == 0 {
  345. homeDir := ""
  346. u, err := user.Current()
  347. if err != nil {
  348. homeDir = os.Getenv("HOME")
  349. if homeDir == "" {
  350. panic(fmt.Sprintf("Could not get current user or $HOME: %s\n", err.Error()))
  351. }
  352. } else {
  353. homeDir = u.HomeDir
  354. }
  355. out = []string{homeDir + "go"}
  356. }
  357. return out
  358. }