pax_global_header00006660000000000000000000000064144653662400014524gustar00rootroot0000000000000052 comment=3bee0271cca517db06ac79c8d1948a4d71097a27 panicparse-2.3.1/000077500000000000000000000000001446536624000136545ustar00rootroot00000000000000panicparse-2.3.1/.gitattributes000066400000000000000000000000101446536624000165360ustar00rootroot00000000000000* -text panicparse-2.3.1/.github/000077500000000000000000000000001446536624000152145ustar00rootroot00000000000000panicparse-2.3.1/.github/workflows/000077500000000000000000000000001446536624000172515ustar00rootroot00000000000000panicparse-2.3.1/.github/workflows/test.yml000066400000000000000000000245311446536624000207600ustar00rootroot00000000000000# Copyright 2020 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. # References: # https://github.com/actions/checkout # https://github.com/actions/setup-go # https://help.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#using-the-github_token-in-a-workflow # https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions/ # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions # https://docs.github.com/en/rest/commits/comments#create-a-commit-comment # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#pull_request # https://docs.github.com/en/actions/learn-github-actions/contexts on: [push, pull_request] name: Run tests jobs: # Runs go test both with code coverage sent to codecov, race detector and # benchmarks. At the end do a quick check to ensure the tests to not leave # files in the tree. test: name: "test: go${{matrix.gover}}.x/${{matrix.os}}" runs-on: "${{matrix.os}}" continue-on-error: true defaults: run: shell: bash strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] # Do not forget to bump every 6 months! gover: ["1.19"] env: PYTHONDONTWRITEBYTECODE: x steps: - name: Turn off git core.autocrlf if: matrix.os == 'windows-latest' run: git config --global core.autocrlf false - uses: actions/checkout@v3 with: fetch-depth: 2 - uses: actions/setup-go@v3 with: go-version: "~${{matrix.gover}}.0" cache: true - name: 'go install necessary tools' if: always() run: | go install github.com/maruel/pat/cmd/ba@latest - name: 'Check: go test -cover' if: always() run: go test -timeout=120s -covermode=count -coverprofile coverage.txt -bench=. -benchtime=1x ./... # Don't send code coverage if anything failed to reduce spam. - uses: codecov/codecov-action@v2 - name: 'Cleanup' if: always() run: rm coverage.txt - name: 'Check: go test -race' run: go test -timeout=120s -race -bench=. -benchtime=1x ./... - name: 'Check: benchmark 📈' run: ba -against HEAD~1 - name: 'Check: go test -short (CGO_ENABLED=0)' env: CGO_ENABLED: 0 run: go test -timeout=120s -short -bench=. -benchtime=1x ./... - name: 'Check: go test -short (32 bits)' if: matrix.os != 'macos-latest' env: GOARCH: 386 run: go test -timeout=120s -short -bench=. -benchtime=1x ./... - name: "Check: tree is clean" if: always() run: | # Nothing should have changed in the tree up to that point and no # unsuspected file was created. TOUCHED=$(git status --porcelain --ignored) if ! test -z "$TOUCHED"; then echo "Oops, something touched these files, please cleanup:" echo "$TOUCHED" git diff false fi # Run linters. This workflow can be merged with the test_all one if desired # to cut on runtime, at the cost of latency. I dislike waiting for results # so I prefer to run them in parallel. lint: name: "lint: go${{matrix.gover}}.x/${{matrix.os}}" runs-on: "${{matrix.os}}" continue-on-error: true defaults: run: shell: bash strategy: fail-fast: false matrix: # You may want to run only on linux to save on cost. Projects with # OS-specific code benefits from explicitly linting on macOS and # Windows. os: [ubuntu-latest, macos-latest, windows-latest] # Do not forget to bump every 6 months! gover: ["1.19"] env: PYTHONDONTWRITEBYTECODE: x steps: - name: Turn off git core.autocrlf if: matrix.os == 'windows-latest' run: git config --global core.autocrlf false - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: go-version: "~${{matrix.gover}}.0" cache: true - name: "Debug" run: | echo HOME = $HOME echo GITHUB_WORKSPACE = $GITHUB_WORKSPACE echo PATH = $PATH echo "" echo $ ls -l $HOME/go/bin ls -la $HOME/go/bin - name: 'go install necessary tools' if: always() run: | go install github.com/gordonklaus/ineffassign@latest go install github.com/securego/gosec/v2/cmd/gosec@latest go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest go install honnef.co/go/tools/cmd/staticcheck@latest - name: 'go install necessary tools (ubuntu)' if: always() && matrix.os == 'ubuntu-latest' run: | go install github.com/client9/misspell/cmd/misspell@latest go install github.com/google/addlicense@latest - name: 'Check: go vet' if: always() run: go vet -unsafeptr=false ./... - name: 'Check: go vet shadow; shadowed variables' if: always() run: | SHADOW_TOOL="$(which shadow)" if [ -f "${SHADOW_TOOL}.exe" ]; then SHADOW_TOOL="${SHADOW_TOOL}.exe" fi go vet -vettool=$SHADOW_TOOL ./... - name: 'Check: inefficient variable assignment' if: always() run: ineffassign ./... - name: 'Check: staticcheck' if: always() run: staticcheck ./... - name: 'Check: gosec' if: always() run: gosec -fmt=golint -quiet ./... # The following checks are not dependent on the OS or go build tags. Only # run them on ubuntu-latest since it's the fastest one. - name: 'Check: no executable was committed (ubuntu)' if: always() && matrix.os == 'ubuntu-latest' run: | if find . -path '*.sh' -prune -o -path ./.git -prune -o -type f -executable -print | grep -e . ; then echo 'Do not commit executables beside shell scripts' false fi - name: 'Check: addlicense; all sources have a license header (ubuntu)' if: always() && matrix.os == 'ubuntu-latest' run: addlicense -check . - name: 'Check: gofmt; code is well formatted (ubuntu)' if: always() && matrix.os == 'ubuntu-latest' run: | FILES=$(gofmt -s -l .) if ! test -z "$FILES"; then echo 'Please run `gofmt -s -w` on the following files:' >> _gofmt.txt echo "" >> _gofmt.txt for FILE in ${FILES}; do echo "- ${FILE}" >> _gofmt.txt done cat _gofmt.txt echo "## ⚠ gofmt Failed" >> ../_comments.txt echo "" >> ../_comments.txt cat _gofmt.txt >> ../_comments.txt echo "" >> ../_comments.txt false fi - name: "Check: misspelling; code doesn't contain misspelling (ubuntu)" if: always() && matrix.os == 'ubuntu-latest' run: | ERR=$(misspell .) if ! test -z "$ERR"; then echo "$ERR" echo "## ⚠ misspell Failed" >> ../_comments.txt echo "" >> ../_comments.txt echo "$ERR" >> ../_comments.txt echo "" >> ../_comments.txt false fi - name: 'Send comments' if: failure() run: | if [ -f ../_comments.txt ]; then URL="${{github.event.issue.pull_request.url}}" if test -z "$URL"; then URL="${{github.api_url}}/repos/${{github.repository}}/commits/${{github.sha}}/comments" fi echo "Sending $(cat ../_comments.txt|wc -l) lines of comments to ${URL}" curl -sS --request POST \ --header "Authorization: Bearer ${{secrets.GITHUB_TOKEN}}" \ --header "Content-Type: application/json" \ --data "$(cat ../_comments.txt | jq -R --slurp '{body: .}')" \ "${URL}" > /dev/null rm ../_comments.txt fi - name: "Check: go generate doesn't modify files" if: always() run: | go generate ./... # Also test for untracked files. go generate should not generate ignored # files either. TOUCHED=$(git status --porcelain --ignored) if ! test -z "$TOUCHED"; then echo "go generate created these files, please fix:" echo "$TOUCHED" false fi - name: "Check: go mod tidy doesn't modify files" if: always() run: | go mod tidy TOUCHED=$(git status --porcelain --ignored) if ! test -z "$TOUCHED"; then echo "go mod tidy was not clean, please update:" git diff false fi # Ensure tests pass on oldest supported Go version. # Do while inside GOPATH, which tests different parts # of the code. This is panicparse specific and will be removed after 2022. old: name: "test: go${{matrix.gover}}/${{matrix.os}}" runs-on: "${{matrix.os}}" continue-on-error: true defaults: run: shell: bash strategy: fail-fast: false matrix: os: [ubuntu-latest] gover: ['1.13.15', '1.14.7'] env: PYTHONDONTWRITEBYTECODE: x GOPATH: ${{github.workspace}} GO111MODULE: off steps: - name: Turn off git core.autocrlf if: matrix.os == 'windows-latest' run: git config --global core.autocrlf false - uses: actions/checkout@v3 with: path: src/github.com/maruel/panicparse - uses: actions/setup-go@v3 with: go-version: "=${{matrix.gover}}" - name: 'Check: go get -d -t' working-directory: src/github.com/maruel/panicparse run: go get -d -t ./... - name: 'Check: go test' working-directory: src/github.com/maruel/panicparse run: go test -timeout=120s ./... codeql: name: "codeql: go${{matrix.gover}}.x/${{matrix.os}}" runs-on: "${{matrix.os}}" continue-on-error: true strategy: fail-fast: false matrix: os: [ubuntu-latest] # Do not forget to bump every 6 months! gover: ["1.19"] permissions: security-events: write steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: go-version: "~${{matrix.gover}}.0" cache: true - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: go - name: Autobuild uses: github/codeql-action/autobuild@v2 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 panicparse-2.3.1/LICENSE000066400000000000000000000261231446536624000146650ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2015 Marc-Antoine Ruel Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. panicparse-2.3.1/README.md000066400000000000000000000124461446536624000151420ustar00rootroot00000000000000# panicparse Parses panic stack traces, densifies and deduplicates goroutines with similar stack traces. Helps debugging crashes and deadlocks in heavily parallelized process. [![PkgGoDev](https://pkg.go.dev/badge/github.com/maruel/panicparse/v2/stack)](https://pkg.go.dev/github.com/maruel/panicparse/v2/stack) [![codecov](https://codecov.io/gh/maruel/panicparse/branch/main/graph/badge.svg?token=izj1cLjUi3)](https://codecov.io/gh/maruel/panicparse) panicparse helps make sense of Go crash dumps: ![Screencast](https://raw.githubusercontent.com/wiki/maruel/panicparse/parse.gif "Screencast") ## Features * Go 1.18 stack trace support. Requires >=go1.13. * Full go module support. * Race detector support. * HTML export. * High performance parsing. * [webstack.SnapshotHandler](https://pkg.go.dev/github.com/maruel/panicparse/v2/stack/webstack#SnapshotHandler) is a http handler that serves a very tight and swell snapshot of your goroutines, much more readable than [net/http/pprof](https://golang.org/pkg/net/http/pprof). * >50% more compact output than original stack dump yet more readable. * Deduplicates redundant goroutine stacks. Useful for large server crashes. * Arguments as pointer IDs instead of raw pointer values. * Pushes stdlib-only stacks at the bottom to help focus on important code. * Parses the source files if available to augment the output. * Works on Windows. 🪟 ## Installation go install github.com/maruel/panicparse/v2/cmd/pp@latest ## Usage ### Piping a stack trace from another process #### TL;DR * Ubuntu (bash v4 or zsh): `|&` * macOS, [install bash 4+](README.md#updating-bash-on-macos), then: `|&` * Windows _or_ macOS with stock bash v3: `2>&1 |` * [Fish](http://fishshell.com/) shell: `^|` #### Longer version `pp` streams its stdin to stdout as long as it doesn't detect any panic. `panic()` and Go's native deadlock detector [print to stderr](https://golang.org/src/runtime/panic1.go) via the native [`print()` function](https://golang.org/pkg/builtin/#print). **Bash v4** or **zsh**: `|&` tells the shell to redirect stderr to stdout, it's an alias for `2>&1 |` ([bash v4](https://www.gnu.org/software/bash/manual/bash.html#Pipelines), [zsh](http://zsh.sourceforge.net/Doc/Release/Shell-Grammar.html#Simple-Commands-_0026-Pipelines)): go test -v |&pp **Windows or macOS native bash** [(which is 3.2.57)](http://meta.ath0.com/2012/02/05/apples-great-gpl-purge/): They don't have this shortcut, so use the long form: go test -v 2>&1 | pp **Fish**: `&|` redirects stderr and stdout. It's an alias for `2>&1 |` ([fish piping](https://fishshell.com/docs/current/index.html#piping)): go test -v &| pp **PowerShell**: [It has broken `2>&1` redirection](https://connect.microsoft.com/PowerShell/feedback/details/765551/in-powershell-v3-you-cant-redirect-stderr-to-stdout-without-generating-error-records). The workaround is to shell out to cmd.exe. :( ### Investigate deadlock On POSIX, use `Ctrl-\` to send SIGQUIT to your process, `pp` will ignore the signal and will parse the stack trace. ### Parsing from a file To dump to a file then parse, pass the file path of a stack trace go test 2> stack.txt pp stack.txt ## Tips ### Disable inlining Starting with go1.11, the toolchain starts to inline more often. This causes traces to be less informative. Starting with go1.17, optimization also interfere with traces. You can use the following to help diagnosing issues: go install -gcflags '-N -l' path/to/foo foo |& pp or go test -gcflags '-N -l' ./... |& pp Run `go tool compile -help` to get the full list of valid values for -gcflags. ### GOTRACEBACK By default, [`GOTRACEBACK`](https://golang.org/pkg/runtime/) defaults to `single`, which means that a panic will only return the current goroutine trace alone. To get all goroutines trace and not just the crashing one, set the environment variable: export GOTRACEBACK=all or `set GOTRACEBACK=all` on Windows. Probably worth to put it in your `.bashrc`. ### Updating bash on macOS Install bash v4+ on macOS via [homebrew](http://brew.sh) or [macports](https://www.macports.org/). Your future self will appreciate having done that. ### If you have `/usr/bin/pp` installed If you try `pp` for the first time and you get: Creating tables and indexes... Done. and/or /usr/bin/pp5.18: No input files specified you may be running the _Perl PAR Packager_ instead of panicparse. You have two choices, either you put `$GOPATH/bin` at the beginning of `$PATH` or use long name `panicparse` with: go install github.com/maruel/panicparse/v2@latest then using `panicparse` instead of `pp`: go test 2> panicparse Hint: You may also use shell aliases alias gp=panicparse go test 2> gp alias p=panicparse go test 2> p ### webstack in action The [webstack.SnapshotHandler](https://pkg.go.dev/github.com/maruel/panicparse/v2/stack/webstack#SnapshotHandler) http.Handler enables glancing at at a snapshot of your process trivially: ![Screencast](https://raw.githubusercontent.com/wiki/maruel/panicparse/panicparse_webstack.gif "Screencast") ## Authors `panicparse` was created with ❤️️ and passion by [Marc-Antoine Ruel](https://github.com/maruel) and [friends](https://github.com/maruel/panicparse/graphs/contributors). panicparse-2.3.1/cmd/000077500000000000000000000000001446536624000144175ustar00rootroot00000000000000panicparse-2.3.1/cmd/panic/000077500000000000000000000000001446536624000155115ustar00rootroot00000000000000panicparse-2.3.1/cmd/panic/internal/000077500000000000000000000000001446536624000173255ustar00rootroot00000000000000panicparse-2.3.1/cmd/panic/internal/incorrect/000077500000000000000000000000001446536624000213155ustar00rootroot00000000000000panicparse-2.3.1/cmd/panic/internal/incorrect/correct.go000066400000000000000000000005461446536624000233120ustar00rootroot00000000000000// Copyright 2020 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 correct is in directory incorrect. If the call stack is // incorrect.Panic(), you know the parsing failed. package correct // Panic panics. func Panic() { panic(42) } panicparse-2.3.1/cmd/panic/internal/internal.go000066400000000000000000000006071446536624000214730ustar00rootroot00000000000000// Copyright 2017 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 internal is for use for panic. package internal // Callback calls back a function through an external then internal function. func Callback(f func()) { callback(f) } func callback(f func()) { f() } panicparse-2.3.1/cmd/panic/internal/utf8/000077500000000000000000000000001446536624000202135ustar00rootroot00000000000000panicparse-2.3.1/cmd/panic/internal/utf8/utf8.go000066400000000000000000000010531446536624000214270ustar00rootroot00000000000000// Copyright 2020 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 ùtf8 tests a package and function with non-ASCII names. // // The filename itself does not contain unicode as this causes issues // importing panicparse under bazel. For more context, see PR #78. package ùtf8 // Strùct is a totally normal structure with a totally normal name. type Strùct struct { } // Pànic panics. func (s *Strùct) Pànic() { panic(42) } panicparse-2.3.1/cmd/panic/main.go000066400000000000000000000641611446536624000167740ustar00rootroot00000000000000// Copyright 2017 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. // panic crashes in various ways. // // It is a tool to help test pp, it is used in its unit tests. // // To install, run: // // go install github.com/maruel/panicparse/v2/cmd/panic // panic -help // panic str |& pp // // Some panics require the race detector with -race: // // go install -race github.com/maruel/panicparse/v2/cmd/panic // panic race |& pp // // To use with optimization (-N) and inlining (-l) disabled, build with // -gcflags '-N -l' like: // // go install -gcflags '-N -l' github.com/maruel/panicparse/v2/cmd/panic package main // To add a new panic stack signature, add it to types type below, keeping the // list ordered by name. If you need utility functions, add it in the section // below. That's it! import ( "fmt" "io" "os" "os/exec" "runtime" "sort" "strings" "sync" "syscall" "time" "github.com/maruel/panicparse/v2/cmd/panic/internal" correct "github.com/maruel/panicparse/v2/cmd/panic/internal/incorrect" ùtf8 "github.com/maruel/panicparse/v2/cmd/panic/internal/utf8" ) func main() { if len(os.Args) == 2 { switch n := os.Args[1]; n { case "-h", "-help", "--help", "help": usage() os.Exit(0) case "dump_commands": // Undocumented command to do a raw dump of the supported commands. This // is used by unit tests in ../../stack. items := make([]string, 0, len(types)) for n := range types { items = append(items, n) } sort.Strings(items) for _, n := range items { fmt.Printf("%s\n", n) } os.Exit(0) default: if f, ok := types[n]; ok { fmt.Printf("GOTRACEBACK=%s\n", os.Getenv("GOTRACEBACK")) if n == "simple" { // Since the map lookup creates another call stack entry, add a // one-off "simple" panic style to test the very minimal case. // types["simple"].f is never called. panic("simple") } f.f() os.Exit(3) } fmt.Fprintf(stdErr, "unknown panic style %q\n", n) os.Exit(1) } } usage() os.Exit(1) } // Mocked in test. var stdErr io.Writer = os.Stderr // Utility functions. func panicint(i int) { panic(i) } func panicfloat64(f float64) { panic(f) } func panicstr(a string) { panic(a) } func panicslicestr(a []string) { panic(a) } func panicArgsElided(a, b, c, d, e, f, g, h, i, j, k int) { panic(a) } func recurse(i int) { if i > 0 { recurse(i - 1) return } panic(42) } func panicRaceDisabled(name string) { help := "'panic %s' can only be used when built with the race detector.\n" + "To build, use:\n" + " go install -race github.com/maruel/panicparse/v2/cmd/panic\n" fmt.Fprintf(stdErr, help, name) } func rerunWithFastCrash() { if os.Getenv("GORACE") != "log_path=stderr halt_on_error=1" { _ = os.Setenv("GORACE", "log_path=stderr halt_on_error=1") /* #nosec G204 */ c := exec.Command(os.Args[0], os.Args[1:]...) c.Stderr = os.Stderr if err, ok := c.Run().(*exec.ExitError); ok { if status, ok := err.Sys().(syscall.WaitStatus); ok { os.Exit(status.ExitStatus()) } os.Exit(1) } os.Exit(0) } } // panicDoRaceWrite and panicDoRaceRead are extracted from panicRace() to make // the stack trace less trivial, but in general folks will do the error with // this code inlined. func panicDoRaceWrite(x *int) { for i := 0; ; i++ { *x = i } } func panicDoRaceRead(x *int) { for i := 0; ; { i += *x } } func panicRace() { if !raceEnabled { panicRaceDisabled("race") return } rerunWithFastCrash() i := 0 // Do two separate calls so that the 'created at' stacks are different. go func() { panicDoRaceWrite(&i) }() go func() { panicDoRaceRead(&i) }() time.Sleep(time.Minute) } //go:noinline func panicChanStruct(x chan struct{}) { panic("test") } /* TODO(maruel): This is not detected! func panicRaceUnaligned() { if !raceEnabled { panicRaceDisabled("race_unaligned") return } rerunWithFastCrash() a := [8]byte{} b := (*int64)(unsafe.Pointer(&a[0])) go func() { for i := 0; ; i++ { a[4] = byte(i) } }() go func() { for { *b++ } }() time.Sleep(time.Minute) } */ // // types is all the supported types of panics. // // Keep the list sorted. // // TODO(maruel): Figure out a way to reliably trigger "(scan)" output: // - disable automatic GC with runtime.SetGCPercent(-1) // - a goroutine with a large number of items in the stack // - large heap to make the scanning process slow enough // - trigger a manual GC with go runtime.GC() // - panic in the meantime // This would still not be deterministic. // // TODO(maruel): Figure out a way to reliably trigger sleep output. var types = map[string]struct { desc string f func() }{ "args_elided": { "too many args in stack line, causing the call arguments to be elided", func() { panicArgsElided(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) }, }, "chan_receive": { "goroutine blocked on <-c", func() { c := make(chan bool) go func() { <-c <-c }() c <- true panic(42) }, }, "chan_send": { "goroutine blocked on c<-", func() { c := make(chan bool) go func() { c <- true c <- true }() <-c panic(42) }, }, "chan_struct": { "panic with an empty chan struct{} as a parameter", func() { panicChanStruct(nil) }, }, "float": { "panic(4.2)", func() { panicfloat64(4.2) }, }, "goroutine_1": { "panic in one goroutine", func() { go func() { panicint(42) }() time.Sleep(time.Minute) }, }, "goroutine_100": { "start 100 goroutines before panicking", func() { var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func() { wg.Done() time.Sleep(time.Minute) }() } wg.Wait() panicint(42) }, }, "goroutine_dedupe_pointers": { "start 100 goroutines with different pointers before panicking", func() { var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func(b *int) { wg.Done() time.Sleep(time.Minute) }(new(int)) } wg.Wait() panicint(42) }, }, "int": { "panic(42)", func() { panicint(42) }, }, "locked": { "thread locked goroutine via runtime.LockOSThread()", func() { runtime.LockOSThread() panic(42) }, }, "other": { "panics with other package in the call stack, with both exported and unexpected functions", func() { internal.Callback(func() { panic("allo") }) }, }, "asleep": { "panics with 'all goroutines are asleep - deadlock'", func() { // When built with the race detector, this hangs. I suspect this is due // because the race detector starts a separate goroutine which causes // checkdead() to not trigger. See checkdead() in src/runtime/proc.go. // https://github.com/golang/go/issues/20588 // // Repro: // go install -race github.com/maruel/panicparse/v2/cmd/panic; panic asleep var mu sync.Mutex mu.Lock() mu.Lock() }, }, "race": { "cause a crash by race detector", panicRace, }, /* TODO(maruel): This is not detected! "race_unaligned": { "cause a crash by race detector with unaligned access", panicRaceUnaligned, }, */ "stack_cut_off": { "recursive calls with too many call lines in traceback, causing higher up calls to missing", func() { // Observed limit is 99. // src/runtime/runtime2.go:const _TracebackMaxFrames = 100 recurse(100) }, }, "stack_cut_off_named": { "named calls with too many call lines in traceback, causing higher up calls to missing", func() { // The tool has difficulty with very deep static calls during linking. // See https://github.com/golang/go/issues/51814 recurse497() }, }, "simple": { // This is not used for real, here for documentation. "skip the map for a shorter stack trace", func() {}, }, "slice_str": { "panic([]string{\"allo\"}) with cap=2", func() { a := make([]string, 1, 2) a[0] = "allo" panicslicestr(a) }, }, "stdlib": { "panics with stdlib in the call stack, with both exported and unexpected functions", func() { strings.FieldsFunc("a", func(rune) bool { panic("allo") }) }, }, "stdlib_and_other": { "panics with both other and stdlib packages in the call stack", func() { strings.FieldsFunc("a", func(rune) bool { internal.Callback(func() { panic("allo") }) return false }) }, }, "str": { "panic(\"allo\")", func() { panicstr("allo") }, }, "mismatched": { "mismatched package and directory names", func() { correct.Panic() }, }, "utf8": { "non-ascii package, struct and method names", func() { s := ùtf8.Strùct{} s.Pànic() }, }, } func usage() { t := `usage: panic This tool is meant to be used with pp to test different parsing scenarios and ensure output on different version of the Go toolchain can be successfully parsed. Set GOTRACEBACK before running this tool to see how it affects the panic output. Built with: ` + runtime.Version() + ` Select the way to panic: ` _, _ = io.WriteString(stdErr, t) names := make([]string, 0, len(types)) m := 0 for n := range types { names = append(names, n) if i := len(n); i > m { m = i } } sort.Strings(names) for _, n := range names { fmt.Fprintf(stdErr, "- %-*s %s\n", m, n, types[n].desc) } } // func recurse00() { panic("the end") } func recurse01() { recurse00() } func recurse02() { recurse01() } func recurse03() { recurse02() } func recurse04() { recurse03() } func recurse05() { recurse04() } func recurse06() { recurse05() } func recurse07() { recurse06() } func recurse08() { recurse07() } func recurse09() { recurse08() } func recurse10() { recurse09() } func recurse11() { recurse10() } func recurse12() { recurse11() } func recurse13() { recurse12() } func recurse14() { recurse13() } func recurse15() { recurse14() } func recurse16() { recurse15() } func recurse17() { recurse16() } func recurse18() { recurse17() } func recurse19() { recurse18() } func recurse20() { recurse19() } func recurse21() { recurse20() } func recurse22() { recurse21() } func recurse23() { recurse22() } func recurse24() { recurse23() } func recurse25() { recurse24() } func recurse26() { recurse25() } func recurse27() { recurse26() } func recurse28() { recurse27() } func recurse29() { recurse28() } func recurse30() { recurse29() } func recurse31() { recurse30() } func recurse32() { recurse31() } func recurse33() { recurse32() } func recurse34() { recurse33() } func recurse35() { recurse34() } func recurse36() { recurse35() } func recurse37() { recurse36() } func recurse38() { recurse37() } func recurse39() { recurse38() } func recurse40() { recurse39() } func recurse41() { recurse40() } func recurse42() { recurse41() } func recurse43() { recurse42() } func recurse44() { recurse43() } func recurse45() { recurse44() } func recurse46() { recurse45() } func recurse47() { recurse46() } func recurse48() { recurse47() } func recurse49() { recurse48() } func recurse50() { recurse49() } func recurse51() { recurse50() } func recurse52() { recurse51() } func recurse53() { recurse52() } func recurse54() { recurse53() } func recurse55() { recurse54() } func recurse56() { recurse55() } func recurse57() { recurse56() } func recurse58() { recurse57() } func recurse59() { recurse58() } func recurse60() { recurse59() } func recurse61() { recurse60() } func recurse62() { recurse61() } func recurse63() { recurse62() } func recurse64() { recurse63() } func recurse65() { recurse64() } func recurse66() { recurse65() } func recurse67() { recurse66() } func recurse68() { recurse67() } func recurse69() { recurse68() } func recurse70() { recurse69() } func recurse71() { recurse70() } func recurse72() { recurse71() } func recurse73() { recurse72() } func recurse74() { recurse73() } func recurse75() { recurse74() } func recurse76() { recurse75() } func recurse77() { recurse76() } func recurse78() { recurse77() } func recurse79() { recurse78() } func recurse80() { recurse79() } func recurse81() { recurse80() } func recurse82() { recurse81() } func recurse83() { recurse82() } func recurse84() { recurse83() } func recurse85() { recurse84() } func recurse86() { recurse85() } func recurse87() { recurse86() } func recurse88() { recurse87() } func recurse89() { recurse88() } func recurse90() { recurse89() } func recurse91() { recurse90() } func recurse92() { recurse91() } func recurse93() { recurse92() } func recurse94() { recurse93() } func recurse95() { recurse94() } func recurse96() { recurse95() } func recurse97() { recurse96() } func recurse98() { recurse97() } func recurse99() { recurse98() } func recurse100() { recurse99() } func recurse101() { recurse100() } func recurse102() { recurse101() } func recurse103() { recurse102() } func recurse104() { recurse103() } func recurse105() { recurse104() } func recurse106() { recurse105() } func recurse107() { recurse106() } func recurse108() { recurse107() } func recurse109() { recurse108() } func recurse110() { recurse109() } func recurse111() { recurse110() } func recurse112() { recurse111() } func recurse113() { recurse112() } func recurse114() { recurse113() } func recurse115() { recurse114() } func recurse116() { recurse115() } func recurse117() { recurse116() } func recurse118() { recurse117() } func recurse119() { recurse118() } func recurse120() { recurse119() } func recurse121() { recurse120() } func recurse122() { recurse121() } func recurse123() { recurse122() } func recurse124() { recurse123() } func recurse125() { recurse124() } func recurse126() { recurse125() } func recurse127() { recurse126() } func recurse128() { recurse127() } func recurse129() { recurse128() } func recurse130() { recurse129() } func recurse131() { recurse130() } func recurse132() { recurse131() } func recurse133() { recurse132() } func recurse134() { recurse133() } func recurse135() { recurse134() } func recurse136() { recurse135() } func recurse137() { recurse136() } func recurse138() { recurse137() } func recurse139() { recurse138() } func recurse140() { recurse139() } func recurse141() { recurse140() } func recurse142() { recurse141() } func recurse143() { recurse142() } func recurse144() { recurse143() } func recurse145() { recurse144() } func recurse146() { recurse145() } func recurse147() { recurse146() } func recurse148() { recurse147() } func recurse149() { recurse148() } func recurse150() { recurse149() } func recurse151() { recurse150() } func recurse152() { recurse151() } func recurse153() { recurse152() } func recurse154() { recurse153() } func recurse155() { recurse154() } func recurse156() { recurse155() } func recurse157() { recurse156() } func recurse158() { recurse157() } func recurse159() { recurse158() } func recurse160() { recurse159() } func recurse161() { recurse160() } func recurse162() { recurse161() } func recurse163() { recurse162() } func recurse164() { recurse163() } func recurse165() { recurse164() } func recurse166() { recurse165() } func recurse167() { recurse166() } func recurse168() { recurse167() } func recurse169() { recurse168() } func recurse170() { recurse169() } func recurse171() { recurse170() } func recurse172() { recurse171() } func recurse173() { recurse172() } func recurse174() { recurse173() } func recurse175() { recurse174() } func recurse176() { recurse175() } func recurse177() { recurse176() } func recurse178() { recurse177() } func recurse179() { recurse178() } func recurse180() { recurse179() } func recurse181() { recurse180() } func recurse182() { recurse181() } func recurse183() { recurse182() } func recurse184() { recurse183() } func recurse185() { recurse184() } func recurse186() { recurse185() } func recurse187() { recurse186() } func recurse188() { recurse187() } func recurse189() { recurse188() } func recurse190() { recurse189() } func recurse191() { recurse190() } func recurse192() { recurse191() } func recurse193() { recurse192() } func recurse194() { recurse193() } func recurse195() { recurse194() } func recurse196() { recurse195() } func recurse197() { recurse196() } func recurse198() { recurse197() } func recurse199() { recurse198() } func recurse200() { recurse199() } func recurse201() { recurse200() } func recurse202() { recurse201() } func recurse203() { recurse202() } func recurse204() { recurse203() } func recurse205() { recurse204() } func recurse206() { recurse205() } func recurse207() { recurse206() } func recurse208() { recurse207() } func recurse209() { recurse208() } func recurse210() { recurse209() } func recurse211() { recurse210() } func recurse212() { recurse211() } func recurse213() { recurse212() } func recurse214() { recurse213() } func recurse215() { recurse214() } func recurse216() { recurse215() } func recurse217() { recurse216() } func recurse218() { recurse217() } func recurse219() { recurse218() } func recurse220() { recurse219() } func recurse221() { recurse220() } func recurse222() { recurse221() } func recurse223() { recurse222() } func recurse224() { recurse223() } func recurse225() { recurse224() } func recurse226() { recurse225() } func recurse227() { recurse226() } func recurse228() { recurse227() } func recurse229() { recurse228() } func recurse230() { recurse229() } func recurse231() { recurse230() } func recurse232() { recurse231() } func recurse233() { recurse232() } func recurse234() { recurse233() } func recurse235() { recurse234() } func recurse236() { recurse235() } func recurse237() { recurse236() } func recurse238() { recurse237() } func recurse239() { recurse238() } func recurse240() { recurse239() } func recurse241() { recurse240() } func recurse242() { recurse241() } func recurse243() { recurse242() } func recurse244() { recurse243() } func recurse245() { recurse244() } func recurse246() { recurse245() } func recurse247() { recurse246() } func recurse248() { recurse247() } func recurse249() { recurse248() } func recurse250() { recurse249() } func recurse251() { recurse250() } func recurse252() { recurse251() } func recurse253() { recurse252() } func recurse254() { recurse253() } func recurse255() { recurse254() } func recurse256() { recurse255() } func recurse257() { recurse256() } func recurse258() { recurse257() } func recurse259() { recurse258() } func recurse260() { recurse259() } func recurse261() { recurse260() } func recurse262() { recurse261() } func recurse263() { recurse262() } func recurse264() { recurse263() } func recurse265() { recurse264() } func recurse266() { recurse265() } func recurse267() { recurse266() } func recurse268() { recurse267() } func recurse269() { recurse268() } func recurse270() { recurse269() } func recurse271() { recurse270() } func recurse272() { recurse271() } func recurse273() { recurse272() } func recurse274() { recurse273() } func recurse275() { recurse274() } func recurse276() { recurse275() } func recurse277() { recurse276() } func recurse278() { recurse277() } func recurse279() { recurse278() } func recurse280() { recurse279() } func recurse281() { recurse280() } func recurse282() { recurse281() } func recurse283() { recurse282() } func recurse284() { recurse283() } func recurse285() { recurse284() } func recurse286() { recurse285() } func recurse287() { recurse286() } func recurse288() { recurse287() } func recurse289() { recurse288() } func recurse290() { recurse289() } func recurse291() { recurse290() } func recurse292() { recurse291() } func recurse293() { recurse292() } func recurse294() { recurse293() } func recurse295() { recurse294() } func recurse296() { recurse295() } func recurse297() { recurse296() } func recurse298() { recurse297() } func recurse299() { recurse298() } func recurse300() { recurse299() } func recurse301() { recurse300() } func recurse302() { recurse301() } func recurse303() { recurse302() } func recurse304() { recurse303() } func recurse305() { recurse304() } func recurse306() { recurse305() } func recurse307() { recurse306() } func recurse308() { recurse307() } func recurse309() { recurse308() } func recurse310() { recurse309() } func recurse311() { recurse310() } func recurse312() { recurse311() } func recurse313() { recurse312() } func recurse314() { recurse313() } func recurse315() { recurse314() } func recurse316() { recurse315() } func recurse317() { recurse316() } func recurse318() { recurse317() } func recurse319() { recurse318() } func recurse320() { recurse319() } func recurse321() { recurse320() } func recurse322() { recurse321() } func recurse323() { recurse322() } func recurse324() { recurse323() } func recurse325() { recurse324() } func recurse326() { recurse325() } func recurse327() { recurse326() } func recurse328() { recurse327() } func recurse329() { recurse328() } func recurse330() { recurse329() } func recurse331() { recurse330() } func recurse332() { recurse331() } func recurse333() { recurse332() } func recurse334() { recurse333() } func recurse335() { recurse334() } func recurse336() { recurse335() } func recurse337() { recurse336() } func recurse338() { recurse337() } func recurse339() { recurse338() } func recurse340() { recurse339() } func recurse341() { recurse340() } func recurse342() { recurse341() } func recurse343() { recurse342() } func recurse344() { recurse343() } func recurse345() { recurse344() } func recurse346() { recurse345() } func recurse347() { recurse346() } func recurse348() { recurse347() } func recurse349() { recurse348() } func recurse350() { recurse349() } func recurse351() { recurse350() } func recurse352() { recurse351() } func recurse353() { recurse352() } func recurse354() { recurse353() } func recurse355() { recurse354() } func recurse356() { recurse355() } func recurse357() { recurse356() } func recurse358() { recurse357() } func recurse359() { recurse358() } func recurse360() { recurse359() } func recurse361() { recurse360() } func recurse362() { recurse361() } func recurse363() { recurse362() } func recurse364() { recurse363() } func recurse365() { recurse364() } func recurse366() { recurse365() } func recurse367() { recurse366() } func recurse368() { recurse367() } func recurse369() { recurse368() } func recurse370() { recurse369() } func recurse371() { recurse370() } func recurse372() { recurse371() } func recurse373() { recurse372() } func recurse374() { recurse373() } func recurse375() { recurse374() } func recurse376() { recurse375() } func recurse377() { recurse376() } func recurse378() { recurse377() } func recurse379() { recurse378() } func recurse380() { recurse379() } func recurse381() { recurse380() } func recurse382() { recurse381() } func recurse383() { recurse382() } func recurse384() { recurse383() } func recurse385() { recurse384() } func recurse386() { recurse385() } func recurse387() { recurse386() } func recurse388() { recurse387() } func recurse389() { recurse388() } func recurse390() { recurse389() } func recurse391() { recurse390() } func recurse392() { recurse391() } func recurse393() { recurse392() } func recurse394() { recurse393() } func recurse395() { recurse394() } func recurse396() { recurse395() } func recurse397() { recurse396() } func recurse398() { recurse397() } func recurse399() { recurse398() } func recurse400() { recurse399() } func recurse401() { recurse400() } func recurse402() { recurse401() } func recurse403() { recurse402() } func recurse404() { recurse403() } func recurse405() { recurse404() } func recurse406() { recurse405() } func recurse407() { recurse406() } func recurse408() { recurse407() } func recurse409() { recurse408() } func recurse410() { recurse409() } func recurse411() { recurse410() } func recurse412() { recurse411() } func recurse413() { recurse412() } func recurse414() { recurse413() } func recurse415() { recurse414() } func recurse416() { recurse415() } func recurse417() { recurse416() } func recurse418() { recurse417() } func recurse419() { recurse418() } func recurse420() { recurse419() } func recurse421() { recurse420() } func recurse422() { recurse421() } func recurse423() { recurse422() } func recurse424() { recurse423() } func recurse425() { recurse424() } func recurse426() { recurse425() } func recurse427() { recurse426() } func recurse428() { recurse427() } func recurse429() { recurse428() } func recurse430() { recurse429() } func recurse431() { recurse430() } func recurse432() { recurse431() } func recurse433() { recurse432() } func recurse434() { recurse433() } func recurse435() { recurse434() } func recurse436() { recurse435() } func recurse437() { recurse436() } func recurse438() { recurse437() } func recurse439() { recurse438() } func recurse440() { recurse439() } func recurse441() { recurse440() } func recurse442() { recurse441() } func recurse443() { recurse442() } func recurse444() { recurse443() } func recurse445() { recurse444() } func recurse446() { recurse445() } func recurse447() { recurse446() } func recurse448() { recurse447() } func recurse449() { recurse448() } func recurse450() { recurse449() } func recurse451() { recurse450() } func recurse452() { recurse451() } func recurse453() { recurse452() } func recurse454() { recurse453() } func recurse455() { recurse454() } func recurse456() { recurse455() } func recurse457() { recurse456() } func recurse458() { recurse457() } func recurse459() { recurse458() } func recurse460() { recurse459() } func recurse461() { recurse460() } func recurse462() { recurse461() } func recurse463() { recurse462() } func recurse464() { recurse463() } func recurse465() { recurse464() } func recurse466() { recurse465() } func recurse467() { recurse466() } func recurse468() { recurse467() } func recurse469() { recurse468() } func recurse470() { recurse469() } func recurse471() { recurse470() } func recurse472() { recurse471() } func recurse473() { recurse472() } func recurse474() { recurse473() } func recurse475() { recurse474() } func recurse476() { recurse475() } func recurse477() { recurse476() } func recurse478() { recurse477() } func recurse479() { recurse478() } func recurse480() { recurse479() } func recurse481() { recurse480() } func recurse482() { recurse481() } func recurse483() { recurse482() } func recurse484() { recurse483() } func recurse485() { recurse484() } func recurse486() { recurse485() } func recurse487() { recurse486() } func recurse488() { recurse487() } func recurse489() { recurse488() } func recurse490() { recurse489() } func recurse491() { recurse490() } func recurse492() { recurse491() } func recurse493() { recurse492() } func recurse494() { recurse493() } func recurse495() { recurse494() } func recurse496() { recurse495() } func recurse497() { recurse496() } panicparse-2.3.1/cmd/panic/main_no_race.go000066400000000000000000000003741446536624000204560ustar00rootroot00000000000000// Copyright 2017 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:build !race // +build !race package main const raceEnabled = false panicparse-2.3.1/cmd/panic/main_race.go000066400000000000000000000003711446536624000177570ustar00rootroot00000000000000// Copyright 2017 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:build race // +build race package main const raceEnabled = true panicparse-2.3.1/cmd/panic/main_test.go000066400000000000000000000020151446536624000200210ustar00rootroot00000000000000// Copyright 2017 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 main import ( "io/ioutil" "os" "testing" ) func TestMain(t *testing.T) { t.Parallel() if !testing.Verbose() { stdErr = ioutil.Discard defer func() { stdErr = os.Stderr }() } for name, l := range types { if name == "simple" { // It's safe. l.f() continue } if name == "goroutine_1" || name == "asleep" { // goroutine_1 panics in a separate goroutine, so it's tricky to catch. // asleep will just hang because there are other goroutine. continue } if name == "race" { if raceEnabled { // It's not safe, it'll crash the program in a way that cannot be trapped. continue } // It's safe. l.f() continue } l := l t.Run(name, func(t *testing.T) { t.Parallel() defer func() { if err := recover(); err == nil { t.Fatal("expected error") } }() l.f() }) } } panicparse-2.3.1/cmd/panicweb/000077500000000000000000000000001446536624000162075ustar00rootroot00000000000000panicparse-2.3.1/cmd/panicweb/internal/000077500000000000000000000000001446536624000200235ustar00rootroot00000000000000panicparse-2.3.1/cmd/panicweb/internal/internal.go000066400000000000000000000030261446536624000221670ustar00rootroot00000000000000// Copyright 2020 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 internal implements the handlers for panicweb so they are in a // separate package than "main". package internal import ( "io/ioutil" "log" "net/http" ) // Unblock unblocks one http server handler. var Unblock = make(chan struct{}) // GetAsync does an HTTP GET to the URL but leaves the actual fetching to a // goroutine. func GetAsync(url string) { /* #nosec G107 */ resp, err := http.Get(url) if err != nil { log.Fatalf("get %s: %v", url, err) } go func() { _, err := ioutil.ReadAll(resp.Body) if err != nil { log.Fatalf("failed to read: %v", err) } _ = resp.Body.Close() log.Fatal("the goal is to not complete this request") }() } // URL1Handler is a http.HandlerFunc that hangs. func URL1Handler(w http.ResponseWriter, req *http.Request) { // Respond the HTTP header to unblock the http.Get() function. w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Length", "100000") w.WriteHeader(200) b := [4096]byte{} _, _ = w.Write(b[:]) <-Unblock } // URL2Handler is a http.HandlerFunc that hangs. func URL2Handler(w http.ResponseWriter, req *http.Request) { // Respond the HTTP header to unblock the http.Get() function. w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Length", "100000") w.WriteHeader(200) b := [4096]byte{} _, _ = w.Write(b[:]) <-Unblock } panicparse-2.3.1/cmd/panicweb/main.go000066400000000000000000000105441446536624000174660ustar00rootroot00000000000000// Copyright 2020 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. // panicweb implements a simulation of a web server that panics. // // It starts a web server, a few handlers and a few hanging clients, then // panics. // // It loads both panicparse's http handler and pprof's one for comparison. // // It is separate from the panic tool because importing "net/http" creates a // background thread, which breaks the "asleep" panic case in tool panic. package main import ( "flag" "fmt" "log" "net" "net/http" /* #nosec G108 */ _ "net/http/pprof" "os" "runtime" "strings" "sync" "time" "github.com/maruel/panicparse/v2/cmd/panicweb/internal" "github.com/maruel/panicparse/v2/stack/webstack" "github.com/mattn/go-colorable" ) var rootPage = []byte(` `) func main() { allowremote := flag.Bool("allowremote", false, "allows access from non-localhost; implies -wait") sleep := flag.Bool("wait", false, "sleep instead of crashing") port := flag.Int("port", 0, "specify a port number, defaults to a ephemeral port; implies -wait") limit := flag.Bool("limit", false, "throttle, port limit") flag.Parse() if *port != 0 || *allowremote { *sleep = true } addr := fmt.Sprintf(":%d", *port) if !*allowremote { addr = "localhost" + addr } ln, err := net.Listen("tcp", addr) if err != nil { log.Fatalf("Failed to listen on localhost: %v", err) } http.HandleFunc("/url1", internal.URL1Handler) http.HandleFunc("/url2", internal.URL2Handler) if *limit { // This is similar to ExampleSnapshotHandler_complex in stack/webstack, // albeit form values are not altered. const delay = time.Second mu := sync.Mutex{} var last time.Time http.HandleFunc("/panicparse", func(w http.ResponseWriter, req *http.Request) { // Only allow requests from localhost or in the 100.64.x.x/10 IPv4 range // (e.g. Tailscale). ok := false if i := strings.LastIndexByte(req.RemoteAddr, ':'); i != -1 { switch ip := req.RemoteAddr[:i]; ip { case "localhost", "127.0.0.1", "[::1]", "::1": ok = true default: p := net.ParseIP(ip).To4() ok = p != nil && p[0] == 100 && p[1] >= 64 && p[1] < 128 } } log.Printf("- %s: %t", req.RemoteAddr, ok) if !ok { http.Error(w, "forbidden", http.StatusForbidden) return } // Serialize the handler. mu.Lock() defer mu.Unlock() // Throttle requests. if time.Since(last) < delay { http.Error(w, "retry later", http.StatusTooManyRequests) return } webstack.SnapshotHandler(w, req) last = time.Now() }) } else { http.HandleFunc("/panicparse", webstack.SnapshotHandler) } http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write(rootPage) }) srv := &http.Server{ Handler: http.DefaultServeMux, ReadHeaderTimeout: 2 * time.Second, } go srv.Serve(ln) // Start many clients. a := ln.Addr() url := fmt.Sprintf("http://%s/", a) if *allowremote { if h, err := os.Hostname(); err == nil { if t, ok := a.(*net.TCPAddr); ok { url = fmt.Sprintf("http://%s:%d/", h, t.Port) } } } for i := 0; i < 10; i++ { internal.GetAsync(url + "url1") } for i := 0; i < 3; i++ { internal.GetAsync(url + "url2") } // Try to get something hung in package golang.org/x/unix. wait := make(chan struct{}) go func() { wait <- struct{}{} sysHang() }() <-wait // It's convoluted but colorable is the only go module used by panicparse // that is both versioned and can be hacked to call back user code. w := writeHang{hung: make(chan struct{}), unblock: make(chan struct{})} v := colorable.NewNonColorable(&w) go v.Write([]byte("foo bar")) <-w.hung if *sleep { fmt.Printf("Compare:\n- %spanicparse\n- %sdebug/pprof/goroutine?debug=2\n", url, url) <-make(chan struct{}) } else { panic("Here's a snapshot of a normal web server.") } } type writeHang struct { hung chan struct{} unblock chan struct{} } func (w *writeHang) Write(b []byte) (int, error) { runtime.LockOSThread() w.hung <- struct{}{} <-w.unblock return 0, nil } panicparse-2.3.1/cmd/panicweb/main_others.go000066400000000000000000000006041446536624000210460ustar00rootroot00000000000000// Copyright 2020 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:build !aix && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows // +build !aix,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows package main func sysHang() { } panicparse-2.3.1/cmd/panicweb/main_unix.go000066400000000000000000000007201446536624000205240ustar00rootroot00000000000000// Copyright 2020 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:build aix || dragonfly || freebsd || linux || netbsd || openbsd || solaris // +build aix dragonfly freebsd linux netbsd openbsd solaris package main import "golang.org/x/sys/unix" func sysHang() { _ = unix.Nanosleep(&unix.Timespec{Sec: 366 * 24 * 60 * 60}, &unix.Timespec{}) } panicparse-2.3.1/cmd/panicweb/main_windows.go000066400000000000000000000005461446536624000212410ustar00rootroot00000000000000// Copyright 2020 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:build windows // +build windows package main import "golang.org/x/sys/windows" func sysHang() { // 49.7 days is enough for everyone. windows.SleepEx(0xFFFFFFFF, false) } panicparse-2.3.1/cmd/pp/000077500000000000000000000000001446536624000150365ustar00rootroot00000000000000panicparse-2.3.1/cmd/pp/main.go000066400000000000000000000011141446536624000163060ustar00rootroot00000000000000// Copyright 2015 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. // pp: analyzes stack dump of Go processes and simplifies it. // // It is mostly useful on servers will large number of identical goroutines, // making the crash dump harder to read than strictly necessary. package main import ( "fmt" "os" "github.com/maruel/panicparse/v2/internal" ) func main() { if err := internal.Main(); err != nil { fmt.Fprintf(os.Stderr, "Failed: %s\n", err) os.Exit(1) } } panicparse-2.3.1/codecov.yml000066400000000000000000000007621446536624000160260ustar00rootroot00000000000000# Copyright 2020 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. # https://docs.codecov.io/docs/codecovyml-reference # and # https://docs.codecov.io/docs/coverage-configuration coverage: precision: 1 range: "40...80" round: nearest status: patch: default: target: 60% threshold: 10% project: default: target: 60% threshold: 10% panicparse-2.3.1/go.mod000066400000000000000000000004301446536624000147570ustar00rootroot00000000000000module github.com/maruel/panicparse/v2 go 1.13 require ( github.com/google/go-cmp v0.5.7 github.com/mattn/go-colorable v0.1.12 github.com/mattn/go-isatty v0.0.14 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f ) panicparse-2.3.1/go.sum000066400000000000000000000025341446536624000150130ustar00rootroot00000000000000github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw= golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= panicparse-2.3.1/internal/000077500000000000000000000000001446536624000154705ustar00rootroot00000000000000panicparse-2.3.1/internal/internaltest/000077500000000000000000000000001446536624000202045ustar00rootroot00000000000000panicparse-2.3.1/internal/internaltest/internaltest.go000066400000000000000000000142421446536624000232520ustar00rootroot00000000000000// Copyright 2020 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 internaltest import ( "errors" "fmt" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" "sync" ) // PanicwebOutput returns the output of panicweb with inlining disabled. // // The function panics if any internal error occurs. func PanicwebOutput() []byte { panicwebOnce.Do(func() { p := build("panicweb", false) if p == "" { panic("building panicweb failed") } defer func() { if err := os.Remove(p); err != nil { panic(err) } }() panicwebOutput = execRun(p) }) out := make([]byte, len(panicwebOutput)) copy(out, panicwebOutput) return out } // PanicOutputs returns a map of the output of every subcommands. // // panic is built with inlining disabled. // // The subcommand "race" is built with the race detector. Others are built // without. In particular "asleep" doesn't work with the race detector. // // The function panics if any internal error occurs. func PanicOutputs() map[string][]byte { panicOutputsOnce.Do(func() { // Extracts the subcommands, then run each of them individually. pplain := build("panic", false) if pplain == "" { // The odd of this failing is close to nil. panic("building panic failed") } defer func() { if err := os.Remove(pplain); err != nil { panic(err) } }() prace := build("panic", true) if prace == "" { // Race detector is not supported on this platform. } else { defer func() { if err := os.Remove(prace); err != nil { panic(err) } }() } // Collect the subcommands. cmds := strings.Split(strings.TrimSpace(string(execRun(pplain, "dump_commands"))), "\n") if len(cmds) == 0 { panic("no command retrieved") } // Collect the output of each subcommand. panicOutputs = map[string][]byte{} for _, cmd := range cmds { cmd = strings.TrimSpace(cmd) p := pplain if cmd == "race" { if prace == "" { // Race detector is not supported. continue } p = prace } if panicOutputs[cmd] = execRun(p, cmd); len(panicOutputs[cmd]) == 0 { panic(fmt.Sprintf("no output for %s", cmd)) } } }) out := make(map[string][]byte, len(panicOutputs)) for k, v := range panicOutputs { w := make([]byte, len(v)) copy(w, v) out[k] = w } return out } // StaticPanicwebOutput returns a constant version of panicweb output for use // in benchmarks. func StaticPanicwebOutput() []byte { return []byte(staticPanicweb) } // StaticPanicRaceOutput returns a constant version of 'panic race' output. func StaticPanicRaceOutput() []byte { return []byte(staticPanicRace) } // IsUsingModules is best guess to know if go module are enabled. // // Panics if an internal error occurs. // // It reads the current value of GO111MODULES. func IsUsingModules() bool { // Calculate the default. We assume developer builds are recent (go1.14 and // later). ver := GetGoMinorVersion() if ver > 0 && ver < 11 { // go1.9.7+ and go1.10.3+ were fixed to tolerate semantic versioning import // but they do not support the environment variable. return false } def := (ver == 0 || ver >= 14) s := os.Getenv("GO111MODULE") return (def && (s == "auto" || s == "")) || s == "on" } // var ( panicwebOnce sync.Once panicwebOutput []byte panicOutputsOnce sync.Once panicOutputs map[string][]byte ) // GetGoMinorVersion returns the Go1 minor version. // // Returns 0 for a developer build, panics if can't parse the version. // // Ignores the revision (go1..). func GetGoMinorVersion() int { ver := runtime.Version() if strings.HasPrefix(ver, "devel ") { return 0 } if !strings.HasPrefix(ver, "go1.") { // This will break on go2. Please submit a PR to fix this once Go2 is // released. panic(fmt.Sprintf("unexpected go version %q", ver)) } v := ver[4:] if i := strings.IndexByte(v, '.'); i != -1 { v = v[:i] } else if i := strings.Index(v, "beta"); i != -1 { v = v[:i] } else if i := strings.Index(v, "rc"); i != -1 { v = v[:i] } m, err := strconv.Atoi(v) if err != nil { panic(fmt.Sprintf("failed to parse %q: %v", ver, err)) } return m } // build creates a temporary file and returns the path to it. func build(tool string, race bool) string { p := filepath.Join(os.TempDir(), tool) if race { p += "_race" } // Starting with go1.11, ioutil.TempFile() supports specifying a suffix. This // is necessary to set the ".exe" suffix on Windows. Until we drop support // for go1.10 and earlier, do the equivalent ourselves in an lousy way. p += fmt.Sprintf("_%d", os.Getpid()) if runtime.GOOS == "windows" { p += ".exe" } path := "github.com/maruel/panicparse/cmd/" if IsUsingModules() { path = "github.com/maruel/panicparse/v2/cmd/" } if err := Compile(path+tool, p, "", true, race); err != nil { _, _ = os.Stderr.WriteString(err.Error()) return "" } return p } var errNoRace = errors.New("platform does not support -race") // Compile compiles sources into an executable. func Compile(in, exe, cwd string, disableInlining, race bool) error { // Disable optimization (-N) and inlining (-l) otherwise the inlining varies // between local execution and remote execution. This can be observed as // Elided being true without any argument. args := []string{"build", "-o", exe} if disableInlining { args = append(args, "-gcflags", "-N -l") } if race { args = append(args, "-race") } /* #nosec G204 */ c := exec.Command("go", append(args, in)...) c.Dir = cwd if out, err := c.CombinedOutput(); err != nil { if race { s := string(out) const e1 = "go test: -race is only supported on " const e2 = "go build: -race is only supported on " if strings.HasPrefix(s, e1) || strings.HasPrefix(s, e2) { return errNoRace } } return fmt.Errorf("compile failure: %w\n%s", err, out) } return nil } // execRun runs a command and returns the combined output. // // It ignores the exit code, since it's meant to run panic, which crashes by // design. func execRun(cmd ...string) []byte { /* #nosec G204 */ c := exec.Command(cmd[0], cmd[1:]...) c.Env = append(os.Environ(), "GOTRACEBACK=all") out, _ := c.CombinedOutput() return out } panicparse-2.3.1/internal/internaltest/static_panic_race.go000066400000000000000000000025371446536624000241750ustar00rootroot00000000000000// Copyright 2020 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 internaltest // staticPanicRace is a snapshot created with: // // go install -race github.com/maruel/panicparse/cmd/panic // panic race |& sed "s#$HOME##g" // // when installed within $GOPATH. const staticPanicRace = ` GOTRACEBACK=all ================== WARNING: DATA RACE Read at 0x00c000014100 by goroutine 8: main.panicDoRaceRead() /go/src/github.com/maruel/panicparse/cmd/panic/main.go:137 +0x3a main.panicRace.func2() /go/src/github.com/maruel/panicparse/cmd/panic/main.go:154 +0x38 Previous write at 0x00c000014100 by goroutine 7: main.panicDoRaceWrite() /go/src/github.com/maruel/panicparse/cmd/panic/main.go:132 +0x41 main.panicRace.func1() /go/src/github.com/maruel/panicparse/cmd/panic/main.go:151 +0x38 Goroutine 8 (running) created at: main.panicRace() /go/src/github.com/maruel/panicparse/cmd/panic/main.go:153 +0xa1 main.main() /go/src/github.com/maruel/panicparse/cmd/panic/main.go:54 +0x6c8 Goroutine 7 (running) created at: main.panicRace() /go/src/github.com/maruel/panicparse/cmd/panic/main.go:150 +0x7f main.main() /go/src/github.com/maruel/panicparse/cmd/panic/main.go:54 +0x6c8 ================== ` panicparse-2.3.1/internal/internaltest/static_panicweb.go000066400000000000000000001665671446536624000237170ustar00rootroot00000000000000// Copyright 2020 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 internaltest // staticPanicweb is a snapshot created by running: // // bash static_panicweb.sh // // Not using go:generate here since it takes 2 minutes to complete. const staticPanicweb = `goroutine 135 [running]: runtime/pprof.writeGoroutineStacks(0x91be20, 0xc0003aa0e0, 0x0, 0x0) /goroot/src/runtime/pprof/pprof.go:665 +0x9d runtime/pprof.writeGoroutine(0x91be20, 0xc0003aa0e0, 0x2, 0x40e256, 0xc0003a2b00) /goroot/src/runtime/pprof/pprof.go:654 +0x44 runtime/pprof.(*Profile).WriteTo(0xbe85e0, 0x91be20, 0xc0003aa0e0, 0x2, 0xc0003aa0e0, 0xc00021b9b0) /goroot/src/runtime/pprof/pprof.go:329 +0x3da net/http/pprof.handler.ServeHTTP(0xc000614161, 0x9, 0x9241a0, 0xc0003aa0e0, 0xc0001b2700) /goroot/src/net/http/pprof/pprof.go:248 +0x33a net/http/pprof.Index(0x9241a0, 0xc0003aa0e0, 0xc0001b2700) /goroot/src/net/http/pprof/pprof.go:271 +0x735 net/http.HandlerFunc.ServeHTTP(0x8a4508, 0x9241a0, 0xc0003aa0e0, 0xc0001b2700) /goroot/src/net/http/server.go:2012 +0x44 net/http.(*ServeMux).ServeHTTP(0xbf6e60, 0x9241a0, 0xc0003aa0e0, 0xc0001b2700) /goroot/src/net/http/server.go:2387 +0x1a5 net/http.serverHandler.ServeHTTP(0xc000194000, 0x9241a0, 0xc0003aa0e0, 0xc0001b2700) /goroot/src/net/http/server.go:2807 +0xa3 net/http.(*conn).serve(0xc0001a63c0, 0x924de0, 0xc000190780) /goroot/src/net/http/server.go:1895 +0x86c created by net/http.(*Server).Serve /goroot/src/net/http/server.go:2933 +0x35c goroutine 1 [chan receive, 2 minutes]: main.main() /gopath/src/github.com/maruel/panicparse/cmd/panicweb/main.go:78 +0x7be goroutine 34 [chan receive, 2 minutes]: github.com/maruel/panicparse/cmd/panicweb/internal.URL1Handler(0x9241a0, 0xc000242000, 0xc000038300) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:43 +0x229 net/http.HandlerFunc.ServeHTTP(0x8a3ee8, 0x9241a0, 0xc000242000, 0xc000038300) /goroot/src/net/http/server.go:2012 +0x44 net/http.(*ServeMux).ServeHTTP(0xbf6e60, 0x9241a0, 0xc000242000, 0xc000038300) /goroot/src/net/http/server.go:2387 +0x1a5 net/http.serverHandler.ServeHTTP(0xc000194000, 0x9241a0, 0xc000242000, 0xc000038300) /goroot/src/net/http/server.go:2807 +0xa3 net/http.(*conn).serve(0xc0001a6000, 0x924de0, 0xc000020780) /goroot/src/net/http/server.go:1895 +0x86c created by net/http.(*Server).Serve /goroot/src/net/http/server.go:2933 +0x35c goroutine 5 [IO wait]: internal/poll.runtime_pollWait(0x7f5224075f48, 0x72, 0x0) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc0000cc318, 0x72, 0x0, 0x0, 0x88206d) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Accept(0xc0000cc300, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:384 +0x1d4 net.(*netFD).accept(0xc0000cc300, 0xecd05bb9ff29cc0c, 0x1000000000000, 0xecd05bb9ff29cc0c) /goroot/src/net/fd_unix.go:238 +0x42 net.(*TCPListener).accept(0xc00000e440, 0x5e7959ba, 0xc00019ae28, 0x4bca86) /goroot/src/net/tcpsock_posix.go:139 +0x32 net.(*TCPListener).Accept(0xc00000e440, 0xc00019ae78, 0x18, 0xc000001680, 0x6caf0c) /goroot/src/net/tcpsock.go:261 +0x64 net/http.(*Server).Serve(0xc000194000, 0x923ee0, 0xc00000e440, 0x0, 0x0) /goroot/src/net/http/server.go:2901 +0x25d net/http.Serve(0x923ee0, 0xc00000e440, 0x91bc00, 0xbf6e60, 0x0, 0x0) /goroot/src/net/http/server.go:2468 +0x6e created by main.main /gopath/src/github.com/maruel/panicparse/cmd/panicweb/main.go:50 +0x3b7 goroutine 22 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f5224075ca8, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc00029c118, 0x72, 0x1000, 0x1000, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc00029c100, 0xc0002b6000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc00029c100, 0xc0002b6000, 0x1000, 0x1000, 0x400, 0x203000, 0x400) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc0002a0020, 0xc0002b6000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*persistConn).Read(0xc0002a8000, 0xc0002b6000, 0x1000, 0x1000, 0xc20dd8, 0x7f524d3a6318, 0xc000298d18) /goroot/src/net/http/transport.go:1825 +0x75 bufio.(*Reader).Read(0xc000290480, 0xc00024cf87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/bufio/bufio.go:226 +0x24f io.(*LimitedReader).Read(0xc0001262a0, 0xc00024cf87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/io/io.go:451 +0x63 net/http.(*body).readLocked(0xc000138400, 0xc00024cf87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/net/http/transfer.go:847 +0x5f net/http.(*body).Read(0xc000138400, 0xc00024cf87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transfer.go:839 +0xf2 net/http.(*bodyEOFSignal).Read(0xc000138440, 0xc00024cf87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transport.go:2649 +0xde bytes.(*Buffer).ReadFrom(0xc0000a2f30, 0x91bc60, 0xc000138440, 0x91bc60, 0x2, 0xc0002860f0) /goroot/src/bytes/buffer.go:204 +0xb1 io/ioutil.readAll(0x91bc60, 0xc000138440, 0x200, 0x0, 0x0, 0x0, 0x0, 0x0) /goroot/src/io/ioutil/ioutil.go:36 +0xe3 io/ioutil.ReadAll(...) /goroot/src/io/ioutil/ioutil.go:45 github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync.func1(0xc000174090) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:26 +0x69 created by github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:25 +0x79 goroutine 20 [select, 2 minutes]: net/http.(*persistConn).readLoop(0xc000226480) /goroot/src/net/http/transport.go:2099 +0x99e created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1647 +0xc56 goroutine 21 [select, 2 minutes]: net/http.(*persistConn).writeLoop(0xc000226480) /goroot/src/net/http/transport.go:2277 +0x11c created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1648 +0xc7b goroutine 9 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f5224075d88, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc00018a098, 0x72, 0x0, 0x1, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc00018a080, 0xc0000caeb1, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc00018a080, 0xc0000caeb1, 0x1, 0x1, 0x6f9080, 0xc00010e0c0, 0xc00008a780) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc000186018, 0xc0000caeb1, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*connReader).backgroundRead(0xc0000caea0) /goroot/src/net/http/server.go:678 +0x58 created by net/http.(*connReader).startBackgroundRead /goroot/src/net/http/server.go:674 +0xd0 goroutine 50 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f5224075e68, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc0000cc598, 0x72, 0x1000, 0x1000, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc0000cc580, 0xc00017a000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc0000cc580, 0xc00017a000, 0x1000, 0x1000, 0x400, 0x203000, 0x400) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc000134030, 0xc00017a000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*persistConn).Read(0xc000226480, 0xc00017a000, 0x1000, 0x1000, 0xc20dd8, 0x7f524d3a8738, 0xc000298518) /goroot/src/net/http/transport.go:1825 +0x75 bufio.(*Reader).Read(0xc000136420, 0xc000304f87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/bufio/bufio.go:226 +0x24f io.(*LimitedReader).Read(0xc000284020, 0xc000304f87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/io/io.go:451 +0x63 net/http.(*body).readLocked(0xc00028c000, 0xc000304f87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/net/http/transfer.go:847 +0x5f net/http.(*body).Read(0xc00028c000, 0xc000304f87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transfer.go:839 +0xf2 net/http.(*bodyEOFSignal).Read(0xc00028c040, 0xc000304f87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transport.go:2649 +0xde bytes.(*Buffer).ReadFrom(0xc00009ef30, 0x91bc60, 0xc00028c040, 0x91bc60, 0x0, 0x0) /goroot/src/bytes/buffer.go:204 +0xb1 io/ioutil.readAll(0x91bc60, 0xc00028c040, 0x200, 0x0, 0x0, 0x0, 0x0, 0x0) /goroot/src/io/ioutil/ioutil.go:36 +0xe3 io/ioutil.ReadAll(...) /goroot/src/io/ioutil/ioutil.go:45 github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync.func1(0xc000280000) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:26 +0x69 created by github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:25 +0x79 goroutine 56 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f5224075a08, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc00018a198, 0x72, 0x0, 0x1, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc00018a180, 0xc0000cb0f1, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc00018a180, 0xc0000cb0f1, 0x1, 0x1, 0x100000000000000, 0x1000000000001, 0x0) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc000186020, 0xc0000cb0f1, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*connReader).backgroundRead(0xc0000cb0e0) /goroot/src/net/http/server.go:678 +0x58 created by net/http.(*connReader).startBackgroundRead /goroot/src/net/http/server.go:674 +0xd0 goroutine 54 [select, 2 minutes]: net/http.(*persistConn).readLoop(0xc0002a8000) /goroot/src/net/http/transport.go:2099 +0x99e created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1647 +0xc56 goroutine 55 [select, 2 minutes]: net/http.(*persistConn).writeLoop(0xc0002a8000) /goroot/src/net/http/transport.go:2277 +0x11c created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1648 +0xc7b goroutine 10 [chan receive, 2 minutes]: github.com/maruel/panicparse/cmd/panicweb/internal.URL1Handler(0x9241a0, 0xc0001940e0, 0xc000038500) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:43 +0x229 net/http.HandlerFunc.ServeHTTP(0x8a3ee8, 0x9241a0, 0xc0001940e0, 0xc000038500) /goroot/src/net/http/server.go:2012 +0x44 net/http.(*ServeMux).ServeHTTP(0xbf6e60, 0x9241a0, 0xc0001940e0, 0xc000038500) /goroot/src/net/http/server.go:2387 +0x1a5 net/http.serverHandler.ServeHTTP(0xc000194000, 0x9241a0, 0xc0001940e0, 0xc000038500) /goroot/src/net/http/server.go:2807 +0xa3 net/http.(*conn).serve(0xc0000dedc0, 0x924de0, 0xc000020900) /goroot/src/net/http/server.go:1895 +0x86c created by net/http.(*Server).Serve /goroot/src/net/http/server.go:2933 +0x35c goroutine 35 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f5224075bc8, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc0000cc698, 0x72, 0x0, 0x1, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc0000cc680, 0xc0000cb091, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc0000cc680, 0xc0000cb091, 0x1, 0x1, 0xc000090768, 0x0, 0x0) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc0000100b0, 0xc0000cb091, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*connReader).backgroundRead(0xc0000cb080) /goroot/src/net/http/server.go:678 +0x58 created by net/http.(*connReader).startBackgroundRead /goroot/src/net/http/server.go:674 +0xd0 goroutine 28 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f5224075848, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc0000cc718, 0x72, 0x0, 0x1, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc0000cc700, 0xc0001826d1, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc0000cc700, 0xc0001826d1, 0x1, 0x1, 0x100000000000f87, 0x1000000000001, 0x0) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc0000100b8, 0xc0001826d1, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*connReader).backgroundRead(0xc0001826c0) /goroot/src/net/http/server.go:678 +0x58 created by net/http.(*connReader).startBackgroundRead /goroot/src/net/http/server.go:674 +0xd0 goroutine 26 [select, 2 minutes]: net/http.(*persistConn).readLoop(0xc0001447e0) /goroot/src/net/http/transport.go:2099 +0x99e created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1647 +0xc56 goroutine 27 [select, 2 minutes]: net/http.(*persistConn).writeLoop(0xc0001447e0) /goroot/src/net/http/transport.go:2277 +0x11c created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1648 +0xc7b goroutine 36 [chan receive, 2 minutes]: github.com/maruel/panicparse/cmd/panicweb/internal.URL1Handler(0x9241a0, 0xc0002b8000, 0xc000038600) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:43 +0x229 net/http.HandlerFunc.ServeHTTP(0x8a3ee8, 0x9241a0, 0xc0002b8000, 0xc000038600) /goroot/src/net/http/server.go:2012 +0x44 net/http.(*ServeMux).ServeHTTP(0xbf6e60, 0x9241a0, 0xc0002b8000, 0xc000038600) /goroot/src/net/http/server.go:2387 +0x1a5 net/http.serverHandler.ServeHTTP(0xc000194000, 0x9241a0, 0xc0002b8000, 0xc000038600) /goroot/src/net/http/server.go:2807 +0xa3 net/http.(*conn).serve(0xc0001a60a0, 0x924de0, 0xc0000209c0) /goroot/src/net/http/server.go:1895 +0x86c created by net/http.(*Server).Serve /goroot/src/net/http/server.go:2933 +0x35c goroutine 37 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f5224075ae8, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc000308118, 0x72, 0x1000, 0x1000, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc000308100, 0xc000303000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc000308100, 0xc000303000, 0x1000, 0x1000, 0x400, 0x203000, 0x400) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc000134050, 0xc000303000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*persistConn).Read(0xc0001447e0, 0xc000303000, 0x1000, 0x1000, 0xc20dd8, 0x7f5224035888, 0xc000294d18) /goroot/src/net/http/transport.go:1825 +0x75 bufio.(*Reader).Read(0xc000136960, 0xc0002c2f87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/bufio/bufio.go:226 +0x24f io.(*LimitedReader).Read(0xc00018e0a0, 0xc0002c2f87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/io/io.go:451 +0x63 net/http.(*body).readLocked(0xc000190140, 0xc0002c2f87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/net/http/transfer.go:847 +0x5f net/http.(*body).Read(0xc000190140, 0xc0002c2f87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transfer.go:839 +0xf2 net/http.(*bodyEOFSignal).Read(0xc000190180, 0xc0002c2f87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transport.go:2649 +0xde bytes.(*Buffer).ReadFrom(0xc0000a3f30, 0x91bc60, 0xc000190180, 0x91bc60, 0x0, 0x0) /goroot/src/bytes/buffer.go:204 +0xb1 io/ioutil.readAll(0x91bc60, 0xc000190180, 0x200, 0x0, 0x0, 0x0, 0x0, 0x0) /goroot/src/io/ioutil/ioutil.go:36 +0xe3 io/ioutil.ReadAll(...) /goroot/src/io/ioutil/ioutil.go:45 github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync.func1(0xc0001ac090) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:26 +0x69 created by github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:25 +0x79 goroutine 59 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f5224075768, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc0000cc898, 0x72, 0x1000, 0x1000, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc0000cc880, 0xc000250000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc0000cc880, 0xc000250000, 0x1000, 0x1000, 0x400, 0x203000, 0x400) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc0000100d0, 0xc000250000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*persistConn).Read(0xc0002265a0, 0xc000250000, 0x1000, 0x1000, 0xc20dd8, 0x7f524d23e3a0, 0xc000295518) /goroot/src/net/http/transport.go:1825 +0x75 bufio.(*Reader).Read(0xc0000b7140, 0xc0001ccf87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/bufio/bufio.go:226 +0x24f io.(*LimitedReader).Read(0xc000284160, 0xc0001ccf87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/io/io.go:451 +0x63 net/http.(*body).readLocked(0xc00028c1c0, 0xc0001ccf87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/net/http/transfer.go:847 +0x5f net/http.(*body).Read(0xc00028c1c0, 0xc0001ccf87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transfer.go:839 +0xf2 net/http.(*bodyEOFSignal).Read(0xc00028c200, 0xc0001ccf87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transport.go:2649 +0xde bytes.(*Buffer).ReadFrom(0xc0001c5f30, 0x91bc60, 0xc00028c200, 0x91bc60, 0x2, 0xc000182570) /goroot/src/bytes/buffer.go:204 +0xb1 io/ioutil.readAll(0x91bc60, 0xc00028c200, 0x200, 0x0, 0x0, 0x0, 0x0, 0x0) /goroot/src/io/ioutil/ioutil.go:36 +0xe3 io/ioutil.ReadAll(...) /goroot/src/io/ioutil/ioutil.go:45 github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync.func1(0xc0002801b0) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:26 +0x69 created by github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:25 +0x79 goroutine 57 [select, 2 minutes]: net/http.(*persistConn).readLoop(0xc0001b6000) /goroot/src/net/http/transport.go:2099 +0x99e created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1647 +0xc56 goroutine 58 [select, 2 minutes]: net/http.(*persistConn).writeLoop(0xc0001b6000) /goroot/src/net/http/transport.go:2277 +0x11c created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1648 +0xc7b goroutine 11 [chan receive, 2 minutes]: github.com/maruel/panicparse/cmd/panicweb/internal.URL1Handler(0x9241a0, 0xc000314000, 0xc0001b2100) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:43 +0x229 net/http.HandlerFunc.ServeHTTP(0x8a3ee8, 0x9241a0, 0xc000314000, 0xc0001b2100) /goroot/src/net/http/server.go:2012 +0x44 net/http.(*ServeMux).ServeHTTP(0xbf6e60, 0x9241a0, 0xc000314000, 0xc0001b2100) /goroot/src/net/http/server.go:2387 +0x1a5 net/http.serverHandler.ServeHTTP(0xc000194000, 0x9241a0, 0xc000314000, 0xc0001b2100) /goroot/src/net/http/server.go:2807 +0xa3 net/http.(*conn).serve(0xc0000dee60, 0x924de0, 0xc000190200) /goroot/src/net/http/server.go:1895 +0x86c created by net/http.(*Server).Serve /goroot/src/net/http/server.go:2933 +0x35c goroutine 12 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f5224075928, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc00018a318, 0x72, 0x1000, 0x1000, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc00018a300, 0xc0002c4000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc00018a300, 0xc0002c4000, 0x1000, 0x1000, 0x400, 0x203000, 0x400) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc0002a0028, 0xc0002c4000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*persistConn).Read(0xc0001b6000, 0xc0002c4000, 0x1000, 0x1000, 0xc20dd8, 0x7f524d23e5c0, 0xc000298d18) /goroot/src/net/http/transport.go:1825 +0x75 bufio.(*Reader).Read(0xc000290540, 0xc0001bef87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/bufio/bufio.go:226 +0x24f io.(*LimitedReader).Read(0xc00000e5a0, 0xc0001bef87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/io/io.go:451 +0x63 net/http.(*body).readLocked(0xc000020a80, 0xc0001bef87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/net/http/transfer.go:847 +0x5f net/http.(*body).Read(0xc000020a80, 0xc0001bef87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transfer.go:839 +0xf2 net/http.(*bodyEOFSignal).Read(0xc000020ac0, 0xc0001bef87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transport.go:2649 +0xde bytes.(*Buffer).ReadFrom(0xc00019df30, 0x91bc60, 0xc000020ac0, 0x91bc60, 0x2, 0xc0002860f0) /goroot/src/bytes/buffer.go:204 +0xb1 io/ioutil.readAll(0x91bc60, 0xc000020ac0, 0x200, 0x0, 0x0, 0x0, 0x0, 0x0) /goroot/src/io/ioutil/ioutil.go:36 +0xe3 io/ioutil.ReadAll(...) /goroot/src/io/ioutil/ioutil.go:45 github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync.func1(0xc0002301b0) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:26 +0x69 created by github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:25 +0x79 goroutine 67 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f5224075688, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc00018a398, 0x72, 0x0, 0x1, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc00018a380, 0xc000182791, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc00018a380, 0xc000182791, 0x1, 0x1, 0x100000000000000, 0x1000000000001, 0x0) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc000186040, 0xc000182791, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*connReader).backgroundRead(0xc000182780) /goroot/src/net/http/server.go:678 +0x58 created by net/http.(*connReader).startBackgroundRead /goroot/src/net/http/server.go:674 +0xd0 goroutine 16 [select, 2 minutes]: net/http.(*persistConn).readLoop(0xc0002265a0) /goroot/src/net/http/transport.go:2099 +0x99e created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1647 +0xc56 goroutine 66 [select, 2 minutes]: net/http.(*persistConn).writeLoop(0xc0002265a0) /goroot/src/net/http/transport.go:2277 +0x11c created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1648 +0xc7b goroutine 41 [chan receive, 2 minutes]: github.com/maruel/panicparse/cmd/panicweb/internal.URL1Handler(0x9241a0, 0xc0002420e0, 0xc0001b2200) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:43 +0x229 net/http.HandlerFunc.ServeHTTP(0x8a3ee8, 0x9241a0, 0xc0002420e0, 0xc0001b2200) /goroot/src/net/http/server.go:2012 +0x44 net/http.(*ServeMux).ServeHTTP(0xbf6e60, 0x9241a0, 0xc0002420e0, 0xc0001b2200) /goroot/src/net/http/server.go:2387 +0x1a5 net/http.serverHandler.ServeHTTP(0xc000194000, 0x9241a0, 0xc0002420e0, 0xc0001b2200) /goroot/src/net/http/server.go:2807 +0xa3 net/http.(*conn).serve(0xc0001a6140, 0x924de0, 0xc0001902c0) /goroot/src/net/http/server.go:1895 +0x86c created by net/http.(*Server).Serve /goroot/src/net/http/server.go:2933 +0x35c goroutine 29 [select, 2 minutes]: net/http.(*persistConn).readLoop(0xc0002a8120) /goroot/src/net/http/transport.go:2099 +0x99e created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1647 +0xc56 goroutine 30 [select, 2 minutes]: net/http.(*persistConn).writeLoop(0xc0002a8120) /goroot/src/net/http/transport.go:2277 +0x11c created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1648 +0xc7b goroutine 68 [chan receive, 2 minutes]: github.com/maruel/panicparse/cmd/panicweb/internal.URL1Handler(0x9241a0, 0xc0002b80e0, 0xc00029e200) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:43 +0x229 net/http.HandlerFunc.ServeHTTP(0x8a3ee8, 0x9241a0, 0xc0002b80e0, 0xc00029e200) /goroot/src/net/http/server.go:2012 +0x44 net/http.(*ServeMux).ServeHTTP(0xbf6e60, 0x9241a0, 0xc0002b80e0, 0xc00029e200) /goroot/src/net/http/server.go:2387 +0x1a5 net/http.serverHandler.ServeHTTP(0xc000194000, 0x9241a0, 0xc0002b80e0, 0xc00029e200) /goroot/src/net/http/server.go:2807 +0xa3 net/http.(*conn).serve(0xc0000defa0, 0x924de0, 0xc00028c280) /goroot/src/net/http/server.go:1895 +0x86c created by net/http.(*Server).Serve /goroot/src/net/http/server.go:2933 +0x35c goroutine 63 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f52240754c8, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc0000cc998, 0x72, 0x0, 0x1, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc0000cc980, 0xc000286671, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc0000cc980, 0xc000286671, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc0000100d8, 0xc000286671, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*connReader).backgroundRead(0xc000286660) /goroot/src/net/http/server.go:678 +0x58 created by net/http.(*connReader).startBackgroundRead /goroot/src/net/http/server.go:674 +0xd0 goroutine 69 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f52240755a8, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc00029c318, 0x72, 0x1000, 0x1000, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc00029c300, 0xc000318000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc00029c300, 0xc000318000, 0x1000, 0x1000, 0x400, 0x203000, 0x400) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc000134058, 0xc000318000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*persistConn).Read(0xc0002a8120, 0xc000318000, 0x1000, 0x1000, 0xc20dd8, 0x7f52240354d0, 0xc000299518) /goroot/src/net/http/transport.go:1825 +0x75 bufio.(*Reader).Read(0xc000136a20, 0xc0002d0f87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/bufio/bufio.go:226 +0x24f io.(*LimitedReader).Read(0xc00000e6a0, 0xc0002d0f87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/io/io.go:451 +0x63 net/http.(*body).readLocked(0xc000020c00, 0xc0002d0f87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/net/http/transfer.go:847 +0x5f net/http.(*body).Read(0xc000020c00, 0xc0002d0f87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transfer.go:839 +0xf2 net/http.(*bodyEOFSignal).Read(0xc000020c40, 0xc0002d0f87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transport.go:2649 +0xde bytes.(*Buffer).ReadFrom(0xc000199f30, 0x91bc60, 0xc000020c40, 0x91bc60, 0x2, 0xc000286510) /goroot/src/bytes/buffer.go:204 +0xb1 io/ioutil.readAll(0x91bc60, 0xc000020c40, 0x200, 0x0, 0x0, 0x0, 0x0, 0x0) /goroot/src/io/ioutil/ioutil.go:36 +0xe3 io/ioutil.ReadAll(...) /goroot/src/io/ioutil/ioutil.go:45 github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync.func1(0xc000230360) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:26 +0x69 created by github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:25 +0x79 goroutine 98 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f52240753e8, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc0000ccb18, 0x72, 0x1000, 0x1000, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc0000ccb00, 0xc0002d2000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc0000ccb00, 0xc0002d2000, 0x1000, 0x1000, 0x400, 0x203000, 0x400) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc0002a0040, 0xc0002d2000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*persistConn).Read(0xc0002266c0, 0xc0002d2000, 0x1000, 0x1000, 0xc20dd8, 0x7f5224037b98, 0xc000258d18) /goroot/src/net/http/transport.go:1825 +0x75 bufio.(*Reader).Read(0xc0002909c0, 0xc0003b8f87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/bufio/bufio.go:226 +0x24f io.(*LimitedReader).Read(0xc000284260, 0xc0003b8f87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/io/io.go:451 +0x63 net/http.(*body).readLocked(0xc00028c400, 0xc0003b8f87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/net/http/transfer.go:847 +0x5f net/http.(*body).Read(0xc00028c400, 0xc0003b8f87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transfer.go:839 +0xf2 net/http.(*bodyEOFSignal).Read(0xc00028c440, 0xc0003b8f87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transport.go:2649 +0xde bytes.(*Buffer).ReadFrom(0xc000390f30, 0x91bc60, 0xc00028c440, 0x91bc60, 0x2, 0xc0000cb680) /goroot/src/bytes/buffer.go:204 +0xb1 io/ioutil.readAll(0x91bc60, 0xc00028c440, 0x200, 0x0, 0x0, 0x0, 0x0, 0x0) /goroot/src/io/ioutil/ioutil.go:36 +0xe3 io/ioutil.ReadAll(...) /goroot/src/io/ioutil/ioutil.go:45 github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync.func1(0xc000280360) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:26 +0x69 created by github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:25 +0x79 goroutine 64 [select, 2 minutes]: net/http.(*persistConn).readLoop(0xc0002266c0) /goroot/src/net/http/transport.go:2099 +0x99e created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1647 +0xc56 goroutine 65 [select, 2 minutes]: net/http.(*persistConn).writeLoop(0xc0002266c0) /goroot/src/net/http/transport.go:2277 +0x11c created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1648 +0xc7b goroutine 82 [chan receive, 2 minutes]: github.com/maruel/panicparse/cmd/panicweb/internal.URL1Handler(0x9241a0, 0xc0003aa000, 0xc00039e000) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:43 +0x229 net/http.HandlerFunc.ServeHTTP(0x8a3ee8, 0x9241a0, 0xc0003aa000, 0xc00039e000) /goroot/src/net/http/server.go:2012 +0x44 net/http.(*ServeMux).ServeHTTP(0xbf6e60, 0x9241a0, 0xc0003aa000, 0xc00039e000) /goroot/src/net/http/server.go:2387 +0x1a5 net/http.serverHandler.ServeHTTP(0xc000194000, 0x9241a0, 0xc0003aa000, 0xc00039e000) /goroot/src/net/http/server.go:2807 +0xa3 net/http.(*conn).serve(0xc000388000, 0x924de0, 0xc000396000) /goroot/src/net/http/server.go:1895 +0x86c created by net/http.(*Server).Serve /goroot/src/net/http/server.go:2933 +0x35c goroutine 83 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f5224075308, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc000382018, 0x72, 0x0, 0x1, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc000382000, 0xc0003840a1, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc000382000, 0xc0003840a1, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc000386000, 0xc0003840a1, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*connReader).backgroundRead(0xc000384090) /goroot/src/net/http/server.go:678 +0x58 created by net/http.(*connReader).startBackgroundRead /goroot/src/net/http/server.go:674 +0xd0 goroutine 104 [chan receive, 2 minutes]: github.com/maruel/panicparse/cmd/panicweb/internal.URL1Handler(0x9241a0, 0xc0002b81c0, 0xc00029e400) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:43 +0x229 net/http.HandlerFunc.ServeHTTP(0x8a3ee8, 0x9241a0, 0xc0002b81c0, 0xc00029e400) /goroot/src/net/http/server.go:2012 +0x44 net/http.(*ServeMux).ServeHTTP(0xbf6e60, 0x9241a0, 0xc0002b81c0, 0xc00029e400) /goroot/src/net/http/server.go:2387 +0x1a5 net/http.serverHandler.ServeHTTP(0xc000194000, 0x9241a0, 0xc0002b81c0, 0xc00029e400) /goroot/src/net/http/server.go:2807 +0xa3 net/http.(*conn).serve(0xc0002c81e0, 0x924de0, 0xc00028c500) /goroot/src/net/http/server.go:1895 +0x86c created by net/http.(*Server).Serve /goroot/src/net/http/server.go:2933 +0x35c goroutine 102 [select, 2 minutes]: net/http.(*persistConn).readLoop(0xc0002a8240) /goroot/src/net/http/transport.go:2099 +0x99e created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1647 +0xc56 goroutine 103 [select, 2 minutes]: net/http.(*persistConn).writeLoop(0xc0002a8240) /goroot/src/net/http/transport.go:2277 +0x11c created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1648 +0xc7b goroutine 84 [chan receive, 2 minutes]: github.com/maruel/panicparse/cmd/panicweb/internal.URL1Handler(0x9241a0, 0xc0002421c0, 0xc0001b2300) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:43 +0x229 net/http.HandlerFunc.ServeHTTP(0x8a3ee8, 0x9241a0, 0xc0002421c0, 0xc0001b2300) /goroot/src/net/http/server.go:2012 +0x44 net/http.(*ServeMux).ServeHTTP(0xbf6e60, 0x9241a0, 0xc0002421c0, 0xc0001b2300) /goroot/src/net/http/server.go:2387 +0x1a5 net/http.serverHandler.ServeHTTP(0xc000194000, 0x9241a0, 0xc0002421c0, 0xc0001b2300) /goroot/src/net/http/server.go:2807 +0xa3 net/http.(*conn).serve(0xc0003880a0, 0x924de0, 0xc000190380) /goroot/src/net/http/server.go:1895 +0x86c created by net/http.(*Server).Serve /goroot/src/net/http/server.go:2933 +0x35c goroutine 73 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f5224075148, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc000382118, 0x72, 0x0, 0x1, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc000382100, 0xc000182821, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc000382100, 0xc000182821, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc000386008, 0xc000182821, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*connReader).backgroundRead(0xc000182810) /goroot/src/net/http/server.go:678 +0x58 created by net/http.(*connReader).startBackgroundRead /goroot/src/net/http/server.go:674 +0xd0 goroutine 42 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f5224075228, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc00029c518, 0x72, 0x1000, 0x1000, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc00029c500, 0xc0002d4000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc00029c500, 0xc0002d4000, 0x1000, 0x1000, 0x400, 0x203000, 0x400) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc0002a0058, 0xc0002d4000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*persistConn).Read(0xc0002a8240, 0xc0002d4000, 0x1000, 0x1000, 0xc20dd8, 0x7f5224077ab8, 0xc000258d18) /goroot/src/net/http/transport.go:1825 +0x75 bufio.(*Reader).Read(0xc000290e40, 0xc00026ef87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/bufio/bufio.go:226 +0x24f io.(*LimitedReader).Read(0xc00018e1a0, 0xc00026ef87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/io/io.go:451 +0x63 net/http.(*body).readLocked(0xc000190440, 0xc00026ef87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/net/http/transfer.go:847 +0x5f net/http.(*body).Read(0xc000190440, 0xc00026ef87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transfer.go:839 +0xf2 net/http.(*bodyEOFSignal).Read(0xc000190480, 0xc00026ef87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transport.go:2649 +0xde bytes.(*Buffer).ReadFrom(0xc0001c7f30, 0x91bc60, 0xc000190480, 0x91bc60, 0x2, 0xc0000cb680) /goroot/src/bytes/buffer.go:204 +0xb1 io/ioutil.readAll(0x91bc60, 0xc000190480, 0x200, 0x0, 0x0, 0x0, 0x0, 0x0) /goroot/src/io/ioutil/ioutil.go:36 +0xe3 io/ioutil.ReadAll(...) /goroot/src/io/ioutil/ioutil.go:45 github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync.func1(0xc0001ac1b0) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:26 +0x69 created by github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:25 +0x79 goroutine 48 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f5224039d48, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc000382198, 0x72, 0x0, 0x1, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc000382180, 0xc0003842e1, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc000382180, 0xc0003842e1, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc000386010, 0xc0003842e1, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*connReader).backgroundRead(0xc0003842d0) /goroot/src/net/http/server.go:678 +0x58 created by net/http.(*connReader).startBackgroundRead /goroot/src/net/http/server.go:674 +0xd0 goroutine 46 [select, 2 minutes]: net/http.(*persistConn).readLoop(0xc0001b6240) /goroot/src/net/http/transport.go:2099 +0x99e created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1647 +0xc56 goroutine 47 [select, 2 minutes]: net/http.(*persistConn).writeLoop(0xc0001b6240) /goroot/src/net/http/transport.go:2277 +0x11c created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1648 +0xc7b goroutine 105 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f5224039f08, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc00029c598, 0x72, 0x0, 0x1, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc00029c580, 0xc000286b51, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc00029c580, 0xc000286b51, 0x1, 0x1, 0x100000000000f87, 0x1000000010000, 0x0) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc0002a0060, 0xc000286b51, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*connReader).backgroundRead(0xc000286b40) /goroot/src/net/http/server.go:678 +0x58 created by net/http.(*connReader).startBackgroundRead /goroot/src/net/http/server.go:674 +0xd0 goroutine 74 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f5224075068, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc00018a518, 0x72, 0x1000, 0x1000, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc00018a500, 0xc0001cf000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc00018a500, 0xc0001cf000, 0x1000, 0x1000, 0x400, 0x203000, 0x400) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc000186058, 0xc0001cf000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*persistConn).Read(0xc0001b6240, 0xc0001cf000, 0x1000, 0x1000, 0xc20dd8, 0x7f5224037a88, 0xc000295518) /goroot/src/net/http/transport.go:1825 +0x75 bufio.(*Reader).Read(0xc000180b40, 0xc0003bcf87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/bufio/bufio.go:226 +0x24f io.(*LimitedReader).Read(0xc00000e7a0, 0xc0003bcf87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/io/io.go:451 +0x63 net/http.(*body).readLocked(0xc000020d40, 0xc0003bcf87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/net/http/transfer.go:847 +0x5f net/http.(*body).Read(0xc000020d40, 0xc0003bcf87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transfer.go:839 +0xf2 net/http.(*bodyEOFSignal).Read(0xc000020d80, 0xc0003bcf87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transport.go:2649 +0xde bytes.(*Buffer).ReadFrom(0xc000391f30, 0x91bc60, 0xc000020d80, 0x91bc60, 0x2, 0xc000182570) /goroot/src/bytes/buffer.go:204 +0xb1 io/ioutil.readAll(0x91bc60, 0xc000020d80, 0x200, 0x0, 0x0, 0x0, 0x0, 0x0) /goroot/src/io/ioutil/ioutil.go:36 +0xe3 io/ioutil.ReadAll(...) /goroot/src/io/ioutil/ioutil.go:45 github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync.func1(0xc000230510) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:26 +0x69 created by github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:25 +0x79 goroutine 80 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f5224039e28, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc0000ccd98, 0x72, 0x1000, 0x1000, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc0000ccd80, 0xc00026d000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc0000ccd80, 0xc00026d000, 0x1000, 0x1000, 0x400, 0x203000, 0x400) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc000010100, 0xc00026d000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*persistConn).Read(0xc0002267e0, 0xc00026d000, 0x1000, 0x1000, 0xc20dd8, 0x7f524d23e0f8, 0xc000258d18) /goroot/src/net/http/transport.go:1825 +0x75 bufio.(*Reader).Read(0xc0000b7b60, 0xc0001dcf87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/bufio/bufio.go:226 +0x24f io.(*LimitedReader).Read(0xc00000e880, 0xc0001dcf87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/io/io.go:451 +0x63 net/http.(*body).readLocked(0xc000020e40, 0xc0001dcf87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/net/http/transfer.go:847 +0x5f net/http.(*body).Read(0xc000020e40, 0xc0001dcf87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transfer.go:839 +0xf2 net/http.(*bodyEOFSignal).Read(0xc000020e80, 0xc0001dcf87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transport.go:2649 +0xde bytes.(*Buffer).ReadFrom(0xc000392f30, 0x91bc60, 0xc000020e80, 0x91bc60, 0x2, 0xc0000cb9b0) /goroot/src/bytes/buffer.go:204 +0xb1 io/ioutil.readAll(0x91bc60, 0xc000020e80, 0x200, 0x0, 0x0, 0x0, 0x0, 0x0) /goroot/src/io/ioutil/ioutil.go:36 +0xe3 io/ioutil.ReadAll(...) /goroot/src/io/ioutil/ioutil.go:45 github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync.func1(0xc000230630) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:26 +0x69 created by github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:25 +0x79 goroutine 78 [select, 2 minutes]: net/http.(*persistConn).readLoop(0xc0002267e0) /goroot/src/net/http/transport.go:2099 +0x99e created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1647 +0xc56 goroutine 79 [select, 2 minutes]: net/http.(*persistConn).writeLoop(0xc0002267e0) /goroot/src/net/http/transport.go:2277 +0x11c created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1648 +0xc7b goroutine 85 [chan receive, 2 minutes]: github.com/maruel/panicparse/cmd/panicweb/internal.URL1Handler(0x9241a0, 0xc0001941c0, 0xc00039e100) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:43 +0x229 net/http.HandlerFunc.ServeHTTP(0x8a3ee8, 0x9241a0, 0xc0001941c0, 0xc00039e100) /goroot/src/net/http/server.go:2012 +0x44 net/http.(*ServeMux).ServeHTTP(0xbf6e60, 0x9241a0, 0xc0001941c0, 0xc00039e100) /goroot/src/net/http/server.go:2387 +0x1a5 net/http.serverHandler.ServeHTTP(0xc000194000, 0x9241a0, 0xc0001941c0, 0xc00039e100) /goroot/src/net/http/server.go:2807 +0xa3 net/http.(*conn).serve(0xc000388140, 0x924de0, 0xc000396140) /goroot/src/net/http/server.go:1895 +0x86c created by net/http.(*Server).Serve /goroot/src/net/http/server.go:2933 +0x35c goroutine 134 [syscall, 2 minutes]: syscall.Syscall(0x23, 0xc000259fb8, 0xc000259fa8, 0x0, 0xc0001aab40, 0xc000259fa8, 0x1) /goroot/src/syscall/asm_linux_amd64.s:18 +0x5 golang.org/x/sys/unix.Nanosleep(0xc000259fb8, 0xc000259fa8, 0x0, 0x1) /gopath/pkg/mod/golang.org/x/sys@v0.0.0-20200223170610-d5e6a3e2c0ae/unix/zsyscall_linux_amd64.go:1160 +0x5f main.sysHang(...) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/main_unix.go:12 main.main.func1(0xc0001aab40) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/main.go:65 +0x71 created by main.main /gopath/src/github.com/maruel/panicparse/cmd/panicweb/main.go:63 +0x548 goroutine 49 [select, 2 minutes]: net/http.(*persistConn).readLoop(0xc000226900) /goroot/src/net/http/transport.go:2099 +0x99e created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1647 +0xc56 goroutine 130 [select, 2 minutes]: net/http.(*persistConn).writeLoop(0xc000226900) /goroot/src/net/http/transport.go:2277 +0x11c created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1648 +0xc7b goroutine 86 [chan receive, 2 minutes]: github.com/maruel/panicparse/cmd/panicweb/internal.URL2Handler(0x9241a0, 0xc0002422a0, 0xc000038b00) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:54 +0x229 net/http.HandlerFunc.ServeHTTP(0x8a3ef0, 0x9241a0, 0xc0002422a0, 0xc000038b00) /goroot/src/net/http/server.go:2012 +0x44 net/http.(*ServeMux).ServeHTTP(0xbf6e60, 0x9241a0, 0xc0002422a0, 0xc000038b00) /goroot/src/net/http/server.go:2387 +0x1a5 net/http.serverHandler.ServeHTTP(0xc000194000, 0x9241a0, 0xc0002422a0, 0xc000038b00) /goroot/src/net/http/server.go:2807 +0xa3 net/http.(*conn).serve(0xc0003881e0, 0x924de0, 0xc000020f00) /goroot/src/net/http/server.go:1895 +0x86c created by net/http.(*Server).Serve /goroot/src/net/http/server.go:2933 +0x35c goroutine 116 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f5224039b88, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc000382218, 0x72, 0x0, 0x1, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc000382200, 0xc0000cbdb1, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc000382200, 0xc0000cbdb1, 0x1, 0x1, 0xf87, 0x1e00, 0x0) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc000386018, 0xc0000cbdb1, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*connReader).backgroundRead(0xc0000cbda0) /goroot/src/net/http/server.go:678 +0x58 created by net/http.(*connReader).startBackgroundRead /goroot/src/net/http/server.go:674 +0xd0 goroutine 87 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f5224039c68, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc0000ccf18, 0x72, 0x1000, 0x1000, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc0000ccf00, 0xc0001db000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc0000ccf00, 0xc0001db000, 0x1000, 0x1000, 0x400, 0x203000, 0x400) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc000186060, 0xc0001db000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*persistConn).Read(0xc000226900, 0xc0001db000, 0x1000, 0x1000, 0xc20dd8, 0x7f5224077700, 0xc000254d18) /goroot/src/net/http/transport.go:1825 +0x75 bufio.(*Reader).Read(0xc000180c00, 0xc000402f87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/bufio/bufio.go:226 +0x24f io.(*LimitedReader).Read(0xc0003ae060, 0xc000402f87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/io/io.go:451 +0x63 net/http.(*body).readLocked(0xc000396200, 0xc000402f87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/net/http/transfer.go:847 +0x5f net/http.(*body).Read(0xc000396200, 0xc000402f87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transfer.go:839 +0xf2 net/http.(*bodyEOFSignal).Read(0xc000396240, 0xc000402f87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transport.go:2649 +0xde bytes.(*Buffer).ReadFrom(0xc000276f30, 0x91bc60, 0xc000396240, 0x91bc60, 0x0, 0x0) /goroot/src/bytes/buffer.go:204 +0xb1 io/ioutil.readAll(0x91bc60, 0xc000396240, 0x200, 0x0, 0x0, 0x0, 0x0, 0x0) /goroot/src/io/ioutil/ioutil.go:36 +0xe3 io/ioutil.ReadAll(...) /goroot/src/io/ioutil/ioutil.go:45 github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync.func1(0xc0003a6090) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:26 +0x69 created by github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:25 +0x79 goroutine 119 [chan receive, 2 minutes]: github.com/maruel/panicparse/cmd/panicweb/internal.URL2Handler(0x9241a0, 0xc0002b8380, 0xc00029e500) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:54 +0x229 net/http.HandlerFunc.ServeHTTP(0x8a3ef0, 0x9241a0, 0xc0002b8380, 0xc00029e500) /goroot/src/net/http/server.go:2012 +0x44 net/http.(*ServeMux).ServeHTTP(0xbf6e60, 0x9241a0, 0xc0002b8380, 0xc00029e500) /goroot/src/net/http/server.go:2387 +0x1a5 net/http.serverHandler.ServeHTTP(0xc000194000, 0x9241a0, 0xc0002b8380, 0xc00029e500) /goroot/src/net/http/server.go:2807 +0xa3 net/http.(*conn).serve(0xc0000df180, 0x924de0, 0xc00028c6c0) /goroot/src/net/http/server.go:1895 +0x86c created by net/http.(*Server).Serve /goroot/src/net/http/server.go:2933 +0x35c goroutine 117 [select, 2 minutes]: net/http.(*persistConn).readLoop(0xc0003c4000) /goroot/src/net/http/transport.go:2099 +0x99e created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1647 +0xc56 goroutine 118 [select, 2 minutes]: net/http.(*persistConn).writeLoop(0xc0003c4000) /goroot/src/net/http/transport.go:2277 +0x11c created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1648 +0xc7b goroutine 106 [chan receive, 2 minutes]: github.com/maruel/panicparse/cmd/panicweb/internal.URL2Handler(0x9241a0, 0xc0002b82a0, 0xc0001b2500) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:54 +0x229 net/http.HandlerFunc.ServeHTTP(0x8a3ef0, 0x9241a0, 0xc0002b82a0, 0xc0001b2500) /goroot/src/net/http/server.go:2012 +0x44 net/http.(*ServeMux).ServeHTTP(0xbf6e60, 0x9241a0, 0xc0002b82a0, 0xc0001b2500) /goroot/src/net/http/server.go:2387 +0x1a5 net/http.serverHandler.ServeHTTP(0xc000194000, 0x9241a0, 0xc0002b82a0, 0xc0001b2500) /goroot/src/net/http/server.go:2807 +0xa3 net/http.(*conn).serve(0xc0002c83c0, 0x924de0, 0xc000190600) /goroot/src/net/http/server.go:1895 +0x86c created by net/http.(*Server).Serve /goroot/src/net/http/server.go:2933 +0x35c goroutine 107 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f52240399c8, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc00029c698, 0x72, 0x0, 0x1, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc00029c680, 0xc000182cd1, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc00029c680, 0xc000182cd1, 0x1, 0x1, 0xf87, 0x1e00, 0x0) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc0002a0068, 0xc000182cd1, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*connReader).backgroundRead(0xc000182cc0) /goroot/src/net/http/server.go:678 +0x58 created by net/http.(*connReader).startBackgroundRead /goroot/src/net/http/server.go:674 +0xd0 goroutine 91 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f5224039aa8, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc000382398, 0x72, 0x1000, 0x1000, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc000382380, 0xc000401000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc000382380, 0xc000401000, 0x1000, 0x1000, 0x400, 0x203000, 0x400) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc000010118, 0xc000401000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*persistConn).Read(0xc0003c4000, 0xc000401000, 0x1000, 0x1000, 0xc20dd8, 0x7f5224035118, 0xc000255d18) /goroot/src/net/http/transport.go:1825 +0x75 bufio.(*Reader).Read(0xc00027e0c0, 0xc0002e4f87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/bufio/bufio.go:226 +0x24f io.(*LimitedReader).Read(0xc0003ae160, 0xc0002e4f87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/io/io.go:451 +0x63 net/http.(*body).readLocked(0xc0003962c0, 0xc0002e4f87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/net/http/transfer.go:847 +0x5f net/http.(*body).Read(0xc0003962c0, 0xc0002e4f87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transfer.go:839 +0xf2 net/http.(*bodyEOFSignal).Read(0xc000396300, 0xc0002e4f87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transport.go:2649 +0xde bytes.(*Buffer).ReadFrom(0xc00038ef30, 0x91bc60, 0xc000396300, 0x91bc60, 0x0, 0x0) /goroot/src/bytes/buffer.go:204 +0xb1 io/ioutil.readAll(0x91bc60, 0xc000396300, 0x200, 0x0, 0x0, 0x0, 0x0, 0x0) /goroot/src/io/ioutil/ioutil.go:36 +0xe3 io/ioutil.ReadAll(...) /goroot/src/io/ioutil/ioutil.go:45 github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync.func1(0xc0003a61b0) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:26 +0x69 created by github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:25 +0x79 goroutine 133 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f52240398e8, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc000382518, 0x72, 0x1000, 0x1000, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc000382500, 0xc0001e1000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc000382500, 0xc0001e1000, 0x1000, 0x1000, 0x400, 0x203000, 0x400) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc000186068, 0xc0001e1000, 0x1000, 0x1000, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*persistConn).Read(0xc0003c4120, 0xc0001e1000, 0x1000, 0x1000, 0xc20dd8, 0x7f524d23fe30, 0xc000256518) /goroot/src/net/http/transport.go:1825 +0x75 bufio.(*Reader).Read(0xc000180d20, 0xc0002f4f87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/bufio/bufio.go:226 +0x24f io.(*LimitedReader).Read(0xc00018e2a0, 0xc0002f4f87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/io/io.go:451 +0x63 net/http.(*body).readLocked(0xc000190700, 0xc0002f4f87, 0xe79, 0xe79, 0x187, 0x0, 0x0) /goroot/src/net/http/transfer.go:847 +0x5f net/http.(*body).Read(0xc000190700, 0xc0002f4f87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transfer.go:839 +0xf2 net/http.(*bodyEOFSignal).Read(0xc000190740, 0xc0002f4f87, 0xe79, 0xe79, 0x0, 0x0, 0x0) /goroot/src/net/http/transport.go:2649 +0xde bytes.(*Buffer).ReadFrom(0xc0002eaf30, 0x91bc60, 0xc000190740, 0x91bc60, 0x2, 0xc000384660) /goroot/src/bytes/buffer.go:204 +0xb1 io/ioutil.readAll(0x91bc60, 0xc000190740, 0x200, 0x0, 0x0, 0x0, 0x0, 0x0) /goroot/src/io/ioutil/ioutil.go:36 +0xe3 io/ioutil.ReadAll(...) /goroot/src/io/ioutil/ioutil.go:45 github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync.func1(0xc0001ac3f0) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:26 +0x69 created by github.com/maruel/panicparse/cmd/panicweb/internal.GetAsync /gopath/src/github.com/maruel/panicparse/cmd/panicweb/internal/internal.go:25 +0x79 goroutine 131 [select, 2 minutes]: net/http.(*persistConn).readLoop(0xc0003c4120) /goroot/src/net/http/transport.go:2099 +0x99e created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1647 +0xc56 goroutine 132 [select, 2 minutes]: net/http.(*persistConn).writeLoop(0xc0003c4120) /goroot/src/net/http/transport.go:2277 +0x11c created by net/http.(*Transport).dialConn /goroot/src/net/http/transport.go:1648 +0xc7b goroutine 108 [IO wait, 2 minutes]: internal/poll.runtime_pollWait(0x7f5224039808, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc0000cd018, 0x72, 0x0, 0x1, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc0000cd000, 0xc000286e21, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc0000cd000, 0xc000286e21, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc000010120, 0xc000286e21, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*connReader).backgroundRead(0xc000286e10) /goroot/src/net/http/server.go:678 +0x58 created by net/http.(*connReader).startBackgroundRead /goroot/src/net/http/server.go:674 +0xd0 goroutine 95 [chan receive, 2 minutes, locked to thread]: main.(*writeHang).Write(0xc000398180, 0xc0003940bf, 0x1, 0x1, 0x1000000010000, 0xc0003a4360, 0x912ec0) /gopath/src/github.com/maruel/panicparse/cmd/panicweb/main.go:92 +0x58 github.com/mattn/go-colorable.(*NonColorable).Write(0xc000398190, 0xc0003940b8, 0x7, 0x7, 0x15, 0xc000024840, 0x11) /gopath/pkg/mod/github.com/mattn/go-colorable@v0.1.6/noncolorable.go:30 +0x2ae created by main.main /gopath/src/github.com/maruel/panicparse/cmd/panicweb/main.go:73 +0x68c goroutine 226 [IO wait]: internal/poll.runtime_pollWait(0x7f5224039728, 0x72, 0xffffffffffffffff) /goroot/src/runtime/netpoll.go:203 +0x55 internal/poll.(*pollDesc).wait(0xc00018a618, 0x72, 0x0, 0x1, 0xffffffffffffffff) /goroot/src/internal/poll/fd_poll_runtime.go:87 +0x45 internal/poll.(*pollDesc).waitRead(...) /goroot/src/internal/poll/fd_poll_runtime.go:92 internal/poll.(*FD).Read(0xc00018a600, 0xc000182e81, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/internal/poll/fd_unix.go:169 +0x19b net.(*netFD).Read(0xc00018a600, 0xc000182e81, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/net/fd_unix.go:202 +0x4f net.(*conn).Read(0xc000186070, 0xc000182e81, 0x1, 0x1, 0x0, 0x0, 0x0) /goroot/src/net/net.go:184 +0x8e net/http.(*connReader).backgroundRead(0xc000182e70) /goroot/src/net/http/server.go:678 +0x58 created by net/http.(*connReader).startBackgroundRead /goroot/src/net/http/server.go:674 +0xd0 ` panicparse-2.3.1/internal/internaltest/static_panicweb.sh000066400000000000000000000013121446536624000236740ustar00rootroot00000000000000#!/bin/bash # Copyright 2020 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. set -eu eval `go env | grep 'GOROOT\|GOPATH'` go install github.com/maruel/panicparse/cmd/panicweb panicweb -port 1212 & trap "trap - TERM && kill -- -$$" INT TERM EXIT sleep 1 echo "Sleeping 2 minutes..." sleep 124 curl -sS 'http://localhost:1212/debug/pprof/goroutine?debug=2' \ | sed -e "s#\t$GOROOT/#\t/goroot/#g" \ | sed -e "s#\t$GOPATH/#\t/gopath/#g" > static_panicweb.txt echo "Copied $(cat static_panicweb.txt | wc -l) lines into static_panicweb.txt." echo "Add static_panicweb.txt content into static_panicweb.go." panicparse-2.3.1/internal/main.go000066400000000000000000000235421446536624000167510ustar00rootroot00000000000000// Copyright 2015 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 internal implements panicparse // // It is mostly useful on servers will large number of identical goroutines, // making the crash dump harder to read than strictly necessary. // // Colors: // - Magenta: first goroutine to be listed. // - Yellow: main package. // - Green: standard library. // - Red: other packages. // // Bright colors are used for exported symbols. package internal import ( "bytes" "errors" "flag" "fmt" "html/template" "io" "io/ioutil" "log" "os" "os/signal" "regexp" "syscall" "github.com/maruel/panicparse/v2/stack" "github.com/mattn/go-colorable" "github.com/mattn/go-isatty" "github.com/mgutz/ansi" ) // resetFG is similar to ansi.Reset except that it doesn't reset the // background color, only the foreground color and the style. // // That much for the "ansi" abstraction layer... const resetFG = ansi.DefaultFG + "\033[m" // defaultPalette is the default recommended palette. var defaultPalette = Palette{ EOLReset: resetFG, RoutineFirst: ansi.ColorCode("magenta+b"), CreatedBy: ansi.LightBlack, Race: ansi.LightRed, Package: ansi.ColorCode("default+b"), SrcFile: resetFG, FuncMain: ansi.ColorCode("yellow+b"), FuncLocationUnknown: ansi.White, FuncLocationUnknownExported: ansi.ColorCode("white+b"), FuncGoMod: ansi.Red, FuncGoModExported: ansi.ColorCode("red+b"), FuncGOPATH: ansi.Cyan, FuncGOPATHExported: ansi.ColorCode("cyan+b"), FuncGoPkg: ansi.Blue, FuncGoPkgExported: ansi.ColorCode("blue+b"), FuncStdLib: ansi.Green, FuncStdLibExported: ansi.ColorCode("green+b"), Arguments: resetFG, } func writeBucketsToConsole(out io.Writer, p *Palette, a *stack.Aggregated, pf pathFormat, needsEnv bool, filter, match *regexp.Regexp) error { if needsEnv { _, _ = io.WriteString(out, "\nTo see all goroutines, visit https://github.com/maruel/panicparse#gotraceback\n\n") } srcLen, pkgLen := calcBucketsLengths(a, pf) multi := len(a.Buckets) > 1 for _, e := range a.Buckets { header := p.BucketHeader(e, pf, multi) if filter != nil && filter.MatchString(header) { continue } if match != nil && !match.MatchString(header) { continue } _, _ = io.WriteString(out, header) _, _ = io.WriteString(out, p.StackLines(&e.Signature, srcLen, pkgLen, pf)) } return nil } func writeGoroutinesToConsole(out io.Writer, p *Palette, s *stack.Snapshot, pf pathFormat, needsEnv bool, filter, match *regexp.Regexp) error { if needsEnv { _, _ = io.WriteString(out, "\nTo see all goroutines, visit https://github.com/maruel/panicparse#gotraceback\n\n") } srcLen, pkgLen := calcGoroutinesLengths(s, pf) multi := len(s.Goroutines) > 1 for _, e := range s.Goroutines { header := p.GoroutineHeader(e, pf, multi) if filter != nil && filter.MatchString(header) { continue } if match != nil && !match.MatchString(header) { continue } _, _ = io.WriteString(out, header) _, _ = io.WriteString(out, p.StackLines(&e.Signature, srcLen, pkgLen, pf)) } return nil } type toHTMLer interface { ToHTML(io.Writer, template.HTML) error } func toHTML(h toHTMLer, p string, needsEnv bool) error { /* #nosec G304 */ f, err := os.Create(p) if err != nil { return err } var footer template.HTML if needsEnv { footer = "To see all goroutines, visit github.com/maruel/panicparse" } err = h.ToHTML(f, footer) if err2 := f.Close(); err == nil { err = err2 } return err } func processInner(out io.Writer, p *Palette, s stack.Similarity, pf pathFormat, html string, filter, match *regexp.Regexp, c *stack.Snapshot, first bool) error { log.Printf("GOROOT=%s", c.RemoteGOROOT) log.Printf("GOPATH=%s", c.RemoteGOPATHs) needsEnv := len(c.Goroutines) == 1 && showBanner() // Bucketing should only be done if no data race was detected. if !c.IsRace() { a := c.Aggregate(s) if html == "" { return writeBucketsToConsole(out, p, a, pf, needsEnv, filter, match) } return toHTML(a, html, needsEnv) } // It's a data race. if html == "" { return writeGoroutinesToConsole(out, p, c, pf, needsEnv, filter, match) } return toHTML(c, html, needsEnv) } // process copies stdin to stdout and processes any "panic: " line found. // // If html is used, a stack trace is written to this file instead. func process(in io.Reader, out io.Writer, p *Palette, s stack.Similarity, pf pathFormat, parse, rebase bool, html string, filter, match *regexp.Regexp) error { opts := stack.DefaultOpts() if !rebase { opts.GuessPaths = false opts.AnalyzeSources = false } if !parse { opts.AnalyzeSources = false } for first := true; ; first = false { c, suffix, err := stack.ScanSnapshot(in, out, opts) if c != nil { // Process it even if an error occurred. if err1 := processInner(out, p, s, pf, html, filter, match, c, first); err == nil { err = err1 } } if err == nil { // This means the whole buffer was not read, loop again. in = io.MultiReader(bytes.NewReader(suffix), in) continue } if len(suffix) != 0 { if _, err1 := out.Write(suffix); err == nil { err = err1 } } if err == io.EOF { return nil } // Parts of the input will be lost. return err } } func showBanner() bool { gtb := os.Getenv("GOTRACEBACK") return gtb == "" || gtb == "single" } // Main is implemented here so both 'pp' and 'panicparse' executables can be // compiled. This is to work around the Perl Package manager 'pp' that is // preinstalled on some OSes. func Main() error { aggressive := flag.Bool("aggressive", false, "Aggressive deduplication including non pointers") parse := flag.Bool("parse", true, "Parses source files to deduct types; use -parse=false to work around bugs in source parser") rebase := flag.Bool("rebase", true, "Guess GOROOT and GOPATH") verboseFlag := flag.Bool("v", false, "Enables verbose logging output") filterFlag := flag.String("f", "", "Regexp to filter out headers that match, ex: -f 'IO wait|syscall'") matchFlag := flag.String("m", "", "Regexp to filter by only headers that match, ex: -m 'semacquire'") // Console only. fullPathArg := flag.Bool("full-path", false, "Print full sources path") relPathArg := flag.Bool("rel-path", false, "Print sources path relative to GOROOT or GOPATH; implies -rebase") noColor := flag.Bool("no-color", !isatty.IsTerminal(os.Stdout.Fd()) || os.Getenv("TERM") == "dumb", "Disable coloring") forceColor := flag.Bool("force-color", false, "Forcibly enable coloring when with stdout is redirected") // HTML only. html := flag.String("html", "", "Output an HTML file") var out io.Writer = os.Stdout p := &defaultPalette flag.CommandLine.Usage = func() { out = os.Stderr if *noColor && !*forceColor { p = &Palette{} } else { out = colorable.NewColorableStderr() } fmt.Fprintf(out, "Usage of %s:\n", os.Args[0]) flag.CommandLine.SetOutput(out) flag.CommandLine.PrintDefaults() fmt.Fprintf(out, "\nLegend:\n") fmt.Fprintf(out, " Type Exported Private\n") fmt.Fprintf(out, " main %smain.Foo()%s %smain.foo()%s\n", p.funcColor(stack.LocationUnknown, true, false), p.EOLReset, p.funcColor(stack.LocationUnknown, true, true), p.EOLReset) fmt.Fprintf(out, " %spkg.Foo()%s %spkg.foo()%s\n", p.funcColor(stack.LocationUnknown, false, false), p.EOLReset, p.funcColor(stack.LocationUnknown, false, true), p.EOLReset) fmt.Fprintf(out, " go.mod %spkg.Foo()%s %spkg.foo()%s\n", p.funcColor(stack.GoMod, false, false), p.EOLReset, p.funcColor(stack.GoMod, false, true), p.EOLReset) fmt.Fprintf(out, " $GOPATH/src %spkg.Foo()%s %spkg.foo()%s\n", p.funcColor(stack.GOPATH, false, false), p.EOLReset, p.funcColor(stack.GOPATH, false, true), p.EOLReset) fmt.Fprintf(out, " $GOPATH/pkg/mod %spkg.Foo()%s %spkg.foo()%s\n", p.funcColor(stack.GoPkg, false, false), p.EOLReset, p.funcColor(stack.GoPkg, false, true), p.EOLReset) fmt.Fprintf(out, " $GOROOT/src %spkg.Foo()%s %spkg.Foo()%s\n", p.funcColor(stack.Stdlib, false, false), p.EOLReset, p.funcColor(stack.Stdlib, false, true), p.EOLReset) } flag.Parse() log.SetFlags(log.Lmicroseconds) if !*verboseFlag { log.SetOutput(ioutil.Discard) } var err error var filter *regexp.Regexp if *filterFlag != "" { if filter, err = regexp.Compile(*filterFlag); err != nil { return err } } var match *regexp.Regexp if *matchFlag != "" { if match, err = regexp.Compile(*matchFlag); err != nil { return err } } s := stack.AnyPointer if *aggressive { s = stack.AnyValue } if *html == "" { if *noColor && !*forceColor { p = &Palette{} } else { out = colorable.NewColorableStdout() } } var in *os.File switch flag.NArg() { case 0: in = os.Stdin // Explicitly silence SIGQUIT, as it is useful to gather the stack dump // from the piped command. signals := make(chan os.Signal, 1) go func() { for { <-signals } }() signal.Notify(signals, os.Interrupt, syscall.SIGQUIT) case 1: // Do not handle SIGQUIT when passed a file to process. name := flag.Arg(0) /* #nosec G304 */ if in, err = os.Open(name); err != nil { return fmt.Errorf("did you mean to specify a valid stack dump file name? %w", err) } /* #nosec G307 */ defer in.Close() default: return errors.New("pipe from stdin or specify a single file") } pf := basePath if *fullPathArg { if *relPathArg { return errors.New("can't use both -full-path and -rel-path") } pf = fullPath } else if *relPathArg { pf = relPath *rebase = true } return process(in, out, p, s, pf, *parse, *rebase, *html, filter, match) } panicparse-2.3.1/internal/main_test.go000066400000000000000000000071501446536624000200050ustar00rootroot00000000000000// Copyright 2015 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 internal import ( "bytes" "flag" "fmt" "io/ioutil" "log" "os" "path/filepath" "regexp" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/maruel/panicparse/v2/internal/internaltest" "github.com/maruel/panicparse/v2/stack" ) func TestProcess(t *testing.T) { t.Parallel() d, err := os.Getwd() if err != nil { t.Fatal(err) } data := []struct { name string palette *Palette simil stack.Similarity path pathFormat filter *regexp.Regexp match *regexp.Regexp want string }{ { name: "BasePath", palette: testPalette, simil: stack.AnyPointer, path: basePath, want: "GOTRACEBACK=all\npanic: simple\n\nC1: runningA\n Emain Fmain.go:74 GmainR()A\n", }, { name: "FullPath", palette: testPalette, simil: stack.AnyValue, path: fullPath, // "/" is used even on Windows. want: fmt.Sprintf("GOTRACEBACK=all\npanic: simple\n\nC1: runningA\n Emain F%s:74 GmainR()A\n", strings.Replace(filepath.Join(filepath.Dir(d), "cmd", "panic", "main.go"), "\\", "/", -1)), }, { name: "NoColor", palette: &Palette{}, simil: stack.AnyValue, path: basePath, want: "GOTRACEBACK=all\npanic: simple\n\n1: running\n main main.go:74 main()\n", }, { name: "Match", palette: testPalette, simil: stack.AnyValue, path: basePath, match: regexp.MustCompile(`notpresent`), want: "GOTRACEBACK=all\npanic: simple\n\n", }, { name: "Filter", palette: testPalette, simil: stack.AnyValue, path: basePath, filter: regexp.MustCompile(`notpresent`), want: "GOTRACEBACK=all\npanic: simple\n\nC1: runningA\n Emain Fmain.go:74 GmainR()A\n", }, } for i, line := range data { line := line t.Run(fmt.Sprintf("%d-%s", i, line.name), func(t *testing.T) { t.Parallel() out := bytes.Buffer{} r := bytes.NewReader(internaltest.PanicOutputs()["simple"]) if err := process(r, &out, line.palette, line.simil, line.path, false, true, "", line.filter, line.match); err != nil { t.Fatal(err) } compareString(t, line.want, out.String()) }) } } func TestProcessTwoSnapshots(t *testing.T) { t.Parallel() out := bytes.Buffer{} in := bytes.Buffer{} in.WriteString("Ya\n") in.Write(internaltest.PanicOutputs()["simple"]) in.WriteString("Ye\n") in.Write(internaltest.PanicOutputs()["int"]) in.WriteString("Yo\n") err := process(&in, &out, &Palette{}, stack.AnyPointer, basePath, false, true, "", nil, nil) if err != nil { t.Fatal(err) } // This is a change detector on main.go. want := ("Ya\n" + "GOTRACEBACK=all\n" + "panic: simple\n\n" + "1: running\n" + " main main.go:74 main()\n" + "Ye\n" + "GOTRACEBACK=all\n" + "panic: 42\n\n" + "1: running\n" + " main main.go:93 panicint(0x2a)\n" + " main main.go:315 glob..func9()\n" + " main main.go:76 main()\n" + "Yo\n") compareString(t, want, out.String()) } func TestMainFn(t *testing.T) { t.Parallel() // It doesn't do anything since stdin is closed. if err := Main(); err != nil { t.Fatal(err) } } // func compareString(t *testing.T, want, got string) { if diff := cmp.Diff(want, got); diff != "" { t.Helper() t.Fatalf("Mismatch (-want +got):\n%s", diff) } } func TestMain(m *testing.M) { flag.Parse() if !testing.Verbose() { log.SetOutput(ioutil.Discard) } // Set the environment variable so the stack doesn't include the info header. os.Setenv("GOTRACEBACK", "all") os.Exit(m.Run()) } panicparse-2.3.1/internal/ui.go000066400000000000000000000134201446536624000164340ustar00rootroot00000000000000// Copyright 2016 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 internal import ( "fmt" "strings" "github.com/maruel/panicparse/v2/stack" ) // Palette defines the color used. // // An empty object Palette{} can be used to disable coloring. type Palette struct { EOLReset string // Routine header. RoutineFirst string // The first routine printed. Routine string // Following routines. CreatedBy string Race string // Call line. Package string SrcFile string FuncMain string FuncLocationUnknown string FuncLocationUnknownExported string FuncGoMod string FuncGoModExported string FuncGOPATH string FuncGOPATHExported string FuncGoPkg string FuncGoPkgExported string FuncStdLib string FuncStdLibExported string Arguments string } // pathFormat determines how much to show. type pathFormat int const ( fullPath pathFormat = iota relPath basePath ) func (pf pathFormat) formatCall(c *stack.Call) string { switch pf { case relPath: if c.RelSrcPath != "" { return fmt.Sprintf("%s:%d", c.RelSrcPath, c.Line) } fallthrough case fullPath: if c.LocalSrcPath != "" { return fmt.Sprintf("%s:%d", c.LocalSrcPath, c.Line) } return fmt.Sprintf("%s:%d", c.RemoteSrcPath, c.Line) default: return fmt.Sprintf("%s:%d", c.SrcName, c.Line) } } func (pf pathFormat) createdByString(s *stack.Signature) string { if len(s.CreatedBy.Calls) == 0 { return "" } return s.CreatedBy.Calls[0].Func.DirName + "." + s.CreatedBy.Calls[0].Func.Name + " @ " + pf.formatCall(&s.CreatedBy.Calls[0]) } // calcBucketsLengths returns the maximum length of the source lines and // package names. func calcBucketsLengths(a *stack.Aggregated, pf pathFormat) (int, int) { srcLen := 0 pkgLen := 0 for _, e := range a.Buckets { for i := range e.Signature.Stack.Calls { if l := len(pf.formatCall(&e.Signature.Stack.Calls[i])); l > srcLen { srcLen = l } if l := len(e.Signature.Stack.Calls[i].Func.DirName); l > pkgLen { pkgLen = l } } } return srcLen, pkgLen } // calcGoroutinesLengths returns the maximum length of the source lines and // package names. func calcGoroutinesLengths(s *stack.Snapshot, pf pathFormat) (int, int) { srcLen := 0 pkgLen := 0 for _, e := range s.Goroutines { for i := range e.Signature.Stack.Calls { if l := len(pf.formatCall(&e.Signature.Stack.Calls[i])); l > srcLen { srcLen = l } if l := len(e.Signature.Stack.Calls[i].Func.DirName); l > pkgLen { pkgLen = l } } } return srcLen, pkgLen } // functionColor returns the color to be used for the function name based on // the type of package the function is in. func (p *Palette) functionColor(c *stack.Call) string { return p.funcColor(c.Location, c.Func.IsPkgMain, c.Func.IsExported) } func (p *Palette) funcColor(l stack.Location, main, exported bool) string { if main { return p.FuncMain } switch l { default: fallthrough case stack.LocationUnknown: if exported { return p.FuncLocationUnknownExported } return p.FuncLocationUnknown case stack.GoMod: if exported { return p.FuncGoModExported } return p.FuncGoMod case stack.GOPATH: if exported { return p.FuncGOPATHExported } return p.FuncGOPATH case stack.GoPkg: if exported { return p.FuncGoPkgExported } return p.FuncGoPkg case stack.Stdlib: if exported { return p.FuncStdLibExported } return p.FuncStdLib } } // routineColor returns the color for the header of the goroutines bucket. func (p *Palette) routineColor(first, multipleBuckets bool) string { if first && multipleBuckets { return p.RoutineFirst } return p.Routine } // BucketHeader prints the header of a goroutine signature. func (p *Palette) BucketHeader(b *stack.Bucket, pf pathFormat, multipleBuckets bool) string { extra := "" if s := b.SleepString(); s != "" { extra += " [" + s + "]" } if b.Locked { extra += " [locked]" } if c := pf.createdByString(&b.Signature); c != "" { extra += p.CreatedBy + " [Created by " + c + "]" } return fmt.Sprintf( "%s%d: %s%s%s\n", p.routineColor(b.First, multipleBuckets), len(b.IDs), b.State, extra, p.EOLReset) } // GoroutineHeader prints the header of a goroutine. func (p *Palette) GoroutineHeader(g *stack.Goroutine, pf pathFormat, multipleGoroutines bool) string { extra := "" if s := g.SleepString(); s != "" { extra += " [" + s + "]" } if g.Locked { extra += " [locked]" } if c := pf.createdByString(&g.Signature); c != "" { extra += p.CreatedBy + " [Created by " + c + "]" } if g.RaceAddr != 0 { r := "read" if g.RaceWrite { r = "write" } extra += fmt.Sprintf("%s%s Race %s @ 0x%08x", p.EOLReset, p.Race, r, g.RaceAddr) } return fmt.Sprintf( "%s%d: %s%s%s\n", p.routineColor(g.First, multipleGoroutines), g.ID, g.State, extra, p.EOLReset) } // callLine prints one stack line. func (p *Palette) callLine(line *stack.Call, srcLen, pkgLen int, pf pathFormat) string { return fmt.Sprintf( " %s%-*s %s%-*s %s%s%s(%s)%s", p.Package, pkgLen, line.Func.DirName, p.SrcFile, srcLen, pf.formatCall(line), p.functionColor(line), line.Func.Name, p.Arguments, &line.Args, p.EOLReset) } // StackLines prints one complete stack trace, without the header. func (p *Palette) StackLines(signature *stack.Signature, srcLen, pkgLen int, pf pathFormat) string { out := make([]string, len(signature.Stack.Calls)) for i := range signature.Stack.Calls { out[i] = p.callLine(&signature.Stack.Calls[i], srcLen, pkgLen, pf) } if signature.Stack.Elided { out = append(out, " (...)") } return strings.Join(out, "\n") + "\n" } panicparse-2.3.1/internal/ui_test.go000066400000000000000000000156121446536624000175000ustar00rootroot00000000000000// Copyright 2016 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 internal import ( "path" "path/filepath" "strings" "testing" "github.com/maruel/panicparse/v2/stack" ) var testPalette = &Palette{ EOLReset: "A", RoutineFirst: "B", Routine: "C", CreatedBy: "D", Package: "E", SrcFile: "F", FuncMain: "G", FuncLocationUnknown: "H", FuncLocationUnknownExported: "I", FuncGoMod: "J", FuncGoModExported: "K", FuncGOPATH: "L", FuncGOPATHExported: "M", FuncGoPkg: "N", FuncGoPkgExported: "O", FuncStdLib: "P", FuncStdLibExported: "Q", Arguments: "R", } func TestCalcBucketsLengths(t *testing.T) { t.Parallel() a := stack.Aggregated{ Buckets: []*stack.Bucket{ { Signature: stack.Signature{ Stack: stack.Stack{ Calls: []stack.Call{ newCallLocal("main.func·001", stack.Args{}, "/home/user/go/src/foo/baz.go", 123), }, }, }, IDs: []int{}, First: true, }, }, } srcLen, pkgLen := calcBucketsLengths(&a, fullPath) // When printing, it prints the remote path, not the transposed local path. compareString(t, "/home/user/go/src/foo/baz.go:123", fullPath.formatCall(&a.Buckets[0].Signature.Stack.Calls[0])) compareInt(t, len("/home/user/go/src/foo/baz.go:123"), srcLen) compareString(t, "main", a.Buckets[0].Signature.Stack.Calls[0].Func.ImportPath) compareInt(t, len("main"), pkgLen) srcLen, pkgLen = calcBucketsLengths(&a, basePath) compareString(t, "baz.go:123", basePath.formatCall(&a.Buckets[0].Signature.Stack.Calls[0])) compareInt(t, len("baz.go:123"), srcLen) compareString(t, "main", a.Buckets[0].Signature.Stack.Calls[0].Func.ImportPath) compareInt(t, len("main"), pkgLen) } func TestBucketHeader(t *testing.T) { t.Parallel() b := stack.Bucket{ Signature: stack.Signature{ State: "chan receive", CreatedBy: stack.Stack{ Calls: []stack.Call{ newCallLocal("main.mainImpl", stack.Args{}, "/home/user/go/src/github.com/foo/bar/baz.go", 74), }, }, SleepMax: 6, SleepMin: 2, }, IDs: []int{1, 2}, First: true, } // When printing, it prints the remote path, not the transposed local path. compareString(t, "B2: chan receive [2~6 minutes]D [Created by main.mainImpl @ /home/user/go/src/github.com/foo/bar/baz.go:74]A\n", testPalette.BucketHeader(&b, fullPath, true)) compareString(t, "C2: chan receive [2~6 minutes]D [Created by main.mainImpl @ /home/user/go/src/github.com/foo/bar/baz.go:74]A\n", testPalette.BucketHeader(&b, fullPath, false)) compareString(t, "B2: chan receive [2~6 minutes]D [Created by main.mainImpl @ github.com/foo/bar/baz.go:74]A\n", testPalette.BucketHeader(&b, relPath, true)) compareString(t, "C2: chan receive [2~6 minutes]D [Created by main.mainImpl @ github.com/foo/bar/baz.go:74]A\n", testPalette.BucketHeader(&b, relPath, false)) compareString(t, "B2: chan receive [2~6 minutes]D [Created by main.mainImpl @ baz.go:74]A\n", testPalette.BucketHeader(&b, basePath, true)) compareString(t, "C2: chan receive [2~6 minutes]D [Created by main.mainImpl @ baz.go:74]A\n", testPalette.BucketHeader(&b, basePath, false)) b = stack.Bucket{ Signature: stack.Signature{ State: "b0rked", SleepMax: 6, SleepMin: 6, Locked: true, }, IDs: []int{}, First: true, } compareString(t, "C0: b0rked [6 minutes] [locked]A\n", testPalette.BucketHeader(&b, basePath, false)) } func TestStackLines(t *testing.T) { t.Parallel() s := &stack.Signature{ State: "idle", Stack: stack.Stack{ Calls: []stack.Call{ newCallLocal( "runtime.Epollwait", stack.Args{ Values: []stack.Arg{ {Value: 4}, {Value: 0x7fff671c7118}, {Value: 0xffffffff00000080}, {}, {Value: 0xffffffff0028c1be}, {}, {}, {}, {}, {}, }, Elided: true, }, "/goroot/src/runtime/sys_linux_amd64.s", 400), newCallLocal( "runtime.netpoll", stack.Args{Values: []stack.Arg{{Value: 0x901b01}, {}}}, "/goroot/src/runtime/netpoll_epoll.go", 68), newCallLocal( "main.Main", stack.Args{Values: []stack.Arg{{Value: 0xc208012000}}}, "/home/user/go/src/main.go", 1472), newCallLocal( "foo.OtherExported", stack.Args{}, "/home/user/go/src/foo/bar.go", 1575), newCallLocal( "foo.otherPrivate", stack.Args{}, "/home/user/go/src/foo/bar.go", 10), }, Elided: true, }, } // When printing, it prints the remote path, not the transposed local path. want := "" + " Eruntime F/goroot/src/runtime/sys_linux_amd64.s:400 QEpollwaitR(4, 0x7fff671c7118, 0xffffffff00000080, 0, 0xffffffff0028c1be, 0, 0, 0, 0, 0, ...)A\n" + " Eruntime F/goroot/src/runtime/netpoll_epoll.go:68 PnetpollR(0x901b01, 0)A\n" + " Emain F/home/user/go/src/main.go:1472 GMainR(0xc208012000)A\n" + " Efoo F/home/user/go/src/foo/bar.go:1575 MOtherExportedR()A\n" + " Efoo F/home/user/go/src/foo/bar.go:10 LotherPrivateR()A\n" + " (...)\n" compareString(t, want, testPalette.StackLines(s, 10, 10, fullPath)) want = "" + " Eruntime Fsys_linux_amd64.s:400 QEpollwaitR(4, 0x7fff671c7118, 0xffffffff00000080, 0, 0xffffffff0028c1be, 0, 0, 0, 0, 0, ...)A\n" + " Eruntime Fnetpoll_epoll.go:68 PnetpollR(0x901b01, 0)A\n" + " Emain Fmain.go:1472 GMainR(0xc208012000)A\n" + " Efoo Fbar.go:1575 MOtherExportedR()A\n" + " Efoo Fbar.go:10 LotherPrivateR()A\n" + " (...)\n" compareString(t, want, testPalette.StackLines(s, 10, 10, basePath)) } // func newFunc(s string) stack.Func { f := stack.Func{} if err := f.Init(s); err != nil { panic(err) } return f } func newCallLocal(f string, a stack.Args, s string, l int) stack.Call { c := stack.Call{Func: newFunc(f), Args: a, RemoteSrcPath: s, Line: l} // Do the equivalent of Call.init(). c.SrcName = filepath.Base(c.RemoteSrcPath) c.DirSrc = path.Join(filepath.Base(c.RemoteSrcPath[:len(c.RemoteSrcPath)-len(c.SrcName)-1]), c.SrcName) const goroot = "/goroot/src/" const gopath = "/home/user/go/src/" const gopathmod = "/home/user/go/pkg/mod/" // Do the equivalent of Call.updateLocations(). if strings.HasPrefix(s, goroot) { c.LocalSrcPath = s c.RelSrcPath = s[len(goroot):] c.Location = stack.Stdlib } else if strings.HasPrefix(s, gopath) { c.LocalSrcPath = s c.RelSrcPath = s[len(gopath):] c.Location = stack.GOPATH } else if strings.HasPrefix(s, gopathmod) { c.LocalSrcPath = s c.RelSrcPath = s[len(gopathmod):] c.Location = stack.GoPkg } return c } func compareInt(t *testing.T, want, got int) { if want != got { t.Helper() t.Fatalf("%d != %d", want, got) } } panicparse-2.3.1/main.go000066400000000000000000000011241446536624000151250ustar00rootroot00000000000000// Copyright 2015 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. // panicparse: analyzes stack dump of Go processes and simplifies it. // // It is mostly useful on servers will large number of identical goroutines, // making the crash dump harder to read than strictly necessary. package main import ( "fmt" "os" "github.com/maruel/panicparse/v2/internal" ) func main() { if err := internal.Main(); err != nil { fmt.Fprintf(os.Stderr, "Failed: %s\n", err) os.Exit(1) } } panicparse-2.3.1/stack/000077500000000000000000000000001446536624000147615ustar00rootroot00000000000000panicparse-2.3.1/stack/OFL.txt000066400000000000000000000107671446536624000161550ustar00rootroot00000000000000Copyright (c) , (), with Reserved Font Name . Copyright (c) , (), with Reserved Font Name . Copyright (c) , (). This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL ----------------------------------------------------------- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ----------------------------------------------------------- PREAMBLE The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. DEFINITIONS "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. "Reserved Font Name" refers to any names specified as such after the copyright statement(s). "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. PERMISSION & CONDITIONS Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. TERMINATION This license becomes null and void if any of the above conditions are not met. DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. panicparse-2.3.1/stack/README.md000066400000000000000000000007201446536624000162370ustar00rootroot00000000000000# Templates Contains an HTML template and an icon. emoji_u1f4a3.png is the bomb emoji U+1F4A3 in Noto Emoji as a PNG. emoji_u1f4a3_64.gif is the product of reducing the image to 64x64 and exporting as GIF. ## Source It can be found online at https://github.com/googlefonts/noto-emoji/blob/HEAD/png/128/emoji_u1f4a3.png ## License Found in [OFL.txt](OFL.txt). An online copy can be retrieved at http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL panicparse-2.3.1/stack/bucket.go000066400000000000000000000057501446536624000165740ustar00rootroot00000000000000// Copyright 2015 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 ( "sort" ) // Similarity is the level at which two call lines arguments must match to be // considered similar enough to coalesce them. type Similarity int const ( // ExactFlags requires same bits (e.g. Locked). ExactFlags Similarity = iota // ExactLines requests the exact same arguments on the call line. ExactLines // AnyPointer considers different pointers a similar call line. AnyPointer // AnyValue accepts any value as similar call line. AnyValue ) // Aggregated is a list of Bucket sorted by repetition count. type Aggregated struct { // Snapshot is a pointer to the structure that was used to generate these // buckets. *Snapshot Buckets []*Bucket // Disallow initialization with unnamed parameters. _ struct{} } // Aggregate merges similar goroutines into buckets. // // The buckets are ordered in library provided order of relevancy. You can // reorder at your choosing. func (s *Snapshot) Aggregate(similar Similarity) *Aggregated { type count struct { ids []int first bool } b := map[*Signature]*count{} // O(n²). Fix eventually. for _, routine := range s.Goroutines { found := false for key, c := range b { // When a match is found, this effectively drops the other goroutine ID. if key.similar(&routine.Signature, similar) { found = true c.ids = append(c.ids, routine.ID) c.first = c.first || routine.First if !key.equal(&routine.Signature) { // Almost but not quite equal. There's different pointers passed // around but the same values. Zap out the different values. newKey := key.merge(&routine.Signature) b[newKey] = c delete(b, key) } break } } if !found { // Create a copy of the Signature, since it will be mutated. key := &Signature{} *key = routine.Signature b[key] = &count{ids: []int{routine.ID}, first: routine.First} } } bs := make([]*Bucket, 0, len(b)) for signature, c := range b { sort.Ints(c.ids) bs = append(bs, &Bucket{Signature: *signature, IDs: c.ids, First: c.first}) } // Do reverse sort. sort.SliceStable(bs, func(i, j int) bool { l := bs[i] r := bs[j] if l.First || r.First { return l.First } if l.Signature.less(&r.Signature) { return true } if r.Signature.less(&l.Signature) { return false } return len(r.IDs) > len(l.IDs) }) return &Aggregated{ Snapshot: s, Buckets: bs, } } // Bucket is a stack trace signature and the list of goroutines that fits this // signature. type Bucket struct { // Signature is the generalized signature for this bucket. Signature // IDs is the ID of each Goroutine with this Signature. IDs []int // First is true if this Bucket contains the first goroutine, e.g. the one // Signature that likely generated the panic() call, if any. First bool // Disallow initialization with unnamed parameters. _ struct{} } panicparse-2.3.1/stack/bucket_test.go000066400000000000000000000227361446536624000176360ustar00rootroot00000000000000// 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 ( "bytes" "io" "io/ioutil" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/maruel/panicparse/v2/internal/internaltest" ) func TestAggregateNotAggressive(t *testing.T) { t.Parallel() // 2 goroutines with similar but not exact same signature. data := []string{ "panic: runtime error: index out of range", "", "goroutine 6 [chan receive]:", "main.func·001({0x11000000, 2}, 3)", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:72 +0x49", "", "goroutine 7 [chan receive]:", "main.func·001({0x21000000, 2}, 3)", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:72 +0x49", "", } s, suffix, err := ScanSnapshot(bytes.NewBufferString(strings.Join(data, "\n")), ioutil.Discard, defaultOpts()) if err != io.EOF { t.Fatal(err) } if s == nil { t.Fatal("expected snapshot") } want := []*Bucket{ { Signature: Signature{ State: "chan receive", Stack: Stack{ Calls: []Call{ newCall( "main.func·001", Args{Values: []Arg{ {IsAggregate: true, Fields: Args{ Values: []Arg{{Value: 0x11000000, IsPtr: true}, {Value: 2}}, }}, {Value: 3}, }}, "/gopath/src/github.com/maruel/panicparse/stack/stack.go", 72), }, }, }, IDs: []int{6}, First: true, }, { Signature: Signature{ State: "chan receive", Stack: Stack{ Calls: []Call{ newCall( "main.func·001", Args{Values: []Arg{ {IsAggregate: true, Fields: Args{ Values: []Arg{{Value: 0x21000000, Name: "#1", IsPtr: true}, {Value: 2}}, }}, {Value: 3}, }}, "/gopath/src/github.com/maruel/panicparse/stack/stack.go", 72), }, }, }, IDs: []int{7}, }, } a := s.Aggregate(ExactLines) compareBuckets(t, want, a.Buckets) if a.Snapshot != s { t.Fatal("unexpected snapshot") } compareString(t, "", string(suffix)) } func TestAggregateExactMatching(t *testing.T) { t.Parallel() // 2 goroutines with the exact same signature. data := []string{ "panic: runtime error: index out of range", "", "goroutine 6 [chan receive]:", "main.func·001({0x11000000, 2}, 3)", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:72 +0x49", "created by main.mainImpl", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:74 +0xeb", "", "goroutine 7 [chan receive]:", "main.func·001({0x11000000, 2}, 3)", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:72 +0x49", "created by main.mainImpl", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:74 +0xeb", "", } s, suffix, err := ScanSnapshot(bytes.NewBufferString(strings.Join(data, "\n")), ioutil.Discard, defaultOpts()) if err != io.EOF { t.Fatal(err) } if s == nil { t.Fatal("expected snapshot") } want := []*Bucket{ { Signature: Signature{ State: "chan receive", CreatedBy: Stack{ Calls: []Call{ newCall( "main.mainImpl", Args{}, "/gopath/src/github.com/maruel/panicparse/stack/stack.go", 74), }, }, Stack: Stack{ Calls: []Call{ newCall( "main.func·001", Args{Values: []Arg{ {IsAggregate: true, Fields: Args{ Values: []Arg{{Value: 0x11000000, Name: "#1", IsPtr: true}, {Value: 2}}, }}, {Value: 3}, }}, "/gopath/src/github.com/maruel/panicparse/stack/stack.go", 72), }, }, }, IDs: []int{6, 7}, First: true, }, } compareBuckets(t, want, s.Aggregate(ExactLines).Buckets) compareString(t, "", string(suffix)) } func TestAggregateAggressive(t *testing.T) { t.Parallel() // 3 goroutines with similar signatures. data := []string{ "panic: runtime error: index out of range", "", "goroutine 6 [chan receive, 10 minutes]:", "main.func·001({0x11000000, 2}, 3)", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:72 +0x49", "", "goroutine 7 [chan receive, 50 minutes]:", "main.func·001({0x21000000, 2}, 3)", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:72 +0x49", "", "goroutine 8 [chan receive, 100 minutes]:", "main.func·001({0x31000000, 2}, 3)", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:72 +0x49", "", } s, suffix, err := ScanSnapshot(bytes.NewBufferString(strings.Join(data, "\n")), ioutil.Discard, defaultOpts()) if err != io.EOF { t.Fatal(err) } if s == nil { t.Fatal("expected snapshot") } want := []*Bucket{ { Signature: Signature{ State: "chan receive", SleepMin: 10, SleepMax: 100, Stack: Stack{ Calls: []Call{ newCall( "main.func·001", Args{Values: []Arg{ {IsAggregate: true, Fields: Args{ Values: []Arg{{Value: 0x11000000, Name: "*", IsPtr: true}, {Value: 2}}, }}, {Value: 3}, }}, "/gopath/src/github.com/maruel/panicparse/stack/stack.go", 72), }, }, }, IDs: []int{6, 7, 8}, First: true, }, } compareBuckets(t, want, s.Aggregate(AnyPointer).Buckets) compareString(t, "", string(suffix)) } func TestAggregateDeadlockPanic(t *testing.T) { t.Parallel() // Test for crash found at https://github.com/maruel/panicparse/issues/56. data := []string{ "panic: deadlock detected at fmut", "", "goroutine 11 [select, 55 minutes]:", "foo.Bar()", " foo/foo.go:467 +0x2b8", "foo.baz(0x3)", " foo/foo.go:643 +0x69", "created by main", " foo/foo.go:631 +0x4b", "", "goroutine 52 [select, 55 minutes]:", "foo.Bar()", " foo/foo.go:467 +0x2b8", "created by bozo", " foo/foo.go:420 +0x33", "", "goroutine 55 [select, 55 minutes]:", "foo.Bar()", " foo/foo.go:467 +0x2b8", "foo.baz(0x1)", " foo/foo.go:643 +0x69", "created by main", " foo/foo.go:631 +0x4b", } s, suffix, err := ScanSnapshot(bytes.NewBufferString(strings.Join(data, "\n")), ioutil.Discard, defaultOpts()) if err != io.EOF { t.Fatal(err) } if s == nil { t.Fatal("expected snapshot") } want := []*Bucket{ { Signature: Signature{ State: "select", CreatedBy: Stack{Calls: []Call{ { Func: Func{Complete: "main", Name: "main"}, RemoteSrcPath: "foo/foo.go", Line: 631, SrcName: "foo.go", }, }}, SleepMin: 55, SleepMax: 55, Stack: Stack{ Calls: []Call{ { Func: Func{ Complete: "foo.Bar", ImportPath: "foo", DirName: "foo", Name: "Bar", IsExported: true, }, RemoteSrcPath: "foo/foo.go", Line: 467, SrcName: "foo.go", ImportPath: "foo", }, { Func: Func{Complete: "foo.baz", ImportPath: "foo", DirName: "foo", Name: "baz"}, Args: Args{Values: []Arg{{Value: 3}}}, RemoteSrcPath: "foo/foo.go", Line: 643, SrcName: "foo.go", ImportPath: "foo", }, }, }, }, IDs: []int{11}, First: true, }, { Signature: Signature{ State: "select", CreatedBy: Stack{Calls: []Call{ { Func: Func{Complete: "main", Name: "main"}, RemoteSrcPath: "foo/foo.go", Line: 631, SrcName: "foo.go", }, }}, SleepMin: 55, SleepMax: 55, Stack: Stack{ Calls: []Call{ { Func: Func{ Complete: "foo.Bar", ImportPath: "foo", DirName: "foo", Name: "Bar", IsExported: true, }, RemoteSrcPath: "foo/foo.go", Line: 467, SrcName: "foo.go", ImportPath: "foo", }, { Func: Func{Complete: "foo.baz", ImportPath: "foo", DirName: "foo", Name: "baz"}, Args: Args{Values: []Arg{{Value: 1}}}, RemoteSrcPath: "foo/foo.go", Line: 643, SrcName: "foo.go", ImportPath: "foo", }, }, }, }, IDs: []int{55}, }, { Signature: Signature{ State: "select", CreatedBy: Stack{ Calls: []Call{ { Func: Func{Complete: "bozo", Name: "bozo"}, RemoteSrcPath: "foo/foo.go", Line: 420, SrcName: "foo.go", }, }, }, SleepMin: 55, SleepMax: 55, Stack: Stack{ Calls: []Call{ { Func: Func{ Complete: "foo.Bar", ImportPath: "foo", DirName: "foo", Name: "Bar", IsExported: true, }, RemoteSrcPath: "foo/foo.go", Line: 467, SrcName: "foo.go", ImportPath: "foo", }, }, }, }, IDs: []int{52}, }, } compareBuckets(t, want, s.Aggregate(AnyPointer).Buckets) compareString(t, "", string(suffix)) } func BenchmarkAggregate(b *testing.B) { b.ReportAllocs() s, suffix, err := ScanSnapshot(bytes.NewReader(internaltest.StaticPanicwebOutput()), ioutil.Discard, defaultOpts()) if err != io.EOF { b.Fatal(err) } if s == nil { b.Fatal("missing context") } if string(suffix) != "" { b.Fatalf("unexpected suffix: %q", string(suffix)) } b.ResetTimer() for i := 0; i < b.N; i++ { buckets := s.Aggregate(AnyPointer).Buckets if len(buckets) < 5 { b.Fatal("expected more buckets") } } } func compareBuckets(t *testing.T, want, got []*Bucket) { if diff := cmp.Diff(want, got); diff != "" { t.Helper() t.Fatalf("Bucket mismatch (-want +got):\n%s", diff) } } panicparse-2.3.1/stack/context.go000066400000000000000000001046121446536624000170000ustar00rootroot00000000000000// 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 "". 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| +)(\\?\\?|\\|.+\\.(?: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. // ", \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: ""}} // 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)) } panicparse-2.3.1/stack/context_test.go000066400000000000000000002060621446536624000200410ustar00rootroot00000000000000// 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" "errors" "fmt" "io" "io/ioutil" "os" "os/exec" "path" "path/filepath" "regexp" "runtime" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/maruel/panicparse/v2/internal/internaltest" ) func TestScanSnapshotErr(t *testing.T) { t.Parallel() data := []*Opts{ nil, {LocalGOROOT: "\\"}, {LocalGOPATHs: []string{"\\"}}, } for _, opts := range data { if _, _, err := ScanSnapshot(&bytes.Buffer{}, ioutil.Discard, opts); err == nil { t.Fatal("expected error") } } } func TestScanSnapshotSynthetic(t *testing.T) { t.Parallel() data := []struct { name string in []string prefix string suffix string err error want []*Goroutine }{ { name: "Nothing", err: io.EOF, }, { name: "NothingEmpty", in: make([]string, 111), prefix: strings.Repeat("\n", 110), err: io.EOF, }, { name: "NothingLong", in: []string{strings.Repeat("a", bufio.MaxScanTokenSize+10)}, prefix: strings.Repeat("a", bufio.MaxScanTokenSize+10), err: io.EOF, }, // One call from main, one from stdlib, one from third party. // Create a long first line that will be ignored. It is to guard against // https://github.com/maruel/panicparse/issues/17. { name: "long,main,stdlib,third", in: []string{ strings.Repeat("a", bufio.MaxScanTokenSize+1), "panic: reflect.Set: value of type", "", "goroutine 1 [running]:", "github.com/cockroachdb/cockroach/storage/engine._Cfunc_DBIterSeek()", " ??:0 +0x6d", "gopkg.in/yaml%2ev2.handleErr(0x433b20)", "\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6", "reflect.Value.assignTo(0x570860, 0x803f3e0, 0x15)", "\t/goroot/src/reflect/value.go:2125 +0x368", "main.main()", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:428 +0x27", "", }, prefix: strings.Repeat("a", bufio.MaxScanTokenSize+1) + "\npanic: reflect.Set: value of type\n\n", err: io.EOF, want: []*Goroutine{ { Signature: Signature{ State: "running", Stack: Stack{ Calls: []Call{ newCall( "github.com/cockroachdb/cockroach/storage/engine._Cfunc_DBIterSeek", Args{}, "??", 0), newCall( "gopkg.in/yaml%2ev2.handleErr", Args{Values: []Arg{{Value: 0x433b20, IsPtr: true}}}, "/gopath/src/gopkg.in/yaml.v2/yaml.go", 153), newCall( "reflect.Value.assignTo", Args{Values: []Arg{{Value: 0x570860, IsPtr: true}, {Value: 0x803f3e0, IsPtr: true}, {Value: 0x15}}}, "/goroot/src/reflect/value.go", 2125), newCall( "main.main", Args{}, "/gopath/src/github.com/maruel/panicparse/stack/stack.go", 428), }, }, }, ID: 1, First: true, }, }, }, { name: "LongWait", in: []string{ "panic: bleh", "", "goroutine 1 [chan send, 100 minutes]:", "gopkg.in/yaml%2ev2.handleErr(0x433b20)", "\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6", "", "goroutine 2 [chan send, locked to thread]:", "gopkg.in/yaml%2ev2.handleErr(0x8033b21)", "\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6", "", "goroutine 3 [chan send, 101 minutes, locked to thread]:", "gopkg.in/yaml%2ev2.handleErr(0x8033b22)", "\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6", "", }, prefix: "panic: bleh\n\n", err: io.EOF, want: []*Goroutine{ { Signature: Signature{ State: "chan send", SleepMin: 100, SleepMax: 100, Stack: Stack{ Calls: []Call{ newCall( "gopkg.in/yaml%2ev2.handleErr", Args{Values: []Arg{{Value: 0x433b20, IsPtr: true}}}, "/gopath/src/gopkg.in/yaml.v2/yaml.go", 153), }, }, }, ID: 1, First: true, }, { Signature: Signature{ State: "chan send", Locked: true, Stack: Stack{ Calls: []Call{ newCall( "gopkg.in/yaml%2ev2.handleErr", Args{Values: []Arg{{Value: 0x8033b21, Name: "#1", IsPtr: true}}}, "/gopath/src/gopkg.in/yaml.v2/yaml.go", 153), }, }, }, ID: 2, }, { Signature: Signature{ State: "chan send", SleepMin: 101, SleepMax: 101, Stack: Stack{ Calls: []Call{ newCall( "gopkg.in/yaml%2ev2.handleErr", Args{Values: []Arg{{Value: 0x8033b22, Name: "#2", IsPtr: true}}}, "/gopath/src/gopkg.in/yaml.v2/yaml.go", 153), }, }, Locked: true, }, ID: 3, }, }, }, { name: "Assembly", in: []string{ "panic: reflect.Set: value of type", "", "goroutine 16 [garbage collection]:", "runtime.switchtoM()", "\t/goroot/src/runtime/asm_amd64.s:198 fp=0xc20cfb80d8 sp=0xc20cfb80d0", "", }, prefix: "panic: reflect.Set: value of type\n\n", err: io.EOF, want: []*Goroutine{ { Signature: Signature{ State: "garbage collection", Stack: Stack{ Calls: []Call{ newCall( "runtime.switchtoM", Args{}, "/goroot/src/runtime/asm_amd64.s", 198), }, }, }, ID: 16, First: true, }, }, }, { name: "Assembly1.3", in: []string{ "panic: reflect.Set: value of type", "", "goroutine 16 [garbage collection]:", "runtime.switchtoM()", "\t/goroot/src/runtime/asm_amd64.s:198 fp=0xc20cfb80d8 sp=0xc20cfb80d0 pc=0x5007be", "", }, prefix: "panic: reflect.Set: value of type\n\n", err: io.EOF, want: []*Goroutine{ { Signature: Signature{ State: "garbage collection", Stack: Stack{ Calls: []Call{ newCall( "runtime.switchtoM", Args{}, "/goroot/src/runtime/asm_amd64.s", 198), }, }, }, ID: 16, First: true, }, }, }, { name: "LineErr", in: []string{ "panic: reflect.Set: value of type", "", "goroutine 1 [running]:", "github.com/maruel/panicparse/stack/stack.recurseType()", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:12345678901234567890", "", }, prefix: "panic: reflect.Set: value of type\n\n", suffix: "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:12345678901234567890\n", err: errors.New("failed to parse int on line: \"/gopath/src/github.com/maruel/panicparse/stack/stack.go:12345678901234567890\""), want: []*Goroutine{ { Signature: Signature{ State: "running", Stack: Stack{ Calls: []Call{ newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0), }, }, }, ID: 1, First: true, }, }, }, { name: "CreatedErr", in: []string{ "panic: reflect.Set: value of type", "", "goroutine 1 [running]:", "github.com/maruel/panicparse/stack/stack.recurseType()", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:1", "created by testing.RunTests", "\t/goroot/src/testing/testing.go:123456789012345678901 +0xa8b", "", }, prefix: "panic: reflect.Set: value of type\n\n", suffix: "\t/goroot/src/testing/testing.go:123456789012345678901 +0xa8b\n", err: errors.New("failed to parse int on line: \"/goroot/src/testing/testing.go:123456789012345678901 +0xa8b\""), want: []*Goroutine{ { Signature: Signature{ State: "running", CreatedBy: Stack{Calls: []Call{newCall("testing.RunTests", Args{}, "", 0)}}, Stack: Stack{ Calls: []Call{ newCall( "github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "/gopath/src/github.com/maruel/panicparse/stack/stack.go", 1), }, }, }, ID: 1, First: true, }, }, }, { name: "ValueErr", in: []string{ "panic: reflect.Set: value of type", "", "goroutine 1 [running]:", "github.com/maruel/panicparse/stack/stack.recurseType(123456789012345678901)", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9", "", }, prefix: "panic: reflect.Set: value of type\n\n", suffix: "github.com/maruel/panicparse/stack/stack.recurseType(123456789012345678901)\n" + "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9\n", err: errors.New("failed to parse int on line: \"github.com/maruel/panicparse/stack/stack.recurseType(123456789012345678901)\""), want: []*Goroutine{ { Signature: Signature{ State: "running", Stack: Stack{ Calls: []Call{ newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0), }, }, }, ID: 1, First: true, }, }, }, { name: "MaxNestingDepth", in: []string{ "panic: reflect.Set: value of type", "", "goroutine 1 [running]:", "github.com/maruel/panicparse/stack/stack.recurseType({{{{{...}}}}})", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9", "", }, prefix: "panic: reflect.Set: value of type\n\n", err: io.EOF, want: []*Goroutine{ { Signature: Signature{ State: "running", Stack: Stack{ Calls: []Call{ newCall( "github.com/maruel/panicparse/stack/stack.recurseType", Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Elided: true, }}}, }}}, }}}, }}}, }}}, }, "/gopath/src/github.com/maruel/panicparse/stack/stack.go", 9), }, }, }, ID: 1, First: true, }, }, }, { name: "MaxNestingDepthExceededErr", in: []string{ "panic: reflect.Set: value of type", "", "goroutine 1 [running]:", "github.com/maruel/panicparse/stack/stack.recurseType({{{{{{...}}}}}})", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9", "", }, prefix: "panic: reflect.Set: value of type\n\n", suffix: "github.com/maruel/panicparse/stack/stack.recurseType({{{{{{...}}}}}})\n" + "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9\n", err: errors.New("nested aggregate-typed arguments exceeded depth limit on line: \"github.com/maruel/panicparse/stack/stack.recurseType({{{{{{...}}}}}})\""), want: []*Goroutine{ { Signature: Signature{ State: "running", Stack: Stack{ Calls: []Call{ newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0), }, }, }, ID: 1, First: true, }, }, }, { name: "UnmatchedOpeningCurlyBracketErr", in: []string{ "panic: reflect.Set: value of type", "", "goroutine 1 [running]:", "github.com/maruel/panicparse/stack/stack.recurseType({{{{{...}}}})", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9", "", }, prefix: "panic: reflect.Set: value of type\n\n", suffix: "github.com/maruel/panicparse/stack/stack.recurseType({{{{{...}}}})\n" + "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9\n", err: errors.New("unmatched opening curly bracket on line: \"github.com/maruel/panicparse/stack/stack.recurseType({{{{{...}}}})\""), want: []*Goroutine{ { Signature: Signature{ State: "running", Stack: Stack{ Calls: []Call{ newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0), }, }, }, ID: 1, First: true, }, }, }, { name: "UnmatchedClosingCurlyBracketErr", in: []string{ "panic: reflect.Set: value of type", "", "goroutine 1 [running]:", "github.com/maruel/panicparse/stack/stack.recurseType({{{{...}}}}})", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9", "", }, prefix: "panic: reflect.Set: value of type\n\n", suffix: "github.com/maruel/panicparse/stack/stack.recurseType({{{{...}}}}})\n" + "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9\n", err: errors.New("unmatched closing curly bracket on line: \"github.com/maruel/panicparse/stack/stack.recurseType({{{{...}}}}})\""), want: []*Goroutine{ { Signature: Signature{ State: "running", Stack: Stack{ Calls: []Call{ newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0), }, }, }, ID: 1, First: true, }, }, }, { name: "InconsistentIndent", in: []string{ " goroutine 1 [running]:", " github.com/maruel/panicparse/stack/stack.recurseType()", " \t/gopath/src/github.com/maruel/panicparse/stack/stack.go:1", "", }, suffix: " \t/gopath/src/github.com/maruel/panicparse/stack/stack.go:1\n", err: errors.New(`inconsistent indentation: " \t/gopath/src/github.com/maruel/panicparse/stack/stack.go:1", expected " "`), want: []*Goroutine{ { Signature: Signature{ State: "running", Stack: Stack{ Calls: []Call{ newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0), }, }, }, ID: 1, First: true, }, }, }, { name: "OrderErr", in: []string{ "panic: reflect.Set: value of type", "", "goroutine 16 [garbage collection]:", "\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6", "runtime.switchtoM()", "\t/goroot/src/runtime/asm_amd64.s:198 fp=0xc20cfb80d8 sp=0xc20cfb80d0", "", }, prefix: "panic: reflect.Set: value of type\n\n", suffix: "\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6\n" + "runtime.switchtoM()\n" + "\t/goroot/src/runtime/asm_amd64.s:198 fp=0xc20cfb80d8 sp=0xc20cfb80d0\n", err: errors.New("expected a function after a goroutine header, got: \"/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6\""), want: []*Goroutine{ { Signature: Signature{State: "garbage collection"}, ID: 16, First: true, }, }, }, { name: "Elided", in: []string{ "panic: reflect.Set: value of type", "", "goroutine 16 [garbage collection]:", "github.com/maruel/panicparse/stack/stack.recurseType(0x9a3ec70, 0x8062580, 0x9a3e818, 0x50a820, 0x803a8a0)", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:53 +0x845 fp=0xc20cfc66d8 sp=0xc20cfc6470", "...additional frames elided...", "created by testing.RunTests", "\t/goroot/src/testing/testing.go:555 +0xa8b", "", }, prefix: "panic: reflect.Set: value of type\n\n", err: io.EOF, want: []*Goroutine{ { Signature: Signature{ State: "garbage collection", CreatedBy: Stack{ Calls: []Call{ newCall( "testing.RunTests", Args{}, "/goroot/src/testing/testing.go", 555), }, }, Stack: Stack{ Calls: []Call{ newCall( "github.com/maruel/panicparse/stack/stack.recurseType", Args{ Values: []Arg{ {Value: 0x9a3ec70, IsPtr: true}, {Value: 0x8062580, IsPtr: true}, {Value: 0x9a3e818, IsPtr: true}, {Value: 0x50a820, IsPtr: true}, {Value: 0x803a8a0, IsPtr: true}, }, }, "/gopath/src/github.com/maruel/panicparse/stack/stack.go", 53), }, Elided: true, }, }, ID: 16, First: true, }, }, }, { name: "Syscall", in: []string{ "panic: reflect.Set: value of type", "", "goroutine 5 [syscall]:", "runtime.notetsleepg(0x918100, 0xffffffffffffffff, 0x1)", "\t/goroot/src/runtime/lock_futex.go:201 +0x52 fp=0xc208018f68 sp=0xc208018f40", "runtime.signal_recv(0x0)", "\t/goroot/src/runtime/sigqueue.go:109 +0x135 fp=0xc208018fa0 sp=0xc208018f68", "os/signal.loop()", "\t/goroot/src/os/signal/signal_unix.go:21 +0x1f fp=0xc208018fe0 sp=0xc208018fa0", "runtime.goexit()", "\t/goroot/src/runtime/asm_amd64.s:2232 +0x1 fp=0xc208018fe8 sp=0xc208018fe0", "created by os/signal.init·1", "\t/goroot/src/os/signal/signal_unix.go:27 +0x35", "", }, prefix: "panic: reflect.Set: value of type\n\n", err: io.EOF, want: []*Goroutine{ { Signature: Signature{ State: "syscall", CreatedBy: Stack{ Calls: []Call{ newCall( "os/signal.init·1", Args{}, "/goroot/src/os/signal/signal_unix.go", 27), }, }, Stack: Stack{ Calls: []Call{ newCall( "runtime.notetsleepg", Args{ Values: []Arg{ {Value: 0x918100, IsPtr: true}, {Value: 0xffffffffffffffff}, {Value: 0x1}, }, }, "/goroot/src/runtime/lock_futex.go", 201), newCall( "runtime.signal_recv", Args{Values: []Arg{{}}}, "/goroot/src/runtime/sigqueue.go", 109), newCall( "os/signal.loop", Args{}, "/goroot/src/os/signal/signal_unix.go", 21), newCall( "runtime.goexit", Args{}, "/goroot/src/runtime/asm_amd64.s", 2232), }, }, }, ID: 5, First: true, }, }, }, { name: "UnavailCreated", in: []string{ "panic: reflect.Set: value of type", "", "goroutine 24 [running]:", "\tgoroutine running on other thread; stack unavailable", "created by github.com/maruel/panicparse/stack.New", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:131 +0x381", "", }, prefix: "panic: reflect.Set: value of type\n\n", err: io.EOF, want: []*Goroutine{ { Signature: Signature{ State: "running", CreatedBy: Stack{ Calls: []Call{ newCall( "github.com/maruel/panicparse/stack.New", Args{}, "/gopath/src/github.com/maruel/panicparse/stack/stack.go", 131), }, }, Stack: Stack{ Calls: []Call{newCall("", Args{}, "", 0)}, }, }, ID: 24, First: true, }, }, }, { name: "Unavail", in: []string{ "panic: reflect.Set: value of type", "", "goroutine 24 [running]:", "\tgoroutine running on other thread; stack unavailable", "", "", }, prefix: "panic: reflect.Set: value of type\n\n", err: io.EOF, want: []*Goroutine{ { Signature: Signature{ State: "running", Stack: Stack{ Calls: []Call{newCall("", Args{}, "", 0)}, }, }, ID: 24, First: true, }, }, }, { name: "UnavailError", in: []string{ "panic: reflect.Set: value of type", "", "goroutine 24 [running]:", "\tgoroutine running on other thread; stack unavailable", "junk", }, prefix: "panic: reflect.Set: value of type\n\n", suffix: "junk", err: errors.New("expected empty line after unavailable stack, got: \"junk\""), want: []*Goroutine{ { Signature: Signature{ State: "running", Stack: Stack{ Calls: []Call{newCall("", Args{}, "", 0)}, }, }, ID: 24, First: true, }, }, }, { name: "NoOffset", in: []string{ "panic: runtime error: index out of range", "", "goroutine 37 [runnable]:", "github.com/maruel/panicparse/stack.func·002()", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:110", "created by github.com/maruel/panicparse/stack.New", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:113 +0x43b", "", }, prefix: "panic: runtime error: index out of range\n\n", err: io.EOF, want: []*Goroutine{ { Signature: Signature{ State: "runnable", CreatedBy: Stack{ Calls: []Call{ newCall( "github.com/maruel/panicparse/stack.New", Args{}, "/gopath/src/github.com/maruel/panicparse/stack/stack.go", 113), }, }, Stack: Stack{ Calls: []Call{ newCall( "github.com/maruel/panicparse/stack.func·002", Args{}, "/gopath/src/github.com/maruel/panicparse/stack/stack.go", 110), }, }, }, ID: 37, First: true, }, }, }, // For coverage of scanLines. { name: "HeaderError", in: []string{ "panic: reflect.Set: value of type", "", "goroutine 1 [running]:", "junk", }, prefix: "panic: reflect.Set: value of type\n\n", suffix: "junk", err: errors.New("expected a function after a goroutine header, got: \"junk\""), want: []*Goroutine{ { Signature: Signature{State: "running"}, ID: 1, First: true, }, }, }, // For coverage of scanLines. { name: "FileError", in: []string{ "panic: reflect.Set: value of type", "", "goroutine 1 [running]:", "github.com/maruel/panicparse/stack.func·002()", "junk", }, prefix: "panic: reflect.Set: value of type\n\n", suffix: "junk", err: errors.New("expected a file after a function, got: \"junk\""), want: []*Goroutine{ { Signature: Signature{ State: "running", Stack: Stack{ Calls: []Call{ newCall("github.com/maruel/panicparse/stack.func·002", Args{}, "", 0), }, }, }, ID: 1, First: true, }, }, }, // For coverage of scanLines. { name: "Created", in: []string{ "panic: reflect.Set: value of type", "", "goroutine 1 [running]:", "github.com/maruel/panicparse/stack.func·002()", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:110", "created by github.com/maruel/panicparse/stack.New", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:131 +0x381", "exit status 2", }, prefix: "panic: reflect.Set: value of type\n\n", suffix: "exit status 2", err: io.EOF, want: []*Goroutine{ { Signature: Signature{ State: "running", CreatedBy: Stack{ Calls: []Call{ newCall( "github.com/maruel/panicparse/stack.New", Args{}, "/gopath/src/github.com/maruel/panicparse/stack/stack.go", 131), }, }, Stack: Stack{ Calls: []Call{ newCall( "github.com/maruel/panicparse/stack.func·002", Args{}, "/gopath/src/github.com/maruel/panicparse/stack/stack.go", 110), }, }, }, ID: 1, First: true, }, }, }, // For coverage of scanLines. { name: "CreatedError", in: []string{ "panic: reflect.Set: value of type", "", "goroutine 1 [running]:", "github.com/maruel/panicparse/stack.func·002()", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:110", "created by github.com/maruel/panicparse/stack.New", "junk", }, prefix: "panic: reflect.Set: value of type\n\n", suffix: "junk", err: errors.New("expected a file after a created line, got: \"junk\""), want: []*Goroutine{ { Signature: Signature{ State: "running", CreatedBy: Stack{ Calls: []Call{ newCall("github.com/maruel/panicparse/stack.New", Args{}, "", 0), }, }, Stack: Stack{ Calls: []Call{ newCall( "github.com/maruel/panicparse/stack.func·002", Args{}, "/gopath/src/github.com/maruel/panicparse/stack/stack.go", 110), }, }, }, ID: 1, First: true, }, }, }, { name: "CCode", in: []string{ "SIGQUIT: quit", "PC=0x43f349", "", "goroutine 0 [idle]:", "runtime.epollwait(0x4, 0x71c7118, 0xffffffff00000080, 0x0, 0xffffffff0028c1be, 0x0, 0x0, 0x0, 0x0, 0x0, ...)", " /goroot/src/runtime/sys_linux_amd64.s:400 +0x19", "runtime.netpoll(0x901b01, 0x0)", " /goroot/src/runtime/netpoll_epoll.go:68 +0xa3", "findrunnable(0x8012000)", " /goroot/src/runtime/proc.c:1472 +0x485", "schedule()", " /goroot/src/runtime/proc.c:1575 +0x151", "runtime.park_m(0x80017a0)", " /goroot/src/runtime/proc.c:1654 +0x113", "runtime.mcall(0x432684)", " /goroot/src/runtime/asm_amd64.s:186 +0x5a", "", }, prefix: "SIGQUIT: quit\nPC=0x43f349\n\n", err: io.EOF, want: []*Goroutine{ { Signature: Signature{ State: "idle", Stack: Stack{ Calls: []Call{ newCall( "runtime.epollwait", Args{ Values: []Arg{ {Value: 0x4}, {Value: 0x71c7118, IsPtr: true}, {Value: 0xffffffff00000080}, {}, {Value: 0xffffffff0028c1be}, {}, {}, {}, {}, {}, }, Elided: true, }, "/goroot/src/runtime/sys_linux_amd64.s", 400), newCall( "runtime.netpoll", Args{Values: []Arg{{Value: 0x901b01, IsPtr: true}, {}}}, "/goroot/src/runtime/netpoll_epoll.go", 68), newCall( "findrunnable", Args{Values: []Arg{{Value: 0x8012000, IsPtr: true}}}, "/goroot/src/runtime/proc.c", 1472), newCall("schedule", Args{}, "/goroot/src/runtime/proc.c", 1575), newCall( "runtime.park_m", Args{Values: []Arg{{Value: 0x80017a0, IsPtr: true}}}, "/goroot/src/runtime/proc.c", 1654), newCall( "runtime.mcall", Args{Values: []Arg{{Value: 0x432684, IsPtr: true}}}, "/goroot/src/runtime/asm_amd64.s", 186), }, }, }, ID: 0, First: true, }, }, }, { name: "WithCarriageReturn", in: []string{ "goroutine 1 [running]:", "github.com/cockroachdb/cockroach/storage/engine._Cfunc_DBIterSeek()", " ??:0 +0x6d", "gopkg.in/yaml%2ev2.handleErr(0x433b20)", "\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6", "reflect.Value.assignTo(0x570860, 0x803f3e0, 0x15)", "\t/goroot/src/reflect/value.go:2125 +0x368", "main.main()", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:428 +0x27", "", }, err: io.EOF, want: []*Goroutine{ { Signature: Signature{ State: "running", Stack: Stack{ Calls: []Call{ newCall( "github.com/cockroachdb/cockroach/storage/engine._Cfunc_DBIterSeek", Args{}, "??", 0), newCall( "gopkg.in/yaml%2ev2.handleErr", Args{Values: []Arg{{Value: 0x433b20, IsPtr: true}}}, "/gopath/src/gopkg.in/yaml.v2/yaml.go", 153), newCall( "reflect.Value.assignTo", Args{Values: []Arg{{Value: 0x570860, IsPtr: true}, {Value: 0x803f3e0, IsPtr: true}, {Value: 0x15}}}, "/goroot/src/reflect/value.go", 2125), newCall( "main.main", Args{}, "/gopath/src/github.com/maruel/panicparse/stack/stack.go", 428), }, }, }, ID: 1, First: true, }, }, }, { name: "WithCarriageReturn1.18Inaccurate", in: []string{ "goroutine 1 [running]:", "github.com/cockroachdb/cockroach/storage/engine._Cfunc_DBIterSeek()", " ??:0 +0x6d", "gopkg.in/yaml%2ev2.handleErr(0x433b20?)", "\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6", "reflect.Value.assignTo(0x570860, 0x803f3e0, 0x15)", "\t/goroot/src/reflect/value.go:2125 +0x368", "main.main()", "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:428 +0x27", "", }, err: io.EOF, want: []*Goroutine{ { Signature: Signature{ State: "running", Stack: Stack{ Calls: []Call{ newCall( "github.com/cockroachdb/cockroach/storage/engine._Cfunc_DBIterSeek", Args{}, "??", 0), newCall( "gopkg.in/yaml%2ev2.handleErr", Args{Values: []Arg{{Value: 0x433b20, IsPtr: true, IsInaccurate: true}}}, "/gopath/src/gopkg.in/yaml.v2/yaml.go", 153), newCall( "reflect.Value.assignTo", Args{Values: []Arg{{Value: 0x570860, IsPtr: true}, {Value: 0x803f3e0, IsPtr: true}, {Value: 0x15}}}, "/goroot/src/reflect/value.go", 2125), newCall( "main.main", Args{}, "/gopath/src/github.com/maruel/panicparse/stack/stack.go", 428), }, }, }, ID: 1, First: true, }, }, }, // goconvey is culprit of this. { name: "Indented", in: []string{ "Failures:", "", " * /home/maruel/go/src/foo/bar_test.go", " Line 209:", " Expected: '(*errors.errorString){s:\"context canceled\"}'", " Actual: 'nil'", " (Should resemble)!", " goroutine 8 [running]:", " foo/bar.TestArchiveFail.func1.2()", " /home/maruel/go/foo/bar_test.go:209 +0x469", " foo/bar.TestArchiveFail(0x3382000)", " /home/maruel/go/src/foo/bar_test.go:155 +0xf1", " testing.tRunner(0x3382000, 0x1615bf8)", " /home/maruel/golang/go/src/testing/testing.go:865 +0xc0", " created by testing.(*T).Run", " /home/maruel/golang/go/src/testing/testing.go:916 +0x35a", "", "", }, prefix: strings.Join([]string{ "Failures:", "", " * /home/maruel/go/src/foo/bar_test.go", " Line 209:", " Expected: '(*errors.errorString){s:\"context canceled\"}'", " Actual: 'nil'", " (Should resemble)!", "", }, "\n"), err: io.EOF, want: []*Goroutine{ { Signature: Signature{ State: "running", CreatedBy: Stack{ Calls: []Call{ newCall( "testing.(*T).Run", Args{}, "/home/maruel/golang/go/src/testing/testing.go", 916), }, }, Stack: Stack{ Calls: []Call{ newCall( "foo/bar.TestArchiveFail.func1.2", Args{}, "/home/maruel/go/foo/bar_test.go", 209), newCall( "foo/bar.TestArchiveFail", Args{Values: []Arg{{Value: 0x3382000, Name: "#1", IsPtr: true}}}, "/home/maruel/go/src/foo/bar_test.go", 155), newCall( "testing.tRunner", Args{Values: []Arg{{Value: 0x3382000, Name: "#1", IsPtr: true}, {Value: 0x1615bf8, IsPtr: true}}}, "/home/maruel/golang/go/src/testing/testing.go", 865), }, }, }, ID: 8, First: true, }, }, }, { name: "Race", in: []string{string(internaltest.StaticPanicRaceOutput())}, prefix: "\nGOTRACEBACK=all\n", want: []*Goroutine{ { Signature: Signature{ State: "running", CreatedBy: Stack{ Calls: []Call{ newCall( "main.panicRace", Args{}, "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", 153, ), newCall( "main.main", Args{}, "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", 54, ), }, }, Stack: Stack{ Calls: []Call{ newCall( "main.panicDoRaceRead", Args{}, "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", 137, ), newCall( "main.panicRace.func2", Args{}, "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", 154), }, }, }, ID: 8, First: true, RaceAddr: 0xc000014100, }, { Signature: Signature{ State: "running", CreatedBy: Stack{ Calls: []Call{ newCall( "main.panicRace", Args{}, "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", 150, ), newCall( "main.main", Args{}, "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", 54, ), }, }, Stack: Stack{ Calls: []Call{ newCall( "main.panicDoRaceWrite", Args{}, "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", 132), newCall( "main.panicRace.func1", Args{}, "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", 151), }, }, }, ID: 7, RaceWrite: true, RaceAddr: 0xc000014100, }, }, }, { name: "RaceHdr1Err", in: []string{ string(raceHeaderFooter), }, prefix: string(raceHeaderFooter), err: io.EOF, }, { name: "RaceHdr2Err", in: []string{ string(raceHeaderFooter), "", }, // TODO(maruel): This is incorrect. prefix: "", err: io.EOF, }, { name: "RaceHdr3Err", in: []string{ string(raceHeaderFooter), string(raceHeader), }, // TODO(maruel): This is incorrect. prefix: "", err: io.EOF, }, { name: "RaceHdr4Err", in: []string{ string(raceHeaderFooter), string(raceHeader), "", }, // TODO(maruel): This is incorrect. prefix: "", err: io.EOF, }, } for i, line := range data { line := line t.Run(fmt.Sprintf("%d-%s", i, line.name), func(t *testing.T) { t.Parallel() prefix := bytes.Buffer{} r := bytes.NewBufferString(strings.Join(line.in, "\n")) s, suffix, err := ScanSnapshot(r, &prefix, defaultOpts()) compareErr(t, line.err, err) if line.want == nil { if s != nil { t.Fatalf("unexpected %v", s) } } else { if s == nil { t.Fatalf("expected snapshot") } compareGoroutines(t, line.want, s.Goroutines) } compareString(t, line.prefix, prefix.String()) rest, err := ioutil.ReadAll(r) compareErr(t, nil, err) compareString(t, line.suffix, string(suffix)+string(rest)) }) } } func TestScanSnapshotSyntheticTwoSnapshots(t *testing.T) { t.Parallel() in := bytes.Buffer{} in.WriteString("Ya\n") in.Write(internaltest.PanicOutputs()["simple"]) in.WriteString("Ye\n") in.Write(internaltest.PanicOutputs()["int"]) in.WriteString("Yo\n") panicParseDir := getPanicParseDir(t) ppDir := pathJoin(panicParseDir, "cmd", "panic") // First stack: prefix := bytes.Buffer{} s, suffix, err := ScanSnapshot(&in, &prefix, defaultOpts()) compareErr(t, nil, err) if !s.guessPaths() { t.Error("expected success") } want := []*Goroutine{ { Signature: Signature{ State: "running", Stack: Stack{ Calls: []Call{ newCallLocal( "main.main", Args{}, pathJoin(ppDir, "main.go"), 74, ), }, }, }, ID: 1, First: true, }, } compareGoroutines(t, want, s.Goroutines) compareString(t, "Ya\nGOTRACEBACK=all\npanic: simple\n\n", prefix.String()) prefix.Reset() r := io.MultiReader(bytes.NewReader(suffix), &in) s, suffix, err = ScanSnapshot(r, &prefix, defaultOpts()) compareErr(t, nil, err) if !s.guessPaths() { t.Error("expected success") } // This is a change detector on internal/main.go. want = []*Goroutine{ { Signature: Signature{ State: "running", Stack: Stack{ Calls: []Call{ newCallLocal( "main.panicint", Args{Values: []Arg{{Value: 42}}}, pathJoin(ppDir, "main.go"), 93, ), newCallLocal( "main.glob..func9", Args{}, pathJoin(ppDir, "main.go"), 315, ), newCallLocal( "main.main", Args{}, pathJoin(ppDir, "main.go"), 76, ), }, }, }, ID: 1, First: true, }, } compareGoroutines(t, want, s.Goroutines) compareString(t, "Ye\nGOTRACEBACK=all\npanic: 42\n\n", prefix.String()) compareString(t, "Yo\n", string(suffix)) } func TestSplitPath(t *testing.T) { t.Parallel() if p := splitPath(""); p != nil { t.Fatalf("expected nil, got: %v", p) } } func TestGetGOPATHs(t *testing.T) { // This test cannot run in parallel. old := os.Getenv("GOPATH") defer os.Setenv("GOPATH", old) os.Setenv("GOPATH", "") if p := getGOPATHs(); len(p) != 1 { // It's the home directory + /go. t.Fatalf("expected only one path: %v", p) } root, err := ioutil.TempDir("", "stack") if err != nil { t.Fatal(err) } defer func() { if err = os.RemoveAll(root); err != nil { t.Error(err) } }() os.Setenv("GOPATH", filepath.Join(root, "a")+string(filepath.ListSeparator)+filepath.Join(root, "b")+string(filepath.Separator)) if p := getGOPATHs(); len(p) != 2 { t.Fatalf("expected two paths: %v", p) } } // TestGomoduleComplex is an integration test that creates a non-trivial tree // of go modules using the "replace" statement. func TestGomoduleComplex(t *testing.T) { // This test cannot run in parallel. if internaltest.GetGoMinorVersion() < 11 { t.Skip("requires go module support") } old := os.Getenv("GOPATH") defer os.Setenv("GOPATH", old) root, err := ioutil.TempDir("", "stack") if err != nil { t.Fatal(err) } defer func() { if err = os.RemoveAll(root); err != nil { t.Error(err) } }() os.Setenv("GOPATH", filepath.Join(root, "go")) os.Setenv("GO111MODULE", "on") tree := map[string]string{ "pkg1/go.mod": "module example.com/pkg1\n" + "require (\n" + "\texample.com/pkg2 v0.0.1\n" + "\texample.com/pkg3 v0.0.1\n" + ")\n" + "replace example.com/pkg2 => ../pkg2\n" + // This is kind of a hack to force testing with a package inside GOPATH, // since this won't normally work by default. "replace example.com/pkg3 => ../go/src/example.com/pkg3\n", "pkg1/cmd/main.go": "package main\n" + "import \"example.com/pkg1/internal\"\n" + "func main() {\n" + "\tinternal.CallCallDie()\n" + "}\n", "pkg1/internal/int.go": "package internal\n" + "import \"example.com/pkg2\"\n" + "func CallCallDie() {\n" + "\tpkg2.CallDie()\n" + "}\n", "pkg2/go.mod": "module example.com/pkg2\n" + "require (\n" + "\texample.com/pkg3 v0.0.1\n" + ")\n" + // This is kind of a hack to force testing with a package inside GOPATH, // since this won't normally work by default. "replace example.com/pkg3 => ../go/src/example.com/pkg3\n", "pkg2/src2.go": "package pkg2\n" + "import \"example.com/pkg3\"\n" + "func CallDie() { pkg3.Die() }\n", "go/src/example.com/pkg3/go.mod": "module example.com/pkg3\n", "go/src/example.com/pkg3/src3.go": "package pkg3\n" + "func Die() { panic(42) }\n", } createTree(t, root, tree) exe := filepath.Join(root, "yo") if runtime.GOOS == "windows" { exe += ".exe" } if err = internaltest.Compile("./cmd", exe, filepath.Join(root, "pkg1"), true, false); err != nil { t.Fatal(err) } out, err := exec.Command(exe).CombinedOutput() if err == nil { t.Error("expected failure") } prefix := bytes.Buffer{} s, suffix, err := ScanSnapshot(bytes.NewReader(out), &prefix, defaultOpts()) compareErr(t, io.EOF, err) if !s.guessPaths() { t.Error("expected success") } if s == nil { t.Fatal("expected snapshot") } if s.IsRace() { t.Fatal("unexpected race") } compareString(t, "panic: 42\n\n", prefix.String()) compareString(t, "", string(suffix)) wantGOROOT := "" compareString(t, wantGOROOT, s.RemoteGOROOT) compareString(t, runtime.GOROOT(), strings.Replace(s.LocalGOROOT, "/", pathSeparator, -1)) rootRemote := root if runtime.GOOS == "windows" { // On Windows, we must make the path to be POSIX style. rootRemote = strings.Replace(root, pathSeparator, "/", -1) } rootLocal := rootRemote if runtime.GOOS == "darwin" { if ver := internaltest.GetGoMinorVersion(); ver > 0 && ver < 19 { // On MacOS, $TMPDIR path is a symlink /var -> /private/var. // // On versions before go1.19, it was somehow evaluated by exec.Cmd.Run(). // This stopped being true on go1.19. I suspect it is a side-effect of // https://go.dev/doc/go1.19#os-exec-path // // This must NOT be run on Windows otherwise the path will be converted // to 8.3 format. if rootRemote, err = filepath.EvalSymlinks(rootLocal); err != nil { t.Fatal(err) } } } // This part is a bit tricky. The symlink is evaluated on the left since, // since it's what is the "remote" path, but it is not on the right since, // which is the "local" path. This difference only exists on MacOS. wantGOPATHs := map[string]string{ pathJoin(rootRemote, "go"): pathJoin(rootLocal, "go"), } if diff := cmp.Diff(s.RemoteGOPATHs, wantGOPATHs); diff != "" { t.Fatalf("+want/-got: %s", diff) } // Local go module search is on the path with symlink evaluated on MacOS. // This is kind of confusing because it is the "remote" path. wantGomods := map[string]string{ pathJoin(rootRemote, "pkg1"): "example.com/pkg1", pathJoin(rootRemote, "pkg2"): "example.com/pkg2", } if diff := cmp.Diff(s.LocalGomods, wantGomods); diff != "" { t.Fatalf("+want/-got: %s", diff) } want := []*Goroutine{ { Signature: Signature{ State: "running", Stack: Stack{ Calls: []Call{ { Func: newFunc("example.com/pkg3.Die"), Args: Args{Elided: true}, RemoteSrcPath: pathJoin(rootRemote, "go", "src", "example.com", "pkg3", "src3.go"), Line: 2, SrcName: "src3.go", DirSrc: "pkg3/src3.go", LocalSrcPath: pathJoin(rootLocal, "go", "src", "example.com", "pkg3", "src3.go"), RelSrcPath: "example.com/pkg3/src3.go", ImportPath: "example.com/pkg3", Location: GOPATH, }, { Func: newFunc("example.com/pkg2.CallDie"), Args: Args{Elided: true}, RemoteSrcPath: pathJoin(rootRemote, "pkg2", "src2.go"), Line: 3, SrcName: "src2.go", DirSrc: "pkg2/src2.go", // Since this was found locally as a go module using the remote // path, this is correct, even if confusing. LocalSrcPath: pathJoin(rootRemote, "pkg2", "src2.go"), RelSrcPath: "src2.go", ImportPath: "example.com/pkg2", Location: GoMod, }, { Func: newFunc("example.com/pkg1/internal.CallCallDie"), RemoteSrcPath: pathJoin(rootRemote, "pkg1", "internal", "int.go"), Line: 2, SrcName: "int.go", DirSrc: "internal/int.go", // Since this was found locally as a go module using the remote // path, this is correct, even if confusing. LocalSrcPath: pathJoin(rootRemote, "pkg1", "internal", "int.go"), RelSrcPath: "internal/int.go", ImportPath: "example.com/pkg1/internal", Location: GoMod, }, { Func: newFunc("main.main"), RemoteSrcPath: pathJoin(rootRemote, "pkg1", "cmd", "main.go"), Line: 4, SrcName: "main.go", DirSrc: "cmd/main.go", // Since this was found locally as a go module using the remote // path, this is correct, even if confusing. LocalSrcPath: pathJoin(rootRemote, "pkg1", "cmd", "main.go"), RelSrcPath: "cmd/main.go", ImportPath: "example.com/pkg1/cmd", Location: GoMod, }, }, }, }, ID: 1, First: true, }, } similarGoroutines(t, want, s.Goroutines) } func TestGoRun(t *testing.T) { t.Parallel() root, err := ioutil.TempDir("", "stack") if err != nil { t.Fatal(err) } defer func() { if err = os.RemoveAll(root); err != nil { t.Error(err) } }() p := filepath.Join(root, "main.go") content := "package main\nfunc main() { panic(42) }\n" if err = ioutil.WriteFile(p, []byte(content), 0600); err != nil { t.Fatal(err) } c := exec.Command("go", "run", p) out, err := c.CombinedOutput() if err == nil { t.Fatal("expected failure") } prefix := bytes.Buffer{} s, suffix, err := ScanSnapshot(bytes.NewReader(out), &prefix, defaultOpts()) compareErr(t, nil, err) compareString(t, "panic: 42\n\n", prefix.String()) compareString(t, "exit status 2\n", string(suffix)) if s == nil { t.Fatal("expected snapshot") } if runtime.GOOS == "windows" { // On Windows, we must make the path to be POSIX style. p = strings.Replace(p, pathSeparator, "/", -1) } want := []*Goroutine{ { Signature: Signature{ State: "running", Stack: Stack{ Calls: []Call{ { Func: Func{ Complete: "main.main", ImportPath: "main", DirName: "main", Name: "main", IsExported: true, IsPkgMain: true, }, RemoteSrcPath: p, Line: 2, SrcName: "main.go", DirSrc: path.Base(path.Dir(p)) + "/main.go", ImportPath: "main", Location: LocationUnknown, }, }, }, }, ID: 1, First: true, }, } similarGoroutines(t, want, s.Goroutines) if !s.guessPaths() { t.Error("expected success") } want[0].Stack.Calls[0].LocalSrcPath = p want[0].Stack.Calls[0].RelSrcPath = "main.go" // This is not technically true, when using go run there's no need for a // go.mod file, but I don't think it's worth handling specifically. want[0].Stack.Calls[0].Location = GoMod similarGoroutines(t, want, s.Goroutines) } // TestPanic runs github.com/maruel/panicparse/v2/cmd/panic with every // supported panic modes. func TestPanic(t *testing.T) { t.Parallel() cmds := internaltest.PanicOutputs() want := map[string]int{ "chan_receive": 2, "chan_send": 2, "goroutine_1": 2, "goroutine_dedupe_pointers": 101, "goroutine_100": 101, } panicParseDir := getPanicParseDir(t) ppDir := pathJoin(panicParseDir, "cmd", "panic") // Test runtime code. For those not in "custom", just assert that they // succeed. custom := map[string]func(*testing.T, *Snapshot, *bytes.Buffer, string){ "args_elided": testPanicArgsElided, "mismatched": testPanicMismatched, "race": testPanicRace, "str": testPanicStr, "utf8": testPanicUTF8, } // Make sure all custom handlers are showing up in cmds. for n := range custom { if _, ok := cmds[n]; !ok { if n == "race" { t.Skip("race is unsupported") } t.Fatalf("untested mode %q:\n%v", n, cmds[n]) } } for cmd, data := range cmds { cmd := cmd data := data t.Run(cmd, func(t *testing.T) { t.Parallel() prefix := bytes.Buffer{} s, suffix, err := ScanSnapshot(bytes.NewReader(data), &prefix, defaultOpts()) if err != nil && err != io.EOF { t.Fatal(err) } if s == nil { t.Fatal("context is nil") } if !s.guessPaths() { t.Fatal("expected GuessPaths to work") } if f := custom[cmd]; f != nil { f(t, s, &prefix, ppDir) return } e := want[cmd] if e == 0 { e = 1 } if got := len(s.Goroutines); got != e { t.Fatalf("unexpected Goroutines; want %d, got %d", e, got) } compareString(t, "", string(suffix)) }) } } func testPanicArgsElided(t *testing.T, s *Snapshot, b *bytes.Buffer, ppDir string) { if s.RemoteGOROOT != "" { t.Fatalf("RemoteGOROOT is %q", s.RemoteGOROOT) } if b.String() != "GOTRACEBACK=all\npanic: 1\n\n" { t.Fatalf("output: %q", b.String()) } want := []*Goroutine{ { Signature: Signature{ State: "running", Stack: Stack{ Calls: []Call{ newCallLocal( "main.panicArgsElided", Args{ Values: []Arg{{Value: 1}, {Value: 2}, {Value: 3}, {Value: 4}, {Value: 5}, {Value: 6}, {Value: 7}, {Value: 8}, {Value: 9}, {Value: 10}}, Elided: true, }, pathJoin(ppDir, "main.go"), 58), newCallLocal("main.glob..func1", Args{}, pathJoin(ppDir, "main.go"), 134), newCallLocal("main.main", Args{}, pathJoin(ppDir, "main.go"), 340), }, }, }, ID: 1, First: true, }, } similarGoroutines(t, want, s.Goroutines) } func testPanicMismatched(t *testing.T, s *Snapshot, b *bytes.Buffer, ppDir string) { if s.RemoteGOROOT != "" { t.Fatalf("RemoteGOROOT is %q", s.RemoteGOROOT) } if b.String() != "GOTRACEBACK=all\npanic: 42\n\n" { t.Fatalf("output: %q", b.String()) } ver := "/v2" if !internaltest.IsUsingModules() { ver = "" } want := []*Goroutine{ { Signature: Signature{ State: "running", Stack: Stack{ Calls: []Call{ newCallLocal( // This is important to note here that the Go runtime prints out // the package path, and not the package name. // // Here the package name is "correct". There is no way to deduce // this from the stack trace. "github.com/maruel/panicparse"+ver+"/cmd/panic/internal/incorrect.Panic", Args{}, pathJoin(ppDir, "internal", "incorrect", "correct.go"), 7), newCallLocal("main.glob..func20", Args{}, pathJoin(ppDir, "main.go"), 314), newCallLocal("main.main", Args{}, pathJoin(ppDir, "main.go"), 340), }, }, }, ID: 1, First: true, }, } similarGoroutines(t, want, s.Goroutines) } func testPanicRace(t *testing.T, s *Snapshot, b *bytes.Buffer, ppDir string) { if s.RemoteGOROOT != "" { t.Fatalf("RemoteGOROOT is %q", s.RemoteGOROOT) } if b.String() != "GOTRACEBACK=all\n" { t.Fatalf("output: %q", b.String()) } want := []*Goroutine{ { Signature: Signature{ State: "running", CreatedBy: Stack{ Calls: []Call{ newCallLocal( "main.panicRace", Args{}, pathJoin(ppDir, "main.go"), 151, ), newCallLocal( "main.main", Args{}, pathJoin(ppDir, "main.go"), 76, ), }, }, Stack: Stack{ Calls: []Call{ newCallLocal( "main.panicDoRaceRead", Args{}, pathJoin(ppDir, "main.go"), 150), newCallLocal( "main.panicRace.func2", Args{}, pathJoin(ppDir, "main.go"), 135), }, }, }, RaceAddr: pointer, }, { Signature: Signature{ State: "running", CreatedBy: Stack{ Calls: []Call{ newCallLocal( "main.panicRace", Args{}, pathJoin(ppDir, "main.go"), 151, ), newCallLocal( "main.main", Args{}, pathJoin(ppDir, "main.go"), 76, ), }, }, Stack: Stack{ Calls: []Call{ newCallLocal( "main.panicDoRaceWrite", Args{}, pathJoin(ppDir, "main.go"), 145), newCallLocal( "main.panicRace.func1", Args{}, pathJoin(ppDir, "main.go"), 132), }, }, }, RaceWrite: true, RaceAddr: pointer, }, } // IDs are not deterministic, so zap them too but take them for the race // detector first. for i, g := range s.Goroutines { g.ID = i + 1 if g.RaceAddr > 4*1024*1024 { g.RaceAddr = pointer } } // Sometimes the read is detected first. if s.Goroutines[0].RaceWrite { want[0], want[1] = want[1], want[0] } // These fields are order-dependent, so set them last. want[0].ID = 1 want[1].ID = 2 want[0].First = true want[1].First = false similarGoroutines(t, want, s.Goroutines) } func testPanicStr(t *testing.T, s *Snapshot, b *bytes.Buffer, ppDir string) { if s.RemoteGOROOT != "" { t.Fatalf("RemoteGOROOT is %q", s.RemoteGOROOT) } if b.String() != "GOTRACEBACK=all\npanic: allo\n\n" { t.Fatalf("output: %q", b.String()) } want := []*Goroutine{ { Signature: Signature{ State: "running", Stack: Stack{ Calls: []Call{ newCallLocal( "main.panicstr", ifCombinedAggregateArgs( Args{Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{Value: 0x123456, IsPtr: true}, {Value: 4}}, }}}}, // else Args{Values: []Arg{{Value: 0x123456, IsPtr: true}, {Value: 4}}}, ), pathJoin(ppDir, "main.go"), 50), newCallLocal("main.glob..func19", Args{}, pathJoin(ppDir, "main.go"), 307), newCallLocal("main.main", Args{}, pathJoin(ppDir, "main.go"), 340), }, }, }, ID: 1, First: true, }, } similarGoroutines(t, want, s.Goroutines) } func testPanicUTF8(t *testing.T, s *Snapshot, b *bytes.Buffer, ppDir string) { if s.RemoteGOROOT != "" { t.Fatalf("RemoteGOROOT is %q", s.RemoteGOROOT) } if b.String() != "GOTRACEBACK=all\npanic: 42\n\n" { t.Fatalf("output: %q", b.String()) } ver := "/v2" if !internaltest.IsUsingModules() { ver = "" } want := []*Goroutine{ { Signature: Signature{ State: "running", Stack: Stack{ Calls: []Call{ newCallLocal( // This is important to note here the inconsistency in the Go // runtime stack generator. The path is escaped, but symbols are // not. "github.com/maruel/panicparse"+ver+"/cmd/panic/internal/utf8.(*Strùct).Pànic", ifCombinedAggregateArgs( Args{Values: []Arg{{Value: 1, IsInaccurate: true}}}, // else Args{Values: []Arg{{Value: 0xc0000b2e48, IsPtr: true, IsInaccurate: true}}}, ), // See TestCallUTF8 in stack_test.go for exercising the methods on // Call in this situation. pathJoin(ppDir, "internal", "utf8", "utf8.go"), 10), newCallLocal("main.glob..func21", Args{}, pathJoin(ppDir, "main.go"), 322), newCallLocal("main.main", Args{}, pathJoin(ppDir, "main.go"), 340), }, }, }, ID: 1, First: true, }, } similarGoroutines(t, want, s.Goroutines) } // TestPanicweb implements the parsing of panicweb output. // // panicweb is a separate binary from the rest of panic because importing the // "net" package causes a background thread to be started, which breaks "panic // asleep". func TestPanicweb(t *testing.T) { t.Parallel() if runtime.GOARCH == "ppc64" || runtime.GOARCH == "ppc64le" { t.Skip("https://github.com/maruel/panicparse/issues/66") } prefix := bytes.Buffer{} s, suffix, err := ScanSnapshot(bytes.NewReader(internaltest.PanicwebOutput()), &prefix, defaultOpts()) if err != io.EOF { t.Fatal(err) } if s == nil { t.Fatal("snapshot is nil") } compareString(t, "panic: Here's a snapshot of a normal web server.\n\n", prefix.String()) compareString(t, "", string(suffix)) if s.RemoteGOROOT != "" { t.Fatalf("unexpected RemoteGOROOT: %q", s.RemoteGOROOT) } if !s.guessPaths() { t.Error("expected success") } if s.RemoteGOROOT != strings.Replace(runtime.GOROOT(), "\\", "/", -1) { t.Fatalf("RemoteGOROOT mismatch; want:%q got:%q", runtime.GOROOT(), s.RemoteGOROOT) } if got := len(s.Goroutines); got < 30 { t.Fatalf("unexpected Goroutines; want at least 30, got %d", got) } // The goal here is not to find the exact match since it'll change across // OSes and Go versions, but to find some of the expected signatures. pwebDir := pathJoin(getPanicParseDir(t), "cmd", "panicweb") // Reduce the goroutines and categorize the signatures. var types []panicwebSignatureType for _, b := range s.Aggregate(AnyPointer).Buckets { types = append(types, identifyPanicwebSignature(t, b, pwebDir)) } // Count the expected types. if v := pstCount(types, pstUnknown); v != 0 { t.Fatalf("found %d unknown signatures", v) } if v := pstCount(types, pstMain); v != 1 { t.Fatalf("found %d pstMain signatures", v) } if v := pstCount(types, pstURL1handler); v != 1 && v != 2 { t.Fatalf("found %d URL1Handler signatures", v) } if v := pstCount(types, pstURL2handler); v != 1 && v != 2 { t.Fatalf("found %d URL2Handler signatures", v) } if v := pstCount(types, pstClient); v == 0 { t.Fatalf("found %d client signatures", v) } if v := pstCount(types, pstServe); v != 1 { t.Fatalf("found %d serve signatures", v) } if v := pstCount(types, pstColorable); v != 1 { t.Fatalf("found %d colorable signatures", v) } if v := pstCount(types, pstStdlib); v < 3 { t.Fatalf("found %d stdlib signatures", v) } } func TestIsGomodule(t *testing.T) { t.Parallel() pwd, err := os.Getwd() if err != nil { t.Fatal(err) } // Our internal functions work with '/' as path separator. parts := splitPath(strings.Replace(pwd, "\\", "/", -1)) gmc := gomodCache{} root, importPath := gmc.isGoModule(parts) if want := strings.Join(parts[:len(parts)-1], "/"); want != root { t.Errorf("want: %q, got: %q", want, root) } if want := "github.com/maruel/panicparse/v2"; want != importPath { t.Errorf("want: %q, got: %q", want, importPath) } got := reModule.FindStringSubmatch("foo\r\nmodule bar\r\nbaz") if diff := cmp.Diff([]string{"module bar\r", "bar"}, got); diff != "" { t.Fatalf("-want, +got:\n%s", diff) } } func TestAtou(t *testing.T) { t.Parallel() if i, b := atou([]byte("a")); i != 0 || b { t.Error("oops") } } func TestTrimLeftSpace(t *testing.T) { t.Parallel() if trimLeftSpace(nil) != nil { t.Error("oops") } } func TestTrimCurlyBrackets(t *testing.T) { t.Parallel() data := []struct { input []byte want []byte wantOpened, wantClosed int }{ {nil, nil, 0, 0}, {[]byte(""), []byte(""), 0, 0}, {[]byte("a"), []byte("a"), 0, 0}, {[]byte("{a"), []byte("a"), 1, 0}, {[]byte("{{a"), []byte("a"), 2, 0}, {[]byte("{{a}}"), []byte("a"), 2, 2}, {[]byte("{a}}"), []byte("a"), 1, 2}, {[]byte("a}}"), []byte("a"), 0, 2}, {[]byte("{}"), []byte(""), 1, 1}, {[]byte("{{}}"), []byte(""), 2, 2}, // Not expected in practice. {[]byte("}{"), []byte("}{"), 0, 0}, {[]byte("{{}}a{{}}"), []byte("}}a{{"), 2, 2}, } for i, line := range data { line := line t.Run(fmt.Sprintf("%d-%s", i, line.input), func(t *testing.T) { gotOpened, got, gotClosed := trimCurlyBrackets(line.input) if !bytes.Equal(line.want, got) { t.Errorf("want %s, got %s", line.want, got) } equiv := bytes.TrimRight(bytes.TrimLeft(line.input, "{"), "}") if !bytes.Equal(line.want, equiv) { t.Errorf("want %s, got %s", line.want, got) } if line.wantOpened != gotOpened { t.Errorf("want %d opening curly brackets, got %d", line.wantOpened, gotOpened) } if line.wantClosed != gotClosed { t.Errorf("want %d closing curly brackets, got %d", line.wantClosed, gotClosed) } }) } } func BenchmarkScanSnapshot_Guess(b *testing.B) { b.ReportAllocs() data := internaltest.StaticPanicwebOutput() opts := defaultOpts() b.ResetTimer() for i := 0; i < b.N; i++ { s, _, err := ScanSnapshot(bytes.NewReader(data), ioutil.Discard, opts) if err != io.EOF { b.Fatal(err) } if s == nil { b.Fatal("missing context") } } } func BenchmarkScanSnapshot_NoGuess(b *testing.B) { b.ReportAllocs() data := internaltest.StaticPanicwebOutput() opts := defaultOpts() b.ResetTimer() for i := 0; i < b.N; i++ { s, _, err := ScanSnapshot(bytes.NewReader(data), ioutil.Discard, opts) if err != io.EOF { b.Fatal(err) } if s == nil { b.Fatal("missing context") } } } func BenchmarkScanSnapshot_Passthru(b *testing.B) { b.ReportAllocs() buf := make([]byte, b.N) for i := range buf { buf[i] = 'i' if i%16 == 0 { buf[i] = '\n' } } prefix := bytes.Buffer{} prefix.Grow(len(buf)) r := bytes.NewReader(buf) opts := defaultOpts() b.ResetTimer() s, suffix, err := ScanSnapshot(r, &prefix, opts) if err != io.EOF { b.Fatal(err) } if s != nil { b.Fatalf("unexpected %v", s) } b.StopTimer() if !bytes.Equal(prefix.Bytes(), buf) { b.Fatal("unexpected prefix") } if len(suffix) != 0 { b.Fatal("unexpected suffix") } } // type panicwebSignatureType int const ( pstUnknown panicwebSignatureType = iota pstMain pstURL1handler pstURL2handler pstClient pstServe pstColorable pstStdlib ) func pstCount(s []panicwebSignatureType, t panicwebSignatureType) int { i := 0 for _, v := range s { if v == t { i++ } } return i } // identifyPanicwebSignature tries to assign one of the predefined signature to // the bucket provided. // // One challenge is that the path will be different depending if this test is // run within GOPATH or outside. func identifyPanicwebSignature(t *testing.T, b *Bucket, pwebDir string) panicwebSignatureType { ver := "" if !isInGOPATH { ver = "/v2" } // The first bucket (the one calling panic()) is deterministic. if b.First { if len(b.IDs) != 1 { t.Fatal("first bucket is not correct") return pstUnknown } want := Signature{ State: "running", Stack: Stack{ Calls: []Call{ newCallLocal("main.main", Args{}, pathJoin(pwebDir, "main.go"), 80), }, }, } similarSignatures(t, &want, &b.Signature) return pstMain } // We should find exactly 10 sleeping routines in the URL1Handler handler // signature and 3 in URL2Handler. if s := b.Stack.Calls[0].Func.Name; s == "URL1Handler" || s == "URL2Handler" { if b.State != "chan receive" { t.Fatalf("suspicious: %#v", b) return pstUnknown } if b.Stack.Calls[0].ImportPath != "github.com/maruel/panicparse"+ver+"/cmd/panicweb/internal" { t.Fatalf("suspicious: %q\n%#v", b.Stack.Calls[0].ImportPath, b) return pstUnknown } if b.Stack.Calls[0].SrcName != "internal.go" { t.Fatalf("suspicious: %#v", b) return pstUnknown } if b.CreatedBy.Calls[0].SrcName != "server.go" { t.Fatalf("suspicious: %#v", b) return pstUnknown } if b.CreatedBy.Calls[0].ImportPath != "net/http" { t.Fatalf("suspicious: %#v", b) return pstUnknown } if b.CreatedBy.Calls[0].Func.Name != "(*Server).Serve" { t.Fatalf("suspicious: %#v", b) return pstUnknown } if s == "URL1Handler" { return pstURL1handler } return pstURL2handler } // Find the client goroutine signatures. For the client, it is likely that // they haven't all bucketed perfectly. if b.CreatedBy.Calls[0].ImportPath == "github.com/maruel/panicparse"+ver+"/cmd/panicweb/internal" && b.CreatedBy.Calls[0].Func.Name == "GetAsync" { // TODO(maruel): More checks. return pstClient } // Now find the two goroutine started by main. if b.CreatedBy.Calls[0].ImportPath == "github.com/maruel/panicparse"+ver+"/cmd/panicweb" && b.CreatedBy.Calls[0].Func.ImportPath == "main" && b.CreatedBy.Calls[0].Func.Name == "main" { if b.State == "IO wait" { return pstServe } if b.State == "chan receive" { // Warning: This is brittle and will fail whenever go-colorable is // updated so only check the string minimum here. if !b.Signature.Locked { t.Fatal("expected Locked") } // This is a change detector on internal/main.go. want := Stack{Calls: []Call{newCallLocal("main.main", Args{}, pathJoin(pwebDir, "main.go"), 145)}} compareStacks(t, &b.Signature.CreatedBy, &want) for i := range b.Signature.Stack.Calls { if strings.HasPrefix(b.Signature.Stack.Calls[i].ImportPath, "github.com/mattn/go-colorable") { return pstColorable } } t.Fatalf("failed to find go-colorable\n%# v", b.Signature.Stack.Calls) } // That's the unix.Nanosleep() or windows.SleepEx() call. if b.State == "syscall" { { want := Stack{ Calls: []Call{ newCallLocal("main.main", Args{}, pathJoin(pwebDir, "main.go"), 63), }, } zapStacks(t, &want, &b.CreatedBy) compareStacks(t, &want, &b.CreatedBy) } if l := len(b.IDs); l != 1 { t.Fatalf("expected 1 goroutine for the signature, got %d", l) } if runtime.GOOS == "windows" { if l := len(b.Stack.Calls); l != 5 { t.Fatalf("expected %d calls, got %d", 5, l) } if s := b.Stack.Calls[0].RelSrcPath; s != "runtime/syscall_windows.go" { t.Fatalf("expected %q file, got %q", "runtime/syscall_windows.go", s) } } else { if l := len(b.Stack.Calls); l != 4 { t.Fatalf("expected %d calls, got %d", 4, l) } // The first item shall be an assembly file independent of the OS on Go // < 1.19. s := b.Stack.Calls[0].RelSrcPath if ver := internaltest.GetGoMinorVersion(); ver > 0 && ver < 19 { if !strings.HasSuffix(s, ".s") { t.Fatalf("expected assembly file, got %q", s) } } else if !strings.HasPrefix(s, "syscall") && !strings.HasSuffix(s, ".go") { t.Fatalf("expected syscall go file, got %q", s) } } // Process the golang.org/x/sys call specifically. path := "golang.org/x/sys/unix" fn := "Nanosleep" mainOS := "main_unix.go" prefix := "golang.org/x/sys@v0.0.0-" expectDate := true usingModules := internaltest.IsUsingModules() if !usingModules { // Assert that there's no version by including the trailing /. prefix = "golang.org/x/sys/" expectDate = false } if runtime.GOOS == "windows" { // This changes across Go version, this check is super fragile. path = "syscall" fn = "Syscall" mainOS = "main_windows.go" prefix = "runtime/syscall_windows.go" expectDate = false } if b.Stack.Calls[1].Func.ImportPath != path || b.Stack.Calls[1].Func.Name != fn { t.Fatalf("expected %q & %q, got %#v", path, fn, b.Stack.Calls[1].Func) } if !strings.HasPrefix(b.Stack.Calls[1].RelSrcPath, prefix) { t.Fatalf("expected %q, got %q", prefix, b.Stack.Calls[1].RelSrcPath) } if expectDate { // Assert that it's using @v0-0-0.- format. ver := strings.SplitN(b.Stack.Calls[1].RelSrcPath[len(prefix):], "/", 2)[0] re := regexp.MustCompile(`^\d{14}-[a-f0-9]{12}$`) if !re.MatchString(ver) { t.Fatalf("unexpected version string %q", ver) } } { want := []Call{ newCallLocal("main.sysHang", Args{}, pathJoin(pwebDir, mainOS), 12), newCallLocal( "main.main.func3", ifCombinedAggregateArgs( Args{}, // else Args{Values: []Arg{{Value: 0xc000140720, Name: "#135", IsPtr: true}}}, ), pathJoin(pwebDir, "main.go"), 65), } got := b.Stack.Calls[2:] if ver := internaltest.GetGoMinorVersion(); (ver > 0 && ver < 18 && !is64Bit) || runtime.GOOS == "windows" { // TODO(maruel): Fix check on Windows. // On go1.17 on 32 bits this is failing but not on go1.18, so only // skip in that case. t.Log("skipping some checks on \n{{- $elided := .Elided -}}\n{{- if .Processed -}}\n{{- $l := len .Processed -}}\n{{- $last := minus $l 1 -}}\n{{- range $i, $e := .Processed -}}\n{{- $e -}}\n{{- $isNotLast := ne $i $last -}}\n{{- if or $elided $isNotLast}}, {{end -}}\n{{- end -}}\n{{- else -}}\n{{- $l := len .Values -}}\n{{- $last := minus $l 1 -}}\n{{- range $i, $e := .Values -}}\n{{- $e.String -}}\n{{- $isNotLast := ne $i $last -}}\n{{- if or $elided $isNotLast}}, {{end -}}\n{{- end -}}\n{{- end -}}\n{{- if $elided}}…{{end -}}\n\n{{- end -}}\n{{- /* Accepts a Call */ -}}\n{{- define \"RenderCreatedBy\" -}}\n\n{{- if and .LocalSrcPath (ne .RemoteSrcPath .LocalSrcPath) -}}\nRemoteSrcPath: {{.RemoteSrcPath}}\n
LocalSrcPath: {{.LocalSrcPath}}\n{{- else -}}\nSrcPath: {{.RemoteSrcPath}}\n{{- end -}}\n
Func: {{.Func.Complete}}\n
Location: {{.Location}}\n
{{.SrcName}}:{{.Line}} \n{{.Func.DirName}}.{{.Func.Name}}()\n
\n{{- end -}}\n{{- /* Accepts a Stack */ -}}\n{{- define \"RenderCalls\" -}}\n\n{{- range $i, $e := .Calls -}}\n\n\n\n\n\n\n{{- end -}}\n{{- if .Elided}}{{end -}}\n
{{$i}}\n{{$e.Func.DirName}}\n\n\n{{- if and $e.LocalSrcPath (ne $e.RemoteSrcPath $e.LocalSrcPath) -}}\nRemoteSrcPath: {{$e.RemoteSrcPath}}\n
LocalSrcPath: {{$e.LocalSrcPath}}\n{{- else -}}\nSrcPath: {{$e.RemoteSrcPath}}\n{{- end -}}\n
Func: {{$e.Func.Complete}}\n
Location: {{$e.Location}}\n
\n{{$e.SrcName}}:{{$e.Line}}\n
\n{{$e.Func.Name}}({{template \"RenderArgs\" $e.Args}})\n
(…)
\n{{- end -}}\n\n\n\n\nPanicParse\n\n\n
\n{{- if .Aggregated -}}\n{{- range $i, $e := .Aggregated.Buckets -}}\n{{$l := len $e.IDs}}\n

Signature #{{$i}}: {{$l}} routine{{if ne 1 $l}}s{{end}}: {{$e.State}}\n{{- if $e.SleepMax -}}\n{{- if ne $e.SleepMin $e.SleepMax}} [{{$e.SleepMin}}~{{$e.SleepMax}} mins]\n{{- else}} [{{$e.SleepMax}} mins]\n{{- end -}}\n{{- end -}}\n

\n{{if $e.Locked}} [locked]\n{{- end -}}\n{{- if $e.CreatedBy.Calls}} Created by: {{template \"RenderCreatedBy\" index $e.CreatedBy.Calls 0}}\n{{- end -}}\n{{template \"RenderCalls\" $e.Signature.Stack}}\n{{- end -}}\n{{- else -}}\n{{- range $i, $e := .Snapshot.Goroutines -}}\n

Routine {{$e.ID}}: {{$e.State}}\n{{- if $e.SleepMax -}}\n{{- if ne $e.SleepMin $e.SleepMax}} [{{$e.SleepMin}}~{{$e.SleepMax}} mins]\n{{- else}} [{{$e.SleepMax}} mins]\n{{- end -}}\n{{- end -}}\n

\n{{if $e.Locked}} [locked]\n{{- end -}}\n{{if $e.RaceAddr}} Race {{if $e.RaceWrite}}write{{else}}read{{end}} @ {{printf \"0x%08X\" $e.RaceAddr}}
\n{{- end -}}\n{{- if $e.CreatedBy.Calls}} Created by: {{template \"RenderCreatedBy\" index $e.CreatedBy.Calls 0}}\n{{- end -}}\n{{template \"RenderCalls\" $e.Signature.Stack}}\n{{- end -}}\n{{- end -}}\n
\n

Metadata

\n
    \n
  • Created on {{.Now.String}}
  • \n
  • {{.Version}}
  • \n{{- if and .Snapshot.LocalGOROOT (ne .Snapshot.RemoteGOROOT .Snapshot.LocalGOROOT) -}}\n
  • GOROOT (remote): {{.Snapshot.RemoteGOROOT}}
  • \n
  • GOROOT (local): {{.Snapshot.LocalGOROOT}}
  • \n{{- else -}}\n
  • GOROOT: {{.Snapshot.RemoteGOROOT}}
  • \n{{- end -}}\n
  • GOPATH: {{template \"Join\" .Snapshot.LocalGOPATHs}}
  • \n{{- if .Snapshot.LocalGomods -}}\n
  • go modules (local):\n
      \n{{- range $path, $import := .Snapshot.LocalGomods -}}\n
    • {{$path}}: {{$import}}
    • \n{{- end -}}\n
    \n
  • \n{{- end -}}\n
  • GOMAXPROCS: {{.GOMAXPROCS}}
  • \n
\n

Legend

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
TypeExportedPrivate
\nPackage main\nSources that are in the main package.\nmain.Foo()main.foo()
\nGo module\nSources located inside a directory containing a\ngo.mod file but outside $GOPATH.\npkg.Foo()pkg.foo()
\n$GOPATH/src/...\nSources located inside the traditional $GOPATH/src\ndirectory.\npkg.Foo()pkg.foo()
\n$GOPATH/pkg/mod/...\nSources located inside the go module dependency\ncache under $GOPATH/pkg/mod. These files are unmodified third parties.\npkg.Foo()pkg.foo()
\nStandard library\nSources from the Go standard library under\n$GOROOT/src/.\npkg.Foo()pkg.foo()
\nUnknown source location\nSources which location was not successfully\ndetermined.\npkg.Foo()pkg.foo()
\n{{- .Footer -}}\n{{- /* Add unnecessary bottom spacing so the last tooltip from the legend is visible. */ -}}\n
\n" // favicon is the bomb emoji U+1F4A3 in Noto Emoji as a 128x128 base64 encoded // PNG. // // See README.md for license and how to retrieve it. const favicon template.HTML = "R0lGODlhQABAAMZhACEhISIiIiMjIyQkJCUlJSYmJicnJygoKCkpKSoqKisrKywsLC0tLS4uLi8vLzAwMDExMTIyMjMzMzQ0NDU1NTY2Njc3Nzg4ODk5OTo6Ojs7Ozw8PD09PT4+Pj8/P0BAQEFBQUJCQkNDQ0REREVFRUZGRkdHR4o1MEhISElJSUpKSktLS0xMTE1NTU5OTk9PT1BQUFFRUVJSUv8fQFNTU1RUVKc+N1VVVVZWVldXV/8mPVhYWFlZWf8oPP8pO1paWltbW1xcXF1dXV5eXl9fX2BgYGFhYf80NWJiYv8+MP9ALvRDNv9JKcxYUfRPQ/9PJvRTSP9VI/9aIPRcUf9dHudhV/9lGv91EP91Ef96Dv99C/9+DP+FB/+MA/+PAf+QAP+RAP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAABAAEAAAAf+gH+Cg4SFhoeIiYqLjI2Oj5CRjSkjJiqSkl6YfyMXCQCgBRUkm45ahF+NJRIBAAEKEhIKAAIepYVgg0uCVINXjCocAwAEGSWEHwIBI7eoXIRHgklSiyQPrhcohx0AEM2FVkqFM4sfBQALIokqBAKX34PijigVrhYpiwwAzN+7g1NOmqzrYACAgRCNFgAwAa8QlCUnQLwTpGKEhgP2tAEbQGBiMyxPdPypYgPUAAYRJDA450oCv0YfAESAl6tQig4MBIACFYCBhmOKVqh4p8IBAFsNB/kgVBEEiBEeE7loYSLF0BQYJEQt5e8PE0E9MMmAocGAAQgdSgwdmvRPlkH+USThoKFhgQIEBtopwAB1a6kug7ZAAsLjRgYIDxowuFtgQCutDWtCEkJZSI4NFShIiJB4AV7HAVD4bWuoiGkiOWSo6KAhAwYLmiE4WJAgbwAPo0kLKmKkSA4VIZx++OCBwwbXFiZEcMCg9oAJudvyNlKDxIgRJLKPEBH8Q4fjsJc3N4CArW5CRZD8MEGihAkUKFLAN1GChAgQxTNcoLDccwES0X1jBBIwkFCVCiuwoKCCQqFQwgghfMCBfvzNdgAHQp33R28rlCAaCy68AMOIMLzwQgssqIACCRFOmBwEDSSAQF+6mZaChyu08EIMMtBQw480yBDDiSus2CIGFUj+8IBnBogQoCS83YjCCi7AQMMNOOSww5Y54FDDWC0UyaIHG+wHY20EAJgUEUawQMKULsRAAw47/ABEEHgStsMNNMDQggomjABCBxlYIIGFBBwgWkNF3CCCJS1YiQMPQAhBBBFFEDGEEEDsMJefKpQgApkXTAABAwgUEIBWTzKyqQhvsvDCnDwEMcR0RuRaxBCd3iDDC0UKygGSEcRogE4SNRMEES2IoFYLMdywAxBD5GqttbsCkUMNfqbAIqGGOqDAAcMkYN4mnH7w6AovyIDDD0Lgeq2uQhQmgwuAjqpBckuSCwputwjhwgcjoMCClTkAwea88w7xw1zAmiDCB2X+TvAAqgS00sC5kVT6AQixwlDDDssyPC8R2tLwAgsojEBxqacikDEoam3yww0cgJzCwSMHIa/JKOfQJ8sul0lBzDMDsEGrh/zQQs4hj6ywydcGrXKwL5uKcSvdMG3IDyNsQLDBsyZcLdW5OozDrytInPWSMnNtAMeO5MDDBmKv64K7PMSLdhFBFBYDvqJ6sK+SC/i706KRTGqBBh6E8GwMUt9qchFC/LBtt99mkKS4Bgyzk5qR3HBDBRlATbYM0lL7MxGZ73lv2y4Py5+xOu2UbCRfUnBB3s9a2bqtl2oaxA+yr3xjCIaXenHcOwGwOyQ/SmBBBh08BanwdtsJBBD+P/CQg68nAlq754cmLvpOTkoiQw0S+L5B5G/mCMP7WOagPw43fFl+oIPSAMxwFz0A1EwSNDAU6jjwgRC8SQU6uh8NJhikGMDABSliD/M2gKT0HSBpPKGbI2TAAQhM4HoMdGBVchQiE5kIgytIQaA2WCiLjSd3O1GA1wpRIAdEgAIojNwIPGSVFSTIiCqQ4YPwAx5TNWBcBCggACqwQ0JgcAE+BGIG5gcCEWDHPfBBAX2sE6Hv1LA/qcLhTj5QxUHISiVZtAAGNsABD4AgBCLw4nXyCALicEADr1HObNIoRQJYpRQtAEECGODDCVTgAlvkQAc8MJzheMADHfijfir+YKpBhk6KU2wjIYyogARgEQLxs8AFMJABDeDtla3BwAU4qaQGMGl90QuAJZqxAoIgQAENeAAqHWkBVV7gmBewQGYmIAHZLCZVuIweFb8xlJUcwJQNcAAEUjKBblKgm7HgjANsmYADFECN0SMA45pREY6Qx5QMyOYDhAkBxDxgnAygzQHygs7oAawhKthAANx5AAQkQAELWAADFprQUiJgn+0A5U6oKEpEDIUCrhDAAApgloIi4KMPNUABCDAAZUgUFBA4ZFsuupMAaJQjBIgpSUvKtZN2Q6WkGcoFasqTAPiUpzYFAAVwqpuhmCOoQR1AB0SYUxVVAKhIBUUE1FI6UUkMhQQV6KdEV+WkqpZiKCgoYRRBOYAHcMASTNWQIdYSKhBw4K0MBNBa1GpVtrJ1BXTNq173qohAAAA7" panicparse-2.3.1/stack/emoji_u1f4a3.png000066400000000000000000000130101446536624000176500ustar00rootroot00000000000000PNG  IHDR>atEXtSoftwareAdobe ImageReadyqe<IDATxIǎx8eZŲdY2m;eACB $6NQ>O9###2 ^͛BA[>[/ӟ'g-=3}۷*kBAi$ V}[)zj@=NڔG6, w{a%U 8_< >S$/PseNU.Ql"t& hWl=Y_؅>&=!صkj wu髧=ަ# [ EtnƯ~boם@q=h ~N Y/"g~Bnn@vQlH6mGQpձ;f=u}gٚx>r f ~At3oiU K'jxM'|hh ¯f`z|=Itp 4Z1!su f{G35CKoaYə1 TS_(%qU{1G.yl&U!eɬMcVGOAII-EެЩڿcoTuuRJH+E˶= 6^D˴| -ܿ__h뭷b w{`h?ke!^cUrFvTb0/ /_]t 6xݻw֭[XoAX~=+G" E}O{"w^нW>y.zjʕ+g@ qǎNl> qO 0 >Bހw+/P-Q:~Qf v֭[M璳3w)% ,@ + lxwV o߮:k9998^ԩSq{wccÐBT>) aO&,|Ν;gl6A߿ W X@9[ųzqo|!3gb9l X0h:$X-*>w",?xk ڎ=bBBaHA Yj *儮;K!8 q'NmZPHB X +b x8~޽8C6,h@h$>o\"* TJ7KRmXii(4 >Ɓ@-"5Kⷵz9˵؈lO^e{  d^!%"'\1]~Og;%l_PH4 < >#._z,ZKYh޴&w ,tHA d(wJ~Ry!t!%%:oB2k! ,8'@7t(M,g}zAc9ۺ|0@_(yX|aoz)r!QeAH@W2)@VB¼&!F|Rt}a2~Bny4|@YpB󵭹as?,< [.]d_!'\"P rY4]Ye'wY0H|p:Ȫ!4^,r}=}y ('|Z|=Rtⅾ6AAB:X-4v *vyr}N̷f^X+&1pu*yY𒱸XZznW}1o /^,! V> C @c'qH,;sxK vR|)~]BzKA{ \P`y X}9Jįa9C s*> 2[AAՁ,/s/9) `wƈv,x1 >8> ҼBɠ&RjnǞ| 4Aro~9 Ux  ':X^@Wia T KweO^o~v2ϮRKM#rA rUJ ,fk/3k-UH00L^'" Hݿ?7{8Ozk/sdI|_"y3bb`BH`?/@0`Fi]< ENt%:oVXaƩ,@$[ 4a @(NA}U?+:@<!ra=Z WL& EㆀYYC%< @ĢaQKT&,,I a7t`PV`]@%l@Y4k`t0 _'Zf%`w:YA r[ja1 V%`ܿ m ӎW4N|7`U < K"hiyPx&Ph1H38 jrV5q`ڂ&^ ?:8%0J`> 8 /€E! Ɓ@ϱ7-?JM)bO\<A% 6N9T+|0n"v4\|0z_*w[kg)&sCa y?>@2%\oeٟ@c60PC ")_"&ŷZٟ%'RE H/`!r{4A}IT. 4η]cPq*4dN onf%C'~o\*Y^W ".oצo "i!iwfnNP"|AmI!SB6Gs L Xbk/ : @C;&2묠sa2u ܯt^A`pE`%*AvP<,gQ8-~|o#PW0h @vTu\aCI:wbQfnA/X@N '2  5,Z3?- 3p~wAo$ %+2',ccCOCuGYVI 3b!7O޴4 $+8`1d(A  ulutaG|I ?+%b0F |bEH׏"%RZ r7d N-!A{|=mևŷ270``p1#BPBaO en {VvY{-/,>@!! @ z~d\dܸφ+ƥ bpp(E "d#}h?'$td @:$@0Z&߫EO{bVWe  {Y{R0%d0@  $ 4?ʣMb@o'ij|@a!h-\/d ,dh`, =Mx'Q}Vp@]^p X  4-4]sF_G @c0YA `h-!'!<~<:\P|qP &fN24h$$Rh-%zV'5P.H^ Zp BM?/E~V>`" EÃBMO nFa"E ``c5JzOCx>E}*sj2 `B0H ,0,a>ٞAZx"d,}_ZXK/f@ P̬ |5 #d[R :l}bMڌg @2d?kuw9jHdžTCV AU `h`O~T:%cAHc] buYހ\ϳZ*9ܠ\/E|ldrAS 0 W*ۤytcY*5ee7eamxf:)dr?d(W[e/gOAAZ)z'UD;&ḅ ߤQW*_|¾nxIENDB`panicparse-2.3.1/stack/emoji_u1f4a3_64.gif000066400000000000000000000037271446536624000201600ustar00rootroot00000000000000GIF89a@@a!!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGG50HHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRR@SSSTTT>7UUUVVVWWW&=XXXYYY(<);ZZZ[[[\\\]]]^^^___```aaa45bbb>0@.C6I)XQOCO&SHU#Z \Q]aWeuuz} ~ ! ,@@)#&*^# $Z_%  `KTW*%#\GIR$(ͅVJ3 "*߃() ߻SN`0%'@b@`b3,Otb$08JFDP cVx[  Tha"$D-A=0ɀJ OAQ$ᠡa)uk.@F0[`@+ kBBYH (Hx^Pmki"9d! !8X oI *bH!~&Dp 6RĈ$C㰗77nBE0A (R7Q3\s}c0P +BP!|~vBGo+  .È0B , N $@_+B 2PÏ4É+"Hg$݈ .@ 8Ö9PX-ɢcmF@”.@;Dx 4Ђ &BX  ZCE %-X@AE1@0* LVO2oœ<1tFZ E hN5-V 1ܰCjC ~":$`&~ /ȀBzB& ArK n#9\ eNTP2b`=A}԰?N>PAJ%Y6 "up@Ql(EX-A T[@<0x~*`ANSl#!`0 항80i} % @ x*iU^@f& ,&U1 pR2nRl@#Qǔ  <` !*@y$@ XR"`N(JD  `"5@0eH8d[.h#)IKʵvCjʓ6p#A@DSUHER:QI DW太b(((aA9pLՐ!*p ZjUutͫ^@;panicparse-2.3.1/stack/example_test.go000066400000000000000000000110031446536624000177750ustar00rootroot00000000000000// 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. // Output is currently different on 32 bits on go1.17 so skip the examples for // now. It's a gross hack. //go:build go1.18 || !386 package stack_test import ( "bytes" "errors" "fmt" "io" "io/ioutil" "log" "os" "os/exec" "path/filepath" "runtime/debug" "github.com/maruel/panicparse/v2/stack" ) // Runs a crashing program and converts it to a dense text format like pp does. func Example_text() { source := `package main import "time" func main() { c := crashy{} go func() { c.die(42.) }() select {} } type crashy struct{} func (c crashy) die(f float32) { time.Sleep(time.Millisecond) panic(int(f)) }` // Skipped error handling to make the example shorter. root, _ := ioutil.TempDir("", "stack") defer os.RemoveAll(root) p := filepath.Join(root, "main.go") ioutil.WriteFile(p, []byte(source), 0600) // Disable both optimization (-N) and inlining (-l). c := exec.Command("go", "run", "-gcflags", "-N -l", p) // This is important, otherwise only the panicking goroutine will be printed. c.Env = append(os.Environ(), "GOTRACEBACK=1") raw, _ := c.CombinedOutput() stream := bytes.NewReader(raw) s, suffix, err := stack.ScanSnapshot(stream, os.Stdout, stack.DefaultOpts()) if err != nil && err != io.EOF { log.Fatal(err) } // Find out similar goroutine traces and group them into buckets. buckets := s.Aggregate(stack.AnyValue).Buckets // Calculate alignment. srcLen := 0 pkgLen := 0 for _, bucket := range buckets { for _, line := range bucket.Signature.Stack.Calls { if l := len(fmt.Sprintf("%s:%d", line.SrcName, line.Line)); l > srcLen { srcLen = l } if l := len(filepath.Base(line.Func.ImportPath)); l > pkgLen { pkgLen = l } } } for _, bucket := range buckets { // Print the goroutine header. extra := "" if s := bucket.SleepString(); s != "" { extra += " [" + s + "]" } if bucket.Locked { extra += " [locked]" } if len(bucket.CreatedBy.Calls) != 0 { extra += fmt.Sprintf(" [Created by %s.%s @ %s:%d]", bucket.CreatedBy.Calls[0].Func.DirName, bucket.CreatedBy.Calls[0].Func.Name, bucket.CreatedBy.Calls[0].SrcName, bucket.CreatedBy.Calls[0].Line) } fmt.Printf("%d: %s%s\n", len(bucket.IDs), bucket.State, extra) // Print the stack lines. for _, line := range bucket.Stack.Calls { fmt.Printf( " %-*s %-*s %s(%s)\n", pkgLen, line.Func.DirName, srcLen, fmt.Sprintf("%s:%d", line.SrcName, line.Line), line.Func.Name, &line.Args) } if bucket.Stack.Elided { io.WriteString(os.Stdout, " (...)\n") } } // If there was any remaining data in the pipe, dump it now. if len(suffix) != 0 { os.Stdout.Write(suffix) } if err == nil { io.Copy(os.Stdout, stream) } // Output: // panic: 42 // // 1: running [Created by main.main @ main.go:7] // main main.go:17 crashy.die(42) // main main.go:8 main.func1() // 1: select (no cases) // main main.go:10 main() // exit status 2 } // Process multiple consecutive goroutine snapshots. func Example_stream() { // Stream of stack traces: var r io.Reader var w io.Writer opts := stack.DefaultOpts() for { s, suffix, err := stack.ScanSnapshot(r, w, opts) if s != nil { // Process the snapshot... } if err != nil && err != io.EOF { if len(suffix) != 0 { w.Write(suffix) } log.Fatal(err) } // Prepend the suffix that was read to the rest of the input stream to // catch the next snapshot signature: r = io.MultiReader(bytes.NewReader(suffix), r) } } // Converts a stack trace from os.Stdin into HTML on os.Stdout, discarding // everything else. func Example_hTML() { s, _, err := stack.ScanSnapshot(os.Stdin, ioutil.Discard, stack.DefaultOpts()) if err != nil && err != io.EOF { log.Fatal(err) } if s != nil { s.Aggregate(stack.AnyValue).ToHTML(os.Stdout, "") } } // A sample parseStack function expects a stdlib stacktrace from runtime.Stack or debug.Stack and returns // the parsed stack object. func Example_simple() { parseStack := func(rawStack []byte) stack.Stack { s, _, err := stack.ScanSnapshot(bytes.NewReader(rawStack), ioutil.Discard, stack.DefaultOpts()) if err != nil && err != io.EOF { panic(err) } if len(s.Goroutines) > 1 { panic(errors.New("provided stacktrace had more than one goroutine")) } return s.Goroutines[0].Signature.Stack } parsedStack := parseStack(debug.Stack()) fmt.Printf("parsedStack: %#v", parsedStack) } panicparse-2.3.1/stack/go1.17_test.go000066400000000000000000000005171446536624000172660ustar00rootroot00000000000000// Copyright 2021 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:build go1.17 // +build go1.17 package stack // See https://github.com/maruel/panicparse/issues/61 for explanation. const combinedAggregateArgs = true panicparse-2.3.1/stack/gopre1.17_test.go000066400000000000000000000005221446536624000177710ustar00rootroot00000000000000// Copyright 2021 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:build !go1.17 // +build !go1.17 package stack // See https://github.com/maruel/panicparse/issues/61 for explanation. const combinedAggregateArgs = false panicparse-2.3.1/stack/goroutines.tpl000066400000000000000000000220341446536624000177010ustar00rootroot00000000000000 {{- /* Join a list */ -}} {{- define "Join" -}} {{- if . -}} {{- $l := len . -}} {{- $last := minus $l 1 -}} {{- range $i, $e := . -}} {{- $e -}} {{- $isNotLast := ne $i $last -}} {{- if $isNotLast}}, {{end -}} {{- end -}} {{- end -}} {{- end -}} {{- /* Accepts a Args */ -}} {{- define "RenderArgs" -}} {{- $elided := .Elided -}} {{- if .Processed -}} {{- $l := len .Processed -}} {{- $last := minus $l 1 -}} {{- range $i, $e := .Processed -}} {{- $e -}} {{- $isNotLast := ne $i $last -}} {{- if or $elided $isNotLast}}, {{end -}} {{- end -}} {{- else -}} {{- $l := len .Values -}} {{- $last := minus $l 1 -}} {{- range $i, $e := .Values -}} {{- $e.String -}} {{- $isNotLast := ne $i $last -}} {{- if or $elided $isNotLast}}, {{end -}} {{- end -}} {{- end -}} {{- if $elided}}…{{end -}} {{- end -}} {{- /* Accepts a Call */ -}} {{- define "RenderCreatedBy" -}} {{- if and .LocalSrcPath (ne .RemoteSrcPath .LocalSrcPath) -}} RemoteSrcPath: {{.RemoteSrcPath}}
LocalSrcPath: {{.LocalSrcPath}} {{- else -}} SrcPath: {{.RemoteSrcPath}} {{- end -}}
Func: {{.Func.Complete}}
Location: {{.Location}}
{{.SrcName}}:{{.Line}} {{.Func.DirName}}.{{.Func.Name}}()
{{- end -}} {{- /* Accepts a Stack */ -}} {{- define "RenderCalls" -}} {{- range $i, $e := .Calls -}} {{- end -}} {{- if .Elided}}{{end -}}
{{$i}} {{$e.Func.DirName}} {{- if and $e.LocalSrcPath (ne $e.RemoteSrcPath $e.LocalSrcPath) -}} RemoteSrcPath: {{$e.RemoteSrcPath}}
LocalSrcPath: {{$e.LocalSrcPath}} {{- else -}} SrcPath: {{$e.RemoteSrcPath}} {{- end -}}
Func: {{$e.Func.Complete}}
Location: {{$e.Location}}
{{$e.SrcName}}:{{$e.Line}}
{{$e.Func.Name}}({{template "RenderArgs" $e.Args}})
(…)
{{- end -}} PanicParse
{{- if .Aggregated -}} {{- range $i, $e := .Aggregated.Buckets -}} {{$l := len $e.IDs}}

Signature #{{$i}}: {{$l}} routine{{if ne 1 $l}}s{{end}}: {{$e.State}} {{- if $e.SleepMax -}} {{- if ne $e.SleepMin $e.SleepMax}} [{{$e.SleepMin}}~{{$e.SleepMax}} mins] {{- else}} [{{$e.SleepMax}} mins] {{- end -}} {{- end -}}

{{if $e.Locked}} [locked] {{- end -}} {{- if $e.CreatedBy.Calls}} Created by: {{template "RenderCreatedBy" index $e.CreatedBy.Calls 0}} {{- end -}} {{template "RenderCalls" $e.Signature.Stack}} {{- end -}} {{- else -}} {{- range $i, $e := .Snapshot.Goroutines -}}

Routine {{$e.ID}}: {{$e.State}} {{- if $e.SleepMax -}} {{- if ne $e.SleepMin $e.SleepMax}} [{{$e.SleepMin}}~{{$e.SleepMax}} mins] {{- else}} [{{$e.SleepMax}} mins] {{- end -}} {{- end -}}

{{if $e.Locked}} [locked] {{- end -}} {{if $e.RaceAddr}} Race {{if $e.RaceWrite}}write{{else}}read{{end}} @ {{printf "0x%08X" $e.RaceAddr}}
{{- end -}} {{- if $e.CreatedBy.Calls}} Created by: {{template "RenderCreatedBy" index $e.CreatedBy.Calls 0}} {{- end -}} {{template "RenderCalls" $e.Signature.Stack}} {{- end -}} {{- end -}}

Metadata

  • Created on {{.Now.String}}
  • {{.Version}}
  • {{- if and .Snapshot.LocalGOROOT (ne .Snapshot.RemoteGOROOT .Snapshot.LocalGOROOT) -}}
  • GOROOT (remote): {{.Snapshot.RemoteGOROOT}}
  • GOROOT (local): {{.Snapshot.LocalGOROOT}}
  • {{- else -}}
  • GOROOT: {{.Snapshot.RemoteGOROOT}}
  • {{- end -}}
  • GOPATH: {{template "Join" .Snapshot.LocalGOPATHs}}
  • {{- if .Snapshot.LocalGomods -}}
  • go modules (local):
      {{- range $path, $import := .Snapshot.LocalGomods -}}
    • {{$path}}: {{$import}}
    • {{- end -}}
  • {{- end -}}
  • GOMAXPROCS: {{.GOMAXPROCS}}

Legend

Type Exported Private
Package main Sources that are in the main package. main.Foo() main.foo()
Go module Sources located inside a directory containing a go.mod file but outside $GOPATH. pkg.Foo() pkg.foo()
$GOPATH/src/... Sources located inside the traditional $GOPATH/src directory. pkg.Foo() pkg.foo()
$GOPATH/pkg/mod/... Sources located inside the go module dependency cache under $GOPATH/pkg/mod. These files are unmodified third parties. pkg.Foo() pkg.foo()
Standard library Sources from the Go standard library under $GOROOT/src/. pkg.Foo() pkg.foo()
Unknown source location Sources which location was not successfully determined. pkg.Foo() pkg.foo()
{{- .Footer -}} {{- /* Add unnecessary bottom spacing so the last tooltip from the legend is visible. */ -}}
panicparse-2.3.1/stack/html.go000066400000000000000000000160401446536624000162550ustar00rootroot00000000000000// Copyright 2017 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 run regen.go package stack import ( "fmt" "html/template" "io" "log" "net/url" "regexp" "runtime" "strings" "time" ) // ToHTML formats the aggregated buckets as HTML to the writer. // // Use footer to add custom HTML at the bottom of the page. func (a *Aggregated) ToHTML(w io.Writer, footer template.HTML) error { data := map[string]interface{}{ "Aggregated": a, "Footer": footer, "Snapshot": a.Snapshot, } return toHTML(w, data) } // ToHTML formats the snapshot as HTML to the writer. // // Use footer to add custom HTML at the bottom of the page. func (s *Snapshot) ToHTML(w io.Writer, footer template.HTML) error { data := map[string]interface{}{ "Footer": footer, "Snapshot": s, } return toHTML(w, data) } // Private stuff. func toHTML(w io.Writer, data map[string]interface{}) error { m := template.FuncMap{ "funcClass": funcClass, "minus": minus, "pkgURL": pkgURL, "srcURL": srcURL, "symbol": symbol, } data["Favicon"] = favicon data["GOMAXPROCS"] = runtime.GOMAXPROCS(0) data["Now"] = time.Now().Truncate(time.Second) data["Version"] = runtime.Version() t, err := template.New("t").Funcs(m).Parse(indexHTML) if err != nil { return err } return t.Execute(w, data) } var reMethodSymbol = regexp.MustCompile(`^\(\*?([^)]+)\)(\..+)$`) func funcClass(c *Call) template.HTML { if c.Func.IsPkgMain { return "FuncMain Exported" } s := c.Location.String() if c.Func.IsExported { s += " Exported" } /* #nosec G203 */ return template.HTML("Func") + template.HTML(template.HTMLEscapeString(s)) } func minus(i, j int) int { return i - j } // pkgURL returns a link to the godoc for the call. func pkgURL(c *Call) template.URL { imp := c.ImportPath // Check for vendored code first. if i := strings.Index(imp, "/vendor/"); i != -1 { imp = imp[i+8:] } ip := escape(imp) if ip == "" { return "" } url := template.URL("") if c.Location == Stdlib { // This always links to the latest release, past releases are not online. // That's somewhat unfortunate. url = "https://golang.org/pkg/" } else { // TODO(maruel): Leverage Location. // Use pkg.go.dev when there's a version (go module) and godoc.org when // there's none (implies branch master). _, branch := getSrcBranchURL(c) if branch == "master" || branch == "" { url = "https://godoc.org/" } else { url = "https://pkg.go.dev/" } } if c.Func.IsExported { return url + ip + template.URL("#") + symbol(&c.Func) } return url + ip } // srcURL returns an URL to the sources. // // TODO(maruel): Support custom local godoc server as it serves files too. func srcURL(c *Call) template.URL { url, _ := getSrcBranchURL(c) return url } func escape(s string) template.URL { // That's the only way I found to get the kind of escaping I wanted, where // '/' is not escaped. u := url.URL{Path: s} /* #nosec G203 */ return template.URL(u.EscapedPath()) } // getSrcBranchURL returns a link to the source on the web and the tag name for // the package version, if possible. func getSrcBranchURL(c *Call) (template.URL, template.URL) { tag := "" if c.Location == Stdlib { // TODO(maruel): This is not strictly speaking correct. The remote could be // running a different Go version from the current executable. ver := runtime.Version() const devel = "devel +" if strings.HasPrefix(ver, devel) { ver = ver[len(devel) : len(devel)+10] } tag = url.QueryEscape(ver) /* #nosec G203 */ return template.URL(fmt.Sprintf("https://github.com/golang/go/blob/%s/src/%s#L%d", tag, escape(c.RelSrcPath), c.Line)), template.URL(tag) } // TODO(maruel): Leverage Location. if rel := c.RelSrcPath; rel != "" { // Check for vendored code first. if i := strings.Index(rel, "/vendor/"); i != -1 { rel = rel[i+8:] } // Specialized support for github and golang.org. This will cover a fair // share of the URLs, but it'd be nice to support others too. Please submit // a PR (including a unit test that I was too lazy to add yet). switch host, rest := splitHost(rel); host { case "github.com": if parts := strings.SplitN(rest, "/", 3); len(parts) == 3 { p, srcTag, tag := splitTag(parts[1]) url := fmt.Sprintf("https://github.com/%s/%s/blob/%s/%s#L%d", escape(parts[0]), p, srcTag, escape(parts[2]), c.Line) /* #nosec G203 */ return template.URL(url), tag } log.Printf("problematic github.com URL: %q", rel) case "golang.org": // https://github.com/golang/build/blob/HEAD/repos/repos.go lists all // the golang.org/x/ packages. if parts := strings.SplitN(rest, "/", 3); len(parts) == 3 && parts[0] == "x" { // parts is: "x", , . p, srcTag, tag := splitTag(parts[1]) // The source of truth is are actually go.googlesource.com, but // github.com has nicer syntax highlighting. url := fmt.Sprintf("https://github.com/golang/%s/blob/%s/%s#L%d", p, srcTag, escape(parts[2]), c.Line) /* #nosec G203 */ return template.URL(url), tag } log.Printf("problematic golang.org URL: %q", rel) default: // For example gopkg.in. In this case there's no known way to find the // link to the source files, but we can still try to extract the version // if fetched from a go module. // Do a best effort to find a version by searching for a '@'. if i := strings.IndexByte(rel, '@'); i != -1 { if j := strings.IndexByte(rel[i:], '/'); j != -1 { tag = rel[i+1 : i+j] } } } } if c.LocalSrcPath != "" { /* #nosec G203 */ return template.URL("file:///" + escape(c.LocalSrcPath)), template.URL(tag) } if c.RemoteSrcPath != "" { /* #nosec G203 */ return template.URL("file:///" + escape(c.RemoteSrcPath)), template.URL(tag) } return "", "" } func splitHost(s string) (string, string) { parts := strings.SplitN(s, "/", 2) if len(parts) != 2 { return parts[0], "" } return parts[0], parts[1] } // "v0.0.0-20200223170610-d5e6a3e2c0ae" var reVersion = regexp.MustCompile(`v\d+\.\d+\.\d+\-\d+\-([a-f0-9]+)`) func splitTag(s string) (string, string, template.URL) { // Default to branch master for non-versioned dependencies. It's not // optimal but it's better than nothing? // TODO(maruel): Replace with HEAD. i := strings.IndexByte(s, '@') if i == -1 { // No tag was found. return s, "master", "master" } // We got a versioned go module. tag := s[i+1:] srcTag := tag if m := reVersion.FindStringSubmatch(tag); len(m) != 0 { srcTag = m[1] } /* #nosec G203 */ return s[:i], url.QueryEscape(srcTag), template.URL(url.QueryEscape(tag)) } // symbol is the hashtag to use to refer to the symbol when looking at // documentation. // // All of godoc/gddo, pkg.go.dev and golang.org/godoc use the same symbol // reference format. func symbol(f *Func) template.URL { s := f.Name if reMethodSymbol.MatchString(s) { // Transform the method form. s = reMethodSymbol.ReplaceAllString(s, "$1$2") } /* #nosec G203 */ return template.URL(url.QueryEscape(s)) } panicparse-2.3.1/stack/html_test.go000066400000000000000000000210251446536624000173130ustar00rootroot00000000000000// 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 ( "bytes" "fmt" "html/template" "io" "io/ioutil" "net/url" "regexp" "runtime" "strings" "testing" "github.com/maruel/panicparse/v2/internal/internaltest" ) func TestAggregated_ToHTML_2Buckets(t *testing.T) { t.Parallel() buf := bytes.Buffer{} if err := getBuckets().ToHTML(&buf, ""); err != nil { t.Fatal(err) } // We expect this to be fairly static across Go versions. We want to know if // it changes significantly, thus assert the approximate size. This is being // tested on travis. if l := buf.Len(); l < 4000 || l > 10000 { t.Fatalf("unexpected length %d", l) } } func TestAggregated_ToHTML_1Bucket(t *testing.T) { t.Parallel() // Exercise a condition when there's only one bucket. buf := bytes.Buffer{} a := getBuckets() a.Buckets = a.Buckets[:1] if err := a.ToHTML(&buf, ""); err != nil { t.Fatal(err) } // We expect this to be fairly static across Go versions. We want to know if // it changes significantly, thus assert the approximate size. This is being // tested on travis. if l := buf.Len(); l < 4000 || l > 10000 { t.Fatalf("unexpected length %d", l) } if strings.Contains(buf.String(), "foo-bar") { t.Fatal("unexpected") } } func TestAggregated_ToHTML_1Bucket_Footer(t *testing.T) { t.Parallel() buf := bytes.Buffer{} a := getBuckets() a.Buckets = a.Buckets[:1] if err := a.ToHTML(&buf, "foo-bar"); err != nil { t.Fatal(err) } if !strings.Contains(buf.String(), "foo-bar") { t.Fatal("expected") } } func TestGenerate(t *testing.T) { t.Parallel() // Confirms that nobody forgot to regenate data.go. htmlRaw, err := loadGoroutines() if err != nil { t.Fatal(err) } if string(htmlRaw) != indexHTML { t.Fatal("please run go generate") } } // TestGetSrcBranchURL also tests pkgURL and srcURL and symbol. func TestGetSrcBranchURL(t *testing.T) { t.Parallel() ver := runtime.Version() const prefix = "devel +" if strings.HasPrefix(ver, prefix) { ver = ver[len(prefix) : len(prefix)+10] } ver = url.QueryEscape(ver) data := []struct { name string c Call url, branch template.URL pkgURL template.URL loc Location }{ { "stdlib", newCallLocal( "net/http.(*Server).Serve", Args{}, goroot+"/src/net/http/server.go", 2933), template.URL("https://github.com/golang/go/blob/" + ver + "/src/net/http/server.go#L2933"), template.URL(ver), "https://golang.org/pkg/net/http#Server.Serve", Stdlib, }, { "gomodref", newCallLocal( "github.com/mattn/go-colorable.(*NonColorable).Write", Args{}, gopath+"/pkg/mod/github.com/mattn/go-colorable@v0.1.6/noncolorable.go", 30), "https://github.com/mattn/go-colorable/blob/v0.1.6/noncolorable.go#L30", "v0.1.6", "https://pkg.go.dev/github.com/mattn/go-colorable@v0.1.6#NonColorable.Write", GoPkg, }, /* TODO(maruel): Fix this. { "gomodref_with_dot", newCallLocal( "gopkg.in/fsnotify%2ev1.NewWatcher", Args{}, gopath+"/pkg/mod/gopkg.in/fsnotify.v1@v1.4.7/inotify.go", 59), "file:////home/user/go/pkg/mod/gopkg.in/fsnotify.v1@v1.4.7/inotify.go", "v1.4.7", "https://pkg.go.dev/gopkg.in/fsnotify.v1@v1.4.7#NewWatcher", GoPkg, }, */ { "gomod_commit_ref", newCallLocal( "golang.org/x/sys/unix.Nanosleep", Args{}, gopath+"/pkg/mod/golang.org/x/sys@v0.0.0-20200223170610-d5e6a3e2c0ae/unix/zsyscall_linux_amd64.go", 1160), "https://github.com/golang/sys/blob/d5e6a3e2c0ae/unix/zsyscall_linux_amd64.go#L1160", "v0.0.0-20200223170610-d5e6a3e2c0ae", "https://pkg.go.dev/golang.org/x/sys@v0.0.0-20200223170610-d5e6a3e2c0ae/unix#Nanosleep", GoPkg, }, { "vendor", newCallLocal( "github.com/maruel/panicparse/vendor/golang.org/x/sys/unix.Nanosleep", Args{}, gopath+"/src/github.com/maruel/panicparse/vendor/golang.org/x/sys/unix/zsyscall_linux_amd64.go", 1100), "https://github.com/golang/sys/blob/master/unix/zsyscall_linux_amd64.go#L1100", "master", "https://godoc.org/golang.org/x/sys/unix#Nanosleep", GOPATH, }, { "windows", Call{RemoteSrcPath: "c:/random.go"}, "file:///c:/random.go", "", "", LocationUnknown, }, { "windows_local", Call{LocalSrcPath: "c:/random.go"}, "file:///c:/random.go", "", "", LocationUnknown, }, { "empty", Call{}, "", "", "", LocationUnknown, }, } for i, line := range data { line := line t.Run(fmt.Sprintf("%d-%s", i, line.name), func(t *testing.T) { t.Parallel() url, branch := getSrcBranchURL(&line.c) if url != line.url { t.Errorf("%q != %q", url, line.url) } if branch != line.branch { t.Errorf("%q != %q", branch, line.branch) } if url := srcURL(&line.c); url != line.url { t.Errorf("%q != %q", url, line.url) } if url := pkgURL(&line.c); url != line.pkgURL { t.Errorf("%q != %q", url, line.pkgURL) } if line.c.Location != line.loc { t.Errorf("%s != %s", line.loc, line.c.Location) } }) } } func TestSymbol(t *testing.T) { t.Parallel() data := []struct { in Func want template.URL }{ { newFunc("github.com/mattn/go-colorable.(*NonColorable).Write"), "NonColorable.Write", }, { newFunc("golang.org/x/sys/unix.Nanosleep"), "Nanosleep", }, { Func{}, "", }, { newFunc("main.baz"), "baz", }, } for i, line := range data { line := line t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { if s := symbol(&line.in); s != line.want { t.Fatalf("%q != %q", s, line.want) } }) } } func TestSnapshot_ToHTML(t *testing.T) { t.Parallel() data := internaltest.PanicOutputs()["race"] if data == nil { t.Skip("-race is unsupported on this platform") } s, _, err := ScanSnapshot(bytes.NewReader(data), ioutil.Discard, DefaultOpts()) if err != nil { t.Fatal(err) } if s.Goroutines == nil { t.Fatal("missing context") } if s.Goroutines[0].RaceAddr == 0 { t.Fatal("expected a race") } if !s.IsRace() { t.Fatal("expected a race") } if err := s.ToHTML(ioutil.Discard, ""); err != nil { t.Fatal(err) } } func BenchmarkAggregated_ToHTML(b *testing.B) { b.ReportAllocs() s, _, err := ScanSnapshot(bytes.NewReader(internaltest.StaticPanicwebOutput()), ioutil.Discard, DefaultOpts()) if err != io.EOF { b.Fatal(err) } if s == nil { b.Fatal("missing context") } a := s.Aggregate(AnyPointer) b.ResetTimer() for i := 0; i < b.N; i++ { if err := a.ToHTML(ioutil.Discard, ""); err != nil { b.Fatal(err) } } } // // loadGoroutines should match what is in regen.go. func loadGoroutines() ([]byte, error) { htmlRaw, err := ioutil.ReadFile("goroutines.tpl") if err != nil { return nil, err } // Strip out leading whitespace. re := regexp.MustCompile("(\n[ \t]*)+") htmlRaw = re.ReplaceAll(htmlRaw, []byte("\n")) return htmlRaw, nil } // getBuckets returns a slice for testing. func getBuckets() *Aggregated { return &Aggregated{ Snapshot: &Snapshot{ LocalGOROOT: runtime.GOROOT(), LocalGOPATHs: []string{"/gopath"}, RemoteGOROOT: "/golang", RemoteGOPATHs: map[string]string{"/gopath": "/gopath"}, LocalGomods: map[string]string{"/tmp": "example.com/foo"}, }, Buckets: []*Bucket{ { Signature: Signature{ State: "chan receive", Stack: Stack{ Calls: []Call{ newCall( "main.func·001", Args{Values: []Arg{{Value: 0x11000000}, {Value: 2}}}, "/gopath/src/github.com/maruel/panicparse/stack/stack.go", 72), { Func: newFunc("sliceInternal"), Args: Args{Values: []Arg{{Value: 0x11000000}, {Value: 2}}}, RemoteSrcPath: "/golang/src/sort/slices.go", Line: 72, Location: Stdlib, }, { Func: newFunc("Slice"), Args: Args{Values: []Arg{{Value: 0x11000000}, {Value: 2}}}, RemoteSrcPath: "/golang/src/sort/slices.go", Line: 72, Location: Stdlib, }, newCall( "DoStuff", Args{Values: []Arg{{Value: 0x11000000}, {Value: 2}}}, "/gopath/src/foo/bar.go", 72), newCall( "doStuffInternal", Args{ Values: []Arg{{Value: 0x11000000}, {Value: 2}}, Elided: true, }, "/gopath/src/foo/bar.go", 72), }, }, }, IDs: []int{1, 2}, First: true, }, { IDs: []int{3}, Signature: Signature{ State: "running", Stack: Stack{Elided: true}, }, }, }, } } panicparse-2.3.1/stack/location_string.go000066400000000000000000000013541446536624000205110ustar00rootroot00000000000000// Code generated by "stringer -type Location"; DO NOT EDIT. package stack import "strconv" func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} _ = x[LocationUnknown-0] _ = x[GoMod-1] _ = x[GOPATH-2] _ = x[GoPkg-3] _ = x[Stdlib-4] _ = x[lastLocation-5] } const _Location_name = "LocationUnknownGoModGOPATHGoPkgStdliblastLocation" var _Location_index = [...]uint8{0, 15, 20, 26, 31, 37, 49} func (i Location) String() string { if i < 0 || i >= Location(len(_Location_index)-1) { return "Location(" + strconv.FormatInt(int64(i), 10) + ")" } return _Location_name[_Location_index[i]:_Location_index[i+1]] } panicparse-2.3.1/stack/reader.go000066400000000000000000000041321446536624000165520ustar00rootroot00000000000000// Copyright 2020 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 ( "bytes" "errors" "io" ) var ( errBufferFull = errors.New("buffer full") ) type reader struct { buf [16 * 1024]byte rd io.Reader r, w int err error } // fill reads a new chunk into the buffer. func (r *reader) fill() { // Slide existing data to beginning. if r.r > 0 { copy(r.buf[:], r.buf[r.r:r.w]) r.w -= r.r r.r = 0 } if r.w >= len(r.buf) { panic("tried to fill full buffer") } // Read new data: try a limited number of times. for i := 100; i > 0; i-- { n, err := r.rd.Read(r.buf[r.w:]) if n < 0 { panic("reader returned negative count from Read") } r.w += n if err != nil { r.err = err return } if n > 0 { return } } r.err = io.ErrNoProgress } func (r *reader) buffered() []byte { return r.buf[r.r:r.w] } func (r *reader) readSlice() ([]byte, error) { for s := 0; ; r.fill() { if i := bytes.IndexByte(r.buf[r.r+s:r.w], '\n'); i >= 0 { i += s line := r.buf[r.r : r.r+i+1] r.r += i + 1 return line, nil } if r.err != nil { line := r.buf[r.r:r.w] r.r = r.w err := r.err r.err = nil return line, err } if r.w-r.r == len(r.buf) { r.r = r.w return r.buf[:], errBufferFull } s = r.w - r.r } } // readLine is our own implementation of ReadBytes(). // // We try to use readSlice() as much as we can but we need to tolerate if an // input line is longer than the buffer specified at Reader creation. Not using // the more complicated slice of slices that Reader.ReadBytes() uses since it // should not happen often here. Instead bootstrap the memory allocation by // starting with 4x buffer size, which should get most cases with a single // allocation. func (r *reader) readLine() ([]byte, error) { var d []byte for { f, err := r.readSlice() if err != errBufferFull { if d == nil { return f, err } return append(d, f...), err } if d == nil { d = make([]byte, 0, len(f)*4) } d = append(d, f...) } } panicparse-2.3.1/stack/regen.go000066400000000000000000000032441446536624000164130ustar00rootroot00000000000000// Copyright 2017 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:build tools // +build tools package main import ( "bytes" "encoding/base64" "fmt" "io/ioutil" "os" "regexp" "strconv" "text/template" ) const content = `// Code generated by regen.go. DO NOT EDIT. package stack import ( "html/template" ) const indexHTML = {{.IndexHTML}} // favicon is the bomb emoji U+1F4A3 in Noto Emoji as a 128x128 base64 encoded // PNG. // // See README.md for license and how to retrieve it. const favicon template.HTML = "{{.Favicon}}" ` // loadGoroutines returns "goroutines.tpl" slightly processed for density. func loadGoroutines() ([]byte, error) { htmlRaw, err := ioutil.ReadFile("goroutines.tpl") if err != nil { return nil, err } // Strip out leading whitespace. re := regexp.MustCompile("(\\n[ \\t]*)+") htmlRaw = re.ReplaceAll(htmlRaw, []byte("\n")) return htmlRaw, nil } func mainImpl() error { htmlRaw, err := loadGoroutines() if err != nil { return err } // See README.md how to generate it. iconRaw, err := ioutil.ReadFile("emoji_u1f4a3_64.gif") if err != nil { return err } t, err := template.New("t").Parse(content) if err != nil { return err } data := map[string]string{ "IndexHTML": strconv.Quote(string(htmlRaw)), "Favicon": base64.StdEncoding.EncodeToString(iconRaw), } b := bytes.Buffer{} if err := t.Execute(&b, data); err != nil { return err } return ioutil.WriteFile("data.go", b.Bytes(), 0666) } func main() { if err := mainImpl(); err != nil { fmt.Fprintf(os.Stderr, "Failed: %s\n", err) os.Exit(1) } } panicparse-2.3.1/stack/source.go000066400000000000000000000230451446536624000166140ustar00rootroot00000000000000// Copyright 2015 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. // This file contains the code to process sources, to be able to deduct the // original types. package stack import ( "bytes" "fmt" "go/ast" "go/parser" "go/token" "io/ioutil" "math" "strconv" "strings" ) // Private stuff. // cacheAST is a cache of parsed Go sources. type cacheAST struct { files map[string][]byte parsed map[string]*parsedFile } // augmentGoroutine processes source files to improve call to be more // descriptive. // // It modifies the routine. func (c *cacheAST) augmentGoroutine(g *Goroutine) error { var err error for i, call := range g.Stack.Calls { // Only load the AST if there's an argument to process. if len(call.Args.Values) == 0 { continue } if err1 := c.loadFile(g.Stack.Calls[i].LocalSrcPath); err1 != nil { //log.Printf("%s", err) err = err1 } if p := c.parsed[call.LocalSrcPath]; p != nil { f, err1 := p.getFuncAST(call.Func.Name, call.Line) if err1 != nil { err = err1 continue } if f != nil { augmentCall(&g.Stack.Calls[i], f) } } } return err } // loadFile loads a Go source file and parses the AST tree. func (c *cacheAST) loadFile(fileName string) error { if fileName == "" { return nil } if _, ok := c.parsed[fileName]; ok { return nil } // Do not attempt to parse the same file twice. c.parsed[fileName] = nil if !strings.HasSuffix(fileName, ".go") { // Ignore C and assembly. return fmt.Errorf("cannot load non-go file %q", fileName) } /* #nosec G304 */ src, err := ioutil.ReadFile(fileName) if err != nil { return err } c.files[fileName] = src fset := token.NewFileSet() parsed, err := parser.ParseFile(fset, fileName, src, 0) if err != nil { return fmt.Errorf("failed to parse %w", err) } c.parsed[fileName] = &parsedFile{ lineToByteOffset: lineToByteOffsets(src), parsed: parsed, } return nil } // lineToByteOffsets extract the line number into raw file offset. // // Inserts a dummy 0 at offset 0 so line offsets can be 1 based. func lineToByteOffsets(src []byte) []int { offsets := []int{0, 0} for offset := 0; offset < len(src); { n := bytes.IndexByte(src[offset:], '\n') if n == -1 { break } offset += n + 1 offsets = append(offsets, offset) } return offsets } // parsedFile is a processed Go source file. type parsedFile struct { lineToByteOffset []int parsed *ast.File } // getFuncAST gets the callee site function AST representation for the code // inside the function f at line l. func (p *parsedFile) getFuncAST(f string, l int) (d *ast.FuncDecl, err error) { if len(p.lineToByteOffset) <= l { // The line number in the stack trace line does not exist in the file. That // can only mean that the sources on disk do not match the sources used to // build the binary. return nil, fmt.Errorf("line %d is over line count of %d", l, len(p.lineToByteOffset)-1) } // Walk the AST to find the lineToByteOffset that fits the line number. var lastFunc *ast.FuncDecl // Inspect() goes depth first. This means for example that a function like: // func a() { // b := func() {} // c() // } // // Were we are looking at the c() call can return confused values. It is // important to look at the actual ast.Node hierarchy. ast.Inspect(p.parsed, func(n ast.Node) bool { if d != nil { return false } if n == nil { return true } if int(n.Pos()) >= p.lineToByteOffset[l] { // We are expecting a ast.CallExpr node. It can be harder to figure out // when there are multiple calls on a single line, as the stack trace // doesn't have file byte offset information, only line based. // gofmt will always format to one function call per line but there can // be edge cases, like: // a = A{Foo(), Bar()} d = lastFunc //p.processNode(call, n) return false } else if f, ok := n.(*ast.FuncDecl); ok { lastFunc = f } return true }) return } func name(n ast.Node) string { switch t := n.(type) { case *ast.InterfaceType: return "interface{}" case *ast.Ident: return t.Name case *ast.SelectorExpr: return t.Sel.Name case *ast.StarExpr: return "*" + name(t.X) case *ast.BasicLit: return t.Value case *ast.Ellipsis: return "..." default: return "" } } // fieldToType returns the type name and whether if it's an ellipsis. func fieldToType(f *ast.Field) (string, bool) { switch arg := f.Type.(type) { case *ast.ArrayType: if arg.Len != nil { // Array. return "[" + name(arg.Len) + "]" + name(arg.Elt), false } // Slice. return "[]" + name(arg.Elt), false case *ast.Ellipsis: return name(arg.Elt), true case *ast.FuncType: // Do not print the function signature to not overload the trace. return "func", false case *ast.Ident: return arg.Name, false case *ast.InterfaceType: return "interface{}", false case *ast.SelectorExpr: return arg.Sel.Name, false case *ast.StarExpr: return "*" + name(arg.X), false case *ast.MapType: return fmt.Sprintf("map[%s]%s", name(arg.Key), name(arg.Value)), false case *ast.ChanType: return fmt.Sprintf("chan %s", name(arg.Value)), false default: // TODO(maruel): Implement anything missing. return "", false } } // extractArgumentsType returns the name of the type of each input argument. func extractArgumentsType(f *ast.FuncDecl) ([]string, bool) { var fields []*ast.Field if f.Recv != nil { if len(f.Recv.List) != 1 { panic("Expect only one receiver; please fix panicparse's code") } // If it is an object receiver (vs a pointer receiver), its address is not // printed in the stack trace so it needs to be ignored. if _, ok := f.Recv.List[0].Type.(*ast.StarExpr); ok { fields = append(fields, f.Recv.List[0]) } } var types []string ellipsis := false for _, arg := range append(fields, f.Type.Params.List...) { // Assert that ellipsis is only set on the last item of fields? var t string t, ellipsis = fieldToType(arg) mult := len(arg.Names) if mult == 0 { mult = 1 } for i := 0; i < mult; i++ { types = append(types, t) } } return types, ellipsis } // augmentCall walks the function and populate call accordingly. func augmentCall(call *Call, f *ast.FuncDecl) { flatArgs := make([]*Arg, 0, len(call.Args.Values)) call.Args.walk(func(arg *Arg) { flatArgs = append(flatArgs, arg) }) pop := func() *Arg { if len(flatArgs) == 0 { return nil } a := flatArgs[0] flatArgs = flatArgs[1:] return a } popFmt := func(fmtFn func(v uint64) string) string { a := pop() if a == nil { return "" } if a.IsOffsetTooLarge { return "_" } return fmtFn(a.Value) } popName := func() string { a := pop() if a == nil { return "" } if len(a.Name) != 0 { return a.Name } if a.IsOffsetTooLarge { return "_" } return fmt.Sprintf("0x%x", a.Value) } types, extra := extractArgumentsType(f) for i := 0; len(flatArgs) != 0; i++ { var t string if i >= len(types) { if !extra { // These are unexpected value! Print them as hex. call.Args.Processed = append(call.Args.Processed, popName()) continue } t = types[len(types)-1] } else { t = types[i] } var str string switch t { case "float32": str = popFmt(func(v uint64) string { f := float64(math.Float32frombits(uint32(v))) return strconv.FormatFloat(f, 'g', -1, 32) }) case "float64": str = popFmt(func(v uint64) string { f := math.Float64frombits(v) return strconv.FormatFloat(f, 'g', -1, 64) }) case "int": str = popFmt(func(v uint64) string { return strconv.FormatInt(int64(int(v)), 10) }) case "int8": str = popFmt(func(v uint64) string { return strconv.FormatInt(int64(int8(v)), 10) }) case "int16": str = popFmt(func(v uint64) string { return strconv.FormatInt(int64(int16(v)), 10) }) case "int32": str = popFmt(func(v uint64) string { return strconv.FormatInt(int64(int32(v)), 10) }) case "int64": str = popFmt(func(v uint64) string { return strconv.FormatInt(int64(v), 10) }) case "uint", "uint8", "uint16", "uint32", "uint64": str = popFmt(func(v uint64) string { return strconv.FormatUint(v, 10) }) case "bool": str = popFmt(func(v uint64) string { if v == 0 { return "false" } return "true" }) case "string": name := popName() lenStr := popFmt(func(v uint64) string { return strconv.FormatUint(v, 10) }) str = fmt.Sprintf("%s(%s, len=%s)", t, name, lenStr) default: if strings.HasPrefix(t, "*") { str = fmt.Sprintf("%s(%s)", t, popName()) } else if strings.HasPrefix(t, "[]") { name := popName() lenStr := popFmt(func(v uint64) string { return strconv.FormatUint(v, 10) }) capStr := popFmt(func(v uint64) string { return strconv.FormatUint(v, 10) }) str = fmt.Sprintf("%s(%s len=%s cap=%s)", t, name, lenStr, capStr) } else { if i < len(call.Args.Values) && call.Args.Values[i].IsAggregate { // If top-level argument is an aggregate-type, include each // of its sub-arguments. v := &call.Args.Values[i].Fields var fields []string v.walk(func(arg *Arg) { fields = append(fields, popName()) }) if v.Elided { fields = append(fields, "...") } str = fmt.Sprintf("%s{%s}", t, strings.Join(fields, ", ")) } else { // Assumes it's an interface. For now, discard the object // value, which is probably not a good idea. str = fmt.Sprintf("%s(%s)", t, popName()) pop() } } } call.Args.Processed = append(call.Args.Processed, str) } } panicparse-2.3.1/stack/source_test.go000066400000000000000000001261631446536624000176600ustar00rootroot00000000000000// Copyright 2015 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 ( "bytes" "fmt" "io/ioutil" "math/bits" "os" "os/exec" "path" "path/filepath" "regexp" "runtime" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/maruel/panicparse/v2/internal/internaltest" ) // goarchList is a list of GOARCH values. type goarchList []string var allPlatforms goarchList = nil func TestAugment(t *testing.T) { t.Parallel() gm := map[string]string{"/root": "main"} newCallSrc := func(f string, a Args, s string, l int) Call { c := newCall(f, a, s, l) // Simulate findRoots(). if !c.updateLocations(goroot, goroot, gm, gopaths) { t.Fatalf("c.updateLocations(%v, %v, %v, %v) failed on %s", goroot, goroot, gm, gopaths, s) } return c } // For test case "negative int32". negInt := uint64(4294967173) negPtr := false if bits.UintSize == 64 { negInt = 828928688005 negPtr = true } type testCase struct { name string input string // Starting with go1.11, inlining is enabled. The stack trace may (it // depends on tool chain version) not contain much information about the // arguments and shows as elided. Non-pointer call may show an elided // argument, while there was no argument listed before. mayBeInlined bool // archBlock lists the CPU architectures to skip this test case on. // // Many test are hard to parse in 32 bits. Eventually we should fix these // but I don't have time for this. // // The list is based on https://github.com/maruel/panicparse/issues/80. // I was able to locally reproduce amd64 and 386 but not the rest. archBlock goarchList want Stack } data := []testCase{ { "local function doesn't interfere", `func main() { f("yo") } func f(s string) { a := func(i int) int { return 1 + i } _ = a(3) panic("ooh") }`, // The function became inlinable in go 1.17. combinedAggregateArgs, goarchList{"arm", "arm64", "ppc64le", "riscv64"}, Stack{ Calls: []Call{ newCallSrc( "main.f", ifCombinedAggregateArgs( Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 2}}, }}}, Processed: []string{"string(0x2fffffff, len=2)"}, }, // else Args{ Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 2}}, Processed: []string{"string(0x2fffffff, len=2)"}, }, ), "/root/main.go", 10), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, { "func", `func main() { f(func() string { return "ooh" }) } func f(a func() string) { panic(a()) }`, true, goarchList{"arm"}, Stack{ Calls: []Call{ newCallSrc( "main.f", Args{ Values: []Arg{{Value: pointer, IsPtr: true}}, Processed: []string{"func(0x2fffffff)"}, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, { "func ellipsis", `func main() { f(func() string { return "ooh" }) } func f(a ...func() string) { panic(a[0]()) }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.f", ifCombinedAggregateArgs( Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 1}, {Value: 1}}, }}}, Processed: []string{"{0x2fffffff, 0x1, 0x1}"}, }, // else Args{ Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 1}, {Value: 1}}, Processed: []string{"(0x2fffffff)", "(0x1)"}, }, ), "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, { "interface{}", `func main() { f(make([]interface{}, 5, 7)) } func f(a []interface{}) { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.f", ifCombinedAggregateArgs( Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 5}, {Value: 7}}, }}}, Processed: []string{"[]interface{}(0x2fffffff len=5 cap=7)"}, }, // else Args{ Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 5}, {Value: 7}}, Processed: []string{"[]interface{}(0x2fffffff len=5 cap=7)"}, }, ), "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, { "[]int", `func main() { f(make([]int, 5, 7)) } func f(a []int) { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.f", ifCombinedAggregateArgs( Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 5}, {Value: 7}}, }}}, Processed: []string{"[]int(0x2fffffff len=5 cap=7)"}, }, // else Args{ Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 5}, {Value: 7}}, Processed: []string{"[]int(0x2fffffff len=5 cap=7)"}, }, ), "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, { "[]interface{}", `func main() { f([]interface{}{"ooh"}) } func f(a []interface{}) { panic(a[0].(string)) }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.f", ifCombinedAggregateArgs( Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 1}, {Value: 1}}, }}}, Processed: []string{"[]interface{}(0x2fffffff len=1 cap=1)"}, }, // else Args{ Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 1}, {Value: 1}}, Processed: []string{"[]interface{}(0x2fffffff len=1 cap=1)"}, }, ), "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, { "map[int]int", `func main() { f(map[int]int{1: 2}) } func f(a map[int]int) { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.f", Args{ Values: []Arg{{Value: pointer, IsPtr: true}}, Processed: []string{"map[int]int(0x2fffffff)"}, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, { "map[interface{}]interface{}", `func main() { f(make(map[interface{}]interface{})) } func f(a map[interface{}]interface{}) { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.f", Args{ Values: []Arg{{Value: pointer, IsPtr: true}}, Processed: []string{"map[interface{}]interface{}(0x2fffffff)"}, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, { "chan int", `func main() { f(make(chan int)) } func f(a chan int) { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.f", Args{ Values: []Arg{{Value: pointer, IsPtr: true}}, Processed: []string{"chan int(0x2fffffff)"}, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, { "chan interface{}", `func main() { f(make(chan interface{})) } func f(a chan interface{}) { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.f", Args{ Values: []Arg{{Value: pointer, IsPtr: true}}, Processed: []string{"chan interface{}(0x2fffffff)"}, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, { "non-pointer method", `func main() { var s S s.f() } type S struct {} func (s S) f() { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.S.f", ifCombinedAggregateArgs( Args{Values: []Arg{{IsAggregate: true, Fields: Args{}}}}, // else Args{}, ), "/root/main.go", 8), newCallSrc("main.main", Args{}, "/root/main.go", 4), }, }, }, { "pointer method", `func main() { var s S s.f() } type S struct {} func (s *S) f() { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.(*S).f", Args{ Values: []Arg{{Value: pointer, IsPtr: true}}, Processed: []string{"*S(0x2fffffff)"}, }, "/root/main.go", 8), newCallSrc("main.main", Args{}, "/root/main.go", 4), }, }, }, { "string", `func main() { f("ooh") } func f(s string) { panic(s) }`, true, goarchList{"arm", "arm64", "ppc64le", "riscv64"}, Stack{ Calls: []Call{ newCallSrc( "main.f", ifCombinedAggregateArgs( Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 3}}, }}}, Processed: []string{"string(0x2fffffff, len=3)"}, }, // else Args{ Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 3}}, Processed: []string{"string(0x2fffffff, len=3)"}, }, ), "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, { "string and int", `func main() { f("ooh", 42) } func f(s string, i int) { panic(s) }`, true, goarchList{"arm", "arm64", "ppc64le", "riscv64"}, Stack{ Calls: []Call{ newCallSrc( "main.f", ifCombinedAggregateArgs( Args{ Values: []Arg{ {IsAggregate: true, Fields: Args{ Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 3}}, }}, {Value: 42}, }, Processed: []string{"string(0x2fffffff, len=3)", "42"}, }, // else Args{ Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 3}, {Value: 42}}, Processed: []string{"string(0x2fffffff, len=3)", "42"}, }, ), "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, { "values are elided", `func main() { f(0, 0, 0, 0, 0, 0, 0, 0, 42, 43, 44, 45, nil) } func f(s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12 int, s13 interface{}) { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.f", Args{ Values: []Arg{{}, {}, {}, {}, {}, {}, {}, {}, {Value: 42}, {Value: 43}}, Processed: []string{"0", "0", "0", "0", "0", "0", "0", "0", "42", "43"}, Elided: true, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, { "error", `import "errors" func main() { f(errors.New("ooh")) } func f(err error) { panic(err.Error()) }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.f", ifCombinedAggregateArgs( Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{Value: pointer, IsPtr: true}, {Value: pointer, IsPtr: true}}, }}}, Processed: []string{"error{0x2fffffff, 0x2fffffff}"}, }, // else Args{ Values: []Arg{{Value: pointer, IsPtr: true}, {Value: pointer, IsPtr: true}}, Processed: []string{"error(0x2fffffff)"}, }, ), "/root/main.go", 7), newCallSrc("main.main", Args{}, "/root/main.go", 4), }, }, }, { "error unnamed", `import "errors" func main() { f(errors.New("ooh")) } func f(error) { panic("ooh") }`, true, goarchList{"386", "arm", "arm64", "mipsle", "mips64le", "ppc64le", "riscv64", "s390x"}, Stack{ Calls: []Call{ newCallSrc( "main.f", ifCombinedAggregateArgs( Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 3}}, }}}, Processed: []string{"error{0x2fffffff, 0x3}"}, }, // else Args{ Values: []Arg{{Value: pointer, IsPtr: true}, {Value: pointer, IsPtr: true}}, Processed: []string{"error(0x2fffffff)"}, }, ), "/root/main.go", 7), newCallSrc("main.main", Args{}, "/root/main.go", 4), }, }, }, { "float32", `func main() { f(0.5) } func f(v float32) { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.f", // The value is NOT a pointer but floating point encoding is not // deterministic. Args{ Values: []Arg{{Value: pointer, IsPtr: true}}, Processed: []string{"0.5"}, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, { "float64", `func main() { f(0.5) } func f(v float64) { panic("ooh") }`, true, goarchList{"386", "arm", "mipsle"}, Stack{ Calls: []Call{ newCallSrc( "main.f", // The value is NOT a pointer but floating point encoding is not // deterministic. Args{ Values: []Arg{{Value: pointer, IsPtr: true}}, Processed: []string{"0.5"}, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, { "uint", `func main() { f(123) } func f(v uint) { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.f", Args{ Values: []Arg{{Value: 123}}, Processed: []string{"123"}, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, { "negative int32", `func main() { f(-123) } func f(v int32) { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.f", Args{ Values: []Arg{{Value: negInt, IsPtr: negPtr}}, Processed: []string{"-123"}, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, { "array", `func main() { f([3]byte{2, 3, 4}) } func f(v2 [3]byte) { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.f", ifCombinedAggregateArgs( Args{ Values: []Arg{ {IsAggregate: true, Fields: Args{ Values: []Arg{{Value: 2}, {Value: 3}, {Value: 4}}, }}, }, Processed: []string{"[3]byte{0x2, 0x3, 0x4}"}, }, // else Args{ Values: []Arg{{Value: pointer, IsPtr: true}}, Processed: []string{"[3]byte(0x2fffffff)"}, }, ), "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, { "deeply nested aggregate type", `func main() { f(a{b{c{d{13}}}}) } func f(v a) { panic("ooh") } type a struct{ b } type b struct{ c } type c struct{ d } type d struct{ i int }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.f", ifCombinedAggregateArgs( Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{Value: 13}}, }}}, }}}, }}}, }}}, Processed: []string{"a{0xd}"}, }, // else Args{ Values: []Arg{{Value: 13}}, Processed: []string{"a(0xd)"}, }, ), "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, { "deeply nested aggregate type with elision", `func main() { f(a{b{c{d{e{13}}}}}) } func f(v a) { panic("ooh") } type a struct{ b } type b struct{ c } type c struct{ d } type d struct{ e } type e struct{ i int }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.f", ifCombinedAggregateArgs( Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Elided: true, }}}, }}}, }}}, }}}, }}}, }, // else Args{ Values: []Arg{{Value: 13}}, Processed: []string{"a(0xd)"}, }, ), "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, { "argument offsets partially too large", `func main() { f(a{b{c{d{e{}}}}}, 13, []int{14, 15}) } func f(v a, i int, s []int) { panic("ooh") } type a struct{ b } type b struct{ c } type c struct{ d } type d struct{ e } type e struct{ i [27]int }`, true, goarchList{"386", "arm", "mipsle"}, Stack{ Calls: []Call{ newCallSrc( "main.f", ifCombinedAggregateArgs( Args{ Values: []Arg{ {IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Elided: true, }}}, }}}, }}}, }}}, }}, {Value: 13}, {IsAggregate: true, Fields: Args{ Values: []Arg{ {Value: pointer, IsPtr: true}, {Value: 2}, {IsOffsetTooLarge: true}, }, }}, }, Processed: []string{"a{}", "13", "[]int(0x2fffffff len=2 cap=_)"}, }, // else Args{ Values: []Arg{{}, {}, {}, {}, {}, {}, {}, {}, {}, {}}, Processed: []string{"a(0x0)", "0", "[]int(0x0 len=0 cap=0)", "0x0", "0x0", "0x0", "0x0"}, Elided: true, }, ), "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, { "argument offsets entirely too large", `func main() { f(a{b{c{d{e{}}}}}, 13, []int{14, 15}) } func f(v a, i int, s []int) { panic("ooh") } type a struct{ b } type b struct{ c } type c struct{ d } type d struct{ e } type e struct{ i [30]int }`, true, goarchList{"386", "arm", "mipsle"}, Stack{ Calls: []Call{ newCallSrc( "main.f", ifCombinedAggregateArgs( Args{ Values: []Arg{ {IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Elided: true, }}}, }}}, }}}, }}}, }}, {IsOffsetTooLarge: true}, {IsAggregate: true, Fields: Args{ Values: []Arg{ {IsOffsetTooLarge: true}, {IsOffsetTooLarge: true}, {IsOffsetTooLarge: true}, }, }}, }, Processed: []string{"a{}", "_", "[]int(_ len=_ cap=_)"}, }, // else Args{ Values: []Arg{{}, {}, {}, {}, {}, {}, {}, {}, {}, {}}, Processed: []string{"a(0x0)", "0", "[]int(0x0 len=0 cap=0)", "0x0", "0x0", "0x0", "0x0"}, Elided: true, }, ), "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, } // The following subtests are adapted from TestTracebackArgs in // src/runtime/traceback_test.go in the Go runtime. Because many // of them are very unstable across different pre1.17 go compiler // versions, we only include them for 1.17 and up. if combinedAggregateArgs { data = append(data, testCase{ "testTracebackArgs1", `func main() { testTracebackArgs1(1, 2, 3, 4, 5) } func testTracebackArgs1(a, b, c, d, e int) int { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.testTracebackArgs1", Args{ Values: []Arg{ {Value: 1}, {Value: 2}, {Value: 3}, {Value: 4}, {Value: 5}, }, Processed: []string{"1", "2", "3", "4", "5"}, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, testCase{ "testTracebackArgs2", `func main() { testTracebackArgs2(false, struct { a, b, c int x [2]int }{1, 2, 3, [2]int{4, 5}}, [0]int{}, [3]byte{6, 7, 8}) } func testTracebackArgs2(a bool, b struct { a, b, c int x [2]int }, _ [0]int, d [3]byte) int { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.testTracebackArgs2", Args{ Values: []Arg{ {Value: 0}, {IsAggregate: true, Fields: Args{ Values: []Arg{ {Value: 1}, {Value: 2}, {Value: 3}, {IsAggregate: true, Fields: Args{ Values: []Arg{ {Value: 4}, {Value: 5}, }, }}, }, }}, {IsAggregate: true}, {IsAggregate: true, Fields: Args{ Values: []Arg{ {Value: 6}, {Value: 7}, {Value: 8}, }, }}, }, Processed: []string{"false", "{0x1, 0x2, 0x3, 0x4, 0x5}", "[0]int{}", "[3]byte{0x6, 0x7, 0x8}"}, }, "/root/main.go", 12), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, testCase{ "testTracebackArgs3", `func main() { testTracebackArgs3([3]byte{1, 2, 3}, 4, 5, 6, [3]byte{7, 8, 9}) } func testTracebackArgs3(x [3]byte, a, b, c int, y [3]byte) int { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.testTracebackArgs3", Args{ Values: []Arg{ {IsAggregate: true, Fields: Args{ Values: []Arg{ {Value: 1}, {Value: 2}, {Value: 3}, }, }}, {Value: 4}, {Value: 5}, {Value: 6}, {IsAggregate: true, Fields: Args{ Values: []Arg{ {Value: 7}, {Value: 8}, {Value: 9}, }, }}, }, Processed: []string{"[3]byte{0x1, 0x2, 0x3}", "4", "5", "6", "[3]byte{0x7, 0x8, 0x9}"}, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, testCase{ "testTracebackArgs4", `func main() { testTracebackArgs4(true, [1][1][1][1][1][1][1][1][1][1]int{}) } func testTracebackArgs4(a bool, x [1][1][1][1][1][1][1][1][1][1]int) int { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.testTracebackArgs4", Args{ Values: []Arg{ {Value: 1}, {IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Elided: true, }}}, }}}, }}}, }}}, }}, }, Processed: []string{"true"}, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, testCase{ "testTracebackArgs5", `func main() { z := [0]int{} testTracebackArgs5(false, struct { x int y [0]int z [2][0]int }{1, z, [2][0]int{}}, z, z, z, z, z, z, z, z, z, z, z, z) } func testTracebackArgs5(a bool, x struct { x int y [0]int z [2][0]int }, _, _, _, _, _, _, _, _, _, _, _, _ [0]int) { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.testTracebackArgs5", Args{ Values: []Arg{ {Value: 0}, {IsAggregate: true, Fields: Args{ Values: []Arg{ {Value: 1}, {IsAggregate: true}, {IsAggregate: true, Fields: Args{ Values: []Arg{ {IsAggregate: true}, {IsAggregate: true}, }, }}, }, }}, {IsAggregate: true}, {IsAggregate: true}, {IsAggregate: true}, {IsAggregate: true}, {IsAggregate: true}, }, Processed: []string{"false", "{0x1}"}, Elided: true, }, "/root/main.go", 15), newCallSrc("main.main", Args{}, "/root/main.go", 4), }, }, }, testCase{ "testTracebackArgs6a", `func main() { testTracebackArgs6a(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) } func testTracebackArgs6a(a, b, c, d, e, f, g, h, i, j int) int { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.testTracebackArgs6a", Args{ Values: []Arg{ {Value: 1}, {Value: 2}, {Value: 3}, {Value: 4}, {Value: 5}, {Value: 6}, {Value: 7}, {Value: 8}, {Value: 9}, {Value: 10}, }, Processed: []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, testCase{ "testTracebackArgs6b", `func main() { testTracebackArgs6b(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) } func testTracebackArgs6b(a, b, c, d, e, f, g, h, i, j, k int) int { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.testTracebackArgs6b", Args{ Values: []Arg{ {Value: 1}, {Value: 2}, {Value: 3}, {Value: 4}, {Value: 5}, {Value: 6}, {Value: 7}, {Value: 8}, {Value: 9}, {Value: 10}, }, Processed: []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, Elided: true, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, testCase{ "testTracebackArgs7a", `func main() { testTracebackArgs7a([10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) } func testTracebackArgs7a(a [10]int) int { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.testTracebackArgs7a", Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{ {Value: 1}, {Value: 2}, {Value: 3}, {Value: 4}, {Value: 5}, {Value: 6}, {Value: 7}, {Value: 8}, {Value: 9}, {Value: 10}, }, }}}, Processed: []string{"[10]int{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa}"}, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, testCase{ "testTracebackArgs7b", `func main() { testTracebackArgs7b([11]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}) } func testTracebackArgs7b(a [11]int) int { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.testTracebackArgs7b", Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{ {Value: 1}, {Value: 2}, {Value: 3}, {Value: 4}, {Value: 5}, {Value: 6}, {Value: 7}, {Value: 8}, {Value: 9}, {Value: 10}, }, Elided: true, }}}, Processed: []string{"[11]int{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, ...}"}, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, testCase{ "testTracebackArgs7c", `func main() { testTracebackArgs7c([10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 11) } func testTracebackArgs7c(a [10]int, b int) int { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.testTracebackArgs7c", Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{ {Value: 1}, {Value: 2}, {Value: 3}, {Value: 4}, {Value: 5}, {Value: 6}, {Value: 7}, {Value: 8}, {Value: 9}, {Value: 10}, }, }}}, Processed: []string{"[10]int{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa}"}, Elided: true, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, testCase{ "testTracebackArgs7d", `func main() { testTracebackArgs7d([11]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, 12) } func testTracebackArgs7d(a [11]int, b int) int { panic("ooh") }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.testTracebackArgs7d", Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{ {Value: 1}, {Value: 2}, {Value: 3}, {Value: 4}, {Value: 5}, {Value: 6}, {Value: 7}, {Value: 8}, {Value: 9}, {Value: 10}, }, Elided: true, }}}, Processed: []string{"[11]int{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, ...}"}, Elided: true, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, testCase{ "testTracebackArgs8a", `func main() { testTracebackArgs8a(testArgsType8a{1, 2, 3, 4, 5, 6, 7, 8, [2]int{9, 10}}) } func testTracebackArgs8a(a testArgsType8a) int { panic("ooh") } type testArgsType8a struct { a, b, c, d, e, f, g, h int i [2]int }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.testTracebackArgs8a", Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{ {Value: 1}, {Value: 2}, {Value: 3}, {Value: 4}, {Value: 5}, {Value: 6}, {Value: 7}, {Value: 8}, {IsAggregate: true, Fields: Args{ Values: []Arg{{Value: 9}, {Value: 10}}, }}, }, }}}, Processed: []string{"testArgsType8a{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa}"}, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, testCase{ "testTracebackArgs8b", `func main() { testTracebackArgs8b(testArgsType8b{1, 2, 3, 4, 5, 6, 7, 8, [3]int{9, 10, 11}}) } func testTracebackArgs8b(a testArgsType8b) int { panic("ooh") } type testArgsType8b struct { a, b, c, d, e, f, g, h int i [3]int }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.testTracebackArgs8b", Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{ {Value: 1}, {Value: 2}, {Value: 3}, {Value: 4}, {Value: 5}, {Value: 6}, {Value: 7}, {Value: 8}, {IsAggregate: true, Fields: Args{ Values: []Arg{{Value: 9}, {Value: 10}}, Elided: true, }}, }, }}}, Processed: []string{"testArgsType8b{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa}"}, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, testCase{ "testTracebackArgs8c", `func main() { testTracebackArgs8c(testArgsType8c{1, 2, 3, 4, 5, 6, 7, 8, [2]int{9, 10}, 11}) } func testTracebackArgs8c(a testArgsType8c) int { panic("ooh") } type testArgsType8c struct { a, b, c, d, e, f, g, h int i [2]int j int }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.testTracebackArgs8c", Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{ {Value: 1}, {Value: 2}, {Value: 3}, {Value: 4}, {Value: 5}, {Value: 6}, {Value: 7}, {Value: 8}, {IsAggregate: true, Fields: Args{ Values: []Arg{{Value: 9}, {Value: 10}}, }}, }, Elided: true, }}}, Processed: []string{"testArgsType8c{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, ...}"}, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, testCase{ "testTracebackArgs8d", `func main() { testTracebackArgs8d(testArgsType8d{1, 2, 3, 4, 5, 6, 7, 8, [3]int{9, 10, 11}, 12}) } func testTracebackArgs8d(a testArgsType8d) int { panic("ooh") } type testArgsType8d struct { a, b, c, d, e, f, g, h int i [3]int j int }`, true, allPlatforms, Stack{ Calls: []Call{ newCallSrc( "main.testTracebackArgs8d", Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{ {Value: 1}, {Value: 2}, {Value: 3}, {Value: 4}, {Value: 5}, {Value: 6}, {Value: 7}, {Value: 8}, {IsAggregate: true, Fields: Args{ Values: []Arg{{Value: 9}, {Value: 10}}, Elided: true, }}, }, Elided: true, }}}, Processed: []string{"testArgsType8d{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, ...}"}, }, "/root/main.go", 6), newCallSrc("main.main", Args{}, "/root/main.go", 3), }, }, }, ) } for i, line := range data { i := i line := line t.Run(fmt.Sprintf("%d-%s", i, line.name), func(t *testing.T) { t.Parallel() for _, arch := range line.archBlock { if runtime.GOARCH == arch { t.Skipf("skipping test on platform %s", arch) } } // Marshal the code a bit to make it nicer. Inject 'package main'. lines := append([]string{"package main"}, strings.Split(line.input, "\n")...) for j := 2; j < len(lines); j++ { // Strip the 3 first tab characters. It's very adhoc but good enough here // and makes test failure much more readable. if lines[j][:3] != "\t\t\t" { t.Fatal("expected line to start with 3 tab characters") } lines[j] = lines[j][3:] } input := strings.Join(lines, "\n") // Create one temporary directory by subtest. root, err := ioutil.TempDir("", "stack") if err != nil { t.Fatalf("failed to create temporary directory: %v", err) } defer func() { if err2 := os.RemoveAll(root); err2 != nil { t.Fatalf("failed to remove temporary directory %q: %v", root, err2) } }() main := filepath.Join(root, "main.go") if err := ioutil.WriteFile(main, []byte(input), 0500); err != nil { t.Fatalf("failed to write %q: %v", main, err) } if runtime.GOOS == "windows" { root = strings.Replace(root, pathSeparator, "/", -1) } const prefix = "/root" for j, c := range line.want.Calls { if strings.HasPrefix(c.RemoteSrcPath, prefix) { line.want.Calls[j].RemoteSrcPath = root + c.RemoteSrcPath[len(prefix):] } if strings.HasPrefix(c.LocalSrcPath, prefix) { line.want.Calls[j].LocalSrcPath = root + c.LocalSrcPath[len(prefix):] } if strings.HasPrefix(c.DirSrc, "root") { line.want.Calls[j].DirSrc = path.Base(root) + c.DirSrc[4:] } } // Run the command up to twice. // Only disable inlining if necessary. disableInline := line.mayBeInlined content := getCrash(t, main, disableInline) t.Log("First") // Warning: this function modifies want. testAugmentCommon(t, content, false, line.want) // If inlining was disabled, try a second time but zap things out. if disableInline { for j := range line.want.Calls { line.want.Calls[j].Args.Processed = nil } content := getCrash(t, main, false) t.Log("Second") testAugmentCommon(t, content, line.mayBeInlined, line.want) } }) } } func testAugmentCommon(t *testing.T, content []byte, mayBeInlined bool, want Stack) { // Analyze it. prefix := bytes.Buffer{} s, suffix, err := ScanSnapshot(bytes.NewBuffer(content), &prefix, defaultOpts()) if err != nil { t.Fatalf("failed to parse input: %v", err) } // On go1.4, there's one less empty line. if got := prefix.String(); got != "panic: ooh\n\n" && got != "panic: ooh\n" { t.Fatalf("Unexpected panic output:\n%#v", got) } compareString(t, "exit status 2\n", string(suffix)) if !s.guessPaths() { t.Error("expected success") } if err := s.augment(); err != nil { t.Errorf("augment() returned %v", err) } got := s.Goroutines[0].Signature.Stack zapPointers(t, &want, &got) // On go1.11 with non-pointer method, it shows elided argument where // there used to be none before. It's only for test case "non-pointer // method". if mayBeInlined { for j := range got.Calls { if !want.Calls[j].Args.Elided { got.Calls[j].Args.Elided = false } if got.Calls[j].Args.Values == nil { want.Calls[j].Args.Values = nil } } } if diff := cmp.Diff(want, got); diff != "" { t.Logf("Different (-want +got):\n%s", diff) t.Logf("Output:\n%s", content) t.FailNow() } } func TestAugmentErr(t *testing.T) { t.Parallel() root, err := ioutil.TempDir("", "stack") if err != nil { t.Fatalf("failed to create temporary directory: %v", err) } defer func() { if err2 := os.RemoveAll(root); err2 != nil { t.Fatalf("failed to remove temporary directory %q: %v", root, err2) } }() tree := map[string]string{ "bad.go": "bad content", "foo.asm": "; good but ignored", "good.go": "package main", "no_access.go": "package main", } createTree(t, root, tree) type dataLine struct { name string src string line int args Args errRe string } // Note: these tests assumes an OS running in English-US locale. That should // eventually be fixed, maybe by using regexes? msgRe := ".+" if runtime.GOOS != "windows" { msgRe = "no such file or directory" } data := []dataLine{ { name: "assembly is skipped", src: "foo.asm", args: Args{Values: []Arg{{}}}, errRe: regexp.QuoteMeta(fmt.Sprintf("cannot load non-go file %q", filepath.Join(root, "foo.asm"))), }, { name: "assembly is skipped (no arg)", src: "foo.asm", }, { name: "invalid line number", src: "good.go", line: 2, args: Args{Values: []Arg{{}}}, errRe: "line 2 is over line count of 1", }, { name: "invalid line number (no arg)", src: "good.go", line: 2, }, { name: "missing file", src: "missing.go", args: Args{Values: []Arg{{}}}, errRe: regexp.QuoteMeta(fmt.Sprintf("open %s: ", filepath.Join(root, "missing.go"))) + msgRe, }, { name: "missing file (no arg)", src: "missing.go", }, { name: "invalid go code (no arg)", src: "bad.go", }, { name: "no I/O access (no arg)", src: "no_access.go", }, } if internaltest.GetGoMinorVersion() > 11 { // The format changed between 1.9 and 1.12. data = append(data, dataLine{ name: "invalid go code", src: "bad.go", args: Args{Values: []Arg{{}}}, errRe: regexp.QuoteMeta(fmt.Sprintf("failed to parse %s:1:1: expected 'package', found bad", filepath.Join(root, "bad.go"))), }) } if runtime.GOOS != "windows" { // Chmod has no effect on Windows. compareErr(t, nil, os.Chmod(filepath.Join(root, "no_access.go"), 0)) data = append(data, dataLine{ name: "no I/O access", src: "no_access.go", args: Args{Values: []Arg{{}}}, errRe: regexp.QuoteMeta(fmt.Sprintf("open %s: permission denied", filepath.Join(root, "no_access.go"))), }) } for i, line := range data { line := line t.Run(fmt.Sprintf("%d-%s", i, line.name), func(t *testing.T) { l := line.line if l == 0 { l = 1 } s := Snapshot{ Goroutines: []*Goroutine{ {Signature: Signature{Stack: Stack{Calls: []Call{ {LocalSrcPath: filepath.Join(root, line.src), Args: line.args, Line: l}, }}}}, }} if err := s.augment(); (line.errRe == "") != (err == nil) { t.Fatalf("want: %q; got: %q", line.errRe, err) } else if err != nil { if m, err2 := regexp.MatchString(line.errRe, err.Error()); err2 != nil { t.Fatal(err2) } else if !m { t.Fatalf("want: %q; got: %q", line.errRe, err) } } }) } } func TestLineToByteOffsets(t *testing.T) { src := "\n\n\n" want := []int{0, 0, 1, 2, 3} if diff := cmp.Diff(want, lineToByteOffsets([]byte(src))); diff != "" { t.Error(diff) } src = "hello" want = []int{0, 0} if diff := cmp.Diff(want, lineToByteOffsets([]byte(src))); diff != "" { t.Error(diff) } src = "this\nis\na\ntest" want = []int{0, 0, 5, 8, 10} if diff := cmp.Diff(want, lineToByteOffsets([]byte(src))); diff != "" { t.Error(diff) } } // const pointer = uint64(0x2fffffff) const pointerStr = "0x2fffffff" const is64Bit = uint64(^uintptr(0)) == ^uint64(0) func overrideEnv(env []string, key, value string) []string { prefix := key + "=" for i, e := range env { if strings.HasPrefix(e, prefix) { env[i] = prefix + value return env } } return append(env, prefix+value) } func getCrash(t *testing.T, main string, disableInline bool) []byte { args := []string{"run"} if disableInline { // Disable both optimization (-N) and inlining (-l). args = append(args, "-gcflags", "-N -l") } cmd := exec.Command("go", append(args, main)...) // Use the Go 1.4 compatible format. cmd.Env = overrideEnv(os.Environ(), "GOTRACEBACK", "1") out, err := cmd.CombinedOutput() if err == nil { t.Helper() t.Fatal("expected error since this is supposed to crash") } return out } // zapPointers zaps out pointers in got. func zapPointers(t *testing.T, want, got *Stack) { for i := range got.Calls { if i >= len(want.Calls) { // When using GOTRACEBACK=2, it'll include runtime.main() and // runtime.goexit(). Ignore these since they could be changed in a future // version. got.Calls = got.Calls[:len(want.Calls)] break } gotArgs := got.Calls[i].Args ptrsToReplace := zapPointersInArgs(t, want.Calls[i].Args, gotArgs) for _, ptr := range ptrsToReplace { for k := range gotArgs.Processed { gotArgs.Processed[k] = strings.Replace(gotArgs.Processed[k], ptr, pointerStr, -1) } } } } func zapPointersInArgs(t *testing.T, want, got Args) (ptrs []string) { for j := range got.Values { if j >= len(want.Values) { break } if want.Values[j].IsAggregate && got.Values[j].IsAggregate { ptrs = append(ptrs, zapPointersInArgs(t, want.Values[j].Fields, got.Values[j].Fields)...) } else if want.Values[j].IsPtr && got.Values[j].IsPtr { // Record the existing pointer value and then replace. ptrs = append(ptrs, fmt.Sprintf("0x%x", got.Values[j].Value)) got.Values[j].Value = want.Values[j].Value } } return ptrs } panicparse-2.3.1/stack/stack.go000066400000000000000000000600321446536624000164160ustar00rootroot00000000000000// Copyright 2015 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 analyzes stack dump of Go processes and simplifies it. // // It is mostly useful on servers will large number of identical goroutines, // making the crash dump harder to read than strictly necessary. package stack import ( "errors" "fmt" "net/url" "os" "sort" "strings" "unicode" "unicode/utf8" ) // Func is a function call in a goroutine stack trace. type Func struct { // Complete is the complete reference. It can be ambiguous in case where a // path contains dots. Complete string // ImportPath is the directory name for this function reference, or "main" if // it was in package main. The package name may not match. ImportPath string // DirName is the directory name containing the package in which the function // is. Normally this matches the package name, but sometimes there's smartass // folks that use a different directory name than the package name. DirName string // Name is the function name or fully quality method name. Name string // IsExported is true if the function is exported. IsExported bool // IsPkgMain is true if it is in the main package. IsPkgMain bool // Disallow initialization with unnamed parameters. _ struct{} } // Init parses the raw function call line from a goroutine stack trace. // // Go stack traces print a mangled function call, this wrapper unmangle the // string before printing and adds other filtering methods. // // The main caveat is that for calls in package main, the package import URL is // left out. func (f *Func) Init(raw string) error { // Format can be: // - gopkg.in/yaml%2ev2.(*Struct).Method (handling dots is tricky) // - main.func·001 (go statements) // - foo (C code) // // The function is optimized to reduce its memory usage. endPkg := 0 if lastSlash := strings.LastIndexByte(raw, '/'); lastSlash != -1 { // Cut the path elements. r := strings.IndexByte(raw[lastSlash+1:], '.') if r == -1 { return errors.New("bad function reference: expected to have at least one dot") } endPkg = lastSlash + r + 1 } else { // It's fine if there's no dot, it happens in C code in go1.4 and lower. endPkg = strings.IndexByte(raw, '.') } // Only the path part is escaped. var err error if f.Complete, err = url.QueryUnescape(raw); err != nil { return fmt.Errorf("bad function reference: %w", err) } // Update the index in the unescaped string. endPkg += len(f.Complete) - len(raw) if endPkg != -1 { f.ImportPath = f.Complete[:endPkg] } f.Name = f.Complete[endPkg+1:] f.DirName = f.ImportPath if i := strings.LastIndexByte(f.DirName, '/'); i != -1 { f.DirName = f.DirName[i+1:] } if f.ImportPath == "main" { f.IsPkgMain = true // Consider main.main to be exported. if f.Name == "main" { f.IsExported = true } } else { parts := strings.Split(f.Name, ".") r, _ := utf8.DecodeRuneInString(parts[len(parts)-1]) f.IsExported = unicode.ToUpper(r) == r } return nil } // String returns Complete. func (f *Func) String() string { return f.Complete } // Arg is an argument on a Call. type Arg struct { // IsAggregate is true if the argument is an aggregate type. If true, the // argument does not contain a value itself, but contains a set of nested // argument fields. If false, the argument contains a single scalar value. IsAggregate bool // The following are set if IsAggregate == false. // Name is a pseudo name given to the argument. Name string // Value is the raw value as found in the stack trace Value uint64 // IsPtr is true if we guess it's a pointer. It's only a guess, it can be // easily confused by a bitmask. IsPtr bool // IsOffsetTooLarge is true if the argument's frame offset was too large, // preventing the argument from being printed in the stack trace. IsOffsetTooLarge bool // IsInaccurate determines if Value is inaccurate. Stacks could have inaccurate values // for arguments passed in registers. Go 1.18 prints a ? for these values. IsInaccurate bool // The following are set if IsAggregate == true. // Fields are the fields/elements of aggregate-typed arguments. Fields Args // Disallow initialization with unnamed parameters. _ struct{} } const zeroToNine = "0123456789" // String prints the argument as the name if present, otherwise as the value. func (a *Arg) String() string { if a.Name != "" { return a.Name } if a.IsOffsetTooLarge { return "_" } if a.IsAggregate { return "{" + a.Fields.String() + "}" } if a.Value < uint64(len(zeroToNine)) { return zeroToNine[a.Value : a.Value+1] } return fmt.Sprintf("0x%x", a.Value) } const ( // With go1.15 on Windows, the pointer floor can be below 1MiB (!) // Assumes all values are above 512KiB and positive are pointers; assuming // that above half the memory is kernel memory. // // This is not always true but this should be good enough to help // implementing AnyPointer. pointerFloor = 512 * 1024 // Assume the stack was generated with the same bitness (32 vs 64) than the // code processing it. pointerCeiling = uint64((^uint(0)) >> 1) ) // equal returns true only if both arguments are exactly equal. func (a *Arg) equal(r *Arg) bool { return a.similar(r, ExactFlags) } // similar returns true if the two Arg are equal or almost but not quite equal. func (a *Arg) similar(r *Arg, similar Similarity) bool { if a.IsAggregate != r.IsAggregate { return false } if a.IsAggregate { return a.Fields.similar(&r.Fields, similar) } switch similar { case ExactFlags, ExactLines: if a.Name != r.Name { return false } if a.IsOffsetTooLarge != r.IsOffsetTooLarge { return false } if a.IsPtr != r.IsPtr { return false } return a.Value == r.Value case AnyValue: return true case AnyPointer: if a.IsOffsetTooLarge != r.IsOffsetTooLarge { return false } if a.IsPtr != r.IsPtr { return false } return a.IsPtr || a.Value == r.Value default: return false } } // Args is a series of function call arguments. type Args struct { // Values is the arguments as shown on the stack trace. They are mangled via // simplification. Values []Arg // Processed is the arguments generated from processing the source files. It // can have a length lower than Values. Processed []string // Elided when set means there was a trailing ", ...". Elided bool // Disallow initialization with unnamed parameters. _ struct{} } func (a *Args) String() string { var v []string if len(a.Processed) != 0 { v = a.Processed } else { v = make([]string, 0, len(a.Values)) for _, item := range a.Values { v = append(v, item.String()) } } if a.Elided { v = append(v, "...") } return strings.Join(v, ", ") } // equal returns true only if both arguments are exactly equal. func (a *Args) equal(r *Args) bool { if a.Elided != r.Elided || len(a.Values) != len(r.Values) { return false } for i, l := range a.Values { if !l.equal(&r.Values[i]) { return false } } return true } // similar returns true if the two Args are equal or almost but not quite // equal. func (a *Args) similar(r *Args, similar Similarity) bool { if a.Elided != r.Elided || len(a.Values) != len(r.Values) { return false } for i, l := range a.Values { if !l.similar(&r.Values[i], similar) { return false } } return true } // merge merges two similar Args, zapping out differences. func (a *Args) merge(r *Args) Args { out := Args{ Values: make([]Arg, len(a.Values)), Elided: a.Elided, } for i, l := range a.Values { rv := &r.Values[i] if l.IsAggregate { out.Values[i].IsAggregate = true out.Values[i].Fields = l.Fields.merge(&rv.Fields) } else if !l.equal(rv) { out.Values[i].Name = "*" out.Values[i].Value = l.Value out.Values[i].IsPtr = l.IsPtr } else { out.Values[i] = l } } return out } // walk traverses all non-aggregate arguments in the Args struct, calling the // provided visitor function with each Arg. func (a *Args) walk(visitor func(arg *Arg)) { for i := range a.Values { arg := &a.Values[i] if arg.IsAggregate { arg.Fields.walk(visitor) } else { visitor(arg) } } } // Location is the source location, if determined. type Location int const ( // LocationUnknown is the default value when Opts.GuessPaths was false. LocationUnknown Location = iota // GoMod is a go module, it is outside $GOPATH and is inside a directory // containing a go.mod file. This is considered a local copy. GoMod // GOPATH is in $GOPATH/src. This is either a dependency fetched via // GO111MODULE=off or intentionally fetched this way. There is no guaranteed // that the local copy is pristine. GOPATH // GoPkg is in $GOPATH/pkg/mod. This is a dependency fetched via go module. // It is considered to be an unmodified external dependency. GoPkg // Stdlib is when it is a Go standard library function. This includes the 'go // test' generated main executable. Stdlib lastLocation ) // Call is an item in the stack trace. // // All paths in this struct are in POSIX format, using "/" as path separator. type Call struct { // The following are initialized on the first line of the call stack. // Func is the fully qualified function name (encoded). Func Func // Args is the call arguments. Args Args // The following are initialized on the second line of the call stack. // RemoteSrcPath is the full path name of the source file as seen in the // trace. RemoteSrcPath string // Line is the line number. Line int // SrcName is the base file name of the source file. SrcName string // DirSrc is one directory plus the file name of the source file. It is a // subset of RemoteSrcPath. DirSrc string // The following are only set if Opts.GuessPaths was set. // LocalSrcPath is the full path name of the source file as seen in the host, // if found. LocalSrcPath string // RelSrcPath is the relative path to GOROOT, GOPATH or LocalGoMods. RelSrcPath string // ImportPath is the fully qualified import path as found on disk (when // Opts.GuessPaths was set). Defaults to Func.ImportPath otherwise. // // In the case of package "main", it returns the underlying path to the main // package instead of "main" if Opts.GuessPaths was set. ImportPath string // Location is the source location, if determined. Location Location // Disallow initialization with unnamed parameters. _ struct{} } // Init initializes RemoteSrcPath, SrcName, DirName and Line. // // For test main, it initializes Location only with Stdlib. // // It does its best educated guess for ImportPath. func (c *Call) init(srcPath string, line int) { c.Line = line if srcPath != "" { c.RemoteSrcPath = srcPath if i := strings.LastIndexByte(c.RemoteSrcPath, '/'); i != -1 { c.SrcName = c.RemoteSrcPath[i+1:] if i = strings.LastIndexByte(c.RemoteSrcPath[:i], '/'); i != -1 { c.DirSrc = c.RemoteSrcPath[i+1:] } } if c.DirSrc == testMainSrc { // Consider _test/_testmain.go as stdlib since it's injected by "go test". c.Location = Stdlib } } c.ImportPath = c.Func.ImportPath } const testMainSrc = "_test" + string(os.PathSeparator) + "_testmain.go" // updateLocations initializes LocalSrcPath, RelSrcPath, Location and ImportPath. // // goroot, localgoroot, localgomod, gomodImportPath and gopaths are expected to // be in "/" format even on Windows. They must not have a trailing "/". // // Returns true if a match was found. func (c *Call) updateLocations(goroot, localgoroot string, localgomods, gopaths map[string]string) bool { // TODO(maruel): Reduce memory allocations. if c.RemoteSrcPath == "" { return false } // Check GOROOT first. if goroot != "" { if prefix := goroot + "/src/"; strings.HasPrefix(c.RemoteSrcPath, prefix) { // Replace remote GOROOT with local GOROOT. c.RelSrcPath = c.RemoteSrcPath[len(prefix):] c.LocalSrcPath = pathJoin(localgoroot, "src", c.RelSrcPath) if i := strings.LastIndexByte(c.RelSrcPath, '/'); i != -1 { c.ImportPath = c.RelSrcPath[:i] } if c.Location == LocationUnknown { c.Location = Stdlib } return true } } // Check GOPATH. // TODO(maruel): Sort for deterministic behavior? for prefix, dest := range gopaths { if p := prefix + "/src/"; strings.HasPrefix(c.RemoteSrcPath, p) { c.RelSrcPath = c.RemoteSrcPath[len(p):] c.LocalSrcPath = pathJoin(dest, "src", c.RelSrcPath) if i := strings.LastIndexByte(c.RelSrcPath, '/'); i != -1 { c.ImportPath = c.RelSrcPath[:i] } if c.Location == LocationUnknown { c.Location = GOPATH } return true } // For modules, the path has to be altered, as it contains the version. if p := prefix + "/pkg/mod/"; strings.HasPrefix(c.RemoteSrcPath, p) { c.RelSrcPath = c.RemoteSrcPath[len(p):] c.LocalSrcPath = pathJoin(dest, "pkg/mod", c.RelSrcPath) if i := strings.LastIndexByte(c.RelSrcPath, '/'); i != -1 { c.ImportPath = c.RelSrcPath[:i] } if c.Location == LocationUnknown { c.Location = GoPkg } return true } } // Check Go modules. // Go module path detection only works with stack traces created on the local // file system. for prefix, pkg := range localgomods { if strings.HasPrefix(c.RemoteSrcPath, prefix+"/") { c.RelSrcPath = c.RemoteSrcPath[len(prefix)+1:] c.LocalSrcPath = c.RemoteSrcPath if i := strings.LastIndexByte(c.RelSrcPath, '/'); i != -1 { c.ImportPath = pkg + "/" + c.RelSrcPath[:i] } else { c.ImportPath = pkg } if c.Location == LocationUnknown { c.Location = GoMod } return true } } // Maybe the path is just absolute and exists? return false } // equal returns true only if both calls are exactly equal. func (c *Call) equal(r *Call) bool { return c.Line == r.Line && c.Func.Complete == r.Func.Complete && c.RemoteSrcPath == r.RemoteSrcPath && c.Args.equal(&r.Args) } // similar returns true if the two Call are equal or almost but not quite // equal. func (c *Call) similar(r *Call, similar Similarity) bool { return c.Line == r.Line && c.Func.Complete == r.Func.Complete && c.RemoteSrcPath == r.RemoteSrcPath && c.Args.similar(&r.Args, similar) } // merge merges two similar Call, zapping out differences. func (c *Call) merge(r *Call) Call { return Call{ Func: c.Func, Args: c.Args.merge(&r.Args), RemoteSrcPath: c.RemoteSrcPath, Line: c.Line, SrcName: c.SrcName, DirSrc: c.DirSrc, LocalSrcPath: c.LocalSrcPath, RelSrcPath: c.RelSrcPath, ImportPath: c.ImportPath, Location: c.Location, } } // Stack is a call stack. type Stack struct { // Calls is the call stack. First is original function, last is leaf // function. Calls []Call // Elided is set when there's >100 items in Stack, currently hardcoded in // package runtime. Elided bool // Disallow initialization with unnamed parameters. _ struct{} } // equal returns true on if both call stacks are exactly equal. func (s *Stack) equal(r *Stack) bool { if len(s.Calls) != len(r.Calls) || s.Elided != r.Elided { return false } for i := range s.Calls { if !s.Calls[i].equal(&r.Calls[i]) { return false } } return true } // similar returns true if the two Stack are equal or almost but not quite // equal. func (s *Stack) similar(r *Stack, similar Similarity) bool { if len(s.Calls) != len(r.Calls) || s.Elided != r.Elided { return false } for i := range s.Calls { if !s.Calls[i].similar(&r.Calls[i], similar) { return false } } return true } // merge merges two similar Stack, zapping out differences. func (s *Stack) merge(r *Stack) *Stack { // Assumes similar stacks have the same length. out := &Stack{ Calls: make([]Call, len(s.Calls)), Elided: s.Elided, } for i := range s.Calls { out.Calls[i] = s.Calls[i].merge(&r.Calls[i]) } return out } // less compares two Stack, where the ones that are less are more // important, so they come up front. // // A Stack with more private functions is 'less' so it is at the top. // Inversely, a Stack with only public functions is 'more' so it is at the // bottom. func (s *Stack) less(r *Stack) bool { lLoc := [lastLocation]int{} rLoc := [lastLocation]int{} lMain := 0 rMain := 0 for _, c := range s.Calls { lLoc[c.Location]++ if c.Func.IsPkgMain { lMain++ } } for _, s := range r.Calls { rLoc[s.Location]++ if s.Func.IsPkgMain { rMain++ } } if lMain > rMain { return true } if lMain < rMain { return false } for i := 1; i < int(lastLocation); i++ { if lLoc[i] > rLoc[i] { return true } if lLoc[i] < rLoc[i] { return false } } // Check unknown code type last. if lLoc[LocationUnknown] > rLoc[LocationUnknown] { return true } if lLoc[LocationUnknown] < rLoc[LocationUnknown] { return false } // Stack lengths are the same and they are mostly of the same kind of location. for x := range s.Calls { if s.Calls[x].Func.Complete < r.Calls[x].Func.Complete { return true } if s.Calls[x].Func.Complete > r.Calls[x].Func.Complete { return false } if s.Calls[x].DirSrc < r.Calls[x].DirSrc { return true } if s.Calls[x].DirSrc > r.Calls[x].DirSrc { return false } if s.Calls[x].Line < r.Calls[x].Line { return true } if s.Calls[x].Line > r.Calls[x].Line { return false } } // Stacks are the same. return false } // updateLocations calls updateLocations on each call frame and returns true if // they were all resolved. func (s *Stack) updateLocations(goroot, localgoroot string, localgomods, gopaths map[string]string) bool { // If there were none, it was "resolved". r := true for i := range s.Calls { r = s.Calls[i].updateLocations(goroot, localgoroot, localgomods, gopaths) && r } return r } // Signature represents the signature of one or multiple goroutines. // // It is effectively the stack trace plus the goroutine internal bits, like // it's state, if it is thread locked, which call site created this goroutine, // etc. type Signature struct { // State is the goroutine state at the time of the snapshot. // // Use git grep 'gopark(|unlock)\(' to find them all plus everything listed // in runtime/traceback.go. Valid values includes: // - chan send, chan receive, select // - finalizer wait, mark wait (idle), // - Concurrent GC wait, GC sweep wait, force gc (idle) // - IO wait, panicwait // - semacquire, semarelease // - sleep, timer goroutine (idle) // - trace reader (blocked) // Stuck cases: // - chan send (nil chan), chan receive (nil chan), select (no cases) // Runnable states: // - idle, runnable, running, syscall, waiting, dead, enqueue, copystack, // Scan states: // - scan, scanrunnable, scanrunning, scansyscall, scanwaiting, scandead, // scanenqueue // // When running under the race detector, the values are 'running' or // 'finished'. State string // CreatedBy is the call stack that created this goroutine, if applicable. // // Normally, the stack is a single Call. // // When the race detector is enabled, a full stack snapshot is available. CreatedBy Stack // SleepMin is the wait time in minutes, if applicable. // // Not set when running under the race detector. SleepMin int // SleepMax is the wait time in minutes, if applicable. // // Not set when running under the race detector. SleepMax int // Stack is the call stack. Stack Stack // Locked is set if the goroutine was locked to an OS thread. // // Not set when running under the race detector. Locked bool // Disallow initialization with unnamed parameters. _ struct{} } // equal returns true only if both signatures are exactly equal. func (s *Signature) equal(r *Signature) bool { if s.State != r.State || !s.CreatedBy.equal(&r.CreatedBy) || s.Locked != r.Locked || s.SleepMin != r.SleepMin || s.SleepMax != r.SleepMax { return false } return s.Stack.equal(&r.Stack) } // similar returns true if the two Signature are equal or almost but not quite // equal. func (s *Signature) similar(r *Signature, similar Similarity) bool { if s.State != r.State || !s.CreatedBy.similar(&r.CreatedBy, similar) { return false } if similar == ExactFlags && s.Locked != r.Locked { return false } return s.Stack.similar(&r.Stack, similar) } // merge merges two similar Signature, zapping out differences. func (s *Signature) merge(r *Signature) *Signature { min := s.SleepMin if r.SleepMin < min { min = r.SleepMin } max := s.SleepMax if r.SleepMax > max { max = r.SleepMax } return &Signature{ State: s.State, // Drop right side. CreatedBy: s.CreatedBy, // Drop right side. SleepMin: min, SleepMax: max, Stack: *s.Stack.merge(&r.Stack), Locked: s.Locked || r.Locked, // TODO(maruel): This is weirdo. } } // less compares two Signature, where the ones that are less are more // important, so they come up front. A Signature with more private functions is // 'less' so it is at the top. Inversely, a Signature with only public // functions is 'more' so it is at the bottom. func (s *Signature) less(r *Signature) bool { if s.Stack.less(&r.Stack) { return true } if r.Stack.less(&s.Stack) { return false } if s.Locked && !r.Locked { return true } if r.Locked && !s.Locked { return false } if s.State < r.State { return true } if s.State > r.State { return false } return false } // SleepString returns a string "N-M minutes" if the goroutine(s) slept for a // long time. // // Returns an empty string otherwise. func (s *Signature) SleepString() string { if s.SleepMax == 0 { return "" } if s.SleepMin != s.SleepMax { return fmt.Sprintf("%d~%d minutes", s.SleepMin, s.SleepMax) } return fmt.Sprintf("%d minutes", s.SleepMax) } // updateLocations calls updateLocations on both CreatedBy and Stack and // returns true if they were both resolved. func (s *Signature) updateLocations(goroot, localgoroot string, localgomods, gopaths map[string]string) bool { r := s.CreatedBy.updateLocations(goroot, localgoroot, localgomods, gopaths) r = s.Stack.updateLocations(goroot, localgoroot, localgomods, gopaths) && r return r } // Goroutine represents the state of one goroutine, including the stack trace. type Goroutine struct { // Signature is the stack trace, internal bits, state, which call site // created it, etc. Signature // ID is the goroutine id. ID int // First is the goroutine first printed, normally the one that crashed. First bool // RaceWrite is true if a race condition was detected, and this goroutine was // race on a write operation, otherwise it was a read. RaceWrite bool // RaceAddr is set to the address when a data race condition was detected. // Otherwise it is 0. RaceAddr uint64 // Disallow initialization with unnamed parameters. _ struct{} } // Private stuff. // nameArguments is a post-processing step where Args are 'named' with numbers. func nameArguments(goroutines []*Goroutine) { // Set a name for any pointer occurring more than once. type object struct { args []*Arg inPrimary bool } objects := map[uint64]object{} // Enumerate all the arguments. primary := true visit := func(arg *Arg) { if arg.IsPtr { objects[arg.Value] = object{ args: append(objects[arg.Value].args, arg), inPrimary: objects[arg.Value].inPrimary || primary, } } } for i, g := range goroutines { primary = i == 0 for _, c := range g.Stack.Calls { c.Args.walk(visit) } // CreatedBy.Args is never set. } order := make(uint64Slice, 0, len(objects)/2) for k, obj := range objects { if len(obj.args) > 1 && obj.inPrimary { order = append(order, k) } } sort.Sort(order) nextID := 1 for _, k := range order { for _, arg := range objects[k].args { arg.Name = fmt.Sprintf("#%d", nextID) } nextID++ } // Now do the rest. This is done so the output is deterministic. order = make(uint64Slice, 0, len(objects)) for k := range objects { order = append(order, k) } sort.Sort(order) for _, k := range order { // Process the remaining pointers, they were not referenced by primary // thread so will have higher IDs. if objects[k].inPrimary { continue } for _, arg := range objects[k].args { arg.Name = fmt.Sprintf("#%d", nextID) } nextID++ } } func pathJoin(s ...string) string { return strings.Join(s, "/") } type uint64Slice []uint64 func (a uint64Slice) Len() int { return len(a) } func (a uint64Slice) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a uint64Slice) Less(i, j int) bool { return a[i] < a[j] } panicparse-2.3.1/stack/stack_test.go000066400000000000000000000362221446536624000174610ustar00rootroot00000000000000// Copyright 2015 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 ( "flag" "fmt" "io/ioutil" "log" "os" "runtime" "strings" "testing" "github.com/google/go-cmp/cmp" ) func TestFuncInit(t *testing.T) { t.Parallel() data := []struct { raw string want Func }{ { "github.com/maruel/panicparse/cmd/panic/internal/%c3%b9tf8.(*Strùct).Pànic", Func{ Complete: "github.com/maruel/panicparse/cmd/panic/internal/ùtf8.(*Strùct).Pànic", ImportPath: "github.com/maruel/panicparse/cmd/panic/internal/ùtf8", DirName: "ùtf8", Name: "(*Strùct).Pànic", IsExported: true, }, }, { "gopkg.in/yaml%2ev2.handleErr", Func{ Complete: "gopkg.in/yaml.v2.handleErr", ImportPath: "gopkg.in/yaml.v2", DirName: "yaml.v2", Name: "handleErr", }, }, { "github.com/maruel/panicparse/vendor/golang.org/x/sys/unix.Nanosleep", Func{ Complete: "github.com/maruel/panicparse/vendor/golang.org/x/sys/unix.Nanosleep", ImportPath: "github.com/maruel/panicparse/vendor/golang.org/x/sys/unix", DirName: "unix", Name: "Nanosleep", IsExported: true, }, }, { "main.func·001", Func{ Complete: "main.func·001", ImportPath: "main", DirName: "main", Name: "func·001", IsPkgMain: true, }, }, { "gc", Func{ Complete: "gc", Name: "gc", }, }, } for _, line := range data { got := newFunc(line.raw) if diff := cmp.Diff(line.want, got); diff != "" { t.Fatalf("Call mismatch (-want +got):\n%s", diff) } } } func TestCallPkg(t *testing.T) { t.Parallel() data := []struct { name string f string s string // Expectations DirSrc string SrcName string LocalSrcPath string RelSrcPath string ImportPath string Location Location }{ { name: "Pkg", f: "gopkg.in/yaml%2ev2.handleErr", s: "/gpremote/src/gopkg.in/yaml.v2/yaml.go", DirSrc: pathJoin("yaml.v2", "yaml.go"), SrcName: "yaml.go", LocalSrcPath: "/gplocal/src/gopkg.in/yaml.v2/yaml.go", RelSrcPath: "gopkg.in/yaml.v2/yaml.go", ImportPath: "gopkg.in/yaml.v2", Location: GOPATH, }, { name: "PkgMod", f: "gopkg.in/yaml%2ev2.handleErr", s: "/gpremote/pkg/mod/gopkg.in/yaml.v2@v2.3.0/yaml.go", DirSrc: pathJoin("yaml.v2@v2.3.0", "yaml.go"), SrcName: "yaml.go", LocalSrcPath: "/gplocal/pkg/mod/gopkg.in/yaml.v2@v2.3.0/yaml.go", RelSrcPath: "gopkg.in/yaml.v2@v2.3.0/yaml.go", ImportPath: "gopkg.in/yaml.v2@v2.3.0", Location: GoPkg, }, { name: "PkgMethod", f: "gopkg.in/yaml%2ev2.(*decoder).unmarshal", s: "/gpremote/src/gopkg.in/yaml.v2/yaml.go", DirSrc: pathJoin("yaml.v2", "yaml.go"), SrcName: "yaml.go", LocalSrcPath: "/gplocal/src/gopkg.in/yaml.v2/yaml.go", RelSrcPath: "gopkg.in/yaml.v2/yaml.go", ImportPath: "gopkg.in/yaml.v2", Location: GOPATH, }, { name: "Stdlib", f: "reflect.Value.assignTo", s: "/grremote/src/reflect/value.go", DirSrc: pathJoin("reflect", "value.go"), SrcName: "value.go", LocalSrcPath: "/grlocal/src/reflect/value.go", RelSrcPath: "reflect/value.go", ImportPath: "reflect", Location: Stdlib, }, { name: "Main", f: "main.main", s: "/gpremote/src/github.com/maruel/panicparse/cmd/pp/main.go", DirSrc: pathJoin("pp", "main.go"), SrcName: "main.go", LocalSrcPath: "/gplocal/src/github.com/maruel/panicparse/cmd/pp/main.go", RelSrcPath: "github.com/maruel/panicparse/cmd/pp/main.go", ImportPath: "github.com/maruel/panicparse/cmd/pp", Location: GOPATH, }, { // See testPanicMismatched in context_test.go. name: "Mismatched", f: "github.com/maruel/panicparse/cmd/panic/internal/incorrect.Panic", s: "/gpremote/src/github.com/maruel/panicparse/cmd/panic/internal/incorrect/correct.go", DirSrc: pathJoin("incorrect", "correct.go"), SrcName: "correct.go", LocalSrcPath: "/gplocal/src/github.com/maruel/panicparse/cmd/panic/internal/incorrect/correct.go", RelSrcPath: "github.com/maruel/panicparse/cmd/panic/internal/incorrect/correct.go", ImportPath: "github.com/maruel/panicparse/cmd/panic/internal/incorrect", Location: GOPATH, }, { // See testPanicUTF8 in context_test.go. name: "UTF8", f: "github.com/maruel/panicparse/cmd/panic/internal/%c3%b9tf8.(*Strùct).Pànic", s: "/gpremote/src/github.com/maruel/panicparse/cmd/panic/internal/ùtf8/utf8.go", DirSrc: pathJoin("ùtf8", "utf8.go"), SrcName: "utf8.go", LocalSrcPath: "/gplocal/src/github.com/maruel/panicparse/cmd/panic/internal/ùtf8/utf8.go", RelSrcPath: "github.com/maruel/panicparse/cmd/panic/internal/ùtf8/utf8.go", ImportPath: "github.com/maruel/panicparse/cmd/panic/internal/ùtf8", Location: GOPATH, }, { name: "C", f: "findrunnable", s: "/grremote/src/runtime/proc.c", DirSrc: pathJoin("runtime", "proc.c"), SrcName: "proc.c", LocalSrcPath: "/grlocal/src/runtime/proc.c", RelSrcPath: "runtime/proc.c", ImportPath: "runtime", Location: Stdlib, }, { name: "Gomod", f: "example.com/foo/bar.Func", s: "/gomod/bar/baz.go", DirSrc: "bar/baz.go", SrcName: "baz.go", LocalSrcPath: "/gomod/bar/baz.go", RelSrcPath: "bar/baz.go", ImportPath: "example.com/foo/bar", Location: GoMod, }, { name: "GomodMain", f: "main.main", s: "/gomod/cmd/panic/main.go", DirSrc: "panic/main.go", SrcName: "main.go", LocalSrcPath: "/gomod/cmd/panic/main.go", RelSrcPath: "cmd/panic/main.go", ImportPath: "example.com/foo/cmd/panic", Location: GoMod, }, } for i, line := range data { line := line t.Run(fmt.Sprintf("%d-%s", i, line.name), func(t *testing.T) { t.Parallel() c := newCall(line.f, Args{}, line.s, 153) compareString(t, line.DirSrc, c.DirSrc) compareString(t, line.SrcName, c.SrcName) // Equivalent of calling GuessPaths(). gp := map[string]string{"/gpremote": "/gplocal"} gm := map[string]string{"/gomod": "example.com/foo"} if !c.updateLocations("/grremote", "/grlocal", gm, gp) { t.Error("Unexpected") } compareString(t, line.ImportPath, c.ImportPath) if line.Location != c.Location { t.Errorf("want %s, got %s", line.Location, c.Location) } compareString(t, line.LocalSrcPath, c.LocalSrcPath) compareString(t, line.RelSrcPath, c.RelSrcPath) }) } } func TestArgs(t *testing.T) { t.Parallel() a := Args{ Values: []Arg{ {Value: 0x4}, {Value: 0x7fff671c7118}, {Value: 0xffffffff00000080}, {}, {Value: 0xffffffff0028c1be}, {Name: "foo"}, {}, {}, {IsOffsetTooLarge: true}, {IsAggregate: true}, {IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true}}, }}}, }}, {IsAggregate: true, Fields: Args{ Values: []Arg{{Value: 0x1}, {Value: 0x7fff671c7118}}, }}, {IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{Value: 0x5}}, Elided: true, }}}, }}, {IsAggregate: true, Fields: Args{Elided: true}}, {IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsOffsetTooLarge: true}, {IsOffsetTooLarge: true}}, }}}, }}, }, Elided: true, } compareString(t, "4, 0x7fff671c7118, 0xffffffff00000080, 0, 0xffffffff0028c1be, foo, "+ "0, 0, _, {}, {{{}}}, {1, 0x7fff671c7118}, {{5, ...}}, {...}, {{_, _}}, ...", a.String()) a = Args{Processed: []string{"yo"}} compareString(t, "yo", a.String()) } func TestSignature(t *testing.T) { t.Parallel() s := getSignature() compareString(t, "", s.SleepString()) s.SleepMax = 10 compareString(t, "0~10 minutes", s.SleepString()) s.SleepMin = 10 compareString(t, "10 minutes", s.SleepString()) } func TestSignature_Equal(t *testing.T) { t.Parallel() s1 := getSignature() s2 := getSignature() if !s1.equal(s2) { t.Fatal("equal") } s2.State = "foo" if s1.equal(s2) { t.Fatal("inequal") } } func TestSignature_Similar(t *testing.T) { t.Parallel() s1 := getSignature() s2 := getSignature() if !s1.similar(s2, ExactFlags) { t.Fatal("equal") } s2.State = "foo" if s1.similar(s2, ExactFlags) { t.Fatal("inequal") } } func TestSignature_Less(t *testing.T) { t.Parallel() s1 := getSignature() s2 := getSignature() if s1.less(s2) || s2.less(s1) { t.Fatal("less") } s2.State = "foo" if !s1.less(s2) || s2.less(s1) { t.Fatal("not less") } s2 = getSignature() s2.Stack.Calls = s2.Stack.Calls[:1] if !s1.less(s2) || s2.less(s1) { t.Fatal("not less") } } // var ( goroot string gopaths map[string]string gopath string gomods map[string]string isInGOPATH bool ) func init() { goroot = strings.Replace(runtime.GOROOT(), "\\", "/", -1) gopaths = map[string]string{} for i, p := range getGOPATHs() { gopaths[p] = p if i == 0 { gopath = p } } // Assumes pwd == this directory. pwd, err := os.Getwd() if err != nil { panic(err) } // Our internal functions work with '/' as path separator. pwd = strings.Replace(pwd, "\\", "/", -1) gomods = map[string]string{} gmc := gomodCache{} if prefix, path := gmc.isGoModule(splitPath(pwd)); prefix != "" { gomods[prefix] = path } // When inside GOPATH, no version is added. When outside, the version path is // added from the reading of module statement in go.mod. for _, p := range getGOPATHs() { if strings.HasPrefix(pwd, p) { isInGOPATH = true break } } } func newFunc(s string) Func { f := Func{} if s != "" { if err := f.Init(s); err != nil { panic(err) } } return f } func newCall(f string, a Args, s string, l int) Call { c := Call{Func: newFunc(f), Args: a} c.init(s, l) return c } func newCallLocal(f string, a Args, s string, l int) Call { c := newCall(f, a, s, l) r := c.updateLocations(goroot, goroot, gomods, gopaths) if !r { panic("Unexpected") } if c.LocalSrcPath == "" || c.RelSrcPath == "" { panic(fmt.Sprintf("newCallLocal(%q, %q): invariant failed; gomods=%v, GOPATHs=%v", f, s, gomods, gopaths)) } return c } func compareErr(t *testing.T, want, got error) { if want == nil && got == nil { return } if want == nil || got == nil { t.Helper() t.Errorf("want: %v, got: %v", want, got) } else if want.Error() != got.Error() { t.Helper() t.Errorf("want: %q, got: %q", want.Error(), got.Error()) } } func compareString(t *testing.T, want, got string) { if want != got { t.Helper() t.Fatalf("%q != %q", want, got) } } // similarGoroutines compares slice of Goroutine to be similar enough. // // Warning: it mutates inputs. func similarGoroutines(t *testing.T, want, got []*Goroutine) { zapGoroutines(t, want, got) if diff := cmp.Diff(want, got); diff != "" { t.Helper() t.Fatalf("Goroutine mismatch (-want +got):\n%s", diff) } } func zapGoroutines(t *testing.T, want, got []*Goroutine) { if len(want) != len(got) { t.Helper() t.Error("different []*Goroutine length") return } for i := range want { // &(*Goroutine).Signature zapSignatures(t, &want[i].Signature, &got[i].Signature) } } // similarSignatures compares Signature to be similar enough. // // Warning: it mutates inputs. func similarSignatures(t *testing.T, want, got *Signature) { zapSignatures(t, want, got) if diff := cmp.Diff(want, got); diff != "" { t.Helper() t.Fatalf("Signature mismatch (-want +got):\n%s", diff) } } func zapSignatures(t *testing.T, want, got *Signature) { // Signature.Stack.([]Call) t.Helper() if len(want.Stack.Calls) != len(got.Stack.Calls) { t.Error("different call length") return } if len(want.CreatedBy.Calls) != 0 && len(got.CreatedBy.Calls) != 0 { if want.CreatedBy.Calls[0].Line != 0 && got.CreatedBy.Calls[0].Line != 0 { want.CreatedBy.Calls[0].Line = 42 got.CreatedBy.Calls[0].Line = 42 } } zapStacks(t, &want.Stack, &got.Stack) } func zapStacks(t *testing.T, want, got *Stack) { if len(want.Calls) != len(got.Calls) { t.Helper() t.Error("different Stack.[]Call length") return } if len(want.Calls) != 0 { t.Helper() } for i := range want.Calls { zapCalls(t, &want.Calls[i], &got.Calls[i]) } } func zapCalls(t *testing.T, want, got *Call) { t.Helper() if want.Line != 0 && got.Line != 0 { want.Line = 42 got.Line = 42 } zapArgs(t, &want.Args, &got.Args) } func zapArgs(t *testing.T, want, got *Args) { if len(want.Values) != len(got.Values) { t.Helper() t.Error("different Args.Values length") return } if len(want.Values) != 0 { t.Helper() } for i := range want.Values { if want.Values[i].Value != 0 && got.Values[i].Value != 0 { want.Values[i].Value = 42 got.Values[i].Value = 42 } if want.Values[i].IsAggregate && got.Values[i].IsAggregate { zapArgs(t, &want.Values[i].Fields, &got.Values[i].Fields) } if want.Values[i].Name != "" && got.Values[i].Name != "" { want.Values[i].Name = "foo" got.Values[i].Name = "foo" } if want.Values[i].IsInaccurate && !got.Values[i].IsInaccurate { want.Values[i].IsInaccurate = false } } } func compareGoroutines(t *testing.T, want, got []*Goroutine) { if diff := cmp.Diff(want, got); diff != "" { t.Helper() t.Fatalf("Goroutine mismatch (-want +got):\n%s", diff) } } func compareStacks(t *testing.T, want, got *Stack) { if diff := cmp.Diff(want, got); diff != "" { t.Helper() t.Fatalf("Stack mismatch (-want +got):\n%s", diff) } } func getSignature() *Signature { return &Signature{ State: "chan receive", Stack: Stack{ Calls: []Call{ { Func: newFunc("main.func·001"), Args: Args{Values: []Arg{{Value: 0x11000000}, {Value: 2}}}, RemoteSrcPath: "/gopath/src/github.com/maruel/panicparse/stack/stack.go", Line: 72, }, { Func: newFunc("sliceInternal"), Args: Args{Values: []Arg{{Value: 0x11000000}, {Value: 2}}}, RemoteSrcPath: "/golang/src/sort/slices.go", Line: 72, Location: Stdlib, }, { Func: newFunc("Slice"), Args: Args{Values: []Arg{{Value: 0x11000000}, {Value: 2}}}, RemoteSrcPath: "/golang/src/sort/slices.go", Line: 72, Location: Stdlib, }, { Func: newFunc("DoStuff"), Args: Args{Values: []Arg{{Value: 0x11000000}, {Value: 2}}}, RemoteSrcPath: "/gopath/src/foo/bar.go", Line: 72, }, { Func: newFunc("doStuffInternal"), Args: Args{ Values: []Arg{{Value: 0x11000000}, {Value: 2}}, Elided: true, }, RemoteSrcPath: "/gopath/src/foo/bar.go", Line: 72, }, }, }, } } // TestMain manages a temporary directory to build on first use ../cmd/panic // and clean up at the end. func TestMain(m *testing.M) { flag.Parse() if !testing.Verbose() { log.SetOutput(ioutil.Discard) } os.Exit(m.Run()) } panicparse-2.3.1/stack/state_string.go000066400000000000000000000026001446536624000200140ustar00rootroot00000000000000// Code generated by "stringer -type state"; DO NOT EDIT. package stack import "strconv" func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} _ = x[looking-0] _ = x[done-1] _ = x[betweenRoutine-2] _ = x[gotRoutineHeader-3] _ = x[gotFunc-4] _ = x[gotCreated-5] _ = x[gotFileFunc-6] _ = x[gotFileCreated-7] _ = x[gotUnavail-8] _ = x[gotRaceHeader1-9] _ = x[gotRaceHeader2-10] _ = x[gotRaceOperationHeader-11] _ = x[gotRaceOperationFunc-12] _ = x[gotRaceOperationFile-13] _ = x[betweenRaceOperations-14] _ = x[gotRaceGoroutineHeader-15] _ = x[gotRaceGoroutineFunc-16] _ = x[gotRaceGoroutineFile-17] _ = x[betweenRaceGoroutines-18] } const _state_name = "lookingdonebetweenRoutinegotRoutineHeadergotFuncgotCreatedgotFileFuncgotFileCreatedgotUnavailgotRaceHeader1gotRaceHeader2gotRaceOperationHeadergotRaceOperationFuncgotRaceOperationFilebetweenRaceOperationsgotRaceGoroutineHeadergotRaceGoroutineFuncgotRaceGoroutineFilebetweenRaceGoroutines" var _state_index = [...]uint16{0, 7, 11, 25, 41, 48, 58, 69, 83, 93, 107, 121, 143, 163, 183, 204, 226, 246, 266, 287} func (i state) String() string { if i < 0 || i >= state(len(_state_index)-1) { return "state(" + strconv.FormatInt(int64(i), 10) + ")" } return _state_name[_state_index[i]:_state_index[i+1]] } panicparse-2.3.1/stack/webstack/000077500000000000000000000000001446536624000165645ustar00rootroot00000000000000panicparse-2.3.1/stack/webstack/example_test.go000066400000000000000000000042571446536624000216150ustar00rootroot00000000000000// Copyright 2020 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 webstack_test import ( "log" "net" "net/http" "strings" "sync" "time" "github.com/maruel/panicparse/v2/stack/webstack" ) func ExampleSnapshotHandler() { http.HandleFunc("/debug/panicparse", webstack.SnapshotHandler) // Access as http://localhost:6060/debug/panicparse log.Println(http.ListenAndServe("localhost:6060", nil)) } func ExampleSnapshotHandler_complex() { // This example does a few things: // - Diables "augment" by default, can be enabled manually with "?augment=1". // - Forces the "maxmem" value to reduce memory pressure in worst case. // - Serializes handler to one at a time. // - Throttles requests to once per second. // - Limit request source IP to localhost and 100.64.x.x/10. (e.g. // Tailscale). const delay = time.Second mu := sync.Mutex{} var last time.Time http.HandleFunc("/debug/panicparse", func(w http.ResponseWriter, req *http.Request) { // Only allow requests from localhost or in the 100.64.x.x/10 IPv4 range. ok := false if i := strings.LastIndexByte(req.RemoteAddr, ':'); i != -1 { switch ip := req.RemoteAddr[:i]; ip { case "localhost", "127.0.0.1", "[::1]", "::1": ok = true default: p := net.ParseIP(ip).To4() ok = p != nil && p[0] == 100 && p[1] >= 64 && p[1] < 128 } } if !ok { http.Error(w, "forbidden", http.StatusForbidden) return } // Serialize the handler. mu.Lock() defer mu.Unlock() // Throttle requests. if time.Since(last) < delay { http.Error(w, "retry later", http.StatusTooManyRequests) return } // Must be called before touching req.Form. req.ParseForm() // Disables source scanning by default since it's heavy. if req.FormValue("augment") == "" { req.Form.Set("augment", "0") } // Reduces maximum memory usage to 32MiB (from 64MiB) for the goroutines // snapshot. req.Form.Set("maxmem", "33554432") webstack.SnapshotHandler(w, req) last = time.Now() }) // Access as http://localhost:6060/debug/panicparse log.Println(http.ListenAndServe("localhost:6060", nil)) } panicparse-2.3.1/stack/webstack/webstack.go000066400000000000000000000072261446536624000207250ustar00rootroot00000000000000// Copyright 2020 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 webstack provides a http.HandlerFunc that serves a snapshot similar // to net/http/pprof.Index(). // // Contrary to net/http/pprof, the handler is not automatically registered. package webstack import ( "bytes" "io" "io/ioutil" "net/http" "runtime" "strconv" "github.com/maruel/panicparse/v2/stack" ) // SnapshotHandler implements http.HandlerFunc to returns a panicparse HTML // format for a snapshot of the current goroutines. // // For best results, compile the executable with optimization (-N) and inlining // (-l) disabled with -gcflags '-N -l'. // // Arguments are passed as form values. If you want to change the default, // override the form values in a wrapper as shown in the example. // // The implementation is designed to be reasonably fast, it currently does a // small amount of disk I/O only for file presence. // // It is a direct replacement for "/debug/pprof/goroutine?debug=2" handler in // net/http/pprof. // // augment: (default: 1) When set to 1, panicparse tries to find the sources on // disk to improve the display of arguments based on type information. This is // slower and should be avoided on high utilization server. // // maxmem: (default: 67108864) maximum amount of temporary memory to use to // generate a snapshot. In practice at least the double of this is used. // Minimum is 1048576. // // similarity: (default: "anypointer") Can be one of stack.Similarity value in // lowercase: "exactflags", "exactlines", "anypointer" or "anyvalue". func SnapshotHandler(w http.ResponseWriter, req *http.Request) { if req.Method != "GET" { http.Error(w, "invalid method", http.StatusMethodNotAllowed) return } maxmem := 64 << 20 if s := req.FormValue("maxmem"); s != "" { var err error if maxmem, err = strconv.Atoi(s); err != nil { http.Error(w, "invalid maxmem value", http.StatusBadRequest) return } } opts := stack.DefaultOpts() if s := req.FormValue("augment"); s != "" { v, err := strconv.Atoi(s) if err != nil || v < 0 || v > 1 { http.Error(w, "invalid augment value", http.StatusBadRequest) return } if v == 0 { opts.AnalyzeSources = false } } c, err := snapshot(maxmem, opts) if err != nil { http.Error(w, "failed to process the snapshot, try a larger maxmem value", http.StatusInternalServerError) return } var s stack.Similarity switch req.FormValue("similarity") { case "exactflags": s = stack.ExactFlags case "exactlines": s = stack.ExactLines case "anypointer", "": s = stack.AnyPointer case "anyvalue": s = stack.AnyValue default: http.Error(w, "invalid similarity value", http.StatusBadRequest) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") _ = c.Aggregate(s).ToHTML(w, "") } // snapshot returns a Context based on the snapshot of the stacks of the // current process. func snapshot(maxmem int, opts *stack.Opts) (*stack.Snapshot, error) { // We don't know how big the buffer needs to be to collect all the // goroutines. Start with 1 MB and try a few times, doubling each time. Give // up and use a truncated trace if maxmem is not enough. buf := make([]byte, 1<<20) if maxmem < len(buf) { maxmem = len(buf) } for i := 0; ; i++ { n := runtime.Stack(buf, true) if n < len(buf) { buf = buf[:n] break } if len(buf) >= maxmem { break } l := len(buf) * 2 if l > maxmem { l = maxmem } buf = make([]byte, l) } s, _, err := stack.ScanSnapshot(bytes.NewReader(buf), ioutil.Discard, opts) // That's expected. if err == io.EOF { err = nil } return s, err } panicparse-2.3.1/stack/webstack/webstack_test.go000066400000000000000000000064351446536624000217650ustar00rootroot00000000000000// Copyright 2020 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 webstack import ( "context" "net/http/httptest" "sync" "testing" ) func TestSnapshotHandler(t *testing.T) { data := []string{ "/debug", "/debug?augment=1", "/debug?maxmem=1", "/debug?maxmem=2097152", "/debug?similarity=exactflags", "/debug?similarity=exactlines", "/debug?similarity=anypointer", "/debug?similarity=anyvalue", } for _, url := range data { url := url t.Run(url, func(t *testing.T) { req := httptest.NewRequest("GET", url, nil) w := httptest.NewRecorder() SnapshotHandler(w, req) if w.Code != 200 { t.Fatalf("%s: %d\n%s", url, w.Code, w.Body.String()) } }) } } func TestSnapshotHandler_Err(t *testing.T) { t.Parallel() data := []string{ "/debug?augment=2", "/debug?maxmem=abc", "/debug?similarity=alike", } for _, url := range data { url := url t.Run(url, func(t *testing.T) { t.Parallel() req := httptest.NewRequest("GET", url, nil) w := httptest.NewRecorder() SnapshotHandler(w, req) if w.Code != 400 { t.Fatalf("%s: %d\n%s", url, w.Code, w.Body.String()) } }) } } func TestSnapshotHandler_Method_POST(t *testing.T) { t.Parallel() req := httptest.NewRequest("POST", "/debug", nil) w := httptest.NewRecorder() SnapshotHandler(w, req) if w.Code != 405 { t.Fatalf("%d\n%s", w.Code, w.Body.String()) } } func TestSnapshotHandler_LargeMemory(t *testing.T) { // Try to create a stack frame over 1MiB in size when serialized to string. // This is tricky since this is dependent on many factors out of our control. // Do this by starting a lot of callbacks with a lot of arguments. wg := sync.WaitGroup{} ctx, cancel := context.WithCancel(context.Background()) defer cancel() a := 0 alive := make(chan struct{}) // Assuming >400 bytes per goroutine, 2500 parallel goroutines is enough to // use more than 1MiB of call stack. We must not put it too high or it'll // crash on Travis. const parallel = 2500 for i := 0; i < parallel; i++ { wg.Add(1) go func() { defer wg.Done() alive <- struct{}{} dummy(ctx, &a, &a, &a, &a, &a, &a, &a, &a, &a) }() } for i := 0; i < parallel; i++ { <-alive } // Normal. req := httptest.NewRequest("GET", "/debug", nil) w := httptest.NewRecorder() SnapshotHandler(w, req) if w.Code != 200 { t.Fatalf("%d\n%s", w.Code, w.Body.String()) } // Cut off. That's 1<<20 + 1 req = httptest.NewRequest("GET", "/debug?maxmem=1048577", nil) w = httptest.NewRecorder() SnapshotHandler(w, req) // It can result in a 500 because the cut off is arbitrary, making parsing to // fail. if w.Code != 200 && w.Code != 500 { t.Fatalf("%d\n%s", w.Code, w.Body.String()) } cancel() wg.Wait() } func BenchmarkSnapshotHandle(b *testing.B) { // TODO(maruel): We should hook runtime.Stack() to make it a deterministic // output with internaltest.StaticPanicwebOutput(). b.ReportAllocs() req := httptest.NewRequest("GET", "/", nil) b.ResetTimer() for i := 0; i < b.N; i++ { w := httptest.NewRecorder() SnapshotHandler(w, req) if w.Code != 200 { b.Fatalf("%d\n%s", w.Code, w.Body.String()) } } } func dummy(ctx context.Context, a1, a2, a3, a4, a5, a6, a7, a8, a9 *int) { <-ctx.Done() }