// The panicwrap package provides functions for capturing and handling // panics in your application. It does this by re-executing the running // application and monitoring stderr for any panics. At the same time, // stdout/stderr/etc. are set to the same values so that data is shuttled // through properly, making the existence of panicwrap mostly transparent. // // Panics are only detected when the subprocess exits with a non-zero // exit status, since this is the only time panics are real. Otherwise, // "panic-like" output is ignored. package panicwrap import ( "bytes" "errors" "github.com/bugsnag/osext" "io" "os" "os/exec" "os/signal" "runtime" "syscall" "time" ) const ( DEFAULT_COOKIE_KEY = "cccf35992f8f3cd8d1d28f0109dd953e26664531" DEFAULT_COOKIE_VAL = "7c28215aca87789f95b406b8dd91aa5198406750" ) // HandlerFunc is the type called when a panic is detected. type HandlerFunc func(string) // WrapConfig is the configuration for panicwrap when wrapping an existing // binary. To get started, in general, you only need the BasicWrap function // that will set this up for you. However, for more customizability, // WrapConfig and Wrap can be used. type WrapConfig struct { // Handler is the function called when a panic occurs. Handler HandlerFunc // The cookie key and value are used within environmental variables // to tell the child process that it is already executing so that // wrap doesn't re-wrap itself. CookieKey string CookieValue string // If true, the panic will not be mirrored to the configured writer // and will instead ONLY go to the handler. This lets you effectively // hide panics from the end user. This is not recommended because if // your handler fails, the panic is effectively lost. HidePanic bool // If true, panicwrap will boot a monitor sub-process and let the parent // run the app. This mode is useful for processes run under supervisors // like runit as signals get sent to the correct codebase. This is not // supported when GOOS=windows, and ignores c.Stderr and c.Stdout. Monitor bool // The amount of time that a process must exit within after detecting // a panic header for panicwrap to assume it is a panic. Defaults to // 300 milliseconds. DetectDuration time.Duration // The writer to send the stderr to. If this is nil, then it defaults // to os.Stderr. Writer io.Writer // The writer to send stdout to. If this is nil, then it defaults to // os.Stdout. Stdout io.Writer } // BasicWrap calls Wrap with the given handler function, using defaults // for everything else. See Wrap and WrapConfig for more information on // functionality and return values. func BasicWrap(f HandlerFunc) (int, error) { return Wrap(&WrapConfig{ Handler: f, }) } // BasicMonitor calls Wrap with Monitor set to true on supported platforms. // It forks your program and runs it again form the start. In one process // BasicMonitor never returns, it just listens on stderr of the other process, // and calls your handler when a panic is seen. In the other it either returns // nil to indicate that the panic monitoring is enabled, or an error to indicate // that something else went wrong. func BasicMonitor(f HandlerFunc) error { exitStatus, err := Wrap(&WrapConfig{ Handler: f, Monitor: runtime.GOOS != "windows", }) if err != nil { return err } if exitStatus >= 0 { os.Exit(exitStatus) } return nil } // Wrap wraps the current executable in a handler to catch panics. It // returns an error if there was an error during the wrapping process. // If the error is nil, then the int result indicates the exit status of the // child process. If the exit status is -1, then this is the child process, // and execution should continue as normal. Otherwise, this is the parent // process and the child successfully ran already, and you should exit the // process with the returned exit status. // // This function should be called very very early in your program's execution. // Ideally, this runs as the first line of code of main. // // Once this is called, the given WrapConfig shouldn't be modified or used // any further. func Wrap(c *WrapConfig) (int, error) { if c.Handler == nil { return -1, errors.New("Handler must be set") } if c.DetectDuration == 0 { c.DetectDuration = 300 * time.Millisecond } if c.Writer == nil { c.Writer = os.Stderr } if c.Monitor { return monitor(c) } else { return wrap(c) } } func wrap(c *WrapConfig) (int, error) { // If we're already wrapped, exit out. if Wrapped(c) { return -1, nil } // Get the path to our current executable exePath, err := osext.Executable() if err != nil { return -1, err } // Pipe the stderr so we can read all the data as we look for panics stderr_r, stderr_w := io.Pipe() // doneCh is closed when we're done, signaling any other goroutines // to end immediately. doneCh := make(chan struct{}) // panicCh is the channel on which the panic text will actually be // sent. panicCh := make(chan string) // On close, make sure to finish off the copying of data to stderr defer func() { defer close(doneCh) stderr_w.Close() <-panicCh }() // Start the goroutine that will watch stderr for any panics go trackPanic(stderr_r, c.Writer, c.DetectDuration, panicCh) // Create the writer for stdout that we're going to use var stdout_w io.Writer = os.Stdout if c.Stdout != nil { stdout_w = c.Stdout } // Build a subcommand to re-execute ourselves. We make sure to // set the environmental variable to include our cookie. We also // set stdin/stdout to match the config. Finally, we pipe stderr // through ourselves in order to watch for panics. cmd := exec.Command(exePath, os.Args[1:]...) cmd.Env = append(os.Environ(), c.CookieKey+"="+c.CookieValue) cmd.Stdin = os.Stdin cmd.Stdout = stdout_w cmd.Stderr = stderr_w if err := cmd.Start(); err != nil { return 1, err } // Listen to signals and capture them forever. We allow the child // process to handle them in some way. sigCh := make(chan os.Signal) signal.Notify(sigCh, os.Interrupt) go func() { defer signal.Stop(sigCh) for { select { case <-doneCh: return case <-sigCh: } } }() if err := cmd.Wait(); err != nil { exitErr, ok := err.(*exec.ExitError) if !ok { // This is some other kind of subprocessing error. return 1, err } exitStatus := 1 if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { exitStatus = status.ExitStatus() } // Close the writer end so that the tracker goroutine ends at some point stderr_w.Close() // Wait on the panic data panicTxt := <-panicCh if panicTxt != "" { if !c.HidePanic { c.Writer.Write([]byte(panicTxt)) } c.Handler(panicTxt) } return exitStatus, nil } return 0, nil } // Wrapped checks if we're already wrapped according to the configuration // given. // // Wrapped is very cheap and can be used early to short-circuit some pre-wrap // logic your application may have. func Wrapped(c *WrapConfig) bool { if c.CookieKey == "" { c.CookieKey = DEFAULT_COOKIE_KEY } if c.CookieValue == "" { c.CookieValue = DEFAULT_COOKIE_VAL } // If the cookie key/value match our environment, then we are the // child, so just exit now and tell the caller that we're the child return os.Getenv(c.CookieKey) == c.CookieValue } // trackPanic monitors the given reader for a panic. If a panic is detected, // it is outputted on the result channel. This will close the channel once // it is complete. func trackPanic(r io.Reader, w io.Writer, dur time.Duration, result chan<- string) { defer close(result) var panicTimer <-chan time.Time panicBuf := new(bytes.Buffer) panicHeader := []byte("panic:") tempBuf := make([]byte, 2048) for { var buf []byte var n int if panicTimer == nil && panicBuf.Len() > 0 { // We're not tracking a panic but the buffer length is // greater than 0. We need to clear out that buffer, but // look for another panic along the way. // First, remove the previous panic header so we don't loop w.Write(panicBuf.Next(len(panicHeader))) // Next, assume that this is our new buffer to inspect n = panicBuf.Len() buf = make([]byte, n) copy(buf, panicBuf.Bytes()) panicBuf.Reset() } else { var err error buf = tempBuf n, err = r.Read(buf) if n <= 0 && err == io.EOF { if panicBuf.Len() > 0 { // We were tracking a panic, assume it was a panic // and return that as the result. result <- panicBuf.String() } return } } if panicTimer != nil { // We're tracking what we think is a panic right now. // If the timer ended, then it is not a panic. isPanic := true select { case <-panicTimer: isPanic = false default: } // No matter what, buffer the text some more. panicBuf.Write(buf[0:n]) if !isPanic { // It isn't a panic, stop tracking. Clean-up will happen // on the next iteration. panicTimer = nil } continue } flushIdx := n idx := bytes.Index(buf[0:n], panicHeader) if idx >= 0 { flushIdx = idx } // Flush to stderr what isn't a panic w.Write(buf[0:flushIdx]) if idx < 0 { // Not a panic so just continue along continue } // We have a panic header. Write we assume is a panic os far. panicBuf.Write(buf[idx:n]) panicTimer = time.After(dur) } }