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