diff --git a/base/error.jl b/base/error.jl index 3282323d0be50..8bb23eba48e38 100644 --- a/base/error.jl +++ b/base/error.jl @@ -91,6 +91,30 @@ function catch_backtrace() return _reformat_bt(bt[], bt2[]) end +""" + catch_stack(task=current_task(); [inclue_bt=true]) + +Get the stack of exceptions currently being handled. For nested catch blocks +there may be more than one current exception in which case the most recently +thrown exception is last in the stack. The stack is returned as a Vector of +`(exception,backtrace)` pairs, or a Vector of exceptions if `include_bt` is +false. + +Explicitly passing `task` will return the current exception stack on an +arbitrary task. This is useful for inspecting tasks which have failed due to +uncaught exceptions. +""" +function catch_stack(task=current_task(); include_bt=true) + raw = ccall(:jl_get_exc_stack, Any, (Any,Cint,Cint), task, include_bt, typemax(Cint)) + formatted = Any[] + stride = include_bt ? 3 : 1 + for i = reverse(1:stride:length(raw)) + e = raw[i] + push!(formatted, include_bt ? (e,Base._reformat_bt(raw[i+1],raw[i+2])) : e) + end + formatted +end + ## keyword arg lowering generates calls to this ## function kwerr(kw, args::Vararg{Any,N}) where {N} @_noinline_meta diff --git a/base/exports.jl b/base/exports.jl index e7cdc8808100b..8f7aa256372d8 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -678,6 +678,7 @@ export # errors backtrace, catch_backtrace, + catch_stack, error, rethrow, retry, diff --git a/src/stackwalk.c b/src/stackwalk.c index 0c288330663a7..09fd03be0b155 100644 --- a/src/stackwalk.c +++ b/src/stackwalk.c @@ -177,6 +177,40 @@ JL_DLLEXPORT void jl_get_backtrace(jl_array_t **btout, jl_array_t **bt2out) decode_backtrace(bt_data, bt_size, btout, bt2out); } +// Return data from the exception stack for `task` as an array of Any, starting +// with the top of the stack and returning up to `max_entries`. If requested by +// setting the `include_bt` flag, backtrace data in bt,bt2 format is +// interleaved. +JL_DLLEXPORT jl_value_t *jl_get_exc_stack(jl_value_t* task, int include_bt, int max_entries) +{ + jl_array_t *stack = NULL; + jl_array_t *bt = NULL; + jl_array_t *bt2 = NULL; + JL_GC_PUSH3(&stack, &bt, &bt2); + stack = jl_alloc_array_1d(jl_array_any_type, 0); + if (!jl_typeis(task, jl_task_type)) + jl_error("Cannot get exception stack from a non-Task type"); + jl_exc_stack_t *s = ((jl_task_t*)task)->exc_stack; + if (!s) + return (jl_value_t*)stack; + size_t itr = s->top; + int i = 0; + while (itr > 0 && i < max_entries) { + jl_array_ptr_1d_push(stack, jl_exc_stack_exception(s, itr)); + if (include_bt) { + decode_backtrace(jl_exc_stack_bt_data(s, itr), + jl_exc_stack_bt_size(s, itr), + &bt, &bt2); + jl_array_ptr_1d_push(stack, (jl_value_t*)bt); + jl_array_ptr_1d_push(stack, (jl_value_t*)bt2); + } + itr = jl_exc_stack_next(s, itr); + i++; + } + JL_GC_POP(); + return (jl_value_t*)stack; +} + #if defined(_OS_WINDOWS_) #ifdef _CPU_X86_64_ static UNWIND_HISTORY_TABLE HistoryTable; diff --git a/test/exceptions.jl b/test/exceptions.jl new file mode 100644 index 0000000000000..24eec56d3e1d4 --- /dev/null +++ b/test/exceptions.jl @@ -0,0 +1,210 @@ +using Test + +@testset "Exception stack nesting" begin + # Basic exception stack handling + try + error("A") + catch + @test length(catch_stack()) == 1 + end + @test length(catch_stack()) == 0 + try + try + error("A") + finally + @test length(catch_stack()) == 1 + end + catch + @test length(catch_stack()) == 1 + end + # Errors stack up + try + error("RootCause") + catch + @test length(catch_stack()) == 1 + try + error("B") + catch + stack = catch_stack() + @test length(stack) == 2 + @test stack[1][1].msg == "RootCause" + @test stack[2][1].msg == "B" + end + # Stack pops correctly + stack = catch_stack() + @test length(stack) == 1 + @test stack[1][1].msg == "RootCause" + end + # Lowering - value position + val = try + error("A") + catch + @test length(catch_stack()) == 1 + 1 + end + @test val == 1 + function test_exc_stack_tailpos() + # exercise lowering code path for tail position + try + error("A") + catch + length(catch_stack()) + end + end + @test test_exc_stack_tailpos() == 1 + @test length(catch_stack()) == 0 +end + +@testset "Exception stacks and gotos" begin + function test_exc_stack_catch_return() + try + error("A") + catch + @test length(catch_stack()) == 1 + return + end + end + test_exc_stack_catch_return() + for i=1:1 + try + error("A") + catch + @test length(catch_stack()) == 1 + break + end + end + for i=1:1 + try + error("A") + catch + @test length(catch_stack()) == 1 + continue + end + end + try + error("A") + catch + @test length(catch_stack()) == 1 + @goto outofcatch + end + @label outofcatch + @test length(catch_stack()) == 0 +end + +@testset "Deep exception stacks" begin + function test_exc_stack_deep(n) + # Generate deep exception stack with recursive handlers + # Note that if you let this overflow the program stack (not the exception + # stack) julia will crash. See #28577 + n != 1 || error("RootCause") + try + test_exc_stack_deep(n-1) + catch + error("n==$n") + end + end + @test try + test_exc_stack_deep(100) + catch + length(catch_stack()) + end == 100 + @test length(catch_stack()) == 0 +end + +@testset "Exception stacks and Tasks" begin + # See #12485 + try + error("A") + catch + t = @task try + error("B") + catch ex + ex + end + yield(t) + @test t.state == :done + @test t.result == ErrorException("B") + # Task exception state is preserved around task switches + @test length(catch_stack()) == 1 + @test catch_stack()[1][1] == ErrorException("A") + end + @test length(catch_stack()) == 0 + # test rethrow() rethrows correct state + bt = [] + try + try + error("A") + catch + bt = catch_backtrace() + t = @task try + error("B") + catch ex + ex + end + yield(t) + @test t.state == :done + @test t.result == ErrorException("B") + @test bt == catch_backtrace() + rethrow() + end + catch exc + @test exc == ErrorException("A") + @test bt == catch_backtrace() + end + @test length(catch_stack()) == 0 + # test rethrow with argument + bt = [] + try + try + error("A") + catch + t = @task try + error("B") + catch ex + ex + end + yield(t) + @test t.state == :done + @test t.result == ErrorException("B") + bt = catch_backtrace() + rethrow(ErrorException("C")) + end + catch exc + @test exc == ErrorException("C") + @test bt == catch_backtrace() + end + @test length(catch_stack()) == 0 + # Exception stacks on other tasks + t = @task try + error("A") + catch + error("B") + end + yield(t) + @test t.state == :failed + @test t.result == ErrorException("B") + @test catch_stack(t, include_bt=false) == [ErrorException("A"), ErrorException("B")] + # Exception stacks for tasks which never get the chance to start + t = @task nothing + @test try + @async Base.throwto(t, ErrorException("expected")) + wait(t) + catch e + e + end == ErrorException("expected") + @test length(catch_stack(t)) == 1 + @test length(catch_stack(t)[1][2]) > 0 # backtrace is nonempty +end + +@testset "rethrow" begin + @test try + rethrow() + catch ex + ex + end == ErrorException("rethrow() not allowed outside a catch block") + @test try + rethrow(ErrorException("A")) + catch ex + ex + end == ErrorException("rethrow(exc) not allowed outside a catch block") +end