blob: e01da8f0eef008d57ee8044aaa4485330c4951a8 [file] [log] [blame]
// 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
}