| // Copyright 2020 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package stack |
| |
| import ( |
| "bufio" |
| "errors" |
| "io" |
| "regexp" |
| "strconv" |
| ) |
| |
| var ( |
| reBlank = regexp.MustCompile(`^\s*$`) |
| reGoroutine = regexp.MustCompile(`^\s*goroutine (\d+) \[([^\]]*)\]:\s*$`) |
| reCall = regexp.MustCompile(`^\s*` + |
| `(created by )?` + //marker |
| `(([\w/.]+/)?[\w]+)\.` + //package |
| `(\(([^:.)]*)\)\.)?` + //optional type |
| `([\w\.]+)` + //function |
| `(\(.*\))?` + // args |
| `\s*$`) |
| rePos = regexp.MustCompile(`^\s*(.*):(\d+)( .*)?$`) |
| |
| errBreakParse = errors.New("break parse") |
| ) |
| |
| // Scanner splits an input stream into lines in a way that is consumable by |
| // the parser. |
| type Scanner struct { |
| lines *bufio.Scanner |
| done bool |
| } |
| |
| // NewScanner creates a scanner on top of a reader. |
| func NewScanner(r io.Reader) *Scanner { |
| s := &Scanner{ |
| lines: bufio.NewScanner(r), |
| } |
| s.Skip() // prefill |
| return s |
| } |
| |
| // Peek returns the next line without consuming it. |
| func (s *Scanner) Peek() string { |
| if s.done { |
| return "" |
| } |
| return s.lines.Text() |
| } |
| |
| // Skip consumes the next line without looking at it. |
| // Normally used after it has already been looked at using Peek. |
| func (s *Scanner) Skip() { |
| if !s.lines.Scan() { |
| s.done = true |
| } |
| } |
| |
| // Next consumes and returns the next line. |
| func (s *Scanner) Next() string { |
| line := s.Peek() |
| s.Skip() |
| return line |
| } |
| |
| // Done returns true if the scanner has reached the end of the underlying |
| // stream. |
| func (s *Scanner) Done() bool { |
| return s.done |
| } |
| |
| // Err returns true if the scanner has reached the end of the underlying |
| // stream. |
| func (s *Scanner) Err() error { |
| return s.lines.Err() |
| } |
| |
| // Match returns the submatchs of the regular expression against the next line. |
| // If it matched the line is also consumed. |
| func (s *Scanner) Match(re *regexp.Regexp) []string { |
| if s.done { |
| return nil |
| } |
| match := re.FindStringSubmatch(s.Peek()) |
| if match != nil { |
| s.Skip() |
| } |
| return match |
| } |
| |
| // SkipBlank skips any number of pure whitespace lines. |
| func (s *Scanner) SkipBlank() { |
| for !s.done { |
| line := s.Peek() |
| if len(line) != 0 && !reBlank.MatchString(line) { |
| return |
| } |
| s.Skip() |
| } |
| } |
| |
| // Parse the current contiguous block of goroutine stack traces until the |
| // scanned content no longer matches. |
| func Parse(scanner *Scanner) (Dump, error) { |
| dump := Dump{} |
| for { |
| gr, ok := parseGoroutine(scanner) |
| if !ok { |
| return dump, nil |
| } |
| dump = append(dump, gr) |
| } |
| } |
| |
| func parseGoroutine(scanner *Scanner) (Goroutine, bool) { |
| match := scanner.Match(reGoroutine) |
| if match == nil { |
| return Goroutine{}, false |
| } |
| id, _ := strconv.ParseInt(match[1], 0, 32) |
| gr := Goroutine{ |
| ID: int(id), |
| State: match[2], |
| } |
| for { |
| frame, ok := parseFrame(scanner) |
| if !ok { |
| scanner.SkipBlank() |
| return gr, true |
| } |
| if frame.Position.Filename != "" { |
| gr.Stack = append(gr.Stack, frame) |
| } |
| } |
| } |
| |
| func parseFrame(scanner *Scanner) (Frame, bool) { |
| fun, ok := parseFunction(scanner) |
| if !ok { |
| return Frame{}, false |
| } |
| frame := Frame{ |
| Function: fun, |
| } |
| frame.Position, ok = parsePosition(scanner) |
| // if ok is false, then this is a broken state. |
| // we got the func but not the file that must follow |
| // the consumed line can be recovered from the frame |
| //TODO: push back the fun raw |
| return frame, ok |
| } |
| |
| func parseFunction(scanner *Scanner) (Function, bool) { |
| match := scanner.Match(reCall) |
| if match == nil { |
| return Function{}, false |
| } |
| return Function{ |
| Package: match[2], |
| Type: match[5], |
| Name: match[6], |
| }, true |
| } |
| |
| func parsePosition(scanner *Scanner) (Position, bool) { |
| match := scanner.Match(rePos) |
| if match == nil { |
| return Position{}, false |
| } |
| line, _ := strconv.ParseInt(match[2], 0, 32) |
| return Position{Filename: match[1], Line: int(line)}, true |
| } |