github.com/muratcelep/terraform@v1.1.0-beta2-not-internal-4/not-internal/logging/panic.go (about)

     1  package logging
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"runtime/debug"
     7  	"strings"
     8  	"sync"
     9  
    10  	"github.com/hashicorp/go-hclog"
    11  )
    12  
    13  // This output is shown if a panic happens.
    14  const panicOutput = `
    15  !!!!!!!!!!!!!!!!!!!!!!!!!!! TERRAFORM CRASH !!!!!!!!!!!!!!!!!!!!!!!!!!!!
    16  
    17  Terraform crashed! This is always indicative of a bug within Terraform.
    18  Please report the crash with Terraform[1] so that we can fix this.
    19  
    20  When reporting bugs, please include your terraform version, the stack trace
    21  shown below, and any additional information which may help replicate the issue.
    22  
    23  [1]: https://github.com/muratcelep/terraform/issues
    24  
    25  !!!!!!!!!!!!!!!!!!!!!!!!!!! TERRAFORM CRASH !!!!!!!!!!!!!!!!!!!!!!!!!!!!
    26  
    27  `
    28  
    29  // PanicHandler is called to recover from an not-internal panic in Terraform, and
    30  // augments the standard stack trace with a more user friendly error message.
    31  // PanicHandler must be called as a defered function, and must be the first
    32  // defer called at the start of a new goroutine.
    33  func PanicHandler() {
    34  	recovered := recover()
    35  	if recovered == nil {
    36  		return
    37  	}
    38  
    39  	fmt.Fprint(os.Stderr, panicOutput)
    40  	fmt.Fprint(os.Stderr, recovered)
    41  
    42  	// When called from a deferred function, debug.PrintStack will include the
    43  	// full stack from the point of the pending panic.
    44  	debug.PrintStack()
    45  
    46  	// An exit code of 11 keeps us out of the way of the detailed exitcodes
    47  	// from plan, and also happens to be the same code as SIGSEGV which is
    48  	// roughly the same type of condition that causes most panics.
    49  	os.Exit(11)
    50  }
    51  
    52  const pluginPanicOutput = `
    53  Stack trace from the %[1]s plugin:
    54  
    55  %s
    56  
    57  Error: The %[1]s plugin crashed!
    58  
    59  This is always indicative of a bug within the plugin. It would be immensely
    60  helpful if you could report the crash with the plugin's maintainers so that it
    61  can be fixed. The output above should help diagnose the issue.
    62  `
    63  
    64  // PluginPanics returns a series of provider panics that were collected during
    65  // execution, and formatted for output.
    66  func PluginPanics() []string {
    67  	return panics.allPanics()
    68  }
    69  
    70  // panicRecorder provides a registry to check for plugin panics that may have
    71  // happened when a plugin suddenly terminates.
    72  type panicRecorder struct {
    73  	sync.Mutex
    74  
    75  	// panics maps the plugin name to the panic output lines received from
    76  	// the logger.
    77  	panics map[string][]string
    78  
    79  	// maxLines is the max number of lines we'll record after seeing a
    80  	// panic header. Since this is going to be printed in the UI output, we
    81  	// don't want to destroy the scrollback. In most cases, the first few lines
    82  	// of the stack trace is all that are required.
    83  	maxLines int
    84  }
    85  
    86  // registerPlugin returns an accumulator function which will accept lines of
    87  // a panic stack trace to collect into an error when requested.
    88  func (p *panicRecorder) registerPlugin(name string) func(string) {
    89  	p.Lock()
    90  	defer p.Unlock()
    91  
    92  	// In most cases we shouldn't be starting a plugin if it already
    93  	// panicked, but clear out previous entries just in case.
    94  	delete(p.panics, name)
    95  
    96  	count := 0
    97  
    98  	// this callback is used by the logger to store panic output
    99  	return func(line string) {
   100  		p.Lock()
   101  		defer p.Unlock()
   102  
   103  		// stop recording if there are too many lines.
   104  		if count > p.maxLines {
   105  			return
   106  		}
   107  		count++
   108  
   109  		p.panics[name] = append(p.panics[name], line)
   110  	}
   111  }
   112  
   113  func (p *panicRecorder) allPanics() []string {
   114  	p.Lock()
   115  	defer p.Unlock()
   116  
   117  	var res []string
   118  	for name, lines := range p.panics {
   119  		if len(lines) == 0 {
   120  			continue
   121  		}
   122  
   123  		res = append(res, fmt.Sprintf(pluginPanicOutput, name, strings.Join(lines, "\n")))
   124  	}
   125  	return res
   126  }
   127  
   128  // logPanicWrapper wraps an hclog.Logger and intercepts and records any output
   129  // that appears to be a panic.
   130  type logPanicWrapper struct {
   131  	hclog.Logger
   132  	panicRecorder func(string)
   133  	inPanic       bool
   134  }
   135  
   136  // go-plugin will create a new named logger for each plugin binary.
   137  func (l *logPanicWrapper) Named(name string) hclog.Logger {
   138  	return &logPanicWrapper{
   139  		Logger:        l.Logger.Named(name),
   140  		panicRecorder: panics.registerPlugin(name),
   141  	}
   142  }
   143  
   144  // we only need to implement Debug, since that is the default output level used
   145  // by go-plugin when encountering unstructured output on stderr.
   146  func (l *logPanicWrapper) Debug(msg string, args ...interface{}) {
   147  	// We don't have access to the binary itself, so guess based on the stderr
   148  	// output if this is the start of the traceback. An occasional false
   149  	// positive shouldn't be a big deal, since this is only retrieved after an
   150  	// error of some sort.
   151  
   152  	panicPrefix := strings.HasPrefix(msg, "panic: ") || strings.HasPrefix(msg, "fatal error: ")
   153  
   154  	l.inPanic = l.inPanic || panicPrefix
   155  
   156  	if l.inPanic && l.panicRecorder != nil {
   157  		l.panicRecorder(msg)
   158  	}
   159  
   160  	l.Logger.Debug(msg, args...)
   161  }