diff --git a/app/app.go b/app/app.go index bc5f325..c59fb66 100644 --- a/app/app.go +++ b/app/app.go @@ -126,9 +126,16 @@ func NotSignedIn(r *http.Request) *AppError { return RedirectToRoute("index", map[string]string{"continue_url": r.URL.String()}) } +func Panic(panicData interface{}) *AppError { + return InternalError( + errors.New(fmt.Sprintf("Panic: %+v\n\n%s", panicData, stack(3))), + "Panic") +} + type AppHandler func(http.ResponseWriter, *http.Request) *AppError func (fn AppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + defer panicRecovery(w, r) if e := fn(w, r); e != nil { handleAppError(e, w, r) } @@ -137,6 +144,7 @@ func (fn AppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { type SignedInAppHandler func(http.ResponseWriter, *http.Request, *AppSignedInState) *AppError func (fn SignedInAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + defer panicRecovery(w, r) session, _ := sessionStore.Get(r, sessionConfig.CookieName) userId, ok := session.Values[sessionConfig.UserIdKey].(int) if !ok { @@ -167,6 +175,12 @@ func (fn SignedInAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } +func panicRecovery(w http.ResponseWriter, r *http.Request) { + if panicData := recover(); panicData != nil { + handleAppError(Panic(panicData), w, r) + } +} + func handleAppError(e *AppError, w http.ResponseWriter, r *http.Request) { c := appengine.NewContext(r) if e.Type == AppErrorTypeGitHubFetch { diff --git a/app/recovery.go b/app/recovery.go new file mode 100644 index 0000000..af9fedc --- /dev/null +++ b/app/recovery.go @@ -0,0 +1,79 @@ +package retrogit + +// From Martini's recovery package: +// https://github.com/go-martini/martini/blob/master/recovery.go + +import ( + "bytes" + "fmt" + "io/ioutil" + "runtime" +) + +var ( + dunno = []byte("???") + centerDot = []byte("·") + dot = []byte(".") + slash = []byte("/") +) + +// stack returns a nicely formated stack frame, skipping skip frames +func stack(skip int) []byte { + buf := new(bytes.Buffer) // the returned data + // As we loop, we open files and read them. These variables record the currently + // loaded file. + var lines [][]byte + var lastFile string + for i := skip; ; i++ { // Skip the expected number of frames + pc, file, line, ok := runtime.Caller(i) + if !ok { + break + } + // Print this much at least. If we can't find the source, it won't show. + fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc) + if file != lastFile { + data, err := ioutil.ReadFile(file) + if err != nil { + continue + } + lines = bytes.Split(data, []byte{'\n'}) + lastFile = file + } + fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line)) + } + return buf.Bytes() +} + +// source returns a space-trimmed slice of the n'th line. +func source(lines [][]byte, n int) []byte { + n-- // in stack trace, lines are 1-indexed but our array is 0-indexed + if n < 0 || n >= len(lines) { + return dunno + } + return bytes.TrimSpace(lines[n]) +} + +// function returns, if possible, the name of the function containing the PC. +func function(pc uintptr) []byte { + fn := runtime.FuncForPC(pc) + if fn == nil { + return dunno + } + name := []byte(fn.Name()) + // The name includes the path name to the package, which is unnecessary + // since the file name is already included. Plus, it has center dots. + // That is, we see + // runtime/debug.*T·ptrmethod + // and want + // *T.ptrmethod + // Also the package path might contains dot (e.g. code.google.com/...), + // so first eliminate the path prefix + if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 { + name = name[lastslash+1:] + } + if period := bytes.Index(name, dot); period >= 0 { + name = name[period+1:] + } + name = bytes.Replace(name, centerDot, dot, -1) + return name +}