diff --git a/monitor.go b/monitor.go new file mode 100644 index 0000000..f58a367 --- /dev/null +++ b/monitor.go @@ -0,0 +1,63 @@ +// +build !windows + +package panicwrap + +import ( + "github.com/mitchellh/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 +} diff --git a/monitor_windows.go b/monitor_windows.go new file mode 100644 index 0000000..06bf3b7 --- /dev/null +++ b/monitor_windows.go @@ -0,0 +1,14 @@ +package panicwrap + +import ( + "github.com/mitchellh/osext" + "io" + "os" + "os/exec" + "os/signal" + "syscall" +) + +func monitor(c *WrapConfig) (int, error) { + return -1, fmt.Errorf("Monitor is not supported on windows") +} diff --git a/panicwrap.go b/panicwrap.go index 222ca16..d1dfe2a 100644 --- a/panicwrap.go +++ b/panicwrap.go @@ -49,6 +49,12 @@ type WrapConfig struct { // 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. @@ -98,6 +104,15 @@ func Wrap(c *WrapConfig) (int, error) { 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 diff --git a/panicwrap_test.go b/panicwrap_test.go index 5db4a94..dd1d77f 100644 --- a/panicwrap_test.go +++ b/panicwrap_test.go @@ -168,6 +168,28 @@ func TestHelperProcess(*testing.T) { 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) @@ -231,7 +253,7 @@ func TestPanicWrap_panicHide(t *testing.T) { t.Fatalf("err: %s", err) } - if !strings.Contains(stdout.String(), "wrapped: 1006") { + if !strings.Contains(stdout.String(), "wrapped:") { t.Fatalf("didn't wrap: %#v", stdout.String()) } @@ -251,7 +273,7 @@ func TestPanicWrap_panicShow(t *testing.T) { t.Fatalf("err: %s", err) } - if !strings.Contains(stdout.String(), "wrapped: 1006") { + if !strings.Contains(stdout.String(), "wrapped:") { t.Fatalf("didn't wrap: %#v", stdout.String()) } @@ -270,7 +292,7 @@ func TestPanicWrap_panicLong(t *testing.T) { t.Fatalf("err: %s", err) } - if !strings.Contains(stdout.String(), "wrapped: 1017") { + if !strings.Contains(stdout.String(), "wrapped:") { t.Fatalf("didn't wrap: %#v", stdout.String()) } } @@ -293,6 +315,22 @@ func TestPanicWrap_panicBoundary(t *testing.T) { } } +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)