diff --git a/coroutine_panic_stack_test.go b/coroutine_panic_stack_test.go new file mode 100644 index 000000000..5f699382d --- /dev/null +++ b/coroutine_panic_stack_test.go @@ -0,0 +1,79 @@ +package lua + +import ( + "context" + "strings" + "testing" +) + +func TestResumePanicIncludesGoStack(t *testing.T) { + var logged string + L := NewState(Options{ + IncludeGoStackTrace: true, + PanicHandler: func(_ *LState, message string) { + logged = message + }, + }) + defer L.Close() + + L.SetGlobal("panic_from_go", L.NewFunction(func(*LState) int { + panic("coroutine panic sentinel") + })) + if err := L.DoString(`function run_panic() panic_from_go() end`); err != nil { + t.Fatal(err) + } + + co := L.NewThreadWithContext(context.Background()) + fn := L.GetGlobal("run_panic").(*LFunction) + state, _, err := L.Resume(co, fn) + if state != ResumeError { + t.Fatalf("resume state = %v, want ResumeError", state) + } + if err == nil { + t.Fatal("resume error is nil") + } + if !strings.Contains(err.Error(), "coroutine panic sentinel") { + t.Fatalf("resume error = %q, want panic message", err) + } + if !strings.Contains(err.Error(), "TestResumePanicIncludesGoStack") { + t.Fatalf("resume error = %q, want Go stack", err) + } + if logged != err.Error() { + t.Fatalf("panic handler message = %q, want returned error", logged) + } +} + +func TestResumePanicHandlerPanicKeepsOriginalError(t *testing.T) { + L := NewState(Options{ + PanicHandler: func(*LState, string) { + panic("panic handler sentinel") + }, + }) + defer L.Close() + + L.SetGlobal("panic_from_go", L.NewFunction(func(*LState) int { + panic("coroutine panic sentinel") + })) + if err := L.DoString(`function run_panic() panic_from_go() end`); err != nil { + t.Fatal(err) + } + + co := L.NewThreadWithContext(context.Background()) + fn := L.GetGlobal("run_panic").(*LFunction) + state, _, err := L.Resume(co, fn) + if state != ResumeError { + t.Fatalf("resume state = %v, want ResumeError", state) + } + if err == nil || !strings.Contains(err.Error(), "coroutine panic sentinel") { + t.Fatalf("resume error = %v, want original panic", err) + } + if strings.Contains(err.Error(), "panic handler sentinel") { + t.Fatalf("resume error = %q, want no handler panic", err) + } + if !co.Dead { + t.Fatal("coroutine is not dead") + } + if status := L.Status(co); status != "dead" { + t.Fatalf("coroutine status = %q, want dead", status) + } +} diff --git a/state.go b/state.go index 24370b2dd..50c379b44 100644 --- a/state.go +++ b/state.go @@ -108,6 +108,7 @@ type Options struct { SkipOpenLibs bool // Tells whether a Go stacktrace should be included in a Lua stacktrace when panics occur. IncludeGoStackTrace bool + PanicHandler func(*LState, string) // If `MinimizeStackMemory` is set, the call stack will be automatically grown or shrank up to a limit of // `CallStackSize` in order to minimize memory usage. This does incur a slight performance penalty. MinimizeStackMemory bool diff --git a/vm.go b/vm.go index 2dbba2dff..5e9963f1d 100644 --- a/vm.go +++ b/vm.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math" + "runtime/debug" "strings" "sync" ) @@ -3135,7 +3136,12 @@ func threadRun(L *LState) { SetErrorMetatable(L, e) } } else { - lv = LString(fmt.Sprint(rcv)) + message := fmt.Sprint(rcv) + if L.Options.IncludeGoStackTrace { + message += "\n" + string(debug.Stack()) + } + reportPanic(L, message) + lv = LString(message) } // Check if there's a protected frame that should catch this error @@ -3163,6 +3169,16 @@ func threadRun(L *LState) { L.mainLoop(L, nil) } +func reportPanic(L *LState, message string) { + if L.Options.PanicHandler == nil { + return + } + defer func() { + _ = recover() + }() + L.Options.PanicHandler(L, message) +} + // handleProtectedError searches for a protected (pcall) frame and handles the error. // Returns true if error was handled, false if it should propagate. func handleProtectedError(L *LState, errValue LValue, _ interface{}) bool {