blob: d216c25abe9a9e2f1b32e9ac87beaac5258012f0 [file] [log] [blame]
// Copyright 2018 Marc-Antoine Ruel. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
//go:generate go install golang.org/x/tools/cmd/stringer@latest
//go:generate stringer -type state
//go:generate stringer -type Location
package stack
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/user"
"path"
"path/filepath"
"regexp"
"runtime"
"sort"
"strconv"
"strings"
"unsafe"
)
// Opts represents options to process the snapshot.
type Opts struct {
// LocalGOROOT is GOROOT with "/" as path separator. No trailing "/". Can be
// unset.
LocalGOROOT string
// LocalGOPATHs is GOPATH with "/" as path separator. No trailing "/". Can be
// unset.
LocalGOPATHs []string
// NameArguments tells panicparse to find the recurring pointer values and
// give them pseudo 'names'.
//
// Since the algorithm is O(n²), this can be worth disabling on live servers.
NameArguments bool
// GuessPaths tells panicparse to guess local RemoteGOROOT and GOPATH for
// what was found in the snapshot.
//
// Initializes in Snapshot the following members: RemoteGOROOT,
// RemoteGOPATHs, LocalGomoduleRoot and GomodImportPath.
//
// This is done by scanning the local disk, so be warned of performance
// impact.
GuessPaths bool
// AnalyzeSources tells panicparse to processes source files to improve calls
// to be more descriptive.
//
// Requires GuessPaths to be true.
AnalyzeSources bool
// Disallow initialization with unnamed parameters.
_ struct{}
}
// DefaultOpts returns default options to process the snapshot.
func DefaultOpts() *Opts {
p := runtime.GOROOT()
if runtime.GOOS == "windows" {
p = strings.Replace(p, pathSeparator, "/", -1)
}
return &Opts{
LocalGOROOT: p,
LocalGOPATHs: getGOPATHs(),
NameArguments: true,
GuessPaths: true,
AnalyzeSources: true,
}
}
func (o *Opts) isValid() bool {
if !o.GuessPaths && o.AnalyzeSources {
return false
}
if strings.Contains(o.LocalGOROOT, "\\") {
return false
}
for _, p := range o.LocalGOPATHs {
if strings.Contains(p, "\\") {
return false
}
}
return true
}
// Snapshot is a parsed runtime.Stack() or race detector dump.
type Snapshot struct {
// Goroutines is the Goroutines found.
//
// They are in the order that they were printed.
Goroutines []*Goroutine
// LocalGOROOT is copied from Opts.
LocalGOROOT string
// LocalGOPATHs is copied from Opts.
LocalGOPATHs []string
// The following members are initialized when Opts.GuessPaths is true.
// RemoteGOROOT is the GOROOT as detected in the traceback, not the on the
// host.
//
// It can be empty if no root was determined, for example the traceback
// contains only non-stdlib source references.
RemoteGOROOT string
// RemoteGOPATHs is the GOPATH as detected in the traceback, with the value
// being the corresponding path mapped to the host if found.
//
// It can be empty if only stdlib code is in the traceback or if no local
// sources were matched up. In the general case there is only one entry in
// the map.
RemoteGOPATHs map[string]string
// LocalGomods are the root directories containing go.mod or that directly
// contained source code as detected in the traceback, with the value being
// the corresponding import path found in the go.mod file.
//
// Uses "/" as path separator. No trailing "/".
//
// Because of the "replace" statement in go.mod, there can be multiple root
// directories. A file run by "go run" is also considered a go module to (a
// certain extent).
//
// It is initialized by findRoots().
//
// Unlike GOROOT and GOPATH, it only works with stack traces created in the
// local file system, hence "Local" prefix.
LocalGomods map[string]string
// Disallow initialization with unnamed parameters.
_ struct{}
}
// ScanSnapshot scans the Reader for the output from runtime.Stack() in br.
//
// Returns nil *Snapshot if no stack trace was detected.
//
// If a Snapshot is returned, you can call the function again to find another
// trace, or do io.Copy(br, out) to flush the rest of the stream.
//
// ParseSnapshot processes the output from runtime.Stack() or the race detector.
//
// Returns a nil *Snapshot if no stack trace was detected and SearchSnapshot()
// was a false positive.
//
// Returns io.EOF if all of reader was read.
//
// The suffix of the stack trace is returned as []byte.
//
// It pipes anything not detected as a panic stack trace from r into out. It
// assumes there is junk before the actual stack trace. The junk is streamed to
// out.
func ScanSnapshot(in io.Reader, prefix io.Writer, opts *Opts) (*Snapshot, []byte, error) {
if opts == nil || !opts.isValid() {
return nil, nil, errors.New("invalid Opts")
}
// TODO(maruel): Validate opts.
s := scanningState{
Snapshot: &Snapshot{
LocalGOROOT: opts.LocalGOROOT,
LocalGOPATHs: opts.LocalGOPATHs,
},
state: looking,
}
r := reader{rd: in}
var err error
var suffix []byte
for err == nil && s.state != done {
var d []byte
if d, err = r.readLine(); len(d) != 0 {
l, err1 := s.scan(d)
if err1 != nil && (err == nil || err == io.EOF) {
err = err1
}
if !l {
if s.state != looking {
suffix = append([]byte{}, d...)
suffix = append(suffix, r.buffered()...)
break
}
if _, err1 = prefix.Write(d); err1 != nil && (err == nil || err == io.EOF) {
err = err1
break
}
}
}
}
if s.Goroutines != nil {
if opts.NameArguments {
nameArguments(s.Goroutines)
}
if opts.GuessPaths {
_ = s.guessPaths()
}
if opts.AnalyzeSources {
_ = s.augment()
}
return s.Snapshot, suffix, err
}
return nil, suffix, err
}
// IsRace returns true if a race detector stack trace was found.
//
// Otherwise, it is a normal goroutines snapshot.
//
// When a race condition was detected, it is preferable to not call Aggregate().
func (s *Snapshot) IsRace() bool {
return s.Goroutines[0].RaceAddr != 0
}
func (s *Snapshot) guessPaths() bool {
b := s.findRoots() == 0
for _, r := range s.Goroutines {
// Note that this is important to call it even if
// s.RemoteGOROOT == s.LocalGOROOT.
b = r.updateLocations(s.RemoteGOROOT, s.LocalGOROOT, s.LocalGomods, s.RemoteGOPATHs) && b
}
return b
}
// augment processes source files to improve calls to be more descriptive.
//
// It modifies goroutines in place. It requires calling guessPaths() to work
// properly.
//
// Returns the last error that occurred while processing files.
func (s *Snapshot) augment() error {
c := cacheAST{
files: map[string][]byte{},
parsed: map[string]*parsedFile{},
}
var err error
for _, g := range s.Goroutines {
if err1 := c.augmentGoroutine(g); err1 != nil {
err = err1
}
}
return err
}
// Private stuff.
const pathSeparator = string(filepath.Separator)
var (
lockedToThread = []byte("locked to thread")
framesElided = []byte("...additional frames elided...")
// gotRaceHeader1, done
raceHeaderFooter = []byte("==================")
// gotRaceHeader2
raceHeader = []byte("WARNING: DATA RACE")
crlf = []byte("\r\n")
lf = []byte("\n")
commaSpace = []byte(", ")
writeCap = []byte("Write")
writeLow = []byte("write")
threeDots = []byte("...")
underscore = []byte("_")
inaccurateQuestionMark = []byte("?")
)
// These are effectively constants.
var (
// gotRoutineHeader
reRoutineHeader = regexp.MustCompile("^([ \t]*)goroutine (\\d+) \\[([^\\]]+)\\]\\:$")
reMinutes = regexp.MustCompile(`^(\d+) minutes$`)
// gotUnavail
reUnavail = regexp.MustCompile("^(?:\t| +)goroutine running on other thread; stack unavailable")
// gotFileFunc, gotRaceOperationFile, gotRaceGoroutineFile
// See gentraceback() in src/runtime/traceback.go for more information.
// - Sometimes the source file comes up as "<autogenerated>". It is the
// compiler than generated these, not the runtime.
// - The tab may be replaced with spaces when a user copy-paste it, handle
// this transparently.
// - "runtime.gopanic" is explicitly replaced with "panic" by gentraceback().
// - The +0x123 byte offset is printed when frame.pc > _func.entry. _func is
// generated by the linker.
// - The +0x123 byte offset is not included with generated code, e.g. unnamed
// functions "func·006()" which is generally go func() { ... }()
// statements. Since the _func is generated at runtime, it's probably why
// _func.entry is not set.
// - C calls may have fp=0x123 sp=0x123 appended. I think it normally happens
// when a signal is not correctly handled. It is printed with m.throwing>0.
// These are discarded.
// - For cgo, the source file may be "??".
reFile = regexp.MustCompile("^(?:\t| +)(\\?\\?|\\<autogenerated\\>|.+\\.(?:c|go|s))\\:(\\d+)(?:| \\+0x[0-9a-f]+)(?:| fp=0x[0-9a-f]+ sp=0x[0-9a-f]+(?:| pc=0x[0-9a-f]+))$")
// gotCreated
// Sadly, it doesn't note the goroutine number so we could cascade them per
// parenthood.
reCreated = regexp.MustCompile("^created by (.+)$")
// gotFunc, gotRaceOperationFunc, gotRaceGoroutineFunc
reFunc = regexp.MustCompile(`^(.+)\((.*)\)$`)
// Race:
// See https://github.com/llvm/llvm-project/blob/HEAD/compiler-rt/lib/tsan/rtl/tsan_report.cpp
// for the code generating these messages. Please note only the block in
// #else // #if !SANITIZER_GO
// is used.
// TODO(maruel): " [failed to restore the stack]\n\n"
// TODO(maruel): "Global var %s of size %zu at %p declared at %s:%zu\n"
// gotRaceOperationHeader
reRaceOperationHeader = regexp.MustCompile(`^(Read|Write) at (0x[0-9a-f]+) by goroutine (\d+):$`)
// gotRaceOperationHeader
reRacePreviousOperationHeader = regexp.MustCompile(`^Previous (read|write) at (0x[0-9a-f]+) by goroutine (\d+):$`)
// gotRaceGoroutineHeader
reRaceGoroutine = regexp.MustCompile(`^Goroutine (\d+) \((running|finished)\) created at:$`)
// TODO(maruel): Use it.
//reRacePreviousOperationMainHeader = regexp.MustCompile("^Previous (read|write) at (0x[0-9a-f]+) by main goroutine:$")
)
// state is the state of the scan to detect and process a stack trace.
type state int
// Initial state is looking. Other states are when a stack trace is detected.
const (
// Haven't found a stack trace yet.
// to: gotRoutineHeader, raceHeader1
looking state = iota
// Done processing a stack trace.
done
// Panic stack trace:
// Signature: ""
// An empty line between goroutines.
// from: gotFileCreated, gotFileFunc
// to: gotRoutineHeader, done
betweenRoutine
// Regexp: reRoutineHeader
// Signature: "goroutine 1 [running]:"
// Goroutine header was found.
// from: looking
// to: gotUnavail, gotFunc
gotRoutineHeader
// Regexp: reFunc
// Signature: "main.main()"
// Function call line was found.
// from: gotRoutineHeader
// to: gotFileFunc
gotFunc
// Regexp: reCreated
// Signature: "created by main.glob..func4"
// Goroutine creation line was found.
// from: gotFileFunc
// to: gotFileCreated
gotCreated
// Regexp: reFile
// Signature: "\t/foo/bar/baz.go:116 +0x35"
// File header was found.
// from: gotFunc
// to: gotFunc, gotCreated, betweenRoutine, done
gotFileFunc
// Regexp: reFile
// Signature: "\t/foo/bar/baz.go:116 +0x35"
// File header was found.
// from: gotCreated
// to: betweenRoutine, done
gotFileCreated
// Regexp: reUnavail
// Signature: "goroutine running on other thread; stack unavailable"
// State when the goroutine stack is instead is reUnavail.
// from: gotRoutineHeader
// to: betweenRoutine, gotCreated
gotUnavail
// Race detector:
// Constant: raceHeaderFooter
// Signature: "=================="
// from: looking
// to: done, gotRaceHeader2
gotRaceHeader1
// Constant: raceHeader
// Signature: "WARNING: DATA RACE"
// from: gotRaceHeader1
// to: done, gotRaceOperationHeader
gotRaceHeader2
// Regexp: reRaceOperationHeader, reRacePreviousOperationHeader
// Signature: "Read at 0x00c0000e4030 by goroutine 7:"
// A race operation was found.
// from: gotRaceHeader2
// to: done, gotRaceOperationFunc
gotRaceOperationHeader
// Regexp: reFunc
// Signature: " main.panicRace.func1()"
// Function that caused the race.
// from: gotRaceOperationHeader
// to: done, gotRaceOperationFile
gotRaceOperationFunc
// Regexp: reFile
// Signature: "\t/foo/bar/baz.go:116 +0x35"
// File header that caused the race.
// from: gotRaceOperationFunc
// to: done, betweenRaceOperations, gotRaceOperationFunc
gotRaceOperationFile
// Signature: ""
// Empty line between race operations or just after.
// from: gotRaceOperationFile
// to: done, gotRaceOperationHeader, gotRaceGoroutineHeader
betweenRaceOperations
// Regexp: reRaceGoroutine
// Signature: "Goroutine 7 (running) created at:"
// Goroutine header.
// from: betweenRaceOperations, betweenRaceGoroutines
// to: done, gotRaceOperationHeader
gotRaceGoroutineHeader
// Regexp: reFunc
// Signature: " main.panicRace.func1()"
// Function that caused the race.
// from: gotRaceGoroutineHeader
// to: done, gotRaceGoroutineFile
gotRaceGoroutineFunc
// Regexp: reFile
// Signature: "\t/foo/bar/baz.go:116 +0x35"
// File header that caused the race.
// from: gotRaceGoroutineFunc
// to: done, betweenRaceGoroutines
gotRaceGoroutineFile
// Signature: ""
// Empty line between race stack traces.
// from: gotRaceGoroutineFile
// to: done, gotRaceGoroutineHeader
betweenRaceGoroutines
)
// scanningState is the state of the scan to detect and process a stack trace
// and stores the traces found.
type scanningState struct {
*Snapshot
state state
prefix []byte
goroutineIndex int
}
// scan scans one line, updates goroutines and move to the next state.
//
// Returns true if the line was processed and thus should not be printed out.
//
// TODO(maruel): Handle corrupted stack cases:
// - missed stack barrier
// - found next stack barrier at 0x123; expected
// - runtime: unexpected return pc for FUNC_NAME called from 0x123
func (s *scanningState) scan(line []byte) (bool, error) {
/* This is very useful to debug issues in the state machine.
defer func() {
log.Printf("scan(%q) -> %s", line, s.state)
}()
//*/
var cur *Goroutine
if len(s.Goroutines) != 0 {
cur = s.Goroutines[len(s.Goroutines)-1]
}
trimmed := line
if bytes.HasSuffix(line, crlf) {
trimmed = line[:len(line)-2]
} else if bytes.HasSuffix(line, lf) {
trimmed = line[:len(line)-1]
} else {
// It's the end of the stream and it's not terminating with EOL character.
if s.state == looking || s.state == done {
return false, nil
}
// Let it flow. It's possible the last line was trimmed and we still want
// to parse it.
}
if len(trimmed) != 0 && len(s.prefix) != 0 {
// This can only be the case if s.state != looking | done or the line is
// empty.
if !bytes.HasPrefix(trimmed, s.prefix) {
prefix := s.prefix
s.state = done
s.prefix = nil
return false, fmt.Errorf("inconsistent indentation: %q, expected %q", trimmed, prefix)
}
trimmed = trimmed[len(s.prefix):]
}
switch s.state {
case done:
return false, nil
case looking:
// We could look for '^panic:' but this is more risky, there can be a lot
// of junk between this and the stack dump.
fallthrough
case betweenRoutine:
// Look for a goroutine header.
if match := reRoutineHeader.FindSubmatch(trimmed); match != nil {
if id, ok := atou(match[2]); ok {
// See runtime/traceback.go.
// "<state>, \d+ minutes, locked to thread"
items := bytes.Split(match[3], commaSpace)
sleep := 0
locked := false
for i := 1; i < len(items); i++ {
if bytes.Equal(items[i], lockedToThread) {
locked = true
continue
}
// Look for duration, if any.
if match2 := reMinutes.FindSubmatch(items[i]); match2 != nil {
sleep, _ = atou(match2[1])
}
}
g := &Goroutine{
Signature: Signature{
State: string(items[0]),
SleepMin: sleep,
SleepMax: sleep,
Locked: locked,
},
ID: id,
First: len(s.Goroutines) == 0,
}
// Increase performance by always allocating 4 goroutines minimally.
if s.Goroutines == nil {
s.Goroutines = make([]*Goroutine, 0, 4)
}
s.Goroutines = append(s.Goroutines, g)
s.state = gotRoutineHeader
s.prefix = append([]byte{}, match[1]...)
return true, nil
}
}
// Switch to race detection mode.
if bytes.Equal(trimmed, raceHeaderFooter) {
// TODO(maruel): We should buffer it in case the next line is not a
// WARNING so we can output it back.
s.state = gotRaceHeader1
return true, nil
}
if s.state != looking {
s.state = done
}
return false, nil
case gotRoutineHeader:
if reUnavail.Match(trimmed) {
// Generate a fake stack entry.
cur.Stack.Calls = []Call{{RemoteSrcPath: "<unavailable>"}}
// Next line is expected to be an empty line.
s.state = gotUnavail
return true, nil
}
c := Call{}
if found, err := parseFunc(&c, trimmed); found {
// Increase performance by always allocating 4 calls minimally.
if cur.Stack.Calls == nil {
cur.Stack.Calls = make([]Call, 0, 4)
}
cur.Stack.Calls = append(cur.Stack.Calls, c)
s.state = gotFunc
return err == nil, err
}
return false, fmt.Errorf("expected a function after a goroutine header, got: %q", bytes.TrimSpace(trimmed))
case gotFunc:
// cur.Stack.Calls is guaranteed to have at least one item.
if found, err := parseFile(&cur.Stack.Calls[len(cur.Stack.Calls)-1], trimmed); err != nil {
return false, err
} else if !found {
return false, fmt.Errorf("expected a file after a function, got: %q", bytes.TrimSpace(trimmed))
}
s.state = gotFileFunc
return true, nil
case gotCreated:
if found, err := parseFile(&cur.CreatedBy.Calls[0], trimmed); err != nil {
return false, err
} else if !found {
return false, fmt.Errorf("expected a file after a created line, got: %q", trimmed)
}
s.state = gotFileCreated
return true, nil
case gotFileFunc:
if match := reCreated.FindSubmatch(trimmed); match != nil {
cur.CreatedBy.Calls = make([]Call, 1)
if err := cur.CreatedBy.Calls[0].Func.Init(string(match[1])); err != nil {
cur.CreatedBy.Calls = nil
return false, err
}
// This initializes ImportPath.
cur.CreatedBy.Calls[0].init("", 0)
s.state = gotCreated
return true, nil
}
if bytes.Equal(trimmed, framesElided) {
cur.Stack.Elided = true
// TODO(maruel): New state.
return true, nil
}
c := Call{}
if found, err := parseFunc(&c, trimmed); found {
// Increase performance by always allocating 4 calls minimally.
if cur.Stack.Calls == nil {
cur.Stack.Calls = make([]Call, 0, 4)
}
cur.Stack.Calls = append(cur.Stack.Calls, c)
s.state = gotFunc
return err == nil, err
}
if len(trimmed) == 0 {
s.state = betweenRoutine
return true, nil
}
s.state = done
return false, nil
case gotFileCreated:
if len(trimmed) == 0 {
s.state = betweenRoutine
return true, nil
}
s.state = done
return false, nil
case gotUnavail:
if len(trimmed) == 0 {
s.state = betweenRoutine
return true, nil
}
if match := reCreated.FindSubmatch(trimmed); match != nil {
cur.CreatedBy.Calls = make([]Call, 1)
if err := cur.CreatedBy.Calls[0].Func.Init(string(match[1])); err != nil {
cur.CreatedBy.Calls = nil
return false, err
}
s.state = gotCreated
return true, nil
}
return false, fmt.Errorf("expected empty line after unavailable stack, got: %q", bytes.TrimSpace(trimmed))
// Race detector.
case gotRaceHeader1:
if bytes.Equal(trimmed, raceHeader) {
// TODO(maruel): We should buffer it in case the next line is not a
// WARNING so we can output it back.
s.state = gotRaceHeader2
return true, nil
}
// TODO(maruel): While this shouldn't error out, it should still force the
// output of raceHeaderFooter.
s.state = looking
s.prefix = nil
return false, nil
case gotRaceHeader2:
if match := reRaceOperationHeader.FindSubmatch(trimmed); match != nil {
w := bytes.Equal(match[1], writeCap)
addr, err := strconv.ParseUint(unsafeString(match[2]), 0, 64)
if err != nil {
return false, fmt.Errorf("failed to parse address on line: %q", bytes.TrimSpace(trimmed))
}
id, ok := atou(match[3])
if !ok {
return false, fmt.Errorf("failed to parse goroutine id on line: %q", bytes.TrimSpace(trimmed))
}
if s.Goroutines != nil {
panic("internal failure; expected s.Goroutines to be nil")
}
s.Goroutines = append(make([]*Goroutine, 0, 4), &Goroutine{ID: id, First: true, RaceWrite: w, RaceAddr: addr})
s.goroutineIndex = len(s.Goroutines) - 1
s.state = gotRaceOperationHeader
return true, nil
}
return false, fmt.Errorf("expected race condition, got: %q", bytes.TrimSpace(trimmed))
case gotRaceOperationHeader:
c := Call{}
if found, err := parseFunc(&c, trimLeftSpace(trimmed)); found {
// Increase performance by always allocating 4 calls minimally.
if cur.Stack.Calls == nil {
cur.Stack.Calls = make([]Call, 0, 4)
}
cur.Stack.Calls = append(cur.Stack.Calls, c)
s.state = gotRaceOperationFunc
return err == nil, err
}
return false, fmt.Errorf("expected a function after a race operation, got: %q", trimmed)
case gotRaceOperationFunc:
if found, err := parseFile(&cur.Stack.Calls[len(cur.Stack.Calls)-1], trimmed); err != nil {
return false, err
} else if !found {
return false, fmt.Errorf("expected a file after a race function, got: %q", trimmed)
}
s.state = gotRaceOperationFile
return true, nil
case gotRaceOperationFile:
if len(trimmed) == 0 {
s.state = betweenRaceOperations
return true, nil
}
c := Call{}
if found, err := parseFunc(&c, trimLeftSpace(trimmed)); found {
cur.Stack.Calls = append(cur.Stack.Calls, c)
s.state = gotRaceOperationFunc
return err == nil, err
}
return false, fmt.Errorf("expected an empty line after a race file, got: %q", trimmed)
case betweenRaceOperations:
// Look for other previous race data operations.
if match := reRacePreviousOperationHeader.FindSubmatch(trimmed); match != nil {
w := bytes.Equal(match[1], writeLow)
addr, err := strconv.ParseUint(unsafeString(match[2]), 0, 64)
if err != nil {
return false, fmt.Errorf("failed to parse address on line: %q", bytes.TrimSpace(trimmed))
}
id, ok := atou(match[3])
if !ok {
return false, fmt.Errorf("failed to parse goroutine id on line: %q", bytes.TrimSpace(trimmed))
}
s.Goroutines = append(s.Goroutines, &Goroutine{ID: id, RaceWrite: w, RaceAddr: addr})
s.goroutineIndex = len(s.Goroutines) - 1
s.state = gotRaceOperationHeader
return true, nil
}
fallthrough
case betweenRaceGoroutines:
if match := reRaceGoroutine.FindSubmatch(trimmed); match != nil {
id, ok := atou(match[1])
if !ok {
return false, fmt.Errorf("failed to parse goroutine id on line: %q", bytes.TrimSpace(trimmed))
}
found := false
for i, g := range s.Goroutines {
if g.ID == id {
g.State = string(match[2])
s.goroutineIndex = i
found = true
break
}
}
if !found {
return false, fmt.Errorf("unexpected goroutine ID on line: %q", bytes.TrimSpace(trimmed))
}
s.state = gotRaceGoroutineHeader
return true, nil
}
return false, fmt.Errorf("expected an operator or goroutine, got: %q", trimmed)
// Race stack traces
case gotRaceGoroutineFunc:
c := s.Goroutines[s.goroutineIndex].CreatedBy.Calls
if found, err := parseFile(&c[len(c)-1], trimmed); err != nil {
return false, err
} else if !found {
return false, fmt.Errorf("expected a file after a race function, got: %q", trimmed)
}
// TODO(maruel): Set s.Goroutines[].CreatedBy.
s.state = gotRaceGoroutineFile
return true, nil
case gotRaceGoroutineFile:
if len(trimmed) == 0 {
s.state = betweenRaceGoroutines
return true, nil
}
if bytes.Equal(trimmed, raceHeaderFooter) {
s.state = done
return true, nil
}
fallthrough
case gotRaceGoroutineHeader:
c := Call{}
if found, err := parseFunc(&c, trimLeftSpace(trimmed)); found {
s.Goroutines[s.goroutineIndex].CreatedBy.Calls = append(s.Goroutines[s.goroutineIndex].CreatedBy.Calls, c)
s.state = gotRaceGoroutineFunc
return err == nil, err
}
return false, fmt.Errorf("expected a function after a race operation or a race file, got: %q", trimmed)
default:
return false, errors.New("internal error")
}
}
// parseFunc only return an error if it also returns true.
//
// Uses reFunc.
func parseFunc(c *Call, line []byte) (bool, error) {
if match := reFunc.FindSubmatch(line); match != nil {
if err := c.Func.Init(string(match[1])); err != nil {
return true, err
}
// It is also done in c.init() but do it here in case of a corrupted trace
// for the file section.
c.ImportPath = c.Func.ImportPath
args, err := parseArgs(match[2])
if err != nil {
return true, fmt.Errorf("%s on line: %q", err, bytes.TrimSpace(line))
}
c.Args = args
return true, nil
}
return false, nil
}
// parseArgs parses a collection of comma-separated arguments into an Args
// struct.
func parseArgs(line []byte) (Args, error) {
const maxDepth = 6 // 5 from traceback.go, +1 for top level
var stack [maxDepth]*Args
var args Args
depth := 0
stack[depth] = &args
for _, s := range bytes.Split(line, commaSpace) {
opened, a, closed := trimCurlyBrackets(s)
for i := 0; i < opened; i++ {
cur := stack[depth]
cur.Values = append(cur.Values, Arg{})
next := &cur.Values[len(cur.Values)-1]
next.IsAggregate = true
depth++
if depth >= maxDepth {
return Args{}, fmt.Errorf("nested aggregate-typed arguments exceeded depth limit")
}
stack[depth] = &next.Fields
}
if len(a) > 0 {
cur := stack[depth]
switch {
case bytes.Equal(a, threeDots):
cur.Elided = true
case bytes.Equal(a, underscore):
arg := Arg{IsOffsetTooLarge: true}
cur.Values = append(cur.Values, arg)
default:
inaccurate := bytes.HasSuffix(a, inaccurateQuestionMark)
if inaccurate {
a = a[:len(a)-len(inaccurateQuestionMark)]
}
v, err := strconv.ParseUint(unsafeString(a), 0, 64)
if err != nil {
return Args{}, errors.New("failed to parse int")
}
// Assume the stack was generated with the same bitness (32 vs 64) as
// the code processing it.
arg := Arg{Value: v, IsPtr: v > pointerFloor && v < pointerCeiling, IsInaccurate: inaccurate}
cur.Values = append(cur.Values, arg)
}
}
for i := 0; i < closed; i++ {
stack[depth] = nil
depth--
if depth < 0 {
return Args{}, errors.New("unmatched closing curly bracket")
}
}
}
if depth != 0 {
return Args{}, errors.New("unmatched opening curly bracket")
}
return args, nil
}
// parseFile only return an error if also processing a Call.
//
// Uses reFile.
func parseFile(c *Call, line []byte) (bool, error) {
if match := reFile.FindSubmatch(line); match != nil {
num, ok := atou(match[2])
if !ok {
return true, fmt.Errorf("failed to parse int on line: %q", bytes.TrimSpace(line))
}
c.init(string(match[1]), num)
return true, nil
}
return false, nil
}
// hasPrefix returns true if any of s is the prefix of p.
func hasPrefix(p string, s map[string]string) bool {
lp := len(p)
for prefix := range s {
if l := len(prefix); lp > l+1 && p[:l] == prefix && p[l] == '/' {
return true
}
}
return false
}
// hasSrcPrefix returns true if any of s is the prefix of p with /src/ or
// /pkg/mod/.
func hasSrcPrefix(p string, s map[string]string) bool {
lp := len(p)
const src = "/src/"
const pkgmod = "/pkg/mod/"
for prefix := range s {
l := len(prefix)
if lp > l+len(src) && p[:l] == prefix && p[l:l+len(src)] == src {
return true
}
if lp > l+len(pkgmod) && p[:l] == prefix && p[l:l+len(pkgmod)] == pkgmod {
return true
}
}
return false
}
// getFiles returns all the source files deduped and ordered.
func getFiles(goroutines []*Goroutine) []string {
files := map[string]struct{}{}
for _, g := range goroutines {
for _, c := range g.Stack.Calls {
files[c.RemoteSrcPath] = struct{}{}
}
}
if len(files) == 0 {
return nil
}
out := make([]string, 0, len(files))
for f := range files {
out = append(out, f)
}
sort.Strings(out)
return out
}
// splitPath splits a path using "/" as separator into its components.
//
// The first item has its initial path separator kept.
func splitPath(p string) []string {
if p == "" {
return nil
}
var out []string
s := ""
for _, c := range p {
if c != '/' || (len(out) == 0 && strings.Count(s, "/") == len(s)) {
s += string(c)
} else if s != "" {
out = append(out, s)
s = ""
}
}
if s != "" {
out = append(out, s)
}
return out
}
// isFile returns true if the path is a valid file.
func isFile(p string) bool {
// TODO(maruel): Is it faster to open the file or to stat it? Worth a perf
// test on Windows.
i, err := os.Stat(p)
return err == nil && !i.IsDir()
}
// isRootedIn returns a root if the file split in parts exists under root.
//
// Uses "/" as path separator.
func isRootedIn(root string, parts []string) string {
for i := 1; i < len(parts); i++ {
suffix := pathJoin(parts[i:]...)
if isFile(pathJoin(root, suffix)) {
return pathJoin(parts[:i]...)
}
}
return ""
}
// reModule find the module line in a go.mod file. It works even on CRLF file.
var reModule = regexp.MustCompile(`(?m)^module\s+([^\n\r]+)\r?$`)
type gomodCache map[string]struct{}
// isGoModule returns the string to the directory containing a go.mod file, and
// the go import path it represents, if found.
func (g *gomodCache) isGoModule(parts []string) (string, string) {
for i := len(parts); i > 0; i-- {
prefix := pathJoin(parts[:i]...)
// Was already looked up.
if _, ok := (*g)[prefix]; ok {
break
}
(*g)[prefix] = struct{}{}
p := pathJoin(prefix, "go.mod")
if runtime.GOOS == "windows" {
p = strings.Replace(p, "/", pathSeparator, -1)
}
b, err := ioutil.ReadFile(p)
if err != nil {
continue
}
if match := reModule.FindSubmatch(b); match != nil {
return prefix, string(match[1])
}
}
return "", ""
}
// findRoots sets member RemoteGOROOT, RemoteGOPATHs and LocalGomods.
//
// This causes disk I/O as it checks for file presence.
//
// Returns the number of missing files.
func (s *Snapshot) findRoots() int {
// TODO(maruel): Reduce memory allocations in this function.
s.RemoteGOPATHs = map[string]string{}
s.LocalGomods = map[string]string{}
missing := 0
gmc := gomodCache{}
for _, f := range getFiles(s.Goroutines) {
// TODO(maruel): Could a stack dump have mixed cases? I think it's
// possible, need to confirm and handle.
//log.Printf(" Analyzing %s", f)
// First checks skip file I/O.
if s.RemoteGOROOT != "" && strings.HasPrefix(f, s.RemoteGOROOT+"/src/") {
// stdlib.
continue
}
if hasSrcPrefix(f, s.RemoteGOPATHs) {
// $GOPATH/src or go.mod dependency in $GOPATH/pkg/mod.
continue
}
if hasPrefix(f, s.LocalGomods) {
continue
}
// At this point, disk will be looked up.
parts := splitPath(f)
// Initializes RemoteGOROOT.
const src = "/src"
if s.RemoteGOROOT == "" {
if r := isRootedIn(s.LocalGOROOT+src, parts); r != "" {
s.RemoteGOROOT = r[:len(r)-len(src)]
//log.Printf("Found RemoteGOROOT=%s", s.RemoteGOROOT)
continue
}
}
// Initializes RemoteGOPATHs.
found := false
for _, l := range s.LocalGOPATHs {
if r := isRootedIn(l+src, parts); r != "" {
//log.Printf("Found RemoteGOPATHs[%s] = %s", r[:len(r)-len(src)], l)
s.RemoteGOPATHs[r[:len(r)-len(src)]] = l
found = true
break
}
const pkgmod = "/pkg/mod"
if r := isRootedIn(l+pkgmod, parts); r != "" {
//log.Printf("Found RemoteGOPATHs[%s] = %s", r[:len(r)-len(pkgmod)], l)
s.RemoteGOPATHs[r[:len(r)-len(pkgmod)]] = l
found = true
break
}
}
if found {
continue
}
// Initializes localGomods.
if len(parts) > 1 {
// Search upward looking for a go.mod.
if root, path := gmc.isGoModule(parts[:len(parts)-1]); root != "" {
s.LocalGomods[root] = path
continue
}
}
if isFile(f) {
// Assumes "go run" was used, thus is package main. Still consider it a
// "go module" but in the weakest sense.
s.LocalGomods[path.Dir(f)] = "main"
continue
}
// If the source is not found, just too bad.
//log.Printf("Failed to find locally: %s", f)
missing++
}
return missing
}
// getGOPATHs returns parsed GOPATH or its default, using "/" as path separator.
func getGOPATHs() []string {
var out []string
if gp := os.Getenv("GOPATH"); gp != "" {
for _, v := range filepath.SplitList(gp) {
// Disallow non-absolute paths?
if v != "" {
if runtime.GOOS == "windows" {
v = strings.Replace(v, pathSeparator, "/", -1)
}
// Trim trailing "/".
if l := len(v); v[l-1] == '/' {
v = v[:l-1]
}
out = append(out, v)
}
}
}
if len(out) == 0 {
homeDir := ""
u, err := user.Current()
if err != nil {
homeDir = os.Getenv("HOME")
if homeDir == "" {
panic(fmt.Sprintf("Could not get current user or $HOME: %s\n", err.Error()))
}
} else {
homeDir = u.HomeDir
}
p := homeDir + "/go"
if runtime.GOOS == "windows" {
p = strings.Replace(p, pathSeparator, "/", -1)
}
out = []string{p}
}
return out
}
// atou is a fast Atoi() function.
//
// It is a very simplified version of strconv.Atoi() that it never go into the
// slow path and it operates on []byte instead of string so it doesn't do
// memory allocation. It will fail on edge cases like prefix of zeros and other
// things that the panic stack trace generator never outputs.
//
// It doesn't handle negative values.
func atou(s []byte) (int, bool) {
if l := len(s); strconv.IntSize == 32 && (0 < l && l < 10) || strconv.IntSize == 64 && (0 < l && l < 19) {
n := 0
for _, ch := range s {
if ch -= '0'; ch > 9 {
return 0, false
}
n = n*10 + int(ch)
}
return n, true
}
return 0, false
}
// trimLeftSpace is the faster equivalent of bytes.TrimLeft(s, "\t ").
func trimLeftSpace(s []byte) []byte {
for i, ch := range s {
if ch != '\t' && ch != ' ' {
return s[i:]
}
}
return nil
}
// trimCurlyBrackets is the faster equivalent of
// bytes.TrimRight(bytes.TrimLeft(s, "{"), "}"). The function
// also returns the number of curly brackets trimmed from the
// left and the right.
func trimCurlyBrackets(s []byte) (int, []byte, int) {
i, j := 0, len(s)
for ; i < j; i++ {
if s[i] != '{' {
break
}
}
for ; i < j; j-- {
if s[j-1] != '}' {
break
}
}
return i, s[i:j], len(s) - j
}
// unsafeString performs an unsafe conversion from a []byte to a string. The
// returned string will share the underlying memory with the []byte which thus
// allows the string to be mutable through the []byte. We're careful to use
// this method only in situations in which the []byte will not be modified.
//
// A workaround for the absence of https://github.com/golang/go/issues/2632.
func unsafeString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}