This repository has been archived by the owner on Jul 22, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 66
/
panicwrap.go
365 lines (310 loc) · 10 KB
/
panicwrap.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
// 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"
"io"
"os"
"os/exec"
"os/signal"
"runtime"
"sync/atomic"
"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
// 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
// Catch and igore these signals in the parent process, let the child
// handle them gracefully.
IgnoreSignals []os.Signal
// Catch these signals in the parent process and manually forward
// them to the child process. Some signals such as SIGINT are usually
// sent to the entire process group so setting it isn't necessary. Other
// signals like SIGTERM are only sent to the parent process and need
// to be forwarded. This defaults to empty.
ForwardSignals []os.Signal
}
// 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,
})
}
// 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 we're already wrapped, exit out.
if Wrapped(c) {
return -1, nil
}
// Get the path to our current executable
exePath, err := os.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
// Windows doesn't support this, but on other platforms pass in
// the original file descriptors so they can be used.
if runtime.GOOS != "windows" {
cmd.ExtraFiles = []*os.File{os.Stdin, os.Stdout, os.Stderr}
}
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)
fwdSigCh := make(chan os.Signal)
if len(c.IgnoreSignals) == 0 {
c.IgnoreSignals = []os.Signal{os.Interrupt}
}
signal.Notify(sigCh, c.IgnoreSignals...)
signal.Notify(fwdSigCh, c.ForwardSignals...)
go func() {
defer signal.Stop(sigCh)
defer signal.Stop(fwdSigCh)
for {
select {
case <-doneCh:
return
case s := <-fwdSigCh:
if cmd.Process != nil {
cmd.Process.Signal(s)
}
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.
//
// It must be only called once with a non-nil configuration as it unsets
// the environment variable it uses to check if we are already wrapped.
// This prevents false positive if your program tries to execute itself
// recursively.
//
// Wrapped is very cheap and can be used early to short-circuit some pre-wrap
// logic your application may have.
//
// If the given configuration is nil, then this will return a cached
// value of Wrapped. This is useful because Wrapped is usually called early
// to verify a process hasn't been wrapped before wrapping. After this,
// the value of Wrapped hardly changes and is process-global, so other
// libraries can check with Wrapped(nil).
func Wrapped(c *WrapConfig) bool {
if c == nil {
return wrapCache.Load().(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
result := os.Getenv(c.CookieKey) == c.CookieValue
if result {
os.Unsetenv(c.CookieKey)
}
wrapCache.Store(result)
return result
}
// wrapCache is the cached value for Wrapped when called with nil
var wrapCache atomic.Value
func init() {
wrapCache.Store(false)
}
// 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)
panicHeaders := [][]byte{
[]byte("panic:"),
[]byte("fatal error: fault"),
}
panicType := -1
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(panicHeaders[panicType])))
// 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
}
panicType = -1
flushIdx := n
for i, header := range panicHeaders {
idx := bytes.Index(buf[0:n], header)
if idx >= 0 {
panicType = i
flushIdx = idx
break
}
}
// Flush to stderr what isn't a panic
w.Write(buf[0:flushIdx])
if panicType == -1 {
// Not a panic so just continue along
continue
}
// We have a panic header. Write we assume is a panic os far.
panicBuf.Write(buf[flushIdx:n])
panicTimer = time.After(dur)
}
}