golang-bugsnag-panicwrap-0.0~git20141111/0000755000175000000620000000000012524024252017262 5ustar jenkinsstaffgolang-bugsnag-panicwrap-0.0~git20141111/LICENSE0000644000175000000620000000207512524024227020275 0ustar jenkinsstaffThe MIT License (MIT) Copyright (c) 2013 Mitchell Hashimoto Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-bugsnag-panicwrap-0.0~git20141111/monitor.go0000644000175000000620000000177712524024227021316 0ustar jenkinsstaff// +build !windows package panicwrap import ( "github.com/bugsnag/osext" "os" "os/exec" "syscall" ) func monitor(c *WrapConfig) (int, error) { // If we're the child process, absorb panics. if Wrapped(c) { panicCh := make(chan string) go trackPanic(os.Stdin, os.Stderr, c.DetectDuration, panicCh) // Wait on the panic data panicTxt := <-panicCh if panicTxt != "" { if !c.HidePanic { os.Stderr.Write([]byte(panicTxt)) } c.Handler(panicTxt) } os.Exit(0) } exePath, err := osext.Executable() if err != nil { return -1, err } cmd := exec.Command(exePath, os.Args[1:]...) read, write, err := os.Pipe() if err != nil { return -1, err } cmd.Stdin = read cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Env = append(os.Environ(), c.CookieKey+"="+c.CookieValue) if err != nil { return -1, err } err = cmd.Start() if err != nil { return -1, err } err = syscall.Dup2(int(write.Fd()), int(os.Stderr.Fd())) if err != nil { return -1, err } return -1, nil } golang-bugsnag-panicwrap-0.0~git20141111/monitor_windows.go0000644000175000000620000000021412524024227023051 0ustar jenkinsstaffpackage panicwrap import "fmt" func monitor(c *WrapConfig) (int, error) { return -1, fmt.Errorf("Monitor is not supported on windows") } golang-bugsnag-panicwrap-0.0~git20141111/panicwrap.go0000644000175000000620000002222212524024227021577 0ustar jenkinsstaff// 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) } } golang-bugsnag-panicwrap-0.0~git20141111/panicwrap_test.go0000644000175000000620000001633012524024227022641 0ustar jenkinsstaffpackage panicwrap import ( "bytes" "fmt" "os" "os/exec" "strings" "testing" "time" ) func helperProcess(s ...string) *exec.Cmd { cs := []string{"-test.run=TestHelperProcess", "--"} cs = append(cs, s...) env := []string{ "GO_WANT_HELPER_PROCESS=1", } cmd := exec.Command(os.Args[0], cs...) cmd.Env = append(env, os.Environ()...) cmd.Stdin = os.Stdin cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout return cmd } // This is executed by `helperProcess` in a separate process in order to // provider a proper sub-process environment to test some of our functionality. func TestHelperProcess(*testing.T) { if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { return } // Find the arguments to our helper, which are the arguments past // the "--" in the command line. args := os.Args for len(args) > 0 { if args[0] == "--" { args = args[1:] break } args = args[1:] } if len(args) == 0 { fmt.Fprintf(os.Stderr, "No command\n") os.Exit(2) } panicHandler := func(s string) { fmt.Fprintf(os.Stdout, "wrapped: %d", len(s)) os.Exit(0) } cmd, args := args[0], args[1:] switch cmd { case "no-panic-ordered-output": exitStatus, err := BasicWrap(panicHandler) if err != nil { fmt.Fprintf(os.Stderr, "wrap error: %s", err) os.Exit(1) } if exitStatus < 0 { for i := 0; i < 1000; i++ { os.Stdout.Write([]byte("a")) os.Stderr.Write([]byte("b")) } os.Exit(0) } os.Exit(exitStatus) case "no-panic-output": fmt.Fprint(os.Stdout, "i am output") fmt.Fprint(os.Stderr, "stderr out") os.Exit(0) case "panic-boundary": exitStatus, err := BasicWrap(panicHandler) if err != nil { fmt.Fprintf(os.Stderr, "wrap error: %s", err) os.Exit(1) } if exitStatus < 0 { // Simulate a panic but on two boundaries... fmt.Fprint(os.Stderr, "pan") os.Stderr.Sync() time.Sleep(100 * time.Millisecond) fmt.Fprint(os.Stderr, "ic: oh crap") os.Exit(2) } os.Exit(exitStatus) case "panic-long": exitStatus, err := BasicWrap(panicHandler) if err != nil { fmt.Fprintf(os.Stderr, "wrap error: %s", err) os.Exit(1) } if exitStatus < 0 { // Make a fake panic by faking the header and adding a // bunch of garbage. fmt.Fprint(os.Stderr, "panic: foo\n\n") for i := 0; i < 1024; i++ { fmt.Fprint(os.Stderr, "foobarbaz") } // Sleep so that it dumps the previous data //time.Sleep(1 * time.Millisecond) time.Sleep(500 * time.Millisecond) // Make a real panic panic("I AM REAL!") } os.Exit(exitStatus) case "panic": hidePanic := false if args[0] == "hide" { hidePanic = true } config := &WrapConfig{ Handler: panicHandler, HidePanic: hidePanic, } exitStatus, err := Wrap(config) if err != nil { fmt.Fprintf(os.Stderr, "wrap error: %s", err) os.Exit(1) } if exitStatus < 0 { panic("uh oh") } os.Exit(exitStatus) case "wrapped": child := false if len(args) > 0 && args[0] == "child" { child = true } config := &WrapConfig{ Handler: panicHandler, } exitStatus, err := Wrap(config) if err != nil { fmt.Fprintf(os.Stderr, "wrap error: %s", err) os.Exit(1) } if exitStatus < 0 { if child { fmt.Printf("%v", Wrapped(config)) } os.Exit(0) } if !child { fmt.Printf("%v", Wrapped(config)) } os.Exit(exitStatus) case "panic-monitor": config := &WrapConfig{ Handler: panicHandler, HidePanic: true, Monitor: true, } exitStatus, err := Wrap(config) if err != nil { fmt.Fprintf(os.Stderr, "wrap error: %s", err) os.Exit(1) } if exitStatus != -1 { fmt.Fprintf(os.Stderr, "wrap error: %s", err) os.Exit(1) } panic("uh oh") default: fmt.Fprintf(os.Stderr, "Unknown command: %q\n", cmd) os.Exit(2) } } func TestPanicWrap_Output(t *testing.T) { stderr := new(bytes.Buffer) stdout := new(bytes.Buffer) p := helperProcess("no-panic-output") p.Stdout = stdout p.Stderr = stderr if err := p.Run(); err != nil { t.Fatalf("err: %s", err) } if !strings.Contains(stdout.String(), "i am output") { t.Fatalf("didn't forward: %#v", stdout.String()) } if !strings.Contains(stderr.String(), "stderr out") { t.Fatalf("didn't forward: %#v", stderr.String()) } } /* TODO(mitchellh): This property would be nice to gain. func TestPanicWrap_Output_Order(t *testing.T) { output := new(bytes.Buffer) p := helperProcess("no-panic-ordered-output") p.Stdout = output p.Stderr = output if err := p.Run(); err != nil { t.Fatalf("err: %s", err) } expectedBuf := new(bytes.Buffer) for i := 0; i < 1000; i++ { expectedBuf.WriteString("ab") } actual := strings.TrimSpace(output.String()) expected := strings.TrimSpace(expectedBuf.String()) if actual != expected { t.Fatalf("bad: %#v", actual) } } */ func TestPanicWrap_panicHide(t *testing.T) { stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) p := helperProcess("panic", "hide") p.Stdout = stdout p.Stderr = stderr if err := p.Run(); err != nil { t.Fatalf("err: %s", err) } if !strings.Contains(stdout.String(), "wrapped:") { t.Fatalf("didn't wrap: %#v", stdout.String()) } if strings.Contains(stderr.String(), "panic:") { t.Fatalf("shouldn't have panic: %#v", stderr.String()) } } func TestPanicWrap_panicShow(t *testing.T) { stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) p := helperProcess("panic", "show") p.Stdout = stdout p.Stderr = stderr if err := p.Run(); err != nil { t.Fatalf("err: %s", err) } if !strings.Contains(stdout.String(), "wrapped:") { t.Fatalf("didn't wrap: %#v", stdout.String()) } if !strings.Contains(stderr.String(), "panic:") { t.Fatalf("should have panic: %#v", stderr.String()) } } func TestPanicWrap_panicLong(t *testing.T) { stdout := new(bytes.Buffer) p := helperProcess("panic-long") p.Stdout = stdout p.Stderr = new(bytes.Buffer) if err := p.Run(); err != nil { t.Fatalf("err: %s", err) } if !strings.Contains(stdout.String(), "wrapped:") { t.Fatalf("didn't wrap: %#v", stdout.String()) } } func TestPanicWrap_panicBoundary(t *testing.T) { // TODO(mitchellh): panics are currently lost on boundaries t.SkipNow() stdout := new(bytes.Buffer) p := helperProcess("panic-boundary") p.Stdout = stdout //p.Stderr = new(bytes.Buffer) if err := p.Run(); err != nil { t.Fatalf("err: %s", err) } if !strings.Contains(stdout.String(), "wrapped: 1015") { t.Fatalf("didn't wrap: %#v", stdout.String()) } } func TestPanicWrap_monitor(t *testing.T) { stdout := new(bytes.Buffer) p := helperProcess("panic-monitor") p.Stdout = stdout //p.Stderr = new(bytes.Buffer) if err := p.Run(); err == nil || err.Error() != "exit status 2" { t.Fatalf("err: %s", err) } if !strings.Contains(stdout.String(), "wrapped:") { t.Fatalf("didn't wrap: %#v", stdout.String()) } } func TestWrapped(t *testing.T) { stdout := new(bytes.Buffer) p := helperProcess("wrapped", "child") p.Stdout = stdout if err := p.Run(); err != nil { t.Fatalf("err: %s", err) } if !strings.Contains(stdout.String(), "true") { t.Fatalf("bad: %#v", stdout.String()) } } func TestWrapped_parent(t *testing.T) { stdout := new(bytes.Buffer) p := helperProcess("wrapped") p.Stdout = stdout if err := p.Run(); err != nil { t.Fatalf("err: %s", err) } if !strings.Contains(stdout.String(), "false") { t.Fatalf("bad: %#v", stdout.String()) } } golang-bugsnag-panicwrap-0.0~git20141111/README.md0000644000175000000620000000713712524024227020553 0ustar jenkinsstaff# panicwrap panicwrap is a Go library that re-executes a Go binary and monitors stderr output from the binary for a panic. When it find a panic, it executes a user-defined handler function. Stdout, stderr, stdin, signals, and exit codes continue to work as normal, making the existence of panicwrap mostly invisble to the end user until a panic actually occurs. Since a panic is truly a bug in the program meant to crash the runtime, globally catching panics within Go applications is not supposed to be possible. Despite this, it is often useful to have a way to know when panics occur. panicwrap allows you to do something with these panics, such as writing them to a file, so that you can track when panics occur. panicwrap is ***not a panic recovery system***. Panics indicate serious problems with your application and _should_ crash the runtime. panicwrap is just meant as a way to monitor for panics. If you still think this is the worst idea ever, read the section below on why. ## Features * **SIMPLE!** * Works with all Go applications on all platforms Go supports * Custom behavior when a panic occurs * Stdout, stderr, stdin, exit codes, and signals continue to work as expected. ## Usage Using panicwrap is simple. It behaves a lot like `fork`, if you know how that works. A basic example is shown below. Because it would be sad to panic while capturing a panic, it is recommended that the handler functions for panicwrap remain relatively simple and well tested. panicwrap itself contains many tests. ```go package main import ( "fmt" "github.com/mitchellh/panicwrap" "os" ) func main() { exitStatus, err := panicwrap.BasicWrap(panicHandler) if err != nil { // Something went wrong setting up the panic wrapper. Unlikely, // but possible. panic(err) } // If exitStatus >= 0, then we're the parent process and the panicwrap // re-executed ourselves and completed. Just exit with the proper status. if exitStatus >= 0 { os.Exit(exitStatus) } // Otherwise, exitStatus < 0 means we're the child. Continue executing as // normal... // Let's say we panic panic("oh shucks") } func panicHandler(output string) { // output contains the full output (including stack traces) of the // panic. Put it in a file or something. fmt.Printf("The child panicked:\n\n%s\n", output) os.Exit(1) } ``` ## How Does it Work? panicwrap works by re-executing the running program (retaining arguments, environmental variables, etc.) and monitoring the stderr of the program. Since Go always outputs panics in a predictable way with a predictable exit code, panicwrap is able to reliably detect panics and allow the parent process to handle them. ## WHY?! Panics should CRASH! Yes, panics _should_ crash. They are 100% always indicative of bugs. However, in some cases, such as user-facing programs (programs like [Packer](http://github.com/mitchellh/packer) or [Docker](http://github.com/dotcloud/docker)), it is up to the user to report such panics. This is unreliable, at best, and it would be better if the program could have a way to automatically report panics. panicwrap provides a way to do this. For backend applications, it is easier to detect crashes (since the application exits). However, it is still nice sometimes to more intelligently log panics in some way. For example, at [HashiCorp](http://www.hashicorp.com), we use panicwrap to log panics to timestamped files with some additional data (configuration settings at the time, environmental variables, etc.) The goal of panicwrap is _not_ to hide panics. It is instead to provide a clean mechanism for handling them before bubbling the up to the user and ultimately crashing.