| // 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 | |
| } |