class DEBUGGER__::Session
Constants
- BREAK_KEYWORDS
- INVALID_PARAMS
Attributes
intercepted_sigint_cmd[R]
process_group[R]
Public Class Methods
new(ui)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 85 def initialize ui @ui = ui @sr = SourceRepository.new @bps = {} # bp.key => bp # [file, line] => LineBreakpoint # "Error" => CatchBreakpoint # "Foo#bar" => MethodBreakpoint # [:watch, ivar] => WatchIVarBreakpoint # [:check, expr] => CheckBreakpoint # @tracers = {} @th_clients = {} # {Thread => ThreadClient} @q_evt = Queue.new @displays = [] @tc = nil @tc_id = 0 @preset_command = nil @postmortem_hook = nil @postmortem = false @intercept_trap_sigint = false @intercepted_sigint_cmd = 'DEFAULT' @process_group = ProcessGroup.new @subsession = nil @frame_map = {} # for DAP: {id => [threadId, frame_depth]} and CDP: {id => frame_depth} @var_map = {1 => [:globals], } # {id => ...} for DAP @src_map = {} # {id => src} @script_paths = [File.absolute_path($0)] # for CDP @obj_map = {} # { object_id => ... } for CDP @tp_thread_begin = nil @tp_load_script = TracePoint.new(:script_compiled){|tp| ThreadClient.current.on_load tp.instruction_sequence, tp.eval_script } @tp_load_script.enable @thread_stopper = thread_stopper activate self.postmortem = CONFIG[:postmortem] end
Public Instance Methods
activate(on_fork: false)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 137 def activate on_fork: false @tp_thread_begin&.disable @tp_thread_begin = nil if on_fork @ui.activate self, on_fork: true else @ui.activate self, on_fork: false end q = Queue.new @session_server = Thread.new do Thread.current.name = 'DEBUGGER__::SESSION@server' Thread.current.abort_on_exception = true # Thread management setup_threads thc = get_thread_client Thread.current thc.mark_as_management if @ui.respond_to?(:reader_thread) && thc = get_thread_client(@ui.reader_thread) thc.mark_as_management end @tp_thread_begin = TracePoint.new(:thread_begin) do |tp| get_thread_client end @tp_thread_begin.enable # session start q << true session_server_main end q.pop end
active?()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 129 def active? !@q_evt.closed? end
add_bp(bp)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1232 def add_bp bp # don't repeat commands that add breakpoints @repl_prev_line = nil if @bps.has_key? bp.key unless bp.duplicable? @ui.puts "duplicated breakpoint: #{bp}" bp.disable end else @bps[bp.key] = bp end end
add_catch_breakpoint(pat)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1320 def add_catch_breakpoint pat bp = CatchBreakpoint.new(pat) add_bp bp end
add_check_breakpoint(expr, path)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1325 def add_check_breakpoint expr, path bp = CheckBreakpoint.new(expr, path) add_bp bp end
add_iseq_breakpoint(iseq, **kw)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1339 def add_iseq_breakpoint iseq, **kw bp = ISeqBreakpoint.new(iseq, [:line], **kw) add_bp bp end
add_line_breakpoint(file, line, **kw)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1330 def add_line_breakpoint file, line, **kw file = resolve_path(file) bp = LineBreakpoint.new(file, line, **kw) add_bp bp rescue Errno::ENOENT => e @ui.puts e.message end
add_preset_commands(name, cmds, kick: true, continue: true)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 300 def add_preset_commands name, cmds, kick: true, continue: true cs = cmds.map{|c| c.each_line.map{|line| line = line.strip.gsub(/\A\s*\#.*/, '').strip line unless line.empty? }.compact }.flatten.compact if @preset_command && !@preset_command.commands.empty? @preset_command.commands += cs else @preset_command = PresetCommand.new(cs, name, continue) end ThreadClient.current.on_init name if kick end
add_tracer(tracer)
click to toggle source
tracers
# File debug-1.4.0/lib/debug/session.rb, line 1346 def add_tracer tracer # don't repeat commands that add tracers @repl_prev_line = nil if @tracers.has_key? tracer.key tracer.disable @ui.puts "Duplicated tracer: #{tracer}" else @tracers[tracer.key] = tracer @ui.puts "Enable #{tracer}" end end
after_fork_parent()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1717 def after_fork_parent @ui.after_fork_parent end
ask(msg, default = 'Y')
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1174 def ask msg, default = 'Y' opts = '[y/n]'.tr(default.downcase, default) input = @ui.ask("#{msg} #{opts} ") input = default if input.empty? case input when 'y', 'Y' true else false end end
before_fork(need_lock = true)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1711 def before_fork need_lock = true if need_lock @process_group.multi_process! end end
bp_index(specific_bp_key)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1209 def bp_index specific_bp_key iterate_bps do |key, bp, i| if key == specific_bp_key return [bp, i] end end nil end
break_at?(file, line)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 133 def break_at? file, line @bps.has_key? [file, line] end
cancel_auto_continue()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1156 def cancel_auto_continue if @preset_command&.auto_continue @preset_command.auto_continue = false end end
capture_exception_frames(*exclude_path) { || ... }
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1608 def capture_exception_frames *exclude_path postmortem_hook = TracePoint.new(:raise){|tp| exc = tp.raised_exception frames = DEBUGGER__.capture_frames(__dir__) exclude_path.each{|ex| if Regexp === ex frames.delete_if{|e| ex =~ e.path} else frames.delete_if{|e| e.path.start_with? ex.to_s} end } exc.instance_variable_set(:@__debugger_postmortem_frames, frames) } postmortem_hook.enable begin yield nil rescue Exception => e if e.instance_variable_defined? :@__debugger_postmortem_frames e else raise end ensure postmortem_hook.disable end end
cdp_event(args)
click to toggle source
# File debug-1.4.0/lib/debug/server_cdp.rb, line 560 def cdp_event args type, req, result = args case type when :backtrace result[:callFrames].each.with_index do |frame, i| frame_id = frame[:callFrameId] @frame_map[frame_id] = i s_id = frame.dig(:location, :scriptId) if File.exist?(s_id) && !@script_paths.include?(s_id) src = File.read(s_id) @ui.fire_event 'Debugger.scriptParsed', scriptId: s_id, url: frame[:url], startLine: 0, startColumn: 0, endLine: src.count("\n"), endColumn: 0, executionContextId: @script_paths.size + 1, hash: src.hash @script_paths << s_id end frame[:scopeChain].each {|s| oid = s.dig(:object, :objectId) @obj_map[oid] = [s[:type], frame_id] } end if oid = result.dig(:data, :objectId) @obj_map[oid] = ['properties'] end @ui.fire_event 'Debugger.paused', **result when :evaluate message = result.delete :message if message fail_response req, code: INVALID_PARAMS, message: message else rs = result.dig(:response, :result) [rs].each{|obj| if oid = obj[:objectId] @obj_map[oid] = ['properties'] end } @ui.respond req, **result[:response] out = result[:output] if out && !out.empty? @ui.fire_event 'Runtime.consoleAPICalled', type: 'log', args: [ type: out.class, value: out ], executionContextId: 1, # Change this number if something goes wrong. timestamp: Time.now.to_f end end when :scope result.each{|obj| if oid = obj.dig(:value, :objectId) @obj_map[oid] = ['properties'] end } @ui.respond req, result: result when :properties result.each_value{|v| v.each{|obj| if oid = obj.dig(:value, :objectId) @obj_map[oid] = ['properties'] end } } @ui.respond req, **result end end
check_postmortem()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1592 def check_postmortem if @postmortem raise PostmortemError, "Can not use this command on postmortem mode." end end
clean_bps()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1226 def clean_bps @bps.delete_if{|_k, bp| bp.deleted? } end
config_command(arg)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1122 def config_command arg case arg when nil CONFIG_SET.each do |k, _| config_show k end when /\Aunset\s+(.+)\z/ if CONFIG_SET[key = $1.to_sym] CONFIG[key] = nil end config_show key when /\A(\w+)\s*=\s*(.+)\z/ config_set $1, $2 when /\A\s*set\s+(\w+)\s+(.+)\z/ config_set $1, $2 when /\A(\w+)\s*<<\s*(.+)\z/ config_set $1, $2, append: true when /\A\s*append\s+(\w+)\s+(.+)\z/ config_set $1, $2 when /\A(\w+)\z/ config_show $1 else @ui.puts "Can not parse parameters: #{arg}" end end
config_set(key, val, append: false)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1106 def config_set key, val, append: false if CONFIG_SET[key = key.to_sym] begin if append CONFIG.append_config(key, val) else CONFIG[key] = val end rescue => e @ui.puts e.message end end config_show key end
config_show(key)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1089 def config_show key key = key.to_sym if CONFIG_SET[key] v = CONFIG[key] kv = "#{key} = #{v.nil? ? '(default)' : v.inspect}" desc = CONFIG_SET[key][1] line = "%-30s \# %s" % [kv, desc] if line.size > SESSION.width @ui.puts "\# #{desc}\n#{kv}" else @ui.puts line end else @ui.puts "Unknown configuration: #{key}. 'config' shows all configurations." end end
dap_event(args)
click to toggle source
# File debug-1.4.0/lib/debug/server_dap.rb, line 524 def dap_event args # puts({dap_event: args}.inspect) type, req, result = args case type when :backtrace result[:stackFrames].each.with_index{|fi, i| fi[:id] = id = @frame_map.size + 1 @frame_map[id] = [req.dig('arguments', 'threadId'), i] if fi[:source] && src = fi[:source][:sourceReference] src_id = @src_map.size + 1 @src_map[src_id] = src fi[:source][:sourceReference] = src_id end } @ui.respond req, result when :scopes frame_id = req.dig('arguments', 'frameId') local_scope = result[:scopes].first local_scope[:variablesReference] = id = @var_map.size + 1 @var_map[id] = [:scope, frame_id] @ui.respond req, result when :scope tid = result.delete :tid register_vars result[:variables], tid @ui.respond req, result when :variable tid = result.delete :tid register_vars result[:variables], tid @ui.respond req, result when :evaluate message = result.delete :message if message @ui.respond req, success: false, message: message else tid = result.delete :tid register_var result, tid @ui.respond req, result end when :completions @ui.respond req, result else raise "unsupported: #{args.inspect}" end end
deactivate()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 174 def deactivate get_thread_client.deactivate @thread_stopper.disable @tp_load_script.disable @tp_thread_begin.disable @bps.each_value{|bp| bp.disable} @th_clients.each_value{|thc| thc.close} @tracers.values.each{|t| t.disable} @q_evt.close @ui&.deactivate @ui = nil end
delete_bp(arg = nil)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1246 def delete_bp arg = nil case arg when nil @bps.each{|key, bp| bp.delete} @bps.clear else del_bp = nil iterate_bps{|key, bp, i| del_bp = bp if i == arg} if del_bp del_bp.delete @bps.delete del_bp.key return [arg, del_bp] end end end
enter_postmortem_session(exc)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1598 def enter_postmortem_session exc return unless exc.instance_variable_defined? :@__debugger_postmortem_frames frames = exc.instance_variable_get(:@__debugger_postmortem_frames) @postmortem = true ThreadClient.current.suspend :postmortem, postmortem_frames: frames, postmortem_exc: exc ensure @postmortem = false end
fail_response(req, **result)
click to toggle source
# File debug-1.4.0/lib/debug/server_cdp.rb, line 514 def fail_response req, **result @ui.respond_fail req, result return :retry end
find_waiting_tc(id)
click to toggle source
# File debug-1.4.0/lib/debug/server_dap.rb, line 397 def find_waiting_tc id @th_clients.each{|th, tc| return tc if tc.id == id && tc.waiting? } return nil end
get_thread_client(th = Thread.current)
click to toggle source
can be called by other threads
# File debug-1.4.0/lib/debug/session.rb, line 1448 def get_thread_client th = Thread.current if @th_clients.has_key? th @th_clients[th] else if Thread.current == @session_server create_thread_client th else ask_thread_client th end end end
in_subsession?()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1525 def in_subsession? @subsession end
inspect()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 325 def inspect "DEBUGGER__::SESSION" end
intercept_trap_sigint(flag) { || ... }
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1686 def intercept_trap_sigint flag, &b prev = @intercept_trap_sigint @intercept_trap_sigint = flag yield ensure @intercept_trap_sigint = prev end
intercept_trap_sigint?()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1682 def intercept_trap_sigint? @intercept_trap_sigint end
intercept_trap_sigint_end()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1699 def intercept_trap_sigint_end @intercept_trap_sigint = false prev, @intercepted_sigint_cmd = @intercepted_sigint_cmd, nil prev end
intercept_trap_sigint_start(prev)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1694 def intercept_trap_sigint_start prev @intercept_trap_sigint = true @intercepted_sigint_cmd = prev end
iterate_bps() { |key, bp, i| ... }
click to toggle source
breakpoint management
# File debug-1.4.0/lib/debug/session.rb, line 1188 def iterate_bps deleted_bps = [] i = 0 @bps.each{|key, bp| if !bp.deleted? yield key, bp, i i += 1 else deleted_bps << bp end } ensure deleted_bps.each{|bp| @bps.delete bp} end
managed_thread_clients()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1392 def managed_thread_clients thcs, _unmanaged_ths = update_thread_list thcs end
method_added(tp)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1564 def method_added tp b = tp.binding if var_name = b.local_variables.first mid = b.local_variable_get(var_name) unresolved = false @bps.each{|k, bp| case bp when MethodBreakpoint if bp.method.nil? if bp.sig_method_name == mid.to_s bp.try_enable(added: true) end end unresolved = true unless bp.enabled? end } unless unresolved METHOD_ADDED_TRACKER.disable end end end
on_load(iseq, src)
click to toggle source
event
# File debug-1.4.0/lib/debug/session.rb, line 1531 def on_load iseq, src DEBUGGER__.info "Load #{iseq.absolute_path || iseq.path}" @sr.add iseq, src pending_line_breakpoints = @bps.find_all do |key, bp| LineBreakpoint === bp && !bp.iseq end pending_line_breakpoints.each do |_key, bp| if bp.path == (iseq.absolute_path || iseq.path) bp.try_activate end end end
on_thread_begin(th)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1423 def on_thread_begin th if @th_clients.has_key? th # TODO: NG? else create_thread_client th end end
parse_break(arg)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1264 def parse_break arg mode = :sig expr = Hash.new{|h, k| h[k] = []} arg.split(' ').each{|w| if BREAK_KEYWORDS.any?{|pat| w == pat} mode = w[0..-2].to_sym else expr[mode] << w end } expr.default_proc = nil expr.transform_values{|v| v.join(' ')} end
pop_event()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 192 def pop_event @q_evt.pop end
postmortem=(is_enable)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1638 def postmortem=(is_enable) if is_enable unless @postmortem_hook @postmortem_hook = TracePoint.new(:raise){|tp| exc = tp.raised_exception frames = DEBUGGER__.capture_frames(__dir__) exc.instance_variable_set(:@__debugger_postmortem_frames, frames) } at_exit{ @postmortem_hook.disable if CONFIG[:postmortem] && (exc = $!) != nil exc = exc.cause while exc.cause begin @ui.puts "Enter postmortem mode with #{exc.inspect}" @ui.puts exc.backtrace.map{|e| ' ' + e} @ui.puts "\n" enter_postmortem_session exc rescue SystemExit exit! rescue Exception => e @ui = STDERR unless @ui @ui.puts "Error while postmortem console: #{e.inspect}" end end } end if !@postmortem_hook.enabled? @postmortem_hook.enable end else if @postmortem_hook && @postmortem_hook.enabled? @postmortem_hook.disable end end end
process_command(line)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 386 def process_command line if line.empty? if @repl_prev_line line = @repl_prev_line else return :retry end else @repl_prev_line = line end /([^\s]+)(?:\s+(.+))?/ =~ line cmd, arg = $1, $2 # p cmd: [cmd, *arg] case cmd ### Control flow # * `s[tep]` # * Step in. Resume the program until next breakable point. # * `s[tep] <n>` # * Step in, resume the program at `<n>`th breakable point. when 's', 'step' cancel_auto_continue check_postmortem step_command :in, arg # * `n[ext]` # * Step over. Resume the program until next line. # * `n[ext] <n>` # * Step over, same as `step <n>`. when 'n', 'next' cancel_auto_continue check_postmortem step_command :next, arg # * `fin[ish]` # * Finish this frame. Resume the program until the current frame is finished. # * `fin[ish] <n>` # * Finish `<n>`th frames. when 'fin', 'finish' cancel_auto_continue check_postmortem if arg&.to_i == 0 raise 'finish command with 0 does not make sense.' end step_command :finish, arg # * `c[ontinue]` # * Resume the program. when 'c', 'continue' cancel_auto_continue leave_subsession :continue # * `q[uit]` or `Ctrl-D` # * Finish debugger (with the debuggee process on non-remote debugging). when 'q', 'quit' if ask 'Really quit?' @ui.quit arg.to_i leave_subsession :continue else return :retry end # * `q[uit]!` # * Same as q[uit] but without the confirmation prompt. when 'q!', 'quit!' @ui.quit arg.to_i leave_subsession nil # * `kill` # * Stop the debuggee process with `Kernel#exit!`. when 'kill' if ask 'Really kill?' exit! (arg || 1).to_i else return :retry end # * `kill!` # * Same as kill but without the confirmation prompt. when 'kill!' exit! (arg || 1).to_i # * `sigint` # * Execute SIGINT handler registered by the debuggee. # * Note that this command should be used just after stop by `SIGINT`. when 'sigint' begin case cmd = @intercepted_sigint_cmd when nil, 'IGNORE', :IGNORE, 'DEFAULT', :DEFAULT # ignore when String eval(cmd) when Proc cmd.call end leave_subsession :continue rescue Exception => e @ui.puts "Exception: #{e}" @ui.puts e.backtrace.map{|line| " #{e}"} return :retry end ### Breakpoint # * `b[reak]` # * Show all breakpoints. # * `b[reak] <line>` # * Set breakpoint on `<line>` at the current frame's file. # * `b[reak] <file>:<line>` or `<file> <line>` # * Set breakpoint on `<file>:<line>`. # * `b[reak] <class>#<name>` # * Set breakpoint on the method `<class>#<name>`. # * `b[reak] <expr>.<name>` # * Set breakpoint on the method `<expr>.<name>`. # * `b[reak] ... if: <expr>` # * break if `<expr>` is true at specified location. # * `b[reak] ... pre: <command>` # * break and run `<command>` before stopping. # * `b[reak] ... do: <command>` # * break and run `<command>`, and continue. # * `b[reak] ... path: <path_regexp>` # * break if the triggering event's path matches <path_regexp>. # * `b[reak] if: <expr>` # * break if: `<expr>` is true at any lines. # * Note that this feature is super slow. when 'b', 'break' check_postmortem if arg == nil show_bps return :retry else case bp = repl_add_breakpoint(arg) when :noretry when nil return :retry else show_bps bp return :retry end end # skip when 'bv' check_postmortem require 'json' h = Hash.new{|h, k| h[k] = []} @bps.each_value{|bp| if LineBreakpoint === bp h[bp.path] << {lnum: bp.line} end } if h.empty? # TODO: clean? else open(".rdb_breakpoints.json", 'w'){|f| JSON.dump(h, f)} end vimsrc = File.join(__dir__, 'bp.vim') system("vim -R -S #{vimsrc} #{@tc.location.path}") if File.exist?(".rdb_breakpoints.json") pp JSON.load(File.read(".rdb_breakpoints.json")) end return :retry # * `catch <Error>` # * Set breakpoint on raising `<Error>`. # * `catch ... if: <expr>` # * stops only if `<expr>` is true as well. # * `catch ... pre: <command>` # * runs `<command>` before stopping. # * `catch ... do: <command>` # * stops and run `<command>`, and continue. # * `catch ... path: <path_regexp>` # * stops if the exception is raised from a path that matches <path_regexp>. when 'catch' check_postmortem if arg bp = repl_add_catch_breakpoint arg show_bps bp if bp else show_bps end return :retry # * `watch @ivar` # * Stop the execution when the result of current scope's `@ivar` is changed. # * Note that this feature is super slow. # * `watch ... if: <expr>` # * stops only if `<expr>` is true as well. # * `watch ... pre: <command>` # * runs `<command>` before stopping. # * `watch ... do: <command>` # * stops and run `<command>`, and continue. # * `watch ... path: <path_regexp>` # * stops if the triggering event's path matches <path_regexp>. when 'wat', 'watch' check_postmortem if arg && arg.match?(/\A@\w+/) repl_add_watch_breakpoint(arg) else show_bps return :retry end # * `del[ete]` # * delete all breakpoints. # * `del[ete] <bpnum>` # * delete specified breakpoint. when 'del', 'delete' check_postmortem bp = case arg when nil show_bps if ask "Remove all breakpoints?", 'N' delete_bp end when /\d+/ delete_bp arg.to_i else nil end @ui.puts "deleted: \##{bp[0]} #{bp[1]}" if bp return :retry ### Information # * `bt` or `backtrace` # * Show backtrace (frame) information. # * `bt <num>` or `backtrace <num>` # * Only shows first `<num>` frames. # * `bt /regexp/` or `backtrace /regexp/` # * Only shows frames with method name or location info that matches `/regexp/`. # * `bt <num> /regexp/` or `backtrace <num> /regexp/` # * Only shows first `<num>` frames with method name or location info that matches `/regexp/`. when 'bt', 'backtrace' case arg when /\A(\d+)\z/ @tc << [:show, :backtrace, arg.to_i, nil] when /\A\/(.*)\/\z/ pattern = $1 @tc << [:show, :backtrace, nil, Regexp.compile(pattern)] when /\A(\d+)\s+\/(.*)\/\z/ max, pattern = $1, $2 @tc << [:show, :backtrace, max.to_i, Regexp.compile(pattern)] else @tc << [:show, :backtrace, nil, nil] end # * `l[ist]` # * Show current frame's source code. # * Next `list` command shows the successor lines. # * `l[ist] -` # * Show predecessor lines as opposed to the `list` command. # * `l[ist] <start>` or `l[ist] <start>-<end>` # * Show current frame's source code from the line <start> to <end> if given. when 'l', 'list' case arg ? arg.strip : nil when /\A(\d+)\z/ @tc << [:show, :list, {start_line: arg.to_i - 1}] when /\A-\z/ @tc << [:show, :list, {dir: -1}] when /\A(\d+)-(\d+)\z/ @tc << [:show, :list, {start_line: $1.to_i - 1, end_line: $2.to_i}] when nil @tc << [:show, :list] else @ui.puts "Can not handle list argument: #{arg}" return :retry end # * `edit` # * Open the current file on the editor (use `EDITOR` environment variable). # * Note that edited file will not be reloaded. # * `edit <file>` # * Open <file> on the editor. when 'edit' if @ui.remote? @ui.puts "not supported on the remote console." return :retry end begin arg = resolve_path(arg) if arg rescue Errno::ENOENT @ui.puts "not found: #{arg}" return :retry end @tc << [:show, :edit, arg] # * `i[nfo]` # * Show information about current frame (local/instance variables and defined constants). # * `i[nfo] l[ocal[s]]` # * Show information about the current frame (local variables) # * It includes `self` as `%self` and a return value as `%return`. # * `i[nfo] i[var[s]]` or `i[nfo] instance` # * Show information about instance variables about `self`. # * `i[nfo] c[onst[s]]` or `i[nfo] constant[s]` # * Show information about accessible constants except toplevel constants. # * `i[nfo] g[lobal[s]]` # * Show information about global variables # * `i[nfo] ... </pattern/>` # * Filter the output with `</pattern/>`. # * `i[nfo] th[read[s]]` # * Show all threads (same as `th[read]`). when 'i', 'info' if /\/(.+)\/\z/ =~ arg pat = Regexp.compile($1) sub = $~.pre_match.strip else sub = arg end case sub when nil @tc << [:show, :default, pat] # something useful when 'l', /^locals?/ @tc << [:show, :locals, pat] when 'i', /^ivars?/i, /^instance[_ ]variables?/i @tc << [:show, :ivars, pat] when 'c', /^consts?/i, /^constants?/i @tc << [:show, :consts, pat] when 'g', /^globals?/i, /^global[_ ]variables?/i @tc << [:show, :globals, pat] when 'th', /threads?/ thread_list return :retry else @ui.puts "unrecognized argument for info command: #{arg}" show_help 'info' return :retry end # * `o[utline]` or `ls` # * Show you available methods, constants, local variables, and instance variables in the current scope. # * `o[utline] <expr>` or `ls <expr>` # * Show you available methods and instance variables of the given object. # * If the object is a class/module, it also lists its constants. when 'outline', 'o', 'ls' @tc << [:show, :outline, arg] # * `display` # * Show display setting. # * `display <expr>` # * Show the result of `<expr>` at every suspended timing. when 'display' if arg && !arg.empty? @displays << arg @tc << [:eval, :try_display, @displays] else @tc << [:eval, :display, @displays] end # * `undisplay` # * Remove all display settings. # * `undisplay <displaynum>` # * Remove a specified display setting. when 'undisplay' case arg when /(\d+)/ if @displays[n = $1.to_i] @displays.delete_at n end @tc << [:eval, :display, @displays] when nil if ask "clear all?", 'N' @displays.clear end return :retry end ### Frame control # * `f[rame]` # * Show the current frame. # * `f[rame] <framenum>` # * Specify a current frame. Evaluation are run on specified frame. when 'frame', 'f' @tc << [:frame, :set, arg] # * `up` # * Specify the upper frame. when 'up' @tc << [:frame, :up] # * `down` # * Specify the lower frame. when 'down' @tc << [:frame, :down] ### Evaluate # * `p <expr>` # * Evaluate like `p <expr>` on the current frame. when 'p' @tc << [:eval, :p, arg.to_s] # * `pp <expr>` # * Evaluate like `pp <expr>` on the current frame. when 'pp' @tc << [:eval, :pp, arg.to_s] # * `eval <expr>` # * Evaluate `<expr>` on the current frame. when 'eval', 'call' if arg == nil || arg.empty? show_help 'eval' @ui.puts "\nTo evaluate the variable `#{cmd}`, use `pp #{cmd}` instead." return :retry else @tc << [:eval, :call, arg] end # * `irb` # * Invoke `irb` on the current frame. when 'irb' if @ui.remote? @ui.puts "not supported on the remote console." return :retry end @tc << [:eval, :irb] # don't repeat irb command @repl_prev_line = nil ### Trace # * `trace` # * Show available tracers list. # * `trace line` # * Add a line tracer. It indicates line events. # * `trace call` # * Add a call tracer. It indicate call/return events. # * `trace exception` # * Add an exception tracer. It indicates raising exceptions. # * `trace object <expr>` # * Add an object tracer. It indicates that an object by `<expr>` is passed as a parameter or a receiver on method call. # * `trace ... </pattern/>` # * Indicates only matched events to `</pattern/>` (RegExp). # * `trace ... into: <file>` # * Save trace information into: `<file>`. # * `trace off <num>` # * Disable tracer specified by `<num>` (use `trace` command to check the numbers). # * `trace off [line|call|pass]` # * Disable all tracers. If `<type>` is provided, disable specified type tracers. when 'trace' if (re = /\s+into:\s*(.+)/) =~ arg into = $1 arg.sub!(re, '') end if (re = /\s\/(.+)\/\z/) =~ arg pattern = $1 arg.sub!(re, '') end case arg when nil @ui.puts 'Tracers:' @tracers.values.each_with_index{|t, i| @ui.puts "* \##{i} #{t}" } @ui.puts return :retry when /\Aline\z/ add_tracer LineTracer.new(@ui, pattern: pattern, into: into) return :retry when /\Acall\z/ add_tracer CallTracer.new(@ui, pattern: pattern, into: into) return :retry when /\Aexception\z/ add_tracer ExceptionTracer.new(@ui, pattern: pattern, into: into) return :retry when /\Aobject\s+(.+)/ @tc << [:trace, :object, $1.strip, {pattern: pattern, into: into}] when /\Aoff\s+(\d+)\z/ if t = @tracers.values[$1.to_i] t.disable @ui.puts "Disable #{t.to_s}" else @ui.puts "Unmatched: #{$1}" end return :retry when /\Aoff(\s+(line|call|exception|object))?\z/ @tracers.values.each{|t| if $2.nil? || t.type == $2 t.disable @ui.puts "Disable #{t.to_s}" end } return :retry else @ui.puts "Unknown trace option: #{arg.inspect}" return :retry end # Record # * `record` # * Show recording status. # * `record [on|off]` # * Start/Stop recording. # * `step back` # * Start replay. Step back with the last execution log. # * `s[tep]` does stepping forward with the last log. # * `step reset` # * Stop replay . when 'record' case arg when nil, 'on', 'off' @tc << [:record, arg&.to_sym] else @ui.puts "unknown command: #{arg}" return :retry end ### Thread control # * `th[read]` # * Show all threads. # * `th[read] <thnum>` # * Switch thread specified by `<thnum>`. when 'th', 'thread' case arg when nil, 'list', 'l' thread_list when /(\d+)/ switch_thread $1.to_i else @ui.puts "unknown thread command: #{arg}" end return :retry ### Configuration # * `config` # * Show all configuration with description. # * `config <name>` # * Show current configuration of <name>. # * `config set <name> <val>` or `config <name> = <val>` # * Set <name> to <val>. # * `config append <name> <val>` or `config <name> << <val>` # * Append `<val>` to `<name>` if it is an array. # * `config unset <name>` # * Set <name> to default. when 'config' config_command arg return :retry # * `source <file>` # * Evaluate lines in `<file>` as debug commands. when 'source' if arg begin cmds = File.readlines(path = File.expand_path(arg)) add_preset_commands path, cmds, kick: true, continue: false rescue Errno::ENOENT @ui.puts "File not found: #{arg}" end else show_help 'source' end return :retry # * `open` # * open debuggee port on UNIX domain socket and wait for attaching. # * Note that `open` command is EXPERIMENTAL. # * `open [<host>:]<port>` # * open debuggee port on TCP/IP with given `[<host>:]<port>` and wait for attaching. # * `open vscode` # * open debuggee port for VSCode and launch VSCode if available. # * `open chrome` # * open debuggee port for Chrome and wait for attaching. when 'open' case arg&.downcase when '', nil repl_open_unix when 'vscode' repl_open_vscode when /\A(.+):(\d+)\z/ repl_open_tcp $1, $2.to_i when /\A(\d+)z/ repl_open_tcp nil, $1.to_i when 'tcp' repl_open_tcp CONFIG[:host], (CONFIG[:port] || 0) when 'chrome', 'cdp' CONFIG[:open_frontend] = 'chrome' repl_open_tcp CONFIG[:host], (CONFIG[:port] || 0) else raise "Unknown arg: #{arg}" end return :retry ### Help # * `h[elp]` # * Show help for all commands. # * `h[elp] <command>` # * Show help for the given command. when 'h', 'help', '?' if arg show_help arg else @ui.puts DEBUGGER__.help end return :retry ### END else @tc << [:eval, :pp, line] =begin @repl_prev_line = nil @ui.puts "unknown command: #{line}" begin require 'did_you_mean' spell_checker = DidYouMean::SpellChecker.new(dictionary: DEBUGGER__.commands) correction = spell_checker.correct(line.split(/\s/).first || '') @ui.puts "Did you mean? #{correction.join(' or ')}" unless correction.empty? rescue LoadError # Don't use D end return :retry =end end rescue Interrupt return :retry rescue SystemExit raise rescue PostmortemError => e @ui.puts e.message return :retry rescue Exception => e @ui.puts "[REPL ERROR] #{e.inspect}" @ui.puts e.backtrace.map{|e| ' ' + e} return :retry end
process_event(evt)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 204 def process_event evt # variable `@internal_info` is only used for test tc, output, ev, @internal_info, *ev_args = evt output.each{|str| @ui.puts str} if ev != :suspend case ev when :thread_begin # special event, tc is nil th = ev_args.shift q = ev_args.shift on_thread_begin th q << true when :init wait_command_loop tc when :load iseq, src = ev_args on_load iseq, src @ui.event :load tc << :continue when :trace trace_id, msg = ev_args if t = @tracers.values.find{|t| t.object_id == trace_id} t.puts msg end tc << :continue when :suspend enter_subsession if ev_args.first != :replay output.each{|str| @ui.puts str} case ev_args.first when :breakpoint bp, i = bp_index ev_args[1] clean_bps unless bp @ui.event :suspend_bp, i, bp, tc.id when :trap @ui.event :suspend_trap, sig = ev_args[1], tc.id if sig == :SIGINT && (@intercepted_sigint_cmd.kind_of?(Proc) || @intercepted_sigint_cmd.kind_of?(String)) @ui.puts "#{@intercepted_sigint_cmd.inspect} is registered as SIGINT handler." @ui.puts "`sigint` command execute it." end else @ui.event :suspended, tc.id end if @displays.empty? wait_command_loop tc else tc << [:eval, :display, @displays] end when :result raise "[BUG] not in subsession" unless @subsession case ev_args.first when :try_display failed_results = ev_args[1] if failed_results.size > 0 i, _msg = failed_results.last if i+1 == @displays.size @ui.puts "canceled: #{@displays.pop}" end end when :method_breakpoint, :watch_breakpoint bp = ev_args[1] if bp add_bp(bp) show_bps bp else # can't make a bp end when :trace_pass obj_id = ev_args[1] obj_inspect = ev_args[2] opt = ev_args[3] add_tracer ObjectTracer.new(@ui, obj_id, obj_inspect, **opt) else # ignore end wait_command_loop tc when :dap_result dap_event ev_args # server.rb wait_command_loop tc when :cdp_result cdp_event ev_args wait_command_loop tc end end
process_info()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1705 def process_info if @process_group.multi? "#{$0}\##{Process.pid}" end end
process_protocol_request(req)
click to toggle source
# File debug-1.4.0/lib/debug/server_cdp.rb, line 521 def process_protocol_request req case req['method'] when 'Debugger.stepOver', 'Debugger.stepInto', 'Debugger.stepOut', 'Debugger.resume', 'Debugger.getScriptSource' @tc << [:cdp, :backtrace, req] when 'Debugger.evaluateOnCallFrame' frame_id = req.dig('params', 'callFrameId') if fid = @frame_map[frame_id] expr = req.dig('params', 'expression') @tc << [:cdp, :evaluate, req, fid, expr] else fail_response req, code: INVALID_PARAMS, message: "'callFrameId' is an invalid" end when 'Runtime.getProperties' oid = req.dig('params', 'objectId') if ref = @obj_map[oid] case ref[0] when 'local' frame_id = ref[1] fid = @frame_map[frame_id] @tc << [:cdp, :scope, req, fid] when 'properties' @tc << [:cdp, :properties, req, oid] when 'script', 'global' # TODO: Support script and global types @ui.respond req return :retry else raise "Unknown type: #{ref.inspect}" end else fail_response req, code: INVALID_PARAMS, message: "'objectId' is an invalid" end end end
prompt()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 345 def prompt if @postmortem '(rdbg:postmortem) ' elsif @process_group.multi? "(rdbg@#{process_info}) " else '(rdbg) ' end end
register_var(v, tid)
click to toggle source
# File debug-1.4.0/lib/debug/server_dap.rb, line 571 def register_var v, tid if (tl_vid = v[:variablesReference]) > 0 vid = @var_map.size + 1 @var_map[vid] = [:variable, tid, tl_vid] v[:variablesReference] = vid end end
register_vars(vars, tid)
click to toggle source
# File debug-1.4.0/lib/debug/server_dap.rb, line 579 def register_vars vars, tid raise tid.inspect unless tid.kind_of?(Integer) vars.each{|v| register_var v, tid } end
rehash_bps()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1218 def rehash_bps bps = @bps.values @bps.clear bps.each{|bp| add_bp bp } end
repl_add_breakpoint(arg)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1278 def repl_add_breakpoint arg expr = parse_break arg.strip cond = expr[:if] cmd = ['break', expr[:pre], expr[:do]] if expr[:pre] || expr[:do] path = Regexp.compile(expr[:path]) if expr[:path] case expr[:sig] when /\A(\d+)\z/ add_line_breakpoint @tc.location.path, $1.to_i, cond: cond, command: cmd when /\A(.+)[:\s+](\d+)\z/ add_line_breakpoint $1, $2.to_i, cond: cond, command: cmd when /\A(.+)([\.\#])(.+)\z/ @tc << [:breakpoint, :method, $1, $2, $3, cond, cmd, path] return :noretry when nil add_check_breakpoint cond, path else @ui.puts "Unknown breakpoint format: #{arg}" @ui.puts show_help 'b' end end
repl_add_catch_breakpoint(arg)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1301 def repl_add_catch_breakpoint arg expr = parse_break arg.strip cond = expr[:if] cmd = ['catch', expr[:pre], expr[:do]] if expr[:pre] || expr[:do] path = Regexp.compile(expr[:path]) if expr[:path] bp = CatchBreakpoint.new(expr[:sig], cond: cond, command: cmd, path: path) add_bp bp end
repl_add_watch_breakpoint(arg)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1311 def repl_add_watch_breakpoint arg expr = parse_break arg.strip cond = expr[:if] cmd = ['watch', expr[:pre], expr[:do]] if expr[:pre] || expr[:do] path = Regexp.compile(expr[:path]) if expr[:path] @tc << [:breakpoint, :watch, expr[:sig], cond, cmd, path] end
repl_open_setup()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1044 def repl_open_setup @tp_thread_begin.disable @ui.activate self if @ui.respond_to?(:reader_thread) && thc = get_thread_client(@ui.reader_thread) thc.mark_as_management end @tp_thread_begin.enable end
repl_open_tcp(host, port, **kw)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1053 def repl_open_tcp host, port, **kw DEBUGGER__.open_tcp host: host, port: port, nonstop: true, **kw repl_open_setup end
repl_open_unix()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1058 def repl_open_unix DEBUGGER__.open_unix nonstop: true repl_open_setup end
repl_open_vscode()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1063 def repl_open_vscode CONFIG[:open_frontend] = 'vscode' repl_open_unix end
reset_ui(ui)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 187 def reset_ui ui @ui.deactivate @ui = ui end
resolve_path(file)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1546 def resolve_path file File.realpath(File.expand_path(file)) rescue Errno::ENOENT case file when '-e', '-' return file else $LOAD_PATH.each do |lp| libpath = File.join(lp, file) return File.realpath(libpath) rescue Errno::ENOENT # next end end raise end
save_int_trap(cmd)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1677 def save_int_trap cmd prev, @intercepted_sigint_cmd = @intercepted_sigint_cmd, cmd prev end
session_server_main()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 196 def session_server_main while evt = pop_event process_event evt end ensure deactivate end
setup_threads()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1410 def setup_threads prev_clients = @th_clients @th_clients = {} Thread.list.each{|th| if tc = prev_clients[th] @th_clients[th] = tc else create_thread_client(th) end } end
show_bps(specific_bp = nil)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1203 def show_bps specific_bp = nil iterate_bps do |key, bp, i| @ui.puts "#%d %s" % [i, bp.to_s] if !specific_bp || bp == specific_bp end end
show_help(arg)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1162 def show_help arg DEBUGGER__.helps.each{|cat, cs| cs.each{|ws, desc| if ws.include? arg @ui.puts desc return end } } @ui.puts "not found: #{arg}" end
source(iseq)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 317 def source iseq if !CONFIG[:no_color] @sr.get_colored(iseq) else @sr.get(iseq) end end
step_command(type, arg)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1068 def step_command type, arg case arg when nil, /\A\d+\z/ if type == :in && @tc.recorder&.replaying? @tc << [:step, type, arg&.to_i] else leave_subsession [:step, type, arg&.to_i] end when /\Aback\z/, /\Areset\z/ if type != :in @ui.puts "only `step #{arg}` is supported." :retry else @tc << [:step, arg.to_sym] end else @ui.puts "Unknown option: #{arg}" :retry end end
switch_thread(n)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1397 def switch_thread n thcs, _unmanaged_ths = update_thread_list if tc = thcs[n] if tc.waiting? @tc = tc else @ui.puts "#{tc.thread} is not controllable yet." end end thread_list end
thread_list()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1378 def thread_list thcs, unmanaged_ths = update_thread_list thcs.each_with_index{|thc, i| @ui.puts "#{@tc == thc ? "--> " : " "}\##{i} #{thc}" } if !unmanaged_ths.empty? @ui.puts "The following threads are not managed yet by the debugger:" unmanaged_ths.each{|th| @ui.puts " " + th.to_s } end end
update_thread_list()
click to toggle source
threads
# File debug-1.4.0/lib/debug/session.rb, line 1360 def update_thread_list list = Thread.list thcs = [] unmanaged = [] list.each{|th| if thc = @th_clients[th] if !thc.management? thcs << thc end else unmanaged << th end } return thcs.sort_by{|thc| thc.id}, unmanaged end
wait_command()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 355 def wait_command if @preset_command if @preset_command.commands.empty? if @preset_command.auto_continue @preset_command = nil leave_subsession :continue return else @preset_command = nil return :retry end else line = @preset_command.commands.shift @ui.puts "(rdbg:#{@preset_command.source}) #{line}" end else @ui.puts "INTERNAL_INFO: #{JSON.generate(@internal_info)}" if ENV['RUBY_DEBUG_TEST_MODE'] line = @ui.readline prompt end case line when String process_command line when Hash process_protocol_request line # defined in server.rb else raise "unexpected input: #{line.inspect}" end end
wait_command_loop(tc)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 329 def wait_command_loop tc @tc = tc loop do case wait_command when :retry # nothing else break end rescue Interrupt @ui.puts "\n^C" retry end end
width()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1588 def width @ui.width end
Private Instance Methods
ask_thread_client(th)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1437 def ask_thread_client th # TODO: Ractor support q2 = Queue.new # tc, output, ev, @internal_info, *ev_args = evt @q_evt << [nil, [], :thread_begin, nil, th, q2] q2.pop @th_clients[th] or raise "unexpected error" end
create_thread_client(th)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1431 def create_thread_client th # TODO: Ractor support raise "Only session_server can create thread_client" unless Thread.current == @session_server @th_clients[th] = ThreadClient.new((@tc_id += 1), @q_evt, Queue.new, th) end
enter_subsession()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1505 def enter_subsession raise "already in subsession" if @subsession @subsession = true stop_all_threads @process_group.lock DEBUGGER__.info "enter_subsession" end
leave_subsession(type)
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1513 def leave_subsession type DEBUGGER__.info "leave_subsession" @process_group.unlock restart_all_threads @tc << type if type @tc = nil @subsession = false rescue Exception => e STDERR.puts [e, e.backtrace].inspect raise end
restart_all_threads()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1495 def restart_all_threads stopper = @thread_stopper stopper.disable if stopper.enabled? waiting_thread_clients.each{|tc| next if @tc == tc tc << :continue } end
running_thread_clients_count()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1460 def running_thread_clients_count @th_clients.count{|th, tc| next if tc.management? next unless tc.running? true } end
stop_all_threads()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1488 def stop_all_threads return if running_thread_clients_count == 0 stopper = @thread_stopper stopper.enable unless stopper.enabled? end
thread_stopper()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1476 def thread_stopper TracePoint.new(:line) do # run on each thread tc = ThreadClient.current next if tc.management? next unless tc.running? next if tc == @tc tc.on_pause end end
waiting_thread_clients()
click to toggle source
# File debug-1.4.0/lib/debug/session.rb, line 1468 def waiting_thread_clients @th_clients.map{|th, tc| next if tc.management? next unless tc.waiting? tc }.compact end