123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- // 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.
- package stack
- import (
- "bufio"
- "bytes"
- "fmt"
- "io"
- "os"
- "os/user"
- "path/filepath"
- "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
- localgoroot string
- 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.
- 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: runtime.GOROOT(),
- 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.GOPATHs)
- }
- }
- return c, err
- }
- // Private stuff.
- func parseDump(r io.Reader, out io.Writer) ([]*Goroutine, error) {
- scanner := bufio.NewScanner(r)
- scanner.Split(scanLines)
- 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
- }
- // scanningState is the state of the scan to detect and process a stack trace.
- //
- // TODO(maruel): Use a formal state machine. Patterns follows:
- // - reRoutineHeader
- // Either:
- // - reUnavail
- // - reFunc + reFile in a loop
- // - reElided
- // Optionally ends with:
- // - reCreated + reFile
- type scanningState struct {
- goroutines []*Goroutine
- goroutine *Goroutine
- created bool
- firstLine bool // firstLine is the first line after the reRoutineHeader header line.
- }
- func (s *scanningState) scan(line string) (string, error) {
- if line == "\n" || line == "\r\n" {
- if s.goroutine != nil {
- // goroutines are separated by an empty line.
- s.goroutine = nil
- return "", nil
- }
- } else if line[len(line)-1] == '\n' {
- if s.goroutine == nil {
- if match := reRoutineHeader.FindStringSubmatch(line); match != nil {
- if id, err := strconv.Atoi(match[1]); err == nil {
- // See runtime/traceback.go.
- // "<state>, \d+ minutes, locked to thread"
- items := strings.Split(match[2], ", ")
- 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,
- }
- s.goroutines = append(s.goroutines, g)
- s.goroutine = g
- s.firstLine = true
- return "", nil
- }
- }
- } else {
- if s.firstLine {
- s.firstLine = false
- if match := reUnavail.FindStringSubmatch(line); match != nil {
- // Generate a fake stack entry.
- s.goroutine.Stack.Calls = []Call{{SrcPath: "<unavailable>"}}
- return "", nil
- }
- }
- if match := reFile.FindStringSubmatch(line); match != nil {
- // Triggers after a reFunc or a reCreated.
- num, err := strconv.Atoi(match[2])
- if err != nil {
- return "", fmt.Errorf("failed to parse int on line: %q", strings.TrimSpace(line))
- }
- if s.created {
- s.created = false
- s.goroutine.CreatedBy.SrcPath = match[1]
- s.goroutine.CreatedBy.Line = num
- } else {
- i := len(s.goroutine.Stack.Calls) - 1
- if i < 0 {
- return "", fmt.Errorf("unexpected order on line: %q", strings.TrimSpace(line))
- }
- s.goroutine.Stack.Calls[i].SrcPath = match[1]
- s.goroutine.Stack.Calls[i].Line = num
- }
- return "", nil
- }
- if match := reCreated.FindStringSubmatch(line); match != nil {
- s.created = true
- s.goroutine.CreatedBy.Func.Raw = match[1]
- return "", nil
- }
- if match := reFunc.FindStringSubmatch(line); match != nil {
- args := Args{}
- for _, a := range strings.Split(match[2], ", ") {
- if a == "..." {
- args.Elided = true
- continue
- }
- if a == "" {
- // Remaining values were dropped.
- break
- }
- v, err := strconv.ParseUint(a, 0, 64)
- if err != nil {
- return "", fmt.Errorf("failed to parse int on line: %q", strings.TrimSpace(line))
- }
- args.Values = append(args.Values, Arg{Value: v})
- }
- s.goroutine.Stack.Calls = append(s.goroutine.Stack.Calls, Call{Func: Func{Raw: match[1]}, Args: args})
- return "", nil
- }
- if match := reElided.FindStringSubmatch(line); match != nil {
- s.goroutine.Stack.Elided = true
- return "", nil
- }
- }
- }
- s.goroutine = nil
- return line, nil
- }
- // hasPathPrefix returns true if any of s is the prefix of p.
- func hasPathPrefix(p string, s map[string]string) bool {
- for prefix := range s {
- if strings.HasPrefix(p, prefix+"/") {
- 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{}{}
- }
- }
- out := make([]string, 0, len(files))
- for f := range files {
- out = append(out, f)
- }
- sort.Strings(out)
- return out
- }
- // splitPath splits a path 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.
- func rootedIn(root string, parts []string) string {
- //log.Printf("rootIn(%s, %v)", root, parts)
- for i := 1; i < len(parts); i++ {
- suffix := filepath.Join(parts[i:]...)
- if isFile(filepath.Join(root, suffix)) {
- return filepath.Join(parts[:i]...)
- }
- }
- return ""
- }
- // findRoots sets member GOROOT and GOPATHs.
- func (c *Context) findRoots() {
- c.GOPATHs = map[string]string{}
- 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)
- if c.GOROOT != "" && strings.HasPrefix(f, c.GOROOT+"/") {
- continue
- }
- if hasPathPrefix(f, c.GOPATHs) {
- continue
- }
- parts := splitPath(f)
- if c.GOROOT == "" {
- if r := rootedIn(c.localgoroot, parts); r != "" {
- c.GOROOT = r
- //log.Printf("Found GOROOT=%s", c.GOROOT)
- continue
- }
- }
- found := false
- for _, l := range c.localgopaths {
- if r := rootedIn(l, parts); r != "" {
- //log.Printf("Found GOPATH=%s", r)
- c.GOPATHs[r] = l
- found = true
- break
- }
- }
- if !found {
- // If the source is not found, just too bad.
- //log.Printf("Failed to find locally: %s / %s", f, goroot)
- }
- }
- }
- func getGOPATHs() []string {
- var out []string
- for _, v := range filepath.SplitList(os.Getenv("GOPATH")) {
- // Disallow non-absolute paths?
- if v != "" {
- 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{homeDir + "go"}
- }
- return out
- }
|