├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── panicwrap.go └── panicwrap_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Mitchell Hashimoto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # panicwrap 2 | 3 | panicwrap is a Go library that re-executes a Go binary and monitors stderr 4 | output from the binary for a panic. When it finds a panic, it executes a 5 | user-defined handler function. Stdout, stderr, stdin, signals, and exit 6 | codes continue to work as normal, making the existence of panicwrap mostly 7 | invisible to the end user until a panic actually occurs. 8 | 9 | Since a panic is truly a bug in the program meant to crash the runtime, 10 | globally catching panics within Go applications is not supposed to be possible. 11 | Despite this, it is often useful to have a way to know when panics occur. 12 | panicwrap allows you to do something with these panics, such as writing them 13 | to a file, so that you can track when panics occur. 14 | 15 | panicwrap is ***not a panic recovery system***. Panics indicate serious 16 | problems with your application and _should_ crash the runtime. panicwrap 17 | is just meant as a way to monitor for panics. If you still think this is 18 | the worst idea ever, read the section below on why. 19 | 20 | ## Features 21 | 22 | * **SIMPLE!** 23 | * Works with all Go applications on all platforms Go supports 24 | * Custom behavior when a panic occurs 25 | * Stdout, stderr, stdin, exit codes, and signals continue to work as 26 | expected. 27 | 28 | ## Usage 29 | 30 | Using panicwrap is simple. It behaves a lot like `fork`, if you know 31 | how that works. A basic example is shown below. 32 | 33 | Because it would be sad to panic while capturing a panic, it is recommended 34 | that the handler functions for panicwrap remain relatively simple and well 35 | tested. panicwrap itself contains many tests. 36 | 37 | ```go 38 | package main 39 | 40 | import ( 41 | "fmt" 42 | "github.com/mitchellh/panicwrap" 43 | "os" 44 | ) 45 | 46 | func main() { 47 | exitStatus, err := panicwrap.BasicWrap(panicHandler) 48 | if err != nil { 49 | // Something went wrong setting up the panic wrapper. Unlikely, 50 | // but possible. 51 | panic(err) 52 | } 53 | 54 | // If exitStatus >= 0, then we're the parent process and the panicwrap 55 | // re-executed ourselves and completed. Just exit with the proper status. 56 | if exitStatus >= 0 { 57 | os.Exit(exitStatus) 58 | } 59 | 60 | // Otherwise, exitStatus < 0 means we're the child. Continue executing as 61 | // normal... 62 | 63 | // Let's say we panic 64 | panic("oh shucks") 65 | } 66 | 67 | func panicHandler(output string) { 68 | // output contains the full output (including stack traces) of the 69 | // panic. Put it in a file or something. 70 | fmt.Printf("The child panicked:\n\n%s\n", output) 71 | os.Exit(1) 72 | } 73 | ``` 74 | 75 | ## How Does it Work? 76 | 77 | panicwrap works by re-executing the running program (retaining arguments, 78 | environmental variables, etc.) and monitoring the stderr of the program. 79 | Since Go always outputs panics in a predictable way with a predictable 80 | exit code, panicwrap is able to reliably detect panics and allow the parent 81 | process to handle them. 82 | 83 | ## WHY?! Panics should CRASH! 84 | 85 | Yes, panics _should_ crash. They are 100% always indicative of bugs and having 86 | information on a production server or application as to what caused the panic is critical. 87 | 88 | ### User Facing 89 | 90 | In user-facing programs (programs like 91 | [Packer](http://github.com/mitchellh/packer) or 92 | [Docker](http://github.com/dotcloud/docker)), it is up to the user to 93 | report such panics. This is unreliable, at best, and it would be better if the 94 | program could have a way to automatically report panics. panicwrap provides 95 | a way to do this. 96 | 97 | ### Server 98 | 99 | For backend applications, it is easier to detect crashes (since the application exits) 100 | and having an idea as to why the crash occurs is equally important; 101 | particularly on a production server. 102 | 103 | At [HashiCorp](http://www.hashicorp.com), 104 | we use panicwrap to log panics to timestamped files with some additional 105 | data (configuration settings at the time, environmental variables, etc.) 106 | 107 | The goal of panicwrap is _not_ to hide panics. It is instead to provide 108 | a clean mechanism for capturing them and ultimately crashing. 109 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mitchellh/panicwrap 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchellh/panicwrap/b3f3dc3c6bac25d9aa700825f5dc0dba75c5f2a8/go.sum -------------------------------------------------------------------------------- /panicwrap.go: -------------------------------------------------------------------------------- 1 | // The panicwrap package provides functions for capturing and handling 2 | // panics in your application. It does this by re-executing the running 3 | // application and monitoring stderr for any panics. At the same time, 4 | // stdout/stderr/etc. are set to the same values so that data is shuttled 5 | // through properly, making the existence of panicwrap mostly transparent. 6 | // 7 | // Panics are only detected when the subprocess exits with a non-zero 8 | // exit status, since this is the only time panics are real. Otherwise, 9 | // "panic-like" output is ignored. 10 | package panicwrap 11 | 12 | import ( 13 | "bytes" 14 | "errors" 15 | "io" 16 | "os" 17 | "os/exec" 18 | "os/signal" 19 | "runtime" 20 | "sync/atomic" 21 | "syscall" 22 | "time" 23 | ) 24 | 25 | const ( 26 | DEFAULT_COOKIE_KEY = "cccf35992f8f3cd8d1d28f0109dd953e26664531" 27 | DEFAULT_COOKIE_VAL = "7c28215aca87789f95b406b8dd91aa5198406750" 28 | ) 29 | 30 | // HandlerFunc is the type called when a panic is detected. 31 | type HandlerFunc func(string) 32 | 33 | // WrapConfig is the configuration for panicwrap when wrapping an existing 34 | // binary. To get started, in general, you only need the BasicWrap function 35 | // that will set this up for you. However, for more customizability, 36 | // WrapConfig and Wrap can be used. 37 | type WrapConfig struct { 38 | // Handler is the function called when a panic occurs. 39 | Handler HandlerFunc 40 | 41 | // The cookie key and value are used within environmental variables 42 | // to tell the child process that it is already executing so that 43 | // wrap doesn't re-wrap itself. 44 | CookieKey string 45 | CookieValue string 46 | 47 | // If true, the panic will not be mirrored to the configured writer 48 | // and will instead ONLY go to the handler. This lets you effectively 49 | // hide panics from the end user. This is not recommended because if 50 | // your handler fails, the panic is effectively lost. 51 | HidePanic bool 52 | 53 | // The amount of time that a process must exit within after detecting 54 | // a panic header for panicwrap to assume it is a panic. Defaults to 55 | // 300 milliseconds. 56 | DetectDuration time.Duration 57 | 58 | // The writer to send the stderr to. If this is nil, then it defaults 59 | // to os.Stderr. 60 | Writer io.Writer 61 | 62 | // The writer to send stdout to. If this is nil, then it defaults to 63 | // os.Stdout. 64 | Stdout io.Writer 65 | 66 | // Catch and igore these signals in the parent process, let the child 67 | // handle them gracefully. 68 | IgnoreSignals []os.Signal 69 | 70 | // Catch these signals in the parent process and manually forward 71 | // them to the child process. Some signals such as SIGINT are usually 72 | // sent to the entire process group so setting it isn't necessary. Other 73 | // signals like SIGTERM are only sent to the parent process and need 74 | // to be forwarded. This defaults to empty. 75 | ForwardSignals []os.Signal 76 | } 77 | 78 | // BasicWrap calls Wrap with the given handler function, using defaults 79 | // for everything else. See Wrap and WrapConfig for more information on 80 | // functionality and return values. 81 | func BasicWrap(f HandlerFunc) (int, error) { 82 | return Wrap(&WrapConfig{ 83 | Handler: f, 84 | }) 85 | } 86 | 87 | // Wrap wraps the current executable in a handler to catch panics. It 88 | // returns an error if there was an error during the wrapping process. 89 | // If the error is nil, then the int result indicates the exit status of the 90 | // child process. If the exit status is -1, then this is the child process, 91 | // and execution should continue as normal. Otherwise, this is the parent 92 | // process and the child successfully ran already, and you should exit the 93 | // process with the returned exit status. 94 | // 95 | // This function should be called very very early in your program's execution. 96 | // Ideally, this runs as the first line of code of main. 97 | // 98 | // Once this is called, the given WrapConfig shouldn't be modified or used 99 | // any further. 100 | func Wrap(c *WrapConfig) (int, error) { 101 | if c.Handler == nil { 102 | return -1, errors.New("Handler must be set") 103 | } 104 | 105 | if c.DetectDuration == 0 { 106 | c.DetectDuration = 300 * time.Millisecond 107 | } 108 | 109 | if c.Writer == nil { 110 | c.Writer = os.Stderr 111 | } 112 | 113 | // If we're already wrapped, exit out. 114 | if Wrapped(c) { 115 | return -1, nil 116 | } 117 | 118 | // Get the path to our current executable 119 | exePath, err := os.Executable() 120 | if err != nil { 121 | return -1, err 122 | } 123 | 124 | // Pipe the stderr so we can read all the data as we look for panics 125 | stderr_r, stderr_w := io.Pipe() 126 | 127 | // doneCh is closed when we're done, signaling any other goroutines 128 | // to end immediately. 129 | doneCh := make(chan struct{}) 130 | 131 | // panicCh is the channel on which the panic text will actually be 132 | // sent. 133 | panicCh := make(chan string) 134 | 135 | // On close, make sure to finish off the copying of data to stderr 136 | defer func() { 137 | defer close(doneCh) 138 | stderr_w.Close() 139 | <-panicCh 140 | }() 141 | 142 | // Start the goroutine that will watch stderr for any panics 143 | go trackPanic(stderr_r, c.Writer, c.DetectDuration, panicCh) 144 | 145 | // Create the writer for stdout that we're going to use 146 | var stdout_w io.Writer = os.Stdout 147 | if c.Stdout != nil { 148 | stdout_w = c.Stdout 149 | } 150 | 151 | // Build a subcommand to re-execute ourselves. We make sure to 152 | // set the environmental variable to include our cookie. We also 153 | // set stdin/stdout to match the config. Finally, we pipe stderr 154 | // through ourselves in order to watch for panics. 155 | cmd := exec.Command(exePath, os.Args[1:]...) 156 | cmd.Env = append(os.Environ(), c.CookieKey+"="+c.CookieValue) 157 | cmd.Stdin = os.Stdin 158 | cmd.Stdout = stdout_w 159 | cmd.Stderr = stderr_w 160 | 161 | // Windows doesn't support this, but on other platforms pass in 162 | // the original file descriptors so they can be used. 163 | if runtime.GOOS != "windows" { 164 | cmd.ExtraFiles = []*os.File{os.Stdin, os.Stdout, os.Stderr} 165 | } 166 | 167 | if err := cmd.Start(); err != nil { 168 | return 1, err 169 | } 170 | 171 | // Listen to signals and capture them forever. We allow the child 172 | // process to handle them in some way. 173 | sigCh := make(chan os.Signal) 174 | fwdSigCh := make(chan os.Signal) 175 | if len(c.IgnoreSignals) == 0 { 176 | c.IgnoreSignals = []os.Signal{os.Interrupt} 177 | } 178 | signal.Notify(sigCh, c.IgnoreSignals...) 179 | signal.Notify(fwdSigCh, c.ForwardSignals...) 180 | go func() { 181 | defer signal.Stop(sigCh) 182 | defer signal.Stop(fwdSigCh) 183 | for { 184 | select { 185 | case <-doneCh: 186 | return 187 | case s := <-fwdSigCh: 188 | if cmd.Process != nil { 189 | cmd.Process.Signal(s) 190 | } 191 | case <-sigCh: 192 | } 193 | } 194 | }() 195 | 196 | if err := cmd.Wait(); err != nil { 197 | exitErr, ok := err.(*exec.ExitError) 198 | if !ok { 199 | // This is some other kind of subprocessing error. 200 | return 1, err 201 | } 202 | 203 | exitStatus := 1 204 | if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { 205 | exitStatus = status.ExitStatus() 206 | } 207 | 208 | // Close the writer end so that the tracker goroutine ends at some point 209 | stderr_w.Close() 210 | 211 | // Wait on the panic data 212 | panicTxt := <-panicCh 213 | if panicTxt != "" { 214 | if !c.HidePanic { 215 | c.Writer.Write([]byte(panicTxt)) 216 | } 217 | 218 | c.Handler(panicTxt) 219 | } 220 | 221 | return exitStatus, nil 222 | } 223 | 224 | return 0, nil 225 | } 226 | 227 | // Wrapped checks if we're already wrapped according to the configuration 228 | // given. 229 | // 230 | // It must be only called once with a non-nil configuration as it unsets 231 | // the environment variable it uses to check if we are already wrapped. 232 | // This prevents false positive if your program tries to execute itself 233 | // recursively. 234 | // 235 | // Wrapped is very cheap and can be used early to short-circuit some pre-wrap 236 | // logic your application may have. 237 | // 238 | // If the given configuration is nil, then this will return a cached 239 | // value of Wrapped. This is useful because Wrapped is usually called early 240 | // to verify a process hasn't been wrapped before wrapping. After this, 241 | // the value of Wrapped hardly changes and is process-global, so other 242 | // libraries can check with Wrapped(nil). 243 | func Wrapped(c *WrapConfig) bool { 244 | if c == nil { 245 | return wrapCache.Load().(bool) 246 | } 247 | 248 | if c.CookieKey == "" { 249 | c.CookieKey = DEFAULT_COOKIE_KEY 250 | } 251 | 252 | if c.CookieValue == "" { 253 | c.CookieValue = DEFAULT_COOKIE_VAL 254 | } 255 | 256 | // If the cookie key/value match our environment, then we are the 257 | // child, so just exit now and tell the caller that we're the child 258 | result := os.Getenv(c.CookieKey) == c.CookieValue 259 | if result { 260 | os.Unsetenv(c.CookieKey) 261 | } 262 | wrapCache.Store(result) 263 | return result 264 | } 265 | 266 | // wrapCache is the cached value for Wrapped when called with nil 267 | var wrapCache atomic.Value 268 | 269 | func init() { 270 | wrapCache.Store(false) 271 | } 272 | 273 | // trackPanic monitors the given reader for a panic. If a panic is detected, 274 | // it is outputted on the result channel. This will close the channel once 275 | // it is complete. 276 | func trackPanic(r io.Reader, w io.Writer, dur time.Duration, result chan<- string) { 277 | defer close(result) 278 | 279 | var panicTimer <-chan time.Time 280 | panicBuf := new(bytes.Buffer) 281 | panicHeaders := [][]byte{ 282 | []byte("panic:"), 283 | []byte("fatal error: fault"), 284 | } 285 | panicType := -1 286 | 287 | tempBuf := make([]byte, 2048) 288 | for { 289 | var buf []byte 290 | var n int 291 | 292 | if panicTimer == nil && panicBuf.Len() > 0 { 293 | // We're not tracking a panic but the buffer length is 294 | // greater than 0. We need to clear out that buffer, but 295 | // look for another panic along the way. 296 | 297 | // First, remove the previous panic header so we don't loop 298 | w.Write(panicBuf.Next(len(panicHeaders[panicType]))) 299 | 300 | // Next, assume that this is our new buffer to inspect 301 | n = panicBuf.Len() 302 | buf = make([]byte, n) 303 | copy(buf, panicBuf.Bytes()) 304 | panicBuf.Reset() 305 | } else { 306 | var err error 307 | buf = tempBuf 308 | n, err = r.Read(buf) 309 | if n <= 0 && err == io.EOF { 310 | if panicBuf.Len() > 0 { 311 | // We were tracking a panic, assume it was a panic 312 | // and return that as the result. 313 | result <- panicBuf.String() 314 | } 315 | 316 | return 317 | } 318 | } 319 | 320 | if panicTimer != nil { 321 | // We're tracking what we think is a panic right now. 322 | // If the timer ended, then it is not a panic. 323 | isPanic := true 324 | select { 325 | case <-panicTimer: 326 | isPanic = false 327 | default: 328 | } 329 | 330 | // No matter what, buffer the text some more. 331 | panicBuf.Write(buf[0:n]) 332 | 333 | if !isPanic { 334 | // It isn't a panic, stop tracking. Clean-up will happen 335 | // on the next iteration. 336 | panicTimer = nil 337 | } 338 | 339 | continue 340 | } 341 | 342 | panicType = -1 343 | flushIdx := n 344 | for i, header := range panicHeaders { 345 | idx := bytes.Index(buf[0:n], header) 346 | if idx >= 0 { 347 | panicType = i 348 | flushIdx = idx 349 | break 350 | } 351 | } 352 | 353 | // Flush to stderr what isn't a panic 354 | w.Write(buf[0:flushIdx]) 355 | 356 | if panicType == -1 { 357 | // Not a panic so just continue along 358 | continue 359 | } 360 | 361 | // We have a panic header. Write we assume is a panic os far. 362 | panicBuf.Write(buf[flushIdx:n]) 363 | panicTimer = time.After(dur) 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /panicwrap_test.go: -------------------------------------------------------------------------------- 1 | package panicwrap 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "regexp" 9 | "strings" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | var wrapRe = regexp.MustCompile(`wrapped: \d{3,4}`) 15 | 16 | func helperProcess(s ...string) *exec.Cmd { 17 | cs := []string{"-test.run=TestHelperProcess", "--"} 18 | cs = append(cs, s...) 19 | env := []string{ 20 | "GO_WANT_HELPER_PROCESS=1", 21 | } 22 | 23 | cmd := exec.Command(os.Args[0], cs...) 24 | cmd.Env = append(env, os.Environ()...) 25 | cmd.Stdin = os.Stdin 26 | cmd.Stderr = os.Stderr 27 | cmd.Stdout = os.Stdout 28 | return cmd 29 | } 30 | 31 | // This is executed by `helperProcess` in a separate process in order to 32 | // provider a proper sub-process environment to test some of our functionality. 33 | func TestHelperProcess(*testing.T) { 34 | if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { 35 | return 36 | } 37 | 38 | // Find the arguments to our helper, which are the arguments past 39 | // the "--" in the command line. 40 | args := os.Args 41 | for len(args) > 0 { 42 | if args[0] == "--" { 43 | args = args[1:] 44 | break 45 | } 46 | 47 | args = args[1:] 48 | } 49 | 50 | if len(args) == 0 { 51 | fmt.Fprintf(os.Stderr, "No command\n") 52 | os.Exit(2) 53 | } 54 | 55 | panicHandler := func(s string) { 56 | fmt.Fprintf(os.Stdout, "wrapped: %d", len(s)) 57 | os.Exit(0) 58 | } 59 | 60 | cmd, args := args[0], args[1:] 61 | switch cmd { 62 | case "no-panic-ordered-output": 63 | exitStatus, err := BasicWrap(panicHandler) 64 | if err != nil { 65 | fmt.Fprintf(os.Stderr, "wrap error: %s", err) 66 | os.Exit(1) 67 | } 68 | 69 | if exitStatus < 0 { 70 | for i := 0; i < 1000; i++ { 71 | os.Stdout.Write([]byte("a")) 72 | os.Stderr.Write([]byte("b")) 73 | } 74 | os.Exit(0) 75 | } 76 | 77 | os.Exit(exitStatus) 78 | case "no-panic-output": 79 | fmt.Fprint(os.Stdout, "i am output") 80 | fmt.Fprint(os.Stderr, "stderr out") 81 | os.Exit(0) 82 | case "panic-boundary": 83 | exitStatus, err := BasicWrap(panicHandler) 84 | 85 | if err != nil { 86 | fmt.Fprintf(os.Stderr, "wrap error: %s", err) 87 | os.Exit(1) 88 | } 89 | 90 | if exitStatus < 0 { 91 | // Simulate a panic but on two boundaries... 92 | fmt.Fprint(os.Stderr, "pan") 93 | os.Stderr.Sync() 94 | time.Sleep(100 * time.Millisecond) 95 | fmt.Fprint(os.Stderr, "ic: oh crap") 96 | os.Exit(2) 97 | } 98 | 99 | os.Exit(exitStatus) 100 | case "panic-long": 101 | exitStatus, err := BasicWrap(panicHandler) 102 | 103 | if err != nil { 104 | fmt.Fprintf(os.Stderr, "wrap error: %s", err) 105 | os.Exit(1) 106 | } 107 | 108 | if exitStatus < 0 { 109 | // Make a fake panic by faking the header and adding a 110 | // bunch of garbage. 111 | fmt.Fprint(os.Stderr, "panic: foo\n\n") 112 | for i := 0; i < 1024; i++ { 113 | fmt.Fprint(os.Stderr, "foobarbaz") 114 | } 115 | 116 | // Sleep so that it dumps the previous data 117 | //time.Sleep(1 * time.Millisecond) 118 | time.Sleep(500 * time.Millisecond) 119 | 120 | // Make a real panic 121 | panic("I AM REAL!") 122 | } 123 | 124 | os.Exit(exitStatus) 125 | case "panic": 126 | hidePanic := false 127 | if args[0] == "hide" { 128 | hidePanic = true 129 | } 130 | 131 | config := &WrapConfig{ 132 | Handler: panicHandler, 133 | HidePanic: hidePanic, 134 | } 135 | 136 | exitStatus, err := Wrap(config) 137 | 138 | if err != nil { 139 | fmt.Fprintf(os.Stderr, "wrap error: %s", err) 140 | os.Exit(1) 141 | } 142 | 143 | if exitStatus < 0 { 144 | panic("uh oh") 145 | } 146 | 147 | os.Exit(exitStatus) 148 | case "wrapped": 149 | child := false 150 | if len(args) > 0 && args[0] == "child" { 151 | child = true 152 | } 153 | config := &WrapConfig{ 154 | Handler: panicHandler, 155 | } 156 | 157 | exitStatus, err := Wrap(config) 158 | if err != nil { 159 | fmt.Fprintf(os.Stderr, "wrap error: %s", err) 160 | os.Exit(1) 161 | } 162 | 163 | if exitStatus < 0 { 164 | if child { 165 | fmt.Printf("%v", Wrapped(nil)) 166 | } 167 | os.Exit(0) 168 | } 169 | 170 | if !child { 171 | fmt.Printf("%v", Wrapped(nil)) 172 | } 173 | os.Exit(exitStatus) 174 | case "recursive": 175 | config := &WrapConfig{ 176 | Handler: panicHandler, 177 | } 178 | 179 | if len(args) > 0 && args[0] == "child" { 180 | fmt.Printf("%v", Wrapped(config)) 181 | os.Exit(0) 182 | } 183 | 184 | exitCode, err := Wrap(config) 185 | if err != nil { 186 | fmt.Fprintf(os.Stderr, "wrap error: %s", err) 187 | os.Exit(1) 188 | } 189 | 190 | if Wrapped(nil) { 191 | p := helperProcess("recursive", "child") 192 | if err := p.Run(); err != nil { 193 | fmt.Fprintf(os.Stderr, "wrap error: %s", err) 194 | os.Exit(1) 195 | } 196 | os.Exit(0) 197 | } 198 | os.Exit(exitCode) 199 | default: 200 | fmt.Fprintf(os.Stderr, "Unknown command: %q\n", cmd) 201 | os.Exit(2) 202 | } 203 | } 204 | 205 | func TestPanicWrap_Output(t *testing.T) { 206 | stderr := new(bytes.Buffer) 207 | stdout := new(bytes.Buffer) 208 | 209 | p := helperProcess("no-panic-output") 210 | p.Stdout = stdout 211 | p.Stderr = stderr 212 | if err := p.Run(); err != nil { 213 | t.Fatalf("err: %s", err) 214 | } 215 | 216 | if !strings.Contains(stdout.String(), "i am output") { 217 | t.Fatalf("didn't forward: %#v", stdout.String()) 218 | } 219 | 220 | if !strings.Contains(stderr.String(), "stderr out") { 221 | t.Fatalf("didn't forward: %#v", stderr.String()) 222 | } 223 | } 224 | 225 | /* 226 | TODO(mitchellh): This property would be nice to gain. 227 | func TestPanicWrap_Output_Order(t *testing.T) { 228 | output := new(bytes.Buffer) 229 | 230 | p := helperProcess("no-panic-ordered-output") 231 | p.Stdout = output 232 | p.Stderr = output 233 | if err := p.Run(); err != nil { 234 | t.Fatalf("err: %s", err) 235 | } 236 | 237 | expectedBuf := new(bytes.Buffer) 238 | for i := 0; i < 1000; i++ { 239 | expectedBuf.WriteString("ab") 240 | } 241 | 242 | actual := strings.TrimSpace(output.String()) 243 | expected := strings.TrimSpace(expectedBuf.String()) 244 | 245 | if actual != expected { 246 | t.Fatalf("bad: %#v", actual) 247 | } 248 | } 249 | */ 250 | 251 | func TestPanicWrap_panicHide(t *testing.T) { 252 | stdout := new(bytes.Buffer) 253 | stderr := new(bytes.Buffer) 254 | 255 | p := helperProcess("panic", "hide") 256 | p.Stdout = stdout 257 | p.Stderr = stderr 258 | if err := p.Run(); err != nil { 259 | t.Fatalf("err: %s", err) 260 | } 261 | 262 | if wrapRe.FindString(stdout.String()) == "" { 263 | t.Fatalf("didn't wrap: %#v", stdout.String()) 264 | } 265 | 266 | if strings.Contains(stderr.String(), "panic:") { 267 | t.Fatalf("shouldn't have panic: %#v", stderr.String()) 268 | } 269 | } 270 | 271 | func TestPanicWrap_panicShow(t *testing.T) { 272 | stdout := new(bytes.Buffer) 273 | stderr := new(bytes.Buffer) 274 | 275 | p := helperProcess("panic", "show") 276 | p.Stdout = stdout 277 | p.Stderr = stderr 278 | if err := p.Run(); err != nil { 279 | t.Fatalf("err: %s", err) 280 | } 281 | 282 | if wrapRe.FindString(stdout.String()) == "" { 283 | t.Fatalf("didn't wrap: %#v", stdout.String()) 284 | } 285 | 286 | if !strings.Contains(stderr.String(), "panic:") { 287 | t.Fatalf("should have panic: %#v", stderr.String()) 288 | } 289 | } 290 | 291 | func TestPanicWrap_panicLong(t *testing.T) { 292 | stdout := new(bytes.Buffer) 293 | 294 | p := helperProcess("panic-long") 295 | p.Stdout = stdout 296 | p.Stderr = new(bytes.Buffer) 297 | if err := p.Run(); err != nil { 298 | t.Fatalf("err: %s", err) 299 | } 300 | 301 | if wrapRe.FindString(stdout.String()) == "" { 302 | t.Fatalf("didn't wrap: %#v", stdout.String()) 303 | } 304 | } 305 | 306 | func TestPanicWrap_panicBoundary(t *testing.T) { 307 | // TODO(mitchellh): panics are currently lost on boundaries 308 | t.SkipNow() 309 | 310 | stdout := new(bytes.Buffer) 311 | 312 | p := helperProcess("panic-boundary") 313 | p.Stdout = stdout 314 | //p.Stderr = new(bytes.Buffer) 315 | if err := p.Run(); err != nil { 316 | t.Fatalf("err: %s", err) 317 | } 318 | 319 | if wrapRe.FindString(stdout.String()) == "" { 320 | t.Fatalf("didn't wrap: %#v", stdout.String()) 321 | } 322 | } 323 | 324 | func TestPanicWrap_recursive(t *testing.T) { 325 | stdout := new(bytes.Buffer) 326 | 327 | p := helperProcess("recursive") 328 | p.Stdout = stdout 329 | if err := p.Run(); err != nil { 330 | t.Fatalf("err: %s", err) 331 | } 332 | 333 | if !strings.Contains(stdout.String(), "false") { 334 | t.Fatalf("Wrapped false positive: %#v", stdout.String()) 335 | } 336 | } 337 | 338 | func TestWrapped(t *testing.T) { 339 | stdout := new(bytes.Buffer) 340 | 341 | p := helperProcess("wrapped", "child") 342 | p.Stdout = stdout 343 | if err := p.Run(); err != nil { 344 | t.Fatalf("err: %s", err) 345 | } 346 | 347 | if !strings.Contains(stdout.String(), "true") { 348 | t.Fatalf("bad: %#v", stdout.String()) 349 | } 350 | } 351 | 352 | func TestWrapped_parent(t *testing.T) { 353 | stdout := new(bytes.Buffer) 354 | 355 | p := helperProcess("wrapped") 356 | p.Stdout = stdout 357 | if err := p.Run(); err != nil { 358 | t.Fatalf("err: %s", err) 359 | } 360 | 361 | if !strings.Contains(stdout.String(), "false") { 362 | t.Fatalf("bad: %#v", stdout.String()) 363 | } 364 | } 365 | --------------------------------------------------------------------------------