# File debug-1.4.0/lib/debug/session.rb, line 84 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
# File debug-1.4.0/lib/debug/session.rb, line 136 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
# File debug-1.4.0/lib/debug/session.rb, line 128 def active? !@q_evt.closed? end
# File debug-1.4.0/lib/debug/session.rb, line 1231 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
# File debug-1.4.0/lib/debug/session.rb, line 1319 def add_catch_breakpoint pat bp = CatchBreakpoint.new(pat) add_bp bp end
# File debug-1.4.0/lib/debug/session.rb, line 1324 def add_check_breakpoint expr, path bp = CheckBreakpoint.new(expr, path) add_bp bp end
# File debug-1.4.0/lib/debug/session.rb, line 1338 def add_iseq_breakpoint iseq, **kw bp = ISeqBreakpoint.new(iseq, [:line], **kw) add_bp bp end
# File debug-1.4.0/lib/debug/session.rb, line 1329 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
# File debug-1.4.0/lib/debug/session.rb, line 299 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
tracers
# File debug-1.4.0/lib/debug/session.rb, line 1345 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
# File debug-1.4.0/lib/debug/session.rb, line 1173 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
# File debug-1.4.0/lib/debug/session.rb, line 1208 def bp_index specific_bp_key iterate_bps do |key, bp, i| if key == specific_bp_key return [bp, i] end end nil end
# File debug-1.4.0/lib/debug/session.rb, line 132 def break_at? file, line @bps.has_key? [file, line] end
# File debug-1.4.0/lib/debug/session.rb, line 1155 def cancel_auto_continue if @preset_command&.auto_continue @preset_command.auto_continue = false end end
# File debug-1.4.0/lib/debug/server_cdp.rb, line 559 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
# File debug-1.4.0/lib/debug/session.rb, line 1225 def clean_bps @bps.delete_if{|_k, bp| bp.deleted? } end
# File debug-1.4.0/lib/debug/session.rb, line 1121 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
# File debug-1.4.0/lib/debug/session.rb, line 1105 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
# File debug-1.4.0/lib/debug/session.rb, line 1088 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
# File debug-1.4.0/lib/debug/server_dap.rb, line 523 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
# File debug-1.4.0/lib/debug/session.rb, line 173 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
# File debug-1.4.0/lib/debug/session.rb, line 1245 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
# File debug-1.4.0/lib/debug/server_cdp.rb, line 513 def fail_response req, **result @ui.respond_fail req, result return :retry end
# File debug-1.4.0/lib/debug/server_dap.rb, line 396 def find_waiting_tc id @th_clients.each{|th, tc| return tc if tc.id == id && tc.waiting? } return nil end
# File debug-1.4.0/lib/debug/session.rb, line 324 def inspect "DEBUGGER__::SESSION" end
breakpoint management
# File debug-1.4.0/lib/debug/session.rb, line 1187 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
# File debug-1.4.0/lib/debug/session.rb, line 1391 def managed_thread_clients thcs, _unmanaged_ths = update_thread_list thcs end
# File debug-1.4.0/lib/debug/session.rb, line 1422 def on_thread_begin th if @th_clients.has_key? th # TODO: NG? else create_thread_client th end end
# File debug-1.4.0/lib/debug/session.rb, line 1263 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
# File debug-1.4.0/lib/debug/session.rb, line 191 def pop_event @q_evt.pop end
# File debug-1.4.0/lib/debug/session.rb, line 385 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] @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 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
# File debug-1.4.0/lib/debug/session.rb, line 203 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
# File debug-1.4.0/lib/debug/server_cdp.rb, line 520 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
# File debug-1.4.0/lib/debug/session.rb, line 344 def prompt if @postmortem '(rdbg:postmortem) ' elsif @process_group.multi? "(rdbg@#{process_info}) " else '(rdbg) ' end end
# File debug-1.4.0/lib/debug/server_dap.rb, line 570 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
# File debug-1.4.0/lib/debug/server_dap.rb, line 578 def register_vars vars, tid raise tid.inspect unless tid.kind_of?(Integer) vars.each{|v| register_var v, tid } end
# File debug-1.4.0/lib/debug/session.rb, line 1217 def rehash_bps bps = @bps.values @bps.clear bps.each{|bp| add_bp bp } end
# File debug-1.4.0/lib/debug/session.rb, line 1277 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
# File debug-1.4.0/lib/debug/session.rb, line 1300 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
# File debug-1.4.0/lib/debug/session.rb, line 1310 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
# File debug-1.4.0/lib/debug/session.rb, line 1043 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
# File debug-1.4.0/lib/debug/session.rb, line 1052 def repl_open_tcp host, port, **kw DEBUGGER__.open_tcp host: host, port: port, nonstop: true, **kw repl_open_setup end
# File debug-1.4.0/lib/debug/session.rb, line 1057 def repl_open_unix DEBUGGER__.open_unix nonstop: true repl_open_setup end
# File debug-1.4.0/lib/debug/session.rb, line 1062 def repl_open_vscode CONFIG[:open_frontend] = 'vscode' repl_open_unix end
# File debug-1.4.0/lib/debug/session.rb, line 186 def reset_ui ui @ui.deactivate @ui = ui end
# File debug-1.4.0/lib/debug/session.rb, line 195 def session_server_main while evt = pop_event process_event evt end ensure deactivate end
# File debug-1.4.0/lib/debug/session.rb, line 1409 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
# File debug-1.4.0/lib/debug/session.rb, line 1202 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
# File debug-1.4.0/lib/debug/session.rb, line 1161 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
# File debug-1.4.0/lib/debug/session.rb, line 316 def source iseq if !CONFIG[:no_color] @sr.get_colored(iseq) else @sr.get(iseq) end end
# File debug-1.4.0/lib/debug/session.rb, line 1067 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
# File debug-1.4.0/lib/debug/session.rb, line 1396 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
# File debug-1.4.0/lib/debug/session.rb, line 1377 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
threads
# File debug-1.4.0/lib/debug/session.rb, line 1359 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
# File debug-1.4.0/lib/debug/session.rb, line 354 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