diff --git a/util/panic.go b/util/panic.go index f7562c5..7e9a5b7 100644 --- a/util/panic.go +++ b/util/panic.go @@ -15,8 +15,11 @@ package util import ( + "bytes" + "fmt" + "io/ioutil" "os" - "runtime/debug" + "runtime" "github.com/b3log/wide/log" ) @@ -24,9 +27,78 @@ import ( // Logger. var logger = log.NewLogger(os.Stdout) +var ( + dunno = []byte("???") + centerDot = []byte("·") + dot = []byte(".") + slash = []byte("/") +) + // Recover recovers a panic. func Recover() { if re := recover(); nil != re { - logger.Errorf("PANIC RECOVERED:\n %v, %s", re, debug.Stack()) + stack := stack() + logger.Errorf("PANIC RECOVERED: %v\n\t%s\n", re, stack) } } + +// stack implements Stack, skipping 2 frames +func stack() []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 := 2; ; i++ { // Caller we care about is the user, 2 frames up + 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 + } + line-- // in stack trace, lines are 1-indexed but our array is 0-indexed + 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 { + if n < 0 || n >= len(lines) { + return dunno + } + return bytes.Trim(lines[n], " \t") +} + +// 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 + // Since the package path might contains dots (e.g. code.google.com/...), + // we first remove the path prefix if there is one. + 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 +} diff --git a/util/panic_test.go b/util/panic_test.go new file mode 100644 index 0000000..cf35693 --- /dev/null +++ b/util/panic_test.go @@ -0,0 +1,23 @@ +// Copyright (c) 2014, B3log +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import "testing" + +func TestRecover(t *testing.T) { + defer Recover() + + panic("test panic") +}