| // 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) | |
| } | |
| /* #nosec G304 */ | |
| 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 { | |
| /* #nosec G103 */ | |
| return *(*string)(unsafe.Pointer(&b)) | |
| } |