1// Copyright 2018 Marc-Antoine Ruel. All rights reserved. 2// Use of this source code is governed under the Apache License, Version 2.0 3// that can be found in the LICENSE file. 4 5//go:generate go get golang.org/x/tools/cmd/stringer 6//go:generate stringer -type state 7 8package stack 9 10import ( 11 "bufio" 12 "bytes" 13 "errors" 14 "fmt" 15 "io" 16 "io/ioutil" 17 "os" 18 "os/user" 19 "path/filepath" 20 "regexp" 21 "runtime" 22 "sort" 23 "strconv" 24 "strings" 25) 26 27// Context is a parsing context. 28// 29// It contains the deduced GOROOT and GOPATH, if guesspaths is true. 30type Context struct { 31 // Goroutines is the Goroutines found. 32 // 33 // They are in the order that they were printed. 34 Goroutines []*Goroutine 35 36 // GOROOT is the GOROOT as detected in the traceback, not the on the host. 37 // 38 // It can be empty if no root was determined, for example the traceback 39 // contains only non-stdlib source references. 40 // 41 // Empty is guesspaths was false. 42 GOROOT string 43 // GOPATHs is the GOPATH as detected in the traceback, with the value being 44 // the corresponding path mapped to the host. 45 // 46 // It can be empty if only stdlib code is in the traceback or if no local 47 // sources were matched up. In the general case there is only one entry in 48 // the map. 49 // 50 // Nil is guesspaths was false. 51 GOPATHs map[string]string 52 53 // localGomoduleRoot is the root directory containing go.mod. It is 54 // considered to be the primary project containing the main executable. It is 55 // initialized by findRoots(). 56 // 57 // It only works with stack traces created in the local file system. 58 localGomoduleRoot string 59 // gomodImportPath is set to the relative import path that localGomoduleRoot 60 // represents. 61 gomodImportPath string 62 63 // localgoroot is GOROOT with "/" as path separator. No trailing "/". 64 localgoroot string 65 // localgopaths is GOPATH with "/" as path separator. No trailing "/". 66 localgopaths []string 67} 68 69// ParseDump processes the output from runtime.Stack(). 70// 71// Returns nil *Context if no stack trace was detected. 72// 73// It pipes anything not detected as a panic stack trace from r into out. It 74// assumes there is junk before the actual stack trace. The junk is streamed to 75// out. 76// 77// If guesspaths is false, no guessing of GOROOT and GOPATH is done, and Call 78// entites do not have LocalSrcPath and IsStdlib filled in. If true, be warned 79// that file presence is done, which means some level of disk I/O. 80func ParseDump(r io.Reader, out io.Writer, guesspaths bool) (*Context, error) { 81 goroutines, err := parseDump(r, out) 82 if len(goroutines) == 0 { 83 return nil, err 84 } 85 c := &Context{ 86 Goroutines: goroutines, 87 localgoroot: strings.Replace(runtime.GOROOT(), "\\", "/", -1), 88 localgopaths: getGOPATHs(), 89 } 90 nameArguments(goroutines) 91 // Corresponding local values on the host for Context. 92 if guesspaths { 93 c.findRoots() 94 for _, r := range c.Goroutines { 95 // Note that this is important to call it even if 96 // c.GOROOT == c.localgoroot. 97 r.updateLocations(c.GOROOT, c.localgoroot, c.localGomoduleRoot, c.gomodImportPath, c.GOPATHs) 98 } 99 } 100 return c, err 101} 102 103// Private stuff. 104 105func parseDump(r io.Reader, out io.Writer) ([]*Goroutine, error) { 106 scanner := bufio.NewScanner(r) 107 scanner.Split(scanLines) 108 // Do not enable race detection parsing yet, since it cannot be returned in 109 // Context at the moment. 110 s := scanningState{} 111 for scanner.Scan() { 112 line, err := s.scan(scanner.Text()) 113 if line != "" { 114 _, _ = io.WriteString(out, line) 115 } 116 if err != nil { 117 return s.goroutines, err 118 } 119 } 120 return s.goroutines, scanner.Err() 121} 122 123// scanLines is similar to bufio.ScanLines except that it: 124// - doesn't drop '\n' 125// - doesn't strip '\r' 126// - returns when the data is bufio.MaxScanTokenSize bytes 127func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) { 128 if atEOF && len(data) == 0 { 129 return 0, nil, nil 130 } 131 if i := bytes.IndexByte(data, '\n'); i >= 0 { 132 return i + 1, data[0 : i+1], nil 133 } 134 if atEOF { 135 return len(data), data, nil 136 } 137 if len(data) >= bufio.MaxScanTokenSize { 138 // Returns the line even if it is not at EOF nor has a '\n', otherwise the 139 // scanner will return bufio.ErrTooLong which is definitely not what we 140 // want. 141 return len(data), data, nil 142 } 143 return 0, nil, nil 144} 145 146const ( 147 lockedToThread = "locked to thread" 148 elided = "...additional frames elided..." 149 // gotRaceHeader1, normal 150 raceHeaderFooter = "==================" 151 // gotRaceHeader2 152 raceHeader = "WARNING: DATA RACE" 153) 154 155// These are effectively constants. 156var ( 157 // gotRoutineHeader 158 reRoutineHeader = regexp.MustCompile("^([ \t]*)goroutine (\\d+) \\[([^\\]]+)\\]\\:$") 159 reMinutes = regexp.MustCompile(`^(\d+) minutes$`) 160 161 // gotUnavail 162 reUnavail = regexp.MustCompile("^(?:\t| +)goroutine running on other thread; stack unavailable") 163 164 // gotFileFunc, gotRaceOperationFile, gotRaceGoroutineFile 165 // See gentraceback() in src/runtime/traceback.go for more information. 166 // - Sometimes the source file comes up as "<autogenerated>". It is the 167 // compiler than generated these, not the runtime. 168 // - The tab may be replaced with spaces when a user copy-paste it, handle 169 // this transparently. 170 // - "runtime.gopanic" is explicitly replaced with "panic" by gentraceback(). 171 // - The +0x123 byte offset is printed when frame.pc > _func.entry. _func is 172 // generated by the linker. 173 // - The +0x123 byte offset is not included with generated code, e.g. unnamed 174 // functions "func·006()" which is generally go func() { ... }() 175 // statements. Since the _func is generated at runtime, it's probably why 176 // _func.entry is not set. 177 // - C calls may have fp=0x123 sp=0x123 appended. I think it normally happens 178 // when a signal is not correctly handled. It is printed with m.throwing>0. 179 // These are discarded. 180 // - For cgo, the source file may be "??". 181 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]+))$") 182 183 // gotCreated 184 // Sadly, it doesn't note the goroutine number so we could cascade them per 185 // parenthood. 186 reCreated = regexp.MustCompile("^created by (.+)$") 187 188 // gotFunc, gotRaceOperationFunc, gotRaceGoroutineFunc 189 reFunc = regexp.MustCompile(`^(.+)\((.*)\)$`) 190 191 // Race: 192 // See https://github.com/llvm/llvm-project/blob/master/compiler-rt/lib/tsan/rtl/tsan_report.cpp 193 // for the code generating these messages. Please note only the block in 194 // #else // #if !SANITIZER_GO 195 // is used. 196 // TODO(maruel): " [failed to restore the stack]\n\n" 197 // TODO(maruel): "Global var %s of size %zu at %p declared at %s:%zu\n" 198 199 // gotRaceOperationHeader 200 reRaceOperationHeader = regexp.MustCompile(`^(Read|Write) at (0x[0-9a-f]+) by goroutine (\d+):$`) 201 202 // gotRaceOperationHeader 203 reRacePreviousOperationHeader = regexp.MustCompile(`^Previous (read|write) at (0x[0-9a-f]+) by goroutine (\d+):$`) 204 205 // gotRaceGoroutineHeader 206 reRaceGoroutine = regexp.MustCompile(`^Goroutine (\d+) \((running|finished)\) created at:$`) 207 208 // TODO(maruel): Use it. 209 //reRacePreviousOperationMainHeader = regexp.MustCompile("^Previous (read|write) at (0x[0-9a-f]+) by main goroutine:$") 210) 211 212// state is the state of the scan to detect and process a stack trace. 213type state int 214 215// Initial state is normal. Other states are when a stack trace is detected. 216const ( 217 // Outside a stack trace. 218 // to: gotRoutineHeader, raceHeader1 219 normal state = iota 220 221 // Panic stack trace: 222 223 // Signature: "" 224 // An empty line between goroutines. 225 // from: gotFileCreated, gotFileFunc 226 // to: gotRoutineHeader, normal 227 betweenRoutine 228 // Regexp: reRoutineHeader 229 // Signature: "goroutine 1 [running]:" 230 // Goroutine header was found. 231 // from: normal 232 // to: gotUnavail, gotFunc 233 gotRoutineHeader 234 // Regexp: reFunc 235 // Signature: "main.main()" 236 // Function call line was found. 237 // from: gotRoutineHeader 238 // to: gotFileFunc 239 gotFunc 240 // Regexp: reCreated 241 // Signature: "created by main.glob..func4" 242 // Goroutine creation line was found. 243 // from: gotFileFunc 244 // to: gotFileCreated 245 gotCreated 246 // Regexp: reFile 247 // Signature: "\t/foo/bar/baz.go:116 +0x35" 248 // File header was found. 249 // from: gotFunc 250 // to: gotFunc, gotCreated, betweenRoutine, normal 251 gotFileFunc 252 // Regexp: reFile 253 // Signature: "\t/foo/bar/baz.go:116 +0x35" 254 // File header was found. 255 // from: gotCreated 256 // to: betweenRoutine, normal 257 gotFileCreated 258 // Regexp: reUnavail 259 // Signature: "goroutine running on other thread; stack unavailable" 260 // State when the goroutine stack is instead is reUnavail. 261 // from: gotRoutineHeader 262 // to: betweenRoutine, gotCreated 263 gotUnavail 264 265 // Race detector: 266 267 // Constant: raceHeaderFooter 268 // Signature: "==================" 269 // from: normal 270 // to: normal, gotRaceHeader2 271 gotRaceHeader1 272 // Constant: raceHeader 273 // Signature: "WARNING: DATA RACE" 274 // from: gotRaceHeader1 275 // to: normal, gotRaceOperationHeader 276 gotRaceHeader2 277 // Regexp: reRaceOperationHeader, reRacePreviousOperationHeader 278 // Signature: "Read at 0x00c0000e4030 by goroutine 7:" 279 // A race operation was found. 280 // from: gotRaceHeader2 281 // to: normal, gotRaceOperationFunc 282 gotRaceOperationHeader 283 // Regexp: reFunc 284 // Signature: " main.panicRace.func1()" 285 // Function that caused the race. 286 // from: gotRaceOperationHeader 287 // to: normal, gotRaceOperationFile 288 gotRaceOperationFunc 289 // Regexp: reFile 290 // Signature: "\t/foo/bar/baz.go:116 +0x35" 291 // File header that caused the race. 292 // from: gotRaceOperationFunc 293 // to: normal, betweenRaceOperations, gotRaceOperationFunc 294 gotRaceOperationFile 295 // Signature: "" 296 // Empty line between race operations or just after. 297 // from: gotRaceOperationFile 298 // to: normal, gotRaceOperationHeader, gotRaceGoroutineHeader 299 betweenRaceOperations 300 301 // Regexp: reRaceGoroutine 302 // Signature: "Goroutine 7 (running) created at:" 303 // Goroutine header. 304 // from: betweenRaceOperations, betweenRaceGoroutines 305 // to: normal, gotRaceOperationHeader 306 gotRaceGoroutineHeader 307 // Regexp: reFunc 308 // Signature: " main.panicRace.func1()" 309 // Function that caused the race. 310 // from: gotRaceGoroutineHeader 311 // to: normal, gotRaceGoroutineFile 312 gotRaceGoroutineFunc 313 // Regexp: reFile 314 // Signature: "\t/foo/bar/baz.go:116 +0x35" 315 // File header that caused the race. 316 // from: gotRaceGoroutineFunc 317 // to: normal, betweenRaceGoroutines 318 gotRaceGoroutineFile 319 // Signature: "" 320 // Empty line between race stack traces. 321 // from: gotRaceGoroutineFile 322 // to: normal, gotRaceGoroutineHeader 323 betweenRaceGoroutines 324) 325 326// raceOp is one of the detected data race operation as detected by the race 327// detector. 328type raceOp struct { 329 write bool 330 addr uint64 331 id int 332 create Stack 333} 334 335// scanningState is the state of the scan to detect and process a stack trace 336// and stores the traces found. 337type scanningState struct { 338 // Determines if race detection is enabled. Currently false since scan() 339 // would swallow the race detector output, but the data is not part of 340 // Context yet. 341 raceDetectionEnabled bool 342 343 // goroutines contains all the goroutines found. 344 goroutines []*Goroutine 345 346 state state 347 prefix string 348 races map[int]*raceOp 349 goroutineID int 350} 351 352// scan scans one line, updates goroutines and move to the next state. 353// 354// TODO(maruel): Handle corrupted stack cases: 355// - missed stack barrier 356// - found next stack barrier at 0x123; expected 357// - runtime: unexpected return pc for FUNC_NAME called from 0x123 358func (s *scanningState) scan(line string) (string, error) { 359 /* This is very useful to debug issues in the state machine. 360 defer func() { 361 log.Printf("scan(%q) -> %s", line, s.state) 362 }() 363 //*/ 364 var cur *Goroutine 365 if len(s.goroutines) != 0 { 366 cur = s.goroutines[len(s.goroutines)-1] 367 } 368 trimmed := line 369 if strings.HasSuffix(line, "\r\n") { 370 trimmed = line[:len(line)-2] 371 } else if strings.HasSuffix(line, "\n") { 372 trimmed = line[:len(line)-1] 373 } else { 374 // There's two cases: 375 // - It's the end of the stream and it's not terminating with EOL character. 376 // - The line is longer than bufio.MaxScanTokenSize 377 if s.state == normal { 378 return line, nil 379 } 380 // Let it flow. It's possible the last line was trimmed and we still want to parse it. 381 } 382 383 if trimmed != "" && s.prefix != "" { 384 // This can only be the case if s.state != normal or the line is empty. 385 if !strings.HasPrefix(trimmed, s.prefix) { 386 prefix := s.prefix 387 s.state = normal 388 s.prefix = "" 389 return "", fmt.Errorf("inconsistent indentation: %q, expected %q", trimmed, prefix) 390 } 391 trimmed = trimmed[len(s.prefix):] 392 } 393 394 switch s.state { 395 case normal: 396 // We could look for '^panic:' but this is more risky, there can be a lot 397 // of junk between this and the stack dump. 398 fallthrough 399 400 case betweenRoutine: 401 // Look for a goroutine header. 402 if match := reRoutineHeader.FindStringSubmatch(trimmed); match != nil { 403 if id, err := strconv.Atoi(match[2]); err == nil { 404 // See runtime/traceback.go. 405 // "<state>, \d+ minutes, locked to thread" 406 items := strings.Split(match[3], ", ") 407 sleep := 0 408 locked := false 409 for i := 1; i < len(items); i++ { 410 if items[i] == lockedToThread { 411 locked = true 412 continue 413 } 414 // Look for duration, if any. 415 if match2 := reMinutes.FindStringSubmatch(items[i]); match2 != nil { 416 sleep, _ = strconv.Atoi(match2[1]) 417 } 418 } 419 g := &Goroutine{ 420 Signature: Signature{ 421 State: items[0], 422 SleepMin: sleep, 423 SleepMax: sleep, 424 Locked: locked, 425 }, 426 ID: id, 427 First: len(s.goroutines) == 0, 428 } 429 // Increase performance by always allocating 4 goroutines minimally. 430 if s.goroutines == nil { 431 s.goroutines = make([]*Goroutine, 0, 4) 432 } 433 s.goroutines = append(s.goroutines, g) 434 s.state = gotRoutineHeader 435 s.prefix = match[1] 436 return "", nil 437 } 438 } 439 // Switch to race detection mode. 440 if s.raceDetectionEnabled && trimmed == raceHeaderFooter { 441 // TODO(maruel): We should buffer it in case the next line is not a 442 // WARNING so we can output it back. 443 s.state = gotRaceHeader1 444 return "", nil 445 } 446 // Fallthrough. 447 s.state = normal 448 s.prefix = "" 449 return line, nil 450 451 case gotRoutineHeader: 452 if reUnavail.MatchString(trimmed) { 453 // Generate a fake stack entry. 454 cur.Stack.Calls = []Call{{SrcPath: "<unavailable>"}} 455 // Next line is expected to be an empty line. 456 s.state = gotUnavail 457 return "", nil 458 } 459 c := Call{} 460 if found, err := parseFunc(&c, trimmed); found { 461 // Increase performance by always allocating 4 calls minimally. 462 if cur.Stack.Calls == nil { 463 cur.Stack.Calls = make([]Call, 0, 4) 464 } 465 cur.Stack.Calls = append(cur.Stack.Calls, c) 466 s.state = gotFunc 467 return "", err 468 } 469 return "", fmt.Errorf("expected a function after a goroutine header, got: %q", strings.TrimSpace(trimmed)) 470 471 case gotFunc: 472 // cur.Stack.Calls is guaranteed to have at least one item. 473 if found, err := parseFile(&cur.Stack.Calls[len(cur.Stack.Calls)-1], trimmed); err != nil { 474 return "", err 475 } else if !found { 476 return "", fmt.Errorf("expected a file after a function, got: %q", strings.TrimSpace(trimmed)) 477 } 478 s.state = gotFileFunc 479 return "", nil 480 481 case gotCreated: 482 if found, err := parseFile(&cur.CreatedBy, trimmed); err != nil { 483 return "", err 484 } else if !found { 485 return "", fmt.Errorf("expected a file after a created line, got: %q", trimmed) 486 } 487 s.state = gotFileCreated 488 return "", nil 489 490 case gotFileFunc: 491 if match := reCreated.FindStringSubmatch(trimmed); match != nil { 492 cur.CreatedBy.Func.Raw = match[1] 493 s.state = gotCreated 494 return "", nil 495 } 496 if elided == trimmed { 497 cur.Stack.Elided = true 498 // TODO(maruel): New state. 499 return "", nil 500 } 501 c := Call{} 502 if found, err := parseFunc(&c, trimmed); found { 503 // Increase performance by always allocating 4 calls minimally. 504 if cur.Stack.Calls == nil { 505 cur.Stack.Calls = make([]Call, 0, 4) 506 } 507 cur.Stack.Calls = append(cur.Stack.Calls, c) 508 s.state = gotFunc 509 return "", err 510 } 511 if trimmed == "" { 512 s.state = betweenRoutine 513 return "", nil 514 } 515 // Back to normal state. 516 s.state = normal 517 s.prefix = "" 518 return line, nil 519 520 case gotFileCreated: 521 if trimmed == "" { 522 s.state = betweenRoutine 523 return "", nil 524 } 525 s.state = normal 526 s.prefix = "" 527 return line, nil 528 529 case gotUnavail: 530 if trimmed == "" { 531 s.state = betweenRoutine 532 return "", nil 533 } 534 if match := reCreated.FindStringSubmatch(trimmed); match != nil { 535 cur.CreatedBy.Func.Raw = match[1] 536 s.state = gotCreated 537 return "", nil 538 } 539 return "", fmt.Errorf("expected empty line after unavailable stack, got: %q", strings.TrimSpace(trimmed)) 540 541 // Race detector. 542 543 case gotRaceHeader1: 544 if raceHeader == trimmed { 545 // TODO(maruel): We should buffer it in case the next line is not a 546 // WARNING so we can output it back. 547 s.state = gotRaceHeader2 548 return "", nil 549 } 550 s.state = normal 551 return line, nil 552 553 case gotRaceHeader2: 554 if match := reRaceOperationHeader.FindStringSubmatch(trimmed); match != nil { 555 w := match[1] == "Write" 556 addr, err := strconv.ParseUint(match[2], 0, 64) 557 if err != nil { 558 return "", fmt.Errorf("failed to parse address on line: %q", strings.TrimSpace(trimmed)) 559 } 560 id, err := strconv.Atoi(match[3]) 561 if err != nil { 562 return "", fmt.Errorf("failed to parse goroutine id on line: %q", strings.TrimSpace(trimmed)) 563 } 564 if s.races != nil { 565 panic("internal failure; expected s.races to be nil") 566 } 567 if s.goroutines != nil { 568 panic("internal failure; expected s.goroutines to be nil") 569 } 570 s.races = make(map[int]*raceOp, 4) 571 s.races[id] = &raceOp{write: w, addr: addr, id: id} 572 s.goroutines = append(make([]*Goroutine, 0, 4), &Goroutine{ID: id, First: true}) 573 s.goroutineID = id 574 s.state = gotRaceOperationHeader 575 return "", nil 576 } 577 s.state = normal 578 return line, nil 579 580 case gotRaceOperationHeader: 581 c := Call{} 582 if found, err := parseFunc(&c, strings.TrimLeft(trimmed, "\t ")); found { 583 // Increase performance by always allocating 4 calls minimally. 584 if cur.Stack.Calls == nil { 585 cur.Stack.Calls = make([]Call, 0, 4) 586 } 587 cur.Stack.Calls = append(cur.Stack.Calls, c) 588 s.state = gotRaceOperationFunc 589 return "", err 590 } 591 return "", fmt.Errorf("expected a function after a race operation, got: %q", trimmed) 592 593 case gotRaceOperationFunc: 594 if found, err := parseFile(&cur.Stack.Calls[len(cur.Stack.Calls)-1], trimmed); err != nil { 595 return "", err 596 } else if !found { 597 return "", fmt.Errorf("expected a file after a race function, got: %q", trimmed) 598 } 599 s.state = gotRaceOperationFile 600 return "", nil 601 602 case gotRaceOperationFile: 603 if trimmed == "" { 604 s.state = betweenRaceOperations 605 return "", nil 606 } 607 c := Call{} 608 if found, err := parseFunc(&c, strings.TrimLeft(trimmed, "\t ")); found { 609 cur.Stack.Calls = append(cur.Stack.Calls, c) 610 s.state = gotRaceOperationFunc 611 return "", err 612 } 613 return "", fmt.Errorf("expected an empty line after a race file, got: %q", trimmed) 614 615 case betweenRaceOperations: 616 // Look for other previous race data operations. 617 if match := reRacePreviousOperationHeader.FindStringSubmatch(trimmed); match != nil { 618 w := match[1] == "write" 619 addr, err := strconv.ParseUint(match[2], 0, 64) 620 if err != nil { 621 return "", fmt.Errorf("failed to parse address on line: %q", strings.TrimSpace(trimmed)) 622 } 623 id, err := strconv.Atoi(match[3]) 624 if err != nil { 625 return "", fmt.Errorf("failed to parse goroutine id on line: %q", strings.TrimSpace(trimmed)) 626 } 627 s.goroutineID = id 628 s.races[s.goroutineID] = &raceOp{write: w, addr: addr, id: id} 629 s.goroutines = append(s.goroutines, &Goroutine{ID: id}) 630 s.state = gotRaceOperationHeader 631 return "", nil 632 } 633 fallthrough 634 635 case betweenRaceGoroutines: 636 if match := reRaceGoroutine.FindStringSubmatch(trimmed); match != nil { 637 id, err := strconv.Atoi(match[1]) 638 if err != nil { 639 return "", fmt.Errorf("failed to parse goroutine id on line: %q", strings.TrimSpace(trimmed)) 640 } 641 found := false 642 for _, g := range s.goroutines { 643 if g.ID == id { 644 g.State = match[2] 645 found = true 646 break 647 } 648 } 649 if !found { 650 return "", fmt.Errorf("unexpected goroutine ID on line: %q", strings.TrimSpace(trimmed)) 651 } 652 s.goroutineID = id 653 s.state = gotRaceGoroutineHeader 654 return "", nil 655 } 656 return "", fmt.Errorf("expected an operator or goroutine, got: %q", trimmed) 657 658 // Race stack traces 659 660 case gotRaceGoroutineFunc: 661 c := s.races[s.goroutineID].create.Calls 662 if found, err := parseFile(&c[len(c)-1], trimmed); err != nil { 663 return "", err 664 } else if !found { 665 return "", fmt.Errorf("expected a file after a race function, got: %q", trimmed) 666 } 667 // TODO(maruel): Set s.goroutines[].CreatedBy. 668 s.state = gotRaceGoroutineFile 669 return "", nil 670 671 case gotRaceGoroutineFile: 672 if trimmed == "" { 673 s.state = betweenRaceGoroutines 674 return "", nil 675 } 676 if trimmed == raceHeaderFooter { 677 // Done. 678 s.state = normal 679 return "", nil 680 } 681 fallthrough 682 683 case gotRaceGoroutineHeader: 684 c := Call{} 685 if found, err := parseFunc(&c, strings.TrimLeft(trimmed, "\t ")); found { 686 // TODO(maruel): Set s.goroutines[].CreatedBy. 687 s.races[s.goroutineID].create.Calls = append(s.races[s.goroutineID].create.Calls, c) 688 s.state = gotRaceGoroutineFunc 689 return "", err 690 } 691 return "", fmt.Errorf("expected a function after a race operation or a race file, got: %q", trimmed) 692 693 default: 694 return "", errors.New("internal error") 695 } 696} 697 698// parseFunc only return an error if also returning a Call. 699// 700// Uses reFunc. 701func parseFunc(c *Call, line string) (bool, error) { 702 if match := reFunc.FindStringSubmatch(line); match != nil { 703 c.Func.Raw = match[1] 704 for _, a := range strings.Split(match[2], ", ") { 705 if a == "..." { 706 c.Args.Elided = true 707 continue 708 } 709 if a == "" { 710 // Remaining values were dropped. 711 break 712 } 713 v, err := strconv.ParseUint(a, 0, 64) 714 if err != nil { 715 return true, fmt.Errorf("failed to parse int on line: %q", strings.TrimSpace(line)) 716 } 717 // Increase performance by always allocating 4 values minimally. 718 if c.Args.Values == nil { 719 c.Args.Values = make([]Arg, 0, 4) 720 } 721 c.Args.Values = append(c.Args.Values, Arg{Value: v}) 722 } 723 return true, nil 724 } 725 return false, nil 726} 727 728// parseFile only return an error if also processing a Call. 729// 730// Uses reFile. 731func parseFile(c *Call, line string) (bool, error) { 732 if match := reFile.FindStringSubmatch(line); match != nil { 733 num, err := strconv.Atoi(match[2]) 734 if err != nil { 735 return true, fmt.Errorf("failed to parse int on line: %q", strings.TrimSpace(line)) 736 } 737 c.SrcPath = match[1] 738 c.Line = num 739 return true, nil 740 } 741 return false, nil 742} 743 744// hasSrcPrefix returns true if any of s is the prefix of p. 745func hasSrcPrefix(p string, s map[string]string) bool { 746 for prefix := range s { 747 if strings.HasPrefix(p, prefix+"/src/") || strings.HasPrefix(p, prefix+"/pkg/mod/") { 748 return true 749 } 750 } 751 return false 752} 753 754// getFiles returns all the source files deduped and ordered. 755func getFiles(goroutines []*Goroutine) []string { 756 files := map[string]struct{}{} 757 for _, g := range goroutines { 758 for _, c := range g.Stack.Calls { 759 files[c.SrcPath] = struct{}{} 760 } 761 } 762 if len(files) == 0 { 763 return nil 764 } 765 out := make([]string, 0, len(files)) 766 for f := range files { 767 out = append(out, f) 768 } 769 sort.Strings(out) 770 return out 771} 772 773// splitPath splits a path using "/" as separator into its components. 774// 775// The first item has its initial path separator kept. 776func splitPath(p string) []string { 777 if p == "" { 778 return nil 779 } 780 var out []string 781 s := "" 782 for _, c := range p { 783 if c != '/' || (len(out) == 0 && strings.Count(s, "/") == len(s)) { 784 s += string(c) 785 } else if s != "" { 786 out = append(out, s) 787 s = "" 788 } 789 } 790 if s != "" { 791 out = append(out, s) 792 } 793 return out 794} 795 796// isFile returns true if the path is a valid file. 797func isFile(p string) bool { 798 // TODO(maruel): Is it faster to open the file or to stat it? Worth a perf 799 // test on Windows. 800 i, err := os.Stat(p) 801 return err == nil && !i.IsDir() 802} 803 804// rootedIn returns a root if the file split in parts is rooted in root. 805// 806// Uses "/" as path separator. 807func rootedIn(root string, parts []string) string { 808 //log.Printf("rootIn(%s, %v)", root, parts) 809 for i := 1; i < len(parts); i++ { 810 suffix := pathJoin(parts[i:]...) 811 if isFile(pathJoin(root, suffix)) { 812 return pathJoin(parts[:i]...) 813 } 814 } 815 return "" 816} 817 818// reModule find the module line in a go.mod file. It works even on CRLF file. 819var reModule = regexp.MustCompile(`(?m)^module\s+([^\n\r]+)\r?$`) 820 821// isGoModule returns the string to the directory containing a go.mod/go.sum 822// files pair, and the go import path it represents, if found. 823func isGoModule(parts []string) (string, string) { 824 for i := len(parts); i > 0; i-- { 825 prefix := pathJoin(parts[:i]...) 826 if isFile(pathJoin(prefix, "go.sum")) { 827 b, err := ioutil.ReadFile(pathJoin(prefix, "go.mod")) 828 if err != nil { 829 continue 830 } 831 if match := reModule.FindSubmatch(b); match != nil { 832 return prefix, string(match[1]) 833 } 834 } 835 } 836 return "", "" 837} 838 839// findRoots sets member GOROOT, GOPATHs and localGomoduleRoot. 840// 841// This causes disk I/O as it checks for file presence. 842// 843// Returns the number of missing files. 844func (c *Context) findRoots() int { 845 c.GOPATHs = map[string]string{} 846 missing := 0 847 for _, f := range getFiles(c.Goroutines) { 848 // TODO(maruel): Could a stack dump have mixed cases? I think it's 849 // possible, need to confirm and handle. 850 //log.Printf(" Analyzing %s", f) 851 852 // First checks skip file I/O. 853 if c.GOROOT != "" && strings.HasPrefix(f, c.GOROOT+"/src/") { 854 // stdlib. 855 continue 856 } 857 if hasSrcPrefix(f, c.GOPATHs) { 858 // $GOPATH/src or go.mod dependency in $GOPATH/pkg/mod. 859 continue 860 } 861 862 // At this point, disk will be looked up. 863 parts := splitPath(f) 864 if c.GOROOT == "" { 865 if r := rootedIn(c.localgoroot+"/src", parts); r != "" { 866 c.GOROOT = r[:len(r)-4] 867 //log.Printf("Found GOROOT=%s", c.GOROOT) 868 continue 869 } 870 } 871 found := false 872 for _, l := range c.localgopaths { 873 if r := rootedIn(l+"/src", parts); r != "" { 874 //log.Printf("Found GOPATH=%s", r[:len(r)-4]) 875 c.GOPATHs[r[:len(r)-4]] = l 876 found = true 877 break 878 } 879 if r := rootedIn(l+"/pkg/mod", parts); r != "" { 880 //log.Printf("Found GOPATH=%s", r[:len(r)-8]) 881 c.GOPATHs[r[:len(r)-8]] = l 882 found = true 883 break 884 } 885 } 886 // If the source is not found, it's probably a go module. 887 if !found { 888 if c.localGomoduleRoot == "" && len(parts) > 1 { 889 // Search upward looking for a go.mod/go.sum pair. 890 c.localGomoduleRoot, c.gomodImportPath = isGoModule(parts[:len(parts)-1]) 891 } 892 if c.localGomoduleRoot != "" && strings.HasPrefix(f, c.localGomoduleRoot+"/") { 893 continue 894 } 895 } 896 if !found { 897 // If the source is not found, just too bad. 898 //log.Printf("Failed to find locally: %s", f) 899 missing++ 900 } 901 } 902 return missing 903} 904 905// getGOPATHs returns parsed GOPATH or its default, using "/" as path separator. 906func getGOPATHs() []string { 907 var out []string 908 if gp := os.Getenv("GOPATH"); gp != "" { 909 for _, v := range filepath.SplitList(gp) { 910 // Disallow non-absolute paths? 911 if v != "" { 912 v = strings.Replace(v, "\\", "/", -1) 913 // Trim trailing "/". 914 if l := len(v); v[l-1] == '/' { 915 v = v[:l-1] 916 } 917 out = append(out, v) 918 } 919 } 920 } 921 if len(out) == 0 { 922 homeDir := "" 923 u, err := user.Current() 924 if err != nil { 925 homeDir = os.Getenv("HOME") 926 if homeDir == "" { 927 panic(fmt.Sprintf("Could not get current user or $HOME: %s\n", err.Error())) 928 } 929 } else { 930 homeDir = u.HomeDir 931 } 932 out = []string{strings.Replace(homeDir+"/go", "\\", "/", -1)} 933 } 934 return out 935} 936