From 46f4c47a19422334f76f593264850e849b6cb11f Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Fri, 11 Mar 2022 17:01:06 +0900 Subject: [PATCH] Allow reentrant breakpoint on Ruby 3.1 On Ruby 3.1 there is `TracePoint.allow_reentry` method to allow reentrant TracePoint events and it allows us to enable nested breakpoints. --- lib/debug/breakpoint.rb | 2 +- lib/debug/session.rb | 38 +++++++++++++++++++----------- lib/debug/thread_client.rb | 47 ++++++++++++++++++++++++++++++-------- 3 files changed, 63 insertions(+), 24 deletions(-) diff --git a/lib/debug/breakpoint.rb b/lib/debug/breakpoint.rb index 001071f25..08997c8f5 100644 --- a/lib/debug/breakpoint.rb +++ b/lib/debug/breakpoint.rb @@ -327,6 +327,7 @@ def initialize expr, path def setup @tp = TracePoint.new(:line){|tp| + next if SESSION.in_subsession? # TODO: Ractor support next if ThreadClient.current.management? next if skip_path?(tp.path) @@ -375,7 +376,6 @@ def watch_eval(tp) def setup @tp = TracePoint.new(:line, :return, :b_return){|tp| - watch_eval(tp) } end diff --git a/lib/debug/session.rb b/lib/debug/session.rb index 85af827e8..b89b00238 100644 --- a/lib/debug/session.rb +++ b/lib/debug/session.rb @@ -105,7 +105,7 @@ def initialize ui @intercept_trap_sigint = false @intercepted_sigint_cmd = 'DEFAULT' @process_group = ProcessGroup.new - @subsession = nil + @subsession_stack = [] @frame_map = {} # for DAP: {id => [threadId, frame_depth]} and CDP: {id => frame_depth} @var_map = {1 => [:globals], } # {id => ...} for DAP @@ -216,6 +216,7 @@ def process_event evt q << true when :init + enter_subsession wait_command_loop tc when :load @@ -258,7 +259,7 @@ def process_event evt end when :result - raise "[BUG] not in subsession" unless @subsession + raise "[BUG] not in subsession" if @subsession_stack.empty? case ev_args.first when :try_display @@ -1540,27 +1541,38 @@ def get_thread_client th = Thread.current end private def enter_subsession - raise "already in subsession" if @subsession - @subsession = true - stop_all_threads - @process_group.lock - DEBUGGER__.info "enter_subsession" + if !@subsession_stack.empty? + DEBUGGER__.info "Enter subsession (nested #{@subsession_stack.size})" + else + DEBUGGER__.info "Enter subsession" + stop_all_threads + @process_group.lock + end + + @subsession_stack << true end private def leave_subsession type - DEBUGGER__.info "leave_subsession" - @process_group.unlock - restart_all_threads + raise '[BUG] leave_subsession: not entered' if @subsession_stack.empty? + @subsession_stack.pop + + if @subsession_stack.empty? + DEBUGGER__.info "Leave subsession" + @process_group.unlock + restart_all_threads + else + DEBUGGER__.info "Leave subsession (nested #{@subsession_stack.size})" + end + @tc << type if type @tc = nil - @subsession = false rescue Exception => e - STDERR.puts [e, e.backtrace].inspect + STDERR.puts PP.pp([e, e.backtrace], ''.dup) raise end def in_subsession? - @subsession + !@subsession_stack.empty? end ## event diff --git a/lib/debug/thread_client.rb b/lib/debug/thread_client.rb index deec71d35..995bb88eb 100644 --- a/lib/debug/thread_client.rb +++ b/lib/debug/thread_client.rb @@ -346,9 +346,39 @@ def step_tp iter, events = [:line, :b_return, :return] ## cmd helpers - # this method is extracted to hide frame_eval's local variables from C method eval's binding - def instance_eval_for_cmethod frame_self, src - frame_self.instance_eval(src) + if TracePoint.respond_to? :allow_reentry + def tp_allow_reentry + TracePoint.allow_reentry do + yield + end + rescue RuntimeError => e + # on the postmortem mode, it is not stopped in TracePoint + if e.message == 'No need to allow reentrance.' + yield + else + raise + end + end + else + def tp_allow_reentry + yield + end + end + + def frame_eval_core src, b + if b + f, _l = b.source_location + + tp_allow_reentry do + b.eval(src, "(rdbg)/#{f}") + end + else + frame_self = current_frame.self + + tp_allow_reentry do + frame_self.instance_eval(src) + end + end end SPECIAL_LOCAL_VARS = [ @@ -365,16 +395,13 @@ def frame_eval src, re_raise: false b.local_variable_set(name, var) if /\%/ !~ name end - result = if b - f, _l = b.source_location - b.eval(src, "(rdbg)/#{f}") - else - frame_self = current_frame.self - instance_eval_for_cmethod(frame_self, src) - end + result = frame_eval_core(src, b) + @success_last_eval = true result + rescue SystemExit + raise rescue Exception => e return yield(e) if block_given?