1// Copyright 2015 Marc-Antoine Ruel. All rights reserved.
2// Use of this source code is governed under the Apache License, Version 2.0
3// that can be found in the LICENSE file.
4
5// Package internal implements panicparse
6//
7// It is mostly useful on servers will large number of identical goroutines,
8// making the crash dump harder to read than strictly necessary.
9//
10// Colors:
11//  - Magenta: first goroutine to be listed.
12//  - Yellow: main package.
13//  - Green: standard library.
14//  - Red: other packages.
15//
16// Bright colors are used for exported symbols.
17package internal
18
19import (
20	"errors"
21	"flag"
22	"fmt"
23	"io"
24	"io/ioutil"
25	"log"
26	"os"
27	"os/signal"
28	"regexp"
29	"syscall"
30
31	"github.com/maruel/panicparse/stack"
32	"github.com/mattn/go-colorable"
33	"github.com/mattn/go-isatty"
34	"github.com/mgutz/ansi"
35)
36
37// resetFG is similar to ansi.Reset except that it doesn't reset the
38// background color, only the foreground color and the style.
39//
40// That much for the "ansi" abstraction layer...
41const resetFG = ansi.DefaultFG + "\033[m"
42
43// defaultPalette is the default recommended palette.
44var defaultPalette = Palette{
45	EOLReset:           resetFG,
46	RoutineFirst:       ansi.ColorCode("magenta+b"),
47	CreatedBy:          ansi.LightBlack,
48	Package:            ansi.ColorCode("default+b"),
49	SrcFile:            resetFG,
50	FuncStdLib:         ansi.Green,
51	FuncStdLibExported: ansi.ColorCode("green+b"),
52	FuncMain:           ansi.ColorCode("yellow+b"),
53	FuncOther:          ansi.Red,
54	FuncOtherExported:  ansi.ColorCode("red+b"),
55	Arguments:          resetFG,
56}
57
58func writeToConsole(out io.Writer, p *Palette, buckets []*stack.Bucket, fullPath, needsEnv bool, filter, match *regexp.Regexp) error {
59	if needsEnv {
60		_, _ = io.WriteString(out, "\nTo see all goroutines, visit https://github.com/maruel/panicparse#gotraceback\n\n")
61	}
62	srcLen, pkgLen := CalcLengths(buckets, fullPath)
63	for _, bucket := range buckets {
64		header := p.BucketHeader(bucket, fullPath, len(buckets) > 1)
65		if filter != nil && filter.MatchString(header) {
66			continue
67		}
68		if match != nil && !match.MatchString(header) {
69			continue
70		}
71		_, _ = io.WriteString(out, header)
72		_, _ = io.WriteString(out, p.StackLines(&bucket.Signature, srcLen, pkgLen, fullPath))
73	}
74	return nil
75}
76
77// process copies stdin to stdout and processes any "panic: " line found.
78//
79// If html is used, a stack trace is written to this file instead.
80func process(in io.Reader, out io.Writer, p *Palette, s stack.Similarity, fullPath, parse, rebase bool, html string, filter, match *regexp.Regexp) error {
81	c, err := stack.ParseDump(in, out, rebase)
82	if c == nil || err != nil {
83		return err
84	}
85	if rebase {
86		log.Printf("GOROOT=%s", c.GOROOT)
87		log.Printf("GOPATH=%s", c.GOPATHs)
88	}
89	needsEnv := len(c.Goroutines) == 1 && showBanner()
90	if parse {
91		stack.Augment(c.Goroutines)
92	}
93	buckets := stack.Aggregate(c.Goroutines, s)
94	if html == "" {
95		return writeToConsole(out, p, buckets, fullPath, needsEnv, filter, match)
96	}
97	return writeToHTML(html, buckets, needsEnv)
98}
99
100func showBanner() bool {
101	if !showGOTRACEBACKBanner {
102		return false
103	}
104	gtb := os.Getenv("GOTRACEBACK")
105	return gtb == "" || gtb == "single"
106}
107
108// Main is implemented here so both 'pp' and 'panicparse' executables can be
109// compiled. This is to work around the Perl Package manager 'pp' that is
110// preinstalled on some OSes.
111func Main() error {
112	aggressive := flag.Bool("aggressive", false, "Aggressive deduplication including non pointers")
113	parse := flag.Bool("parse", true, "Parses source files to deduct types; use -parse=false to work around bugs in source parser")
114	rebase := flag.Bool("rebase", true, "Guess GOROOT and GOPATH")
115	verboseFlag := flag.Bool("v", false, "Enables verbose logging output")
116	filterFlag := flag.String("f", "", "Regexp to filter out headers that match, ex: -f 'IO wait|syscall'")
117	matchFlag := flag.String("m", "", "Regexp to filter by only headers that match, ex: -m 'semacquire'")
118	// Console only.
119	fullPath := flag.Bool("full-path", false, "Print full sources path")
120	noColor := flag.Bool("no-color", !isatty.IsTerminal(os.Stdout.Fd()) || os.Getenv("TERM") == "dumb", "Disable coloring")
121	forceColor := flag.Bool("force-color", false, "Forcibly enable coloring when with stdout is redirected")
122	// HTML only.
123	html := flag.String("html", "", "Output an HTML file")
124	flag.Parse()
125
126	log.SetFlags(log.Lmicroseconds)
127	if !*verboseFlag {
128		log.SetOutput(ioutil.Discard)
129	}
130
131	var err error
132	var filter *regexp.Regexp
133	if *filterFlag != "" {
134		if filter, err = regexp.Compile(*filterFlag); err != nil {
135			return err
136		}
137	}
138
139	var match *regexp.Regexp
140	if *matchFlag != "" {
141		if match, err = regexp.Compile(*matchFlag); err != nil {
142			return err
143		}
144	}
145
146	s := stack.AnyPointer
147	if *aggressive {
148		s = stack.AnyValue
149	}
150
151	var out io.Writer = os.Stdout
152	p := &defaultPalette
153	if *html == "" {
154		if *noColor && !*forceColor {
155			p = &Palette{}
156		} else {
157			out = colorable.NewColorableStdout()
158		}
159	}
160
161	var in *os.File
162	switch flag.NArg() {
163	case 0:
164		in = os.Stdin
165		// Explicitly silence SIGQUIT, as it is useful to gather the stack dump
166		// from the piped command..
167		signals := make(chan os.Signal)
168		go func() {
169			for {
170				<-signals
171			}
172		}()
173		signal.Notify(signals, os.Interrupt, syscall.SIGQUIT)
174
175	case 1:
176		// Do not handle SIGQUIT when passed a file to process.
177		name := flag.Arg(0)
178		if in, err = os.Open(name); err != nil {
179			return fmt.Errorf("did you mean to specify a valid stack dump file name? %s", err)
180		}
181		defer in.Close()
182
183	default:
184		return errors.New("pipe from stdin or specify a single file")
185	}
186	return process(in, out, p, s, *fullPath, *parse, *rebase, *html, filter, match)
187}
188