mirror of https://github.com/miguelmota/cointop
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
936 lines
27 KiB
Go
936 lines
27 KiB
Go
// 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 get golang.org/x/tools/cmd/stringer
|
|
//go:generate stringer -type state
|
|
|
|
package stack
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Context is a parsing context.
|
|
//
|
|
// It contains the deduced GOROOT and GOPATH, if guesspaths is true.
|
|
type Context struct {
|
|
// Goroutines is the Goroutines found.
|
|
//
|
|
// They are in the order that they were printed.
|
|
Goroutines []*Goroutine
|
|
|
|
// GOROOT 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.
|
|
//
|
|
// Empty is guesspaths was false.
|
|
GOROOT string
|
|
// GOPATHs is the GOPATH as detected in the traceback, with the value being
|
|
// the corresponding path mapped to the host.
|
|
//
|
|
// 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.
|
|
//
|
|
// Nil is guesspaths was false.
|
|
GOPATHs map[string]string
|
|
|
|
// localGomoduleRoot is the root directory containing go.mod. It is
|
|
// considered to be the primary project containing the main executable. It is
|
|
// initialized by findRoots().
|
|
//
|
|
// It only works with stack traces created in the local file system.
|
|
localGomoduleRoot string
|
|
// gomodImportPath is set to the relative import path that localGomoduleRoot
|
|
// represents.
|
|
gomodImportPath string
|
|
|
|
// localgoroot is GOROOT with "/" as path separator. No trailing "/".
|
|
localgoroot string
|
|
// localgopaths is GOPATH with "/" as path separator. No trailing "/".
|
|
localgopaths []string
|
|
}
|
|
|
|
// ParseDump processes the output from runtime.Stack().
|
|
//
|
|
// Returns nil *Context if no stack trace was detected.
|
|
//
|
|
// 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.
|
|
//
|
|
// If guesspaths is false, no guessing of GOROOT and GOPATH is done, and Call
|
|
// entites do not have LocalSrcPath and IsStdlib filled in. If true, be warned
|
|
// that file presence is done, which means some level of disk I/O.
|
|
func ParseDump(r io.Reader, out io.Writer, guesspaths bool) (*Context, error) {
|
|
goroutines, err := parseDump(r, out)
|
|
if len(goroutines) == 0 {
|
|
return nil, err
|
|
}
|
|
c := &Context{
|
|
Goroutines: goroutines,
|
|
localgoroot: strings.Replace(runtime.GOROOT(), "\\", "/", -1),
|
|
localgopaths: getGOPATHs(),
|
|
}
|
|
nameArguments(goroutines)
|
|
// Corresponding local values on the host for Context.
|
|
if guesspaths {
|
|
c.findRoots()
|
|
for _, r := range c.Goroutines {
|
|
// Note that this is important to call it even if
|
|
// c.GOROOT == c.localgoroot.
|
|
r.updateLocations(c.GOROOT, c.localgoroot, c.localGomoduleRoot, c.gomodImportPath, c.GOPATHs)
|
|
}
|
|
}
|
|
return c, err
|
|
}
|
|
|
|
// Private stuff.
|
|
|
|
func parseDump(r io.Reader, out io.Writer) ([]*Goroutine, error) {
|
|
scanner := bufio.NewScanner(r)
|
|
scanner.Split(scanLines)
|
|
// Do not enable race detection parsing yet, since it cannot be returned in
|
|
// Context at the moment.
|
|
s := scanningState{}
|
|
for scanner.Scan() {
|
|
line, err := s.scan(scanner.Text())
|
|
if line != "" {
|
|
_, _ = io.WriteString(out, line)
|
|
}
|
|
if err != nil {
|
|
return s.goroutines, err
|
|
}
|
|
}
|
|
return s.goroutines, scanner.Err()
|
|
}
|
|
|
|
// scanLines is similar to bufio.ScanLines except that it:
|
|
// - doesn't drop '\n'
|
|
// - doesn't strip '\r'
|
|
// - returns when the data is bufio.MaxScanTokenSize bytes
|
|
func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
|
if atEOF && len(data) == 0 {
|
|
return 0, nil, nil
|
|
}
|
|
if i := bytes.IndexByte(data, '\n'); i >= 0 {
|
|
return i + 1, data[0 : i+1], nil
|
|
}
|
|
if atEOF {
|
|
return len(data), data, nil
|
|
}
|
|
if len(data) >= bufio.MaxScanTokenSize {
|
|
// Returns the line even if it is not at EOF nor has a '\n', otherwise the
|
|
// scanner will return bufio.ErrTooLong which is definitely not what we
|
|
// want.
|
|
return len(data), data, nil
|
|
}
|
|
return 0, nil, nil
|
|
}
|
|
|
|
const (
|
|
lockedToThread = "locked to thread"
|
|
elided = "...additional frames elided..."
|
|
// gotRaceHeader1, normal
|
|
raceHeaderFooter = "=================="
|
|
// gotRaceHeader2
|
|
raceHeader = "WARNING: DATA RACE"
|
|
)
|
|
|
|
// 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/master/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 normal. Other states are when a stack trace is detected.
|
|
const (
|
|
// Outside a stack trace.
|
|
// to: gotRoutineHeader, raceHeader1
|
|
normal state = iota
|
|
|
|
// Panic stack trace:
|
|
|
|
// Signature: ""
|
|
// An empty line between goroutines.
|
|
// from: gotFileCreated, gotFileFunc
|
|
// to: gotRoutineHeader, normal
|
|
betweenRoutine
|
|
// Regexp: reRoutineHeader
|
|
// Signature: "goroutine 1 [running]:"
|
|
// Goroutine header was found.
|
|
// from: normal
|
|
// 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, normal
|
|
gotFileFunc
|
|
// Regexp: reFile
|
|
// Signature: "\t/foo/bar/baz.go:116 +0x35"
|
|
// File header was found.
|
|
// from: gotCreated
|
|
// to: betweenRoutine, normal
|
|
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: normal
|
|
// to: normal, gotRaceHeader2
|
|
gotRaceHeader1
|
|
// Constant: raceHeader
|
|
// Signature: "WARNING: DATA RACE"
|
|
// from: gotRaceHeader1
|
|
// to: normal, gotRaceOperationHeader
|
|
gotRaceHeader2
|
|
// Regexp: reRaceOperationHeader, reRacePreviousOperationHeader
|
|
// Signature: "Read at 0x00c0000e4030 by goroutine 7:"
|
|
// A race operation was found.
|
|
// from: gotRaceHeader2
|
|
// to: normal, gotRaceOperationFunc
|
|
gotRaceOperationHeader
|
|
// Regexp: reFunc
|
|
// Signature: " main.panicRace.func1()"
|
|
// Function that caused the race.
|
|
// from: gotRaceOperationHeader
|
|
// to: normal, gotRaceOperationFile
|
|
gotRaceOperationFunc
|
|
// Regexp: reFile
|
|
// Signature: "\t/foo/bar/baz.go:116 +0x35"
|
|
// File header that caused the race.
|
|
// from: gotRaceOperationFunc
|
|
// to: normal, betweenRaceOperations, gotRaceOperationFunc
|
|
gotRaceOperationFile
|
|
// Signature: ""
|
|
// Empty line between race operations or just after.
|
|
// from: gotRaceOperationFile
|
|
// to: normal, gotRaceOperationHeader, gotRaceGoroutineHeader
|
|
betweenRaceOperations
|
|
|
|
// Regexp: reRaceGoroutine
|
|
// Signature: "Goroutine 7 (running) created at:"
|
|
// Goroutine header.
|
|
// from: betweenRaceOperations, betweenRaceGoroutines
|
|
// to: normal, gotRaceOperationHeader
|
|
gotRaceGoroutineHeader
|
|
// Regexp: reFunc
|
|
// Signature: " main.panicRace.func1()"
|
|
// Function that caused the race.
|
|
// from: gotRaceGoroutineHeader
|
|
// to: normal, gotRaceGoroutineFile
|
|
gotRaceGoroutineFunc
|
|
// Regexp: reFile
|
|
// Signature: "\t/foo/bar/baz.go:116 +0x35"
|
|
// File header that caused the race.
|
|
// from: gotRaceGoroutineFunc
|
|
// to: normal, betweenRaceGoroutines
|
|
gotRaceGoroutineFile
|
|
// Signature: ""
|
|
// Empty line between race stack traces.
|
|
// from: gotRaceGoroutineFile
|
|
// to: normal, gotRaceGoroutineHeader
|
|
betweenRaceGoroutines
|
|
)
|
|
|
|
// raceOp is one of the detected data race operation as detected by the race
|
|
// detector.
|
|
type raceOp struct {
|
|
write bool
|
|
addr uint64
|
|
id int
|
|
create Stack
|
|
}
|
|
|
|
// scanningState is the state of the scan to detect and process a stack trace
|
|
// and stores the traces found.
|
|
type scanningState struct {
|
|
// Determines if race detection is enabled. Currently false since scan()
|
|
// would swallow the race detector output, but the data is not part of
|
|
// Context yet.
|
|
raceDetectionEnabled bool
|
|
|
|
// goroutines contains all the goroutines found.
|
|
goroutines []*Goroutine
|
|
|
|
state state
|
|
prefix string
|
|
races map[int]*raceOp
|
|
goroutineID int
|
|
}
|
|
|
|
// scan scans one line, updates goroutines and move to the next state.
|
|
//
|
|
// 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 string) (string, 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 strings.HasSuffix(line, "\r\n") {
|
|
trimmed = line[:len(line)-2]
|
|
} else if strings.HasSuffix(line, "\n") {
|
|
trimmed = line[:len(line)-1]
|
|
} else {
|
|
// There's two cases:
|
|
// - It's the end of the stream and it's not terminating with EOL character.
|
|
// - The line is longer than bufio.MaxScanTokenSize
|
|
if s.state == normal {
|
|
return line, nil
|
|
}
|
|
// Let it flow. It's possible the last line was trimmed and we still want to parse it.
|
|
}
|
|
|
|
if trimmed != "" && s.prefix != "" {
|
|
// This can only be the case if s.state != normal or the line is empty.
|
|
if !strings.HasPrefix(trimmed, s.prefix) {
|
|
prefix := s.prefix
|
|
s.state = normal
|
|
s.prefix = ""
|
|
return "", fmt.Errorf("inconsistent indentation: %q, expected %q", trimmed, prefix)
|
|
}
|
|
trimmed = trimmed[len(s.prefix):]
|
|
}
|
|
|
|
switch s.state {
|
|
case normal:
|
|
// 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.FindStringSubmatch(trimmed); match != nil {
|
|
if id, err := strconv.Atoi(match[2]); err == nil {
|
|
// See runtime/traceback.go.
|
|
// "<state>, \d+ minutes, locked to thread"
|
|
items := strings.Split(match[3], ", ")
|
|
sleep := 0
|
|
locked := false
|
|
for i := 1; i < len(items); i++ {
|
|
if items[i] == lockedToThread {
|
|
locked = true
|
|
continue
|
|
}
|
|
// Look for duration, if any.
|
|
if match2 := reMinutes.FindStringSubmatch(items[i]); match2 != nil {
|
|
sleep, _ = strconv.Atoi(match2[1])
|
|
}
|
|
}
|
|
g := &Goroutine{
|
|
Signature: Signature{
|
|
State: 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 = match[1]
|
|
return "", nil
|
|
}
|
|
}
|
|
// Switch to race detection mode.
|
|
if s.raceDetectionEnabled && 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 "", nil
|
|
}
|
|
// Fallthrough.
|
|
s.state = normal
|
|
s.prefix = ""
|
|
return line, nil
|
|
|
|
case gotRoutineHeader:
|
|
if reUnavail.MatchString(trimmed) {
|
|
// Generate a fake stack entry.
|
|
cur.Stack.Calls = []Call{{SrcPath: "<unavailable>"}}
|
|
// Next line is expected to be an empty line.
|
|
s.state = gotUnavail
|
|
return "", 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
|
|
}
|
|
return "", fmt.Errorf("expected a function after a goroutine header, got: %q", strings.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 "", err
|
|
} else if !found {
|
|
return "", fmt.Errorf("expected a file after a function, got: %q", strings.TrimSpace(trimmed))
|
|
}
|
|
s.state = gotFileFunc
|
|
return "", nil
|
|
|
|
case gotCreated:
|
|
if found, err := parseFile(&cur.CreatedBy, trimmed); err != nil {
|
|
return "", err
|
|
} else if !found {
|
|
return "", fmt.Errorf("expected a file after a created line, got: %q", trimmed)
|
|
}
|
|
s.state = gotFileCreated
|
|
return "", nil
|
|
|
|
case gotFileFunc:
|
|
if match := reCreated.FindStringSubmatch(trimmed); match != nil {
|
|
cur.CreatedBy.Func.Raw = match[1]
|
|
s.state = gotCreated
|
|
return "", nil
|
|
}
|
|
if elided == trimmed {
|
|
cur.Stack.Elided = true
|
|
// TODO(maruel): New state.
|
|
return "", 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
|
|
}
|
|
if trimmed == "" {
|
|
s.state = betweenRoutine
|
|
return "", nil
|
|
}
|
|
// Back to normal state.
|
|
s.state = normal
|
|
s.prefix = ""
|
|
return line, nil
|
|
|
|
case gotFileCreated:
|
|
if trimmed == "" {
|
|
s.state = betweenRoutine
|
|
return "", nil
|
|
}
|
|
s.state = normal
|
|
s.prefix = ""
|
|
return line, nil
|
|
|
|
case gotUnavail:
|
|
if trimmed == "" {
|
|
s.state = betweenRoutine
|
|
return "", nil
|
|
}
|
|
if match := reCreated.FindStringSubmatch(trimmed); match != nil {
|
|
cur.CreatedBy.Func.Raw = match[1]
|
|
s.state = gotCreated
|
|
return "", nil
|
|
}
|
|
return "", fmt.Errorf("expected empty line after unavailable stack, got: %q", strings.TrimSpace(trimmed))
|
|
|
|
// Race detector.
|
|
|
|
case gotRaceHeader1:
|
|
if raceHeader == trimmed {
|
|
// 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 "", nil
|
|
}
|
|
s.state = normal
|
|
return line, nil
|
|
|
|
case gotRaceHeader2:
|
|
if match := reRaceOperationHeader.FindStringSubmatch(trimmed); match != nil {
|
|
w := match[1] == "Write"
|
|
addr, err := strconv.ParseUint(match[2], 0, 64)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to parse address on line: %q", strings.TrimSpace(trimmed))
|
|
}
|
|
id, err := strconv.Atoi(match[3])
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to parse goroutine id on line: %q", strings.TrimSpace(trimmed))
|
|
}
|
|
if s.races != nil {
|
|
panic("internal failure; expected s.races to be nil")
|
|
}
|
|
if s.goroutines != nil {
|
|
panic("internal failure; expected s.goroutines to be nil")
|
|
}
|
|
s.races = make(map[int]*raceOp, 4)
|
|
s.races[id] = &raceOp{write: w, addr: addr, id: id}
|
|
s.goroutines = append(make([]*Goroutine, 0, 4), &Goroutine{ID: id, First: true})
|
|
s.goroutineID = id
|
|
s.state = gotRaceOperationHeader
|
|
return "", nil
|
|
}
|
|
s.state = normal
|
|
return line, nil
|
|
|
|
case gotRaceOperationHeader:
|
|
c := Call{}
|
|
if found, err := parseFunc(&c, strings.TrimLeft(trimmed, "\t ")); 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
|
|
}
|
|
return "", 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 "", err
|
|
} else if !found {
|
|
return "", fmt.Errorf("expected a file after a race function, got: %q", trimmed)
|
|
}
|
|
s.state = gotRaceOperationFile
|
|
return "", nil
|
|
|
|
case gotRaceOperationFile:
|
|
if trimmed == "" {
|
|
s.state = betweenRaceOperations
|
|
return "", nil
|
|
}
|
|
c := Call{}
|
|
if found, err := parseFunc(&c, strings.TrimLeft(trimmed, "\t ")); found {
|
|
cur.Stack.Calls = append(cur.Stack.Calls, c)
|
|
s.state = gotRaceOperationFunc
|
|
return "", err
|
|
}
|
|
return "", 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.FindStringSubmatch(trimmed); match != nil {
|
|
w := match[1] == "write"
|
|
addr, err := strconv.ParseUint(match[2], 0, 64)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to parse address on line: %q", strings.TrimSpace(trimmed))
|
|
}
|
|
id, err := strconv.Atoi(match[3])
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to parse goroutine id on line: %q", strings.TrimSpace(trimmed))
|
|
}
|
|
s.goroutineID = id
|
|
s.races[s.goroutineID] = &raceOp{write: w, addr: addr, id: id}
|
|
s.goroutines = append(s.goroutines, &Goroutine{ID: id})
|
|
s.state = gotRaceOperationHeader
|
|
return "", nil
|
|
}
|
|
fallthrough
|
|
|
|
case betweenRaceGoroutines:
|
|
if match := reRaceGoroutine.FindStringSubmatch(trimmed); match != nil {
|
|
id, err := strconv.Atoi(match[1])
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to parse goroutine id on line: %q", strings.TrimSpace(trimmed))
|
|
}
|
|
found := false
|
|
for _, g := range s.goroutines {
|
|
if g.ID == id {
|
|
g.State = match[2]
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return "", fmt.Errorf("unexpected goroutine ID on line: %q", strings.TrimSpace(trimmed))
|
|
}
|
|
s.goroutineID = id
|
|
s.state = gotRaceGoroutineHeader
|
|
return "", nil
|
|
}
|
|
return "", fmt.Errorf("expected an operator or goroutine, got: %q", trimmed)
|
|
|
|
// Race stack traces
|
|
|
|
case gotRaceGoroutineFunc:
|
|
c := s.races[s.goroutineID].create.Calls
|
|
if found, err := parseFile(&c[len(c)-1], trimmed); err != nil {
|
|
return "", err
|
|
} else if !found {
|
|
return "", fmt.Errorf("expected a file after a race function, got: %q", trimmed)
|
|
}
|
|
// TODO(maruel): Set s.goroutines[].CreatedBy.
|
|
s.state = gotRaceGoroutineFile
|
|
return "", nil
|
|
|
|
case gotRaceGoroutineFile:
|
|
if trimmed == "" {
|
|
s.state = betweenRaceGoroutines
|
|
return "", nil
|
|
}
|
|
if trimmed == raceHeaderFooter {
|
|
// Done.
|
|
s.state = normal
|
|
return "", nil
|
|
}
|
|
fallthrough
|
|
|
|
case gotRaceGoroutineHeader:
|
|
c := Call{}
|
|
if found, err := parseFunc(&c, strings.TrimLeft(trimmed, "\t ")); found {
|
|
// TODO(maruel): Set s.goroutines[].CreatedBy.
|
|
s.races[s.goroutineID].create.Calls = append(s.races[s.goroutineID].create.Calls, c)
|
|
s.state = gotRaceGoroutineFunc
|
|
return "", err
|
|
}
|
|
return "", fmt.Errorf("expected a function after a race operation or a race file, got: %q", trimmed)
|
|
|
|
default:
|
|
return "", errors.New("internal error")
|
|
}
|
|
}
|
|
|
|
// parseFunc only return an error if also returning a Call.
|
|
//
|
|
// Uses reFunc.
|
|
func parseFunc(c *Call, line string) (bool, error) {
|
|
if match := reFunc.FindStringSubmatch(line); match != nil {
|
|
c.Func.Raw = match[1]
|
|
for _, a := range strings.Split(match[2], ", ") {
|
|
if a == "..." {
|
|
c.Args.Elided = true
|
|
continue
|
|
}
|
|
if a == "" {
|
|
// Remaining values were dropped.
|
|
break
|
|
}
|
|
v, err := strconv.ParseUint(a, 0, 64)
|
|
if err != nil {
|
|
return true, fmt.Errorf("failed to parse int on line: %q", strings.TrimSpace(line))
|
|
}
|
|
// Increase performance by always allocating 4 values minimally.
|
|
if c.Args.Values == nil {
|
|
c.Args.Values = make([]Arg, 0, 4)
|
|
}
|
|
c.Args.Values = append(c.Args.Values, Arg{Value: v})
|
|
}
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// parseFile only return an error if also processing a Call.
|
|
//
|
|
// Uses reFile.
|
|
func parseFile(c *Call, line string) (bool, error) {
|
|
if match := reFile.FindStringSubmatch(line); match != nil {
|
|
num, err := strconv.Atoi(match[2])
|
|
if err != nil {
|
|
return true, fmt.Errorf("failed to parse int on line: %q", strings.TrimSpace(line))
|
|
}
|
|
c.SrcPath = match[1]
|
|
c.Line = num
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// hasSrcPrefix returns true if any of s is the prefix of p.
|
|
func hasSrcPrefix(p string, s map[string]string) bool {
|
|
for prefix := range s {
|
|
if strings.HasPrefix(p, prefix+"/src/") || strings.HasPrefix(p, prefix+"/pkg/mod/") {
|
|
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.SrcPath] = 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()
|
|
}
|
|
|
|
// rootedIn returns a root if the file split in parts is rooted in root.
|
|
//
|
|
// Uses "/" as path separator.
|
|
func rootedIn(root string, parts []string) string {
|
|
//log.Printf("rootIn(%s, %v)", root, parts)
|
|
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?$`)
|
|
|
|
// isGoModule returns the string to the directory containing a go.mod/go.sum
|
|
// files pair, and the go import path it represents, if found.
|
|
func isGoModule(parts []string) (string, string) {
|
|
for i := len(parts); i > 0; i-- {
|
|
prefix := pathJoin(parts[:i]...)
|
|
if isFile(pathJoin(prefix, "go.sum")) {
|
|
b, err := ioutil.ReadFile(pathJoin(prefix, "go.mod"))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if match := reModule.FindSubmatch(b); match != nil {
|
|
return prefix, string(match[1])
|
|
}
|
|
}
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
// findRoots sets member GOROOT, GOPATHs and localGomoduleRoot.
|
|
//
|
|
// This causes disk I/O as it checks for file presence.
|
|
//
|
|
// Returns the number of missing files.
|
|
func (c *Context) findRoots() int {
|
|
c.GOPATHs = map[string]string{}
|
|
missing := 0
|
|
for _, f := range getFiles(c.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 c.GOROOT != "" && strings.HasPrefix(f, c.GOROOT+"/src/") {
|
|
// stdlib.
|
|
continue
|
|
}
|
|
if hasSrcPrefix(f, c.GOPATHs) {
|
|
// $GOPATH/src or go.mod dependency in $GOPATH/pkg/mod.
|
|
continue
|
|
}
|
|
|
|
// At this point, disk will be looked up.
|
|
parts := splitPath(f)
|
|
if c.GOROOT == "" {
|
|
if r := rootedIn(c.localgoroot+"/src", parts); r != "" {
|
|
c.GOROOT = r[:len(r)-4]
|
|
//log.Printf("Found GOROOT=%s", c.GOROOT)
|
|
continue
|
|
}
|
|
}
|
|
found := false
|
|
for _, l := range c.localgopaths {
|
|
if r := rootedIn(l+"/src", parts); r != "" {
|
|
//log.Printf("Found GOPATH=%s", r[:len(r)-4])
|
|
c.GOPATHs[r[:len(r)-4]] = l
|
|
found = true
|
|
break
|
|
}
|
|
if r := rootedIn(l+"/pkg/mod", parts); r != "" {
|
|
//log.Printf("Found GOPATH=%s", r[:len(r)-8])
|
|
c.GOPATHs[r[:len(r)-8]] = l
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
// If the source is not found, it's probably a go module.
|
|
if !found {
|
|
if c.localGomoduleRoot == "" && len(parts) > 1 {
|
|
// Search upward looking for a go.mod/go.sum pair.
|
|
c.localGomoduleRoot, c.gomodImportPath = isGoModule(parts[:len(parts)-1])
|
|
}
|
|
if c.localGomoduleRoot != "" && strings.HasPrefix(f, c.localGomoduleRoot+"/") {
|
|
continue
|
|
}
|
|
}
|
|
if !found {
|
|
// 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 != "" {
|
|
v = strings.Replace(v, "\\", "/", -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
|
|
}
|
|
out = []string{strings.Replace(homeDir+"/go", "\\", "/", -1)}
|
|
}
|
|
return out
|
|
}
|