class DEBUGGER__::ThreadClient
Constants
- MAX_LENGTH
- SPECIAL_LOCAL_VARS
- SUPPORT_TARGET_THREAD
Attributes
check_bp_fulfillment_map[R]
id[R]
recorder[R]
thread[R]
Public Class Methods
current()
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 54 def self.current Thread.current.debug_thread_client ||= SESSION.get_thread_client end
new(id, q_evt, q_cmd, thr = Thread.current)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 111 def initialize id, q_evt, q_cmd, thr = Thread.current @is_management = false @id = id @thread = thr @target_frames = nil @q_evt = q_evt @q_cmd = q_cmd @step_tp = nil @output = [] @frame_formatter = method(:default_frame_formatter) @var_map = {} # { thread_local_var_id => obj } for DAP @obj_map = {} # { object_id => obj } for CDP @recorder = nil @mode = :waiting @current_frame_index = 0 # every thread should maintain its own CheckBreakpoint fulfillment state @check_bp_fulfillment_map = {} # { check_bp => boolean } set_mode :running thr.instance_variable_set(:@__thread_client_id, id) ::DEBUGGER__.info("Thread \##{@id} is created.") end
Public Instance Methods
<<(req)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 210 def << req debug_cmd(req) @q_cmd << req end
activate_irb_integration()
click to toggle source
# File debug-1.9.1/lib/debug/irb_integration.rb, line 18 def activate_irb_integration IRB.setup(location, argv: []) workspace = IRB::WorkSpace.new(current_frame&.binding || TOPLEVEL_BINDING) irb = IRB::Irb.new(workspace) IRB.conf[:MAIN_CONTEXT] = irb.context IRB::Debug.setup(irb) IRB::Context.prepend(IrbPatch) end
assemble_arguments(args)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 68 def assemble_arguments(args) args.map do |arg| "#{colorize_cyan(arg[:name])}=#{arg[:value]}" end.join(", ") end
class_method_map(classes)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 796 def class_method_map(classes) dumped = Array.new classes.reject { |mod| mod >= Object }.map do |mod| methods = mod.public_instance_methods(false).select do |m| dumped.push(m) unless dumped.include?(m) end [mod, methods] end.reverse end
close()
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 178 def close @q_cmd.close end
collect_locals(frame)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 548 def collect_locals(frame) locals = [] if s = frame&.self locals << ["%self", s] end special_local_variables frame do |name, val| locals << [name, val] end if vars = frame&.local_variables vars.each{|var, val| locals << [var, val] } end locals end
constant_name?(name)
click to toggle source
TODO: support non-ASCII Constant name
# File debug-1.9.1/lib/debug/thread_client.rb, line 809 def constant_name? name case name when /\A::\b/ constant_name? $~.post_match when /\A[A-Z]\w*/ post = $~.post_match if post.empty? true else constant_name? post end else false end end
current_frame()
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 536 def current_frame get_frame(@current_frame_index) end
dap_eval(b, expr, _context, prompt: '(repl_eval)')
click to toggle source
# File debug-1.9.1/lib/debug/server_dap.rb, line 789 def dap_eval b, expr, _context, prompt: '(repl_eval)' begin tp_allow_reentry do b.eval(expr.to_s, prompt) end rescue Exception => e e end end
deactivate()
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 134 def deactivate @step_tp.disable if @step_tp end
debug_cmd(cmds)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 1274 def debug_cmd(cmds) DEBUGGER__.debug{ cmd, *args = *cmds args = args.map { |arg| DEBUGGER__.safe_inspect(arg) } "#{inspect} receives Cmd { type: #{cmd.inspect}, args: #{args} } from Session" } end
debug_event(ev, args)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 1261 def debug_event(ev, args) DEBUGGER__.debug{ args = args.map { |arg| DEBUGGER__.safe_inspect(arg) } "#{inspect} sends Event { type: #{ev.inspect}, args: #{args} } to Session" } end
debug_mode(old_mode, new_mode)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 1268 def debug_mode(old_mode, new_mode) DEBUGGER__.debug{ "#{inspect} changes mode (#{old_mode} -> #{new_mode})" } end
debug_suspend(event)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 1282 def debug_suspend(event) DEBUGGER__.debug{ "#{inspect} is suspended for #{event.inspect}" } end
default_frame_formatter(frame)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 74 def default_frame_formatter frame call_identifier_str = case frame.frame_type when :block level, block_loc = frame.block_identifier args = frame.parameters_info if !args.empty? args_str = " {|#{assemble_arguments(args)}|}" end "#{colorize_blue("block")}#{args_str} in #{colorize_blue(block_loc + level)}" when :method ci = frame.method_identifier args = frame.parameters_info if !args.empty? args_str = "(#{assemble_arguments(args)})" end "#{colorize_blue(ci)}#{args_str}" when :c colorize_blue(frame.c_identifier) when :other colorize_blue(frame.other_identifier) end location_str = colorize(frame.location_str, [:GREEN]) result = "#{call_identifier_str} at #{location_str}" if return_str = frame.return_str result += " #=> #{colorize_magenta(return_str)}" end result end
evaluate_result(r)
click to toggle source
# File debug-1.9.1/lib/debug/server_cdp.rb, line 1217 def evaluate_result r v = variable nil, r v[:value] end
event!(ev, *args)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 221 def event! ev, *args debug_event(ev, args) @q_evt << [self, @output, ev, generate_info, *args] @output = [] end
exceptionDetails(exc, text)
click to toggle source
# File debug-1.9.1/lib/debug/server_cdp.rb, line 1165 def exceptionDetails exc, text frames = [ { columnNumber: 0, functionName: 'eval', lineNumber: 0, url: '' } ] exc.backtrace_locations&.each do |loc| break if loc.path == __FILE__ path = loc.absolute_path || loc.path frames << { columnNumber: 0, functionName: loc.base_label, lineNumber: loc.lineno - 1, url: path } end { exceptionId: 1, text: text, lineNumber: 0, columnNumber: 0, exception: evaluate_result(exc), stackTrace: { callFrames: frames } } end
fiber_blocking() { || ... }
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 865 def fiber_blocking ::Fiber.blocking{yield} end
frame_eval(src, re_raise: false, binding_location: false) { |e| ... }
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 435 def frame_eval src, re_raise: false, binding_location: false @success_last_eval = false b = current_frame&.eval_binding || TOPLEVEL_BINDING special_local_variables current_frame do |name, var| b.local_variable_set(name, var) if /\%/ !~ name end result = frame_eval_core(src, b, binding_location: binding_location) @success_last_eval = true result rescue SystemExit raise rescue Exception => e return yield(e) if block_given? puts "eval error: #{e}" e.backtrace_locations&.each do |loc| break if loc.path == __FILE__ puts " #{loc}" end raise if re_raise end
frame_eval_core(src, b, binding_location: false)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 404 def frame_eval_core src, b, binding_location: false saved_target_frames = @target_frames saved_current_frame_index = @current_frame_index if b file, lineno = b.source_location tp_allow_reentry do if binding_location b.eval(src, file, lineno) else b.eval(src, "(rdbg)/#{file}") end end else frame_self = current_frame.self tp_allow_reentry do frame_self.instance_eval(src) end end ensure @target_frames = saved_target_frames @current_frame_index = saved_current_frame_index end
frame_str(i, frame: @target_frames[i])
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 752 def frame_str(i, frame: @target_frames[i]) cur_str = (@current_frame_index == i ? '=>' : ' ') prefix = "#{cur_str}##{i}" frame_string = @frame_formatter.call(frame) "#{prefix}\t#{frame_string}" end
generate_info()
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 215 def generate_info return unless current_frame { location: current_frame.location_str, line: current_frame.location.lineno } end
get_consts(expr = nil, only_self: false, &block)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 612 def get_consts expr = nil, only_self: false, &block if expr && !expr.empty? begin _self = frame_eval(expr, re_raise: true) rescue Exception # ignore else if M_KIND_OF_P.bind_call(_self, Module) iter_consts _self, &block return else puts "#{_self.inspect} (by #{expr}) is not a Module." end end elsif _self = current_frame&.self cs = {} if M_KIND_OF_P.bind_call(_self, Module) cs[_self] = :self else _self = M_CLASS.bind_call(_self) cs[_self] = :self unless only_self end unless only_self _self.ancestors.each{|c| break if c == Object; cs[c] = :ancestors} if b = current_frame&.binding b.eval('::Module.nesting').each{|c| cs[c] = :nesting unless cs.has_key? c} end end names = {} cs.each{|c, _| iter_consts c, names, &block } end end
get_frame(index)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 540 def get_frame(index) if @target_frames @target_frames[index] else nil end end
get_src(frame, max_lines:, start_line: nil, end_line: nil, dir: +1)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 463 def get_src(frame, max_lines:, start_line: nil, end_line: nil, dir: +1) if file_lines = frame.file_lines frame_line = frame.location.lineno - 1 if CONFIG[:no_lineno] lines = file_lines else lines = file_lines.map.with_index do |e, i| cur = i == frame_line ? '=>' : ' ' line = colorize_dim('%4d|' % (i+1)) "#{cur}#{line} #{e}" end end unless start_line if frame.show_line if dir > 0 start_line = frame.show_line else end_line = frame.show_line - max_lines start_line = [end_line - max_lines, 0].max end else start_line = [frame_line - max_lines/2, 0].max end end unless end_line end_line = [start_line + max_lines, lines.size].min end if start_line != end_line && max_lines [start_line, end_line, lines] end else # no file lines nil end rescue Exception => e p e pp e.backtrace exit! end
inspect()
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 182 def inspect if bt = @thread.backtrace "#<DBG:TC #{self.id}:#{@mode}@#{bt[-1]}>" else # bt can be nil "#<DBG:TC #{self.id}:#{@mode}>" end end
internalProperty(name, obj)
click to toggle source
# File debug-1.9.1/lib/debug/server_cdp.rb, line 1222 def internalProperty name, obj v = variable name, obj v.delete :configurable v.delete :enumerable v end
iter_consts(c, names = {}) { |name, value| ... }
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 599 def iter_consts c, names = {} c.constants(false).sort.each{|name| next if names.has_key? name names[name] = nil begin value = c.const_get(name) rescue Exception => e value = e end yield name, value } end
location()
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 64 def location current_frame&.location end
make_breakpoint(args)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 825 def make_breakpoint args case args.first when :method klass_name, op, method_name, cond, cmd, path = args[1..] bp = MethodBreakpoint.new(current_frame&.eval_binding || TOPLEVEL_BINDING, klass_name, op, method_name, cond: cond, command: cmd, path: path) begin bp.enable rescue NameError => e if bp.klass puts "Unknown method name: \"#{e.name}\"" else # klass_name can not be evaluated if constant_name? klass_name puts "Unknown constant name: \"#{e.name}\"" else # only Class name is allowed puts "Not a constant name: \"#{klass_name}\"" bp = nil end end Session.activate_method_added_trackers if bp rescue Exception => e puts e.inspect bp = nil end bp when :watch ivar, object, result, cond, command, path = args[1..] WatchIVarBreakpoint.new(ivar, object, result, cond: cond, command: command, path: path) else raise "unknown breakpoint: #{args}" end end
management?()
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 138 def management? @is_management end
mark_as_management()
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 142 def mark_as_management @is_management = true end
name()
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 174 def name "##{@id} #{@thread.name || @thread.backtrace.last}" end
on_breakpoint(tp, bp)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 250 def on_breakpoint tp, bp suspend tp.event, tp, bp: bp end
on_init(name)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 242 def on_init name wait_reply [:init, name] end
on_load(iseq, eval_src)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 238 def on_load iseq, eval_src wait_reply [:load, iseq, eval_src] end
on_pause()
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 262 def on_pause suspend :pause end
on_trace(trace_id, msg)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 246 def on_trace trace_id, msg wait_reply [:trace, trace_id, msg] end
on_trap(sig)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 254 def on_trap sig if waiting? # raise Interrupt else suspend :trap, sig: sig end end
outline_method(o, klass, obj)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 782 def outline_method(o, klass, obj) begin singleton_class = M_SINGLETON_CLASS.bind_call(obj) rescue TypeError singleton_class = nil end maps = class_method_map((singleton_class || klass).ancestors) maps.each do |mod, methods| name = mod == singleton_class ? "#{klass}.methods" : "#{mod}#methods" o.dump(name, methods) end end
preview(name, obj)
click to toggle source
# File debug-1.9.1/lib/debug/server_cdp.rb, line 1297 def preview name, obj case obj when Array pd = propertyDescriptor name, obj overflow = false if obj.size > 100 obj = obj[0..99] overflow = true end hash = obj.each_with_index.to_h{|o, i| [i.to_s, o]} preview_ pd[:value], hash, overflow when Hash pd = propertyDescriptor name, obj overflow = false if obj.size > 100 obj = obj.to_a[0..99].to_h overflow = true end preview_ pd[:value], obj, overflow else nil end end
preview_(value, hash, overflow)
click to toggle source
# File debug-1.9.1/lib/debug/server_cdp.rb, line 1254 def preview_ value, hash, overflow # The reason for not using "map" method is to prevent the object overriding it from causing bugs. # https://github.com/ruby/debug/issues/781 props = [] hash.each{|k, v| pd = propertyDescriptor k, v props << { name: pd[:name], type: pd[:value][:type], value: pd[:value][:description] } } { type: value[:type], subtype: value[:subtype], description: value[:description], overflow: overflow, properties: props } end
process_cdp(args)
click to toggle source
# File debug-1.9.1/lib/debug/server_cdp.rb, line 962 def process_cdp args type = args.shift req = args.shift case type when :backtrace exception = nil result = { reason: 'other', callFrames: @target_frames.map.with_index{|frame, i| exception = frame.raised_exception if frame == current_frame && frame.has_raised_exception path = frame.realpath || frame.path if frame.iseq.nil? lineno = 0 else lineno = frame.iseq.first_line - 1 end { callFrameId: SecureRandom.hex(16), functionName: frame.name, functionLocation: { # scriptId: N, # filled by SESSION lineNumber: lineno }, location: { # scriptId: N, # filled by SESSION lineNumber: frame.location.lineno - 1 # The line number is 0-based. }, url: path, scopeChain: [ { type: 'local', object: { type: 'object', objectId: rand.to_s } }, { type: 'script', object: { type: 'object', objectId: rand.to_s } }, { type: 'global', object: { type: 'object', objectId: rand.to_s } } ], this: { type: 'object' } } } } if exception result[:data] = evaluate_result exception result[:reason] = 'exception' end event! :protocol_result, :backtrace, req, result when :evaluate res = {} fid, expr, group = args frame = @target_frames[fid] message = nil if frame && (b = frame.eval_binding) special_local_variables frame do |name, var| b.local_variable_set(name, var) if /\%/ !~name end result = nil case group when 'popover' case expr # Chrome doesn't read instance variables when /\A\$\S/ safe_global_variables.each{|gvar| if gvar.to_s == expr result = eval(gvar.to_s) break false end } and (message = "Error: Not defined global variable: #{expr.inspect}") when /(\A((::[A-Z]|[A-Z])\w*)+)/ unless result = search_const(b, $1) message = "Error: Not defined constant: #{expr.inspect}" end else begin result = b.local_variable_get(expr) rescue NameError # try to check method if M_RESPOND_TO_P.bind_call(b.receiver, expr, include_all: true) result = M_METHOD.bind_call(b.receiver, expr) else message = "Error: Can not evaluate: #{expr.inspect}" end end end when 'console', 'watch-group' begin orig_stdout = $stdout $stdout = StringIO.new result = b.eval(expr.to_s, '(DEBUG CONSOLE)') rescue Exception => e result = e res[:exceptionDetails] = exceptionDetails(e, 'Uncaught') ensure output = $stdout.string $stdout = orig_stdout end else message = "Error: unknown objectGroup: #{group}" end else result = Exception.new("Error: Can not evaluate on this frame") end res[:result] = evaluate_result(result) event! :protocol_result, :evaluate, req, message: message, response: res, output: output when :scope fid = args.shift frame = @target_frames[fid] if b = frame.binding vars = b.local_variables.map{|name| v = b.local_variable_get(name) variable(name, v) } special_local_variables frame do |name, val| vars.unshift variable(name, val) end vars.unshift variable('%self', b.receiver) elsif lvars = frame.local_variables vars = lvars.map{|var, val| variable(var, val) } else vars = [variable('%self', frame.self)] special_local_variables frame do |name, val| vars.unshift variable(name, val) end end event! :protocol_result, :scope, req, vars when :properties oid = args.shift result = [] prop = [] if obj = @obj_map[oid] case obj when Array result = obj.map.with_index{|o, i| variable i.to_s, o } when Hash result = obj.map{|k, v| variable(k, v) } when Struct result = obj.members.map{|m| variable(m, obj[m]) } when String prop = [ internalProperty('#length', obj.length), internalProperty('#encoding', obj.encoding) ] when Class, Module result = obj.instance_variables.map{|iv| variable(iv, obj.instance_variable_get(iv)) } prop = [internalProperty('%ancestors', obj.ancestors[1..])] when Range prop = [ internalProperty('#begin', obj.begin), internalProperty('#end', obj.end), ] end result += M_INSTANCE_VARIABLES.bind_call(obj).map{|iv| variable(iv, M_INSTANCE_VARIABLE_GET.bind_call(obj, iv)) } prop += [internalProperty('#class', M_CLASS.bind_call(obj))] end event! :protocol_result, :properties, req, result: result, internalProperties: prop when :exception oid = args.shift exc = nil if obj = @obj_map[oid] exc = exceptionDetails obj, obj.to_s end event! :protocol_result, :exception, req, exceptionDetails: exc end end
process_dap(args)
click to toggle source
# File debug-1.9.1/lib/debug/server_dap.rb, line 799 def process_dap args # pp tc: self, args: args type = args.shift req = args.shift case type when :backtrace start_frame = req.dig('arguments', 'startFrame') || 0 levels = req.dig('arguments', 'levels') || 1_000 frames = [] @target_frames.each_with_index do |frame, i| next if i < start_frame path = frame.realpath || frame.path next if skip_path?(path) && !SESSION.stop_stepping?(path, frame.location.lineno) break if (levels -= 1) < 0 source_name = path ? File.basename(path) : frame.location.to_s if (path && File.exist?(path)) && (local_path = UI_DAP.remote_to_local_path(path)) # ok else ref = frame.file_lines end frames << { id: i, # id is refilled by SESSION name: frame.name, line: frame.location.lineno, column: 1, source: { name: source_name, path: (local_path || path), sourceReference: ref, }, } end event! :protocol_result, :backtrace, req, { stackFrames: frames, totalFrames: @target_frames.size, } when :scopes fid = args.shift frame = get_frame(fid) lnum = if frame.binding frame.binding.local_variables.size elsif vars = frame.local_variables vars.size else 0 end event! :protocol_result, :scopes, req, scopes: [{ name: 'Local variables', presentationHint: 'locals', # variablesReference: N, # filled by SESSION namedVariables: lnum, indexedVariables: 0, expensive: false, }, { name: 'Global variables', presentationHint: 'globals', variablesReference: 1, # GLOBAL namedVariables: safe_global_variables.size, indexedVariables: 0, expensive: false, }] when :scope fid = args.shift frame = get_frame(fid) vars = collect_locals(frame).map do |var, val| variable(var, val) end event! :protocol_result, :scope, req, variables: vars, tid: self.id when :variable vid = args.shift obj = @var_map[vid] if obj case req.dig('arguments', 'filter') when 'indexed' start = req.dig('arguments', 'start') || 0 count = req.dig('arguments', 'count') || obj.size vars = (start ... (start + count)).map{|i| variable(i.to_s, obj[i]) } else vars = [] case obj when Hash vars = obj.map{|k, v| variable(value_inspect(k), v,) } when Struct vars = obj.members.map{|m| variable(m, obj[m]) } when String vars = [ variable('#length', obj.length), variable('#encoding', obj.encoding), ] printed_str = value_inspect(obj) vars << variable('#dump', NaiveString.new(obj)) if printed_str.end_with?('...') when Class, Module vars << variable('%ancestors', obj.ancestors[1..]) when Range vars = [ variable('#begin', obj.begin), variable('#end', obj.end), ] end unless NaiveString === obj vars += M_INSTANCE_VARIABLES.bind_call(obj).sort.map{|iv| variable(iv, M_INSTANCE_VARIABLE_GET.bind_call(obj, iv)) } vars.unshift variable('#class', M_CLASS.bind_call(obj)) end end end event! :protocol_result, :variable, req, variables: (vars || []), tid: self.id when :evaluate fid, expr, context = args frame = get_frame(fid) message = nil if frame && (b = frame.eval_binding) special_local_variables frame do |name, var| b.local_variable_set(name, var) if /\%/ !~ name end case context when 'repl', 'watch' result = dap_eval b, expr, context, prompt: '(DEBUG CONSOLE)' when 'hover' case expr when /\A\@\S/ begin result = M_INSTANCE_VARIABLE_GET.bind_call(b.receiver, expr) rescue NameError message = "Error: Not defined instance variable: #{expr.inspect}" end when /\A\$\S/ safe_global_variables.each{|gvar| if gvar.to_s == expr result = eval(gvar.to_s) break false end } and (message = "Error: Not defined global variable: #{expr.inspect}") when /\Aself$/ result = b.receiver when /(\A((::[A-Z]|[A-Z])\w*)+)/ unless result = search_const(b, $1) message = "Error: Not defined constants: #{expr.inspect}" end else begin result = b.local_variable_get(expr) rescue NameError # try to check method if M_RESPOND_TO_P.bind_call(b.receiver, expr, include_all: true) result = M_METHOD.bind_call(b.receiver, expr) else message = "Error: Can not evaluate: #{expr.inspect}" end end end else message = "Error: unknown context: #{context}" end else result = 'Error: Can not evaluate on this frame' end event! :protocol_result, :evaluate, req, message: message, tid: self.id, **evaluate_result(result) when :completions fid, text = args frame = get_frame(fid) if (b = frame&.binding) && word = text&.split(/[\s\{]/)&.last words = IRB::InputCompletor::retrieve_completion_data(word, bind: b).compact end event! :protocol_result, :completions, req, targets: (words || []).map{|phrase| detail = nil if /\b([_a-zA-Z]\w*[!\?]?)\z/ =~ phrase w = $1 else w = phrase end begin v = b.local_variable_get(w) detail ="(variable: #{value_inspect(v)})" rescue NameError end { label: phrase, text: w, detail: detail, } } else if respond_to? mid = "custom_dap_request_#{type}" __send__ mid, req else raise "Unknown request: #{args.inspect}" end end end
propertyDescriptor(name, obj)
click to toggle source
# File debug-1.9.1/lib/debug/server_cdp.rb, line 1321 def propertyDescriptor name, obj case obj when Array propertyDescriptor_ name, obj, 'object', subtype: 'array' when Hash propertyDescriptor_ name, obj, 'object', subtype: 'map' when String propertyDescriptor_ name, obj, 'string', description: obj when TrueClass, FalseClass propertyDescriptor_ name, obj, 'boolean' when Symbol propertyDescriptor_ name, obj, 'symbol' when Integer, Float propertyDescriptor_ name, obj, 'number' when Exception bt = '' if log = obj.backtrace_locations log.each do |loc| break if loc.path == __FILE__ bt += " #{loc}\n" end end propertyDescriptor_ name, obj, 'object', description: "#{obj.inspect}\n#{bt}", subtype: 'error' else propertyDescriptor_ name, obj, 'object' end end
propertyDescriptor_(name, obj, type, description: nil, subtype: nil)
click to toggle source
# File debug-1.9.1/lib/debug/server_cdp.rb, line 1229 def propertyDescriptor_ name, obj, type, description: nil, subtype: nil description = DEBUGGER__.safe_inspect(obj, short: true) if description.nil? oid = rand.to_s @obj_map[oid] = obj prop = { name: name, value: { type: type, description: description, value: obj, objectId: oid }, configurable: true, # TODO: Change these parts because enumerable: true # they are not necessarily `true`. } if type == 'object' v = prop[:value] v.delete :value v[:subtype] = subtype if subtype v[:className] = (klass = M_CLASS.bind_call(obj)).name || klass.to_s end prop end
puts(str = '')
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 196 def puts str = '' if @recorder&.replaying? prefix = colorize_dim("[replay] ") end case str when nil @output << "\n" when Array str.each{|s| puts s} else @output << "#{prefix}#{str.chomp}\n" end end
puts_variable_info(label, obj, pat)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 665 def puts_variable_info label, obj, pat return if pat && pat !~ label begin inspected = DEBUGGER__.safe_inspect(obj) rescue Exception => e inspected = e.inspect end mono_info = "#{label} = #{inspected}" w = SESSION::width if mono_info.length >= w maximum_value_width = w - "#{label} = ".length valstr = truncate(inspected, width: maximum_value_width) else valstr = colored_inspect(obj, width: 2 ** 30) valstr = inspected if valstr.lines.size > 1 end info = "#{colorize_cyan(label)} = #{valstr}" puts info end
replay_suspend()
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 323 def replay_suspend # @recorder.current_position suspend :replay, replay_frames: @recorder.current_frame end
running?()
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 166 def running? @mode == :running end
search_const(b, expr)
click to toggle source
# File debug-1.9.1/lib/debug/server_cdp.rb, line 1196 def search_const b, expr cs = expr.delete_prefix('::').split('::') [Object, *b.eval('::Module.nesting')].reverse_each{|mod| if cs.all?{|c| if mod.const_defined?(c) begin mod = mod.const_get(c) rescue Exception false end else false end } # if-body return mod end } false end
set_mode(mode)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 146 def set_mode mode debug_mode(@mode, mode) # STDERR.puts "#{@mode} => #{mode} @ #{caller.inspect}" # pp caller # mode transition check case mode when :running raise "#{mode} is given, but #{mode}" unless self.waiting? when :waiting # TODO: there is waiting -> waiting # raise "#{mode} is given, but #{mode}" unless self.running? else raise "unknown mode: #{mode}" end # DEBUGGER__.warn "#{@mode} => #{mode} @ #{self.inspect}" @mode = mode end
show_by_editor(path = nil)
click to toggle source
cmd: show edit
# File debug-1.9.1/lib/debug/thread_client.rb, line 700 def show_by_editor path = nil unless path if current_frame path = current_frame.path else return # can't get path end end if File.exist?(path) if editor = (ENV['RUBY_DEBUG_EDITOR'] || ENV['EDITOR']) puts "command: #{editor}" puts " path: #{path}" require 'shellwords' system(*Shellwords.split(editor), path) else puts "can not find editor setting: ENV['RUBY_DEBUG_EDITOR'] or ENV['EDITOR']" end else puts "Can not find file: #{path}" end end
show_consts(pat, expr = nil, only_self: false)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 650 def show_consts pat, expr = nil, only_self: false get_consts expr, only_self: only_self do |name, value| puts_variable_info name, value, pat end end
show_frame(i=0)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 748 def show_frame i=0 puts frame_str(i) end
show_frames(max = nil, pattern = nil)
click to toggle source
cmd: show frames
# File debug-1.9.1/lib/debug/thread_client.rb, line 725 def show_frames max = nil, pattern = nil if @target_frames && (max ||= @target_frames.size) > 0 frames = [] @target_frames.each_with_index{|f, i| # we need to use FrameInfo#matchable_location because #location_str is for display # and it may change based on configs (e.g. use_short_path) next if pattern && !(f.name.match?(pattern) || f.matchable_location.match?(pattern)) # avoid using skip_path? because we still want to display internal frames next if skip_config_skip_path?(f.matchable_location) frames << [i, f] } size = frames.size max.times{|i| break unless frames[i] index, frame = frames[i] puts frame_str(index, frame: frame) } puts " # and #{size - max} frames (use `bt' command for all frames)" if max < size end end
show_globals(pat)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 656 def show_globals pat safe_global_variables.sort.each{|name| next if SKIP_GLOBAL_LIST.include? name value = eval(name.to_s) puts_variable_info name, value, pat } end
show_ivars(pat, expr = nil)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 583 def show_ivars pat, expr = nil if expr && !expr.empty? _self = frame_eval(expr); elsif _self = current_frame&.self else _self = nil end if _self M_INSTANCE_VARIABLES.bind_call(_self).sort.each{|iv| value = M_INSTANCE_VARIABLE_GET.bind_call(_self, iv) puts_variable_info iv, value, pat } end end
show_locals(pat)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 577 def show_locals pat collect_locals(current_frame).each do |var, val| puts_variable_info(var, val, pat) end end
show_outline(expr)
click to toggle source
cmd: show outline
# File debug-1.9.1/lib/debug/thread_client.rb, line 761 def show_outline expr begin obj = frame_eval(expr, re_raise: true) rescue Exception # ignore else o = Output.new(@output) locals = current_frame&.local_variables klass = M_CLASS.bind_call(obj) klass = obj if Class == klass || Module == klass o.dump("constants", obj.constants) if M_RESPOND_TO_P.bind_call(obj, :constants) outline_method(o, klass, obj) o.dump("instance variables", M_INSTANCE_VARIABLES.bind_call(obj)) o.dump("class variables", klass.class_variables) o.dump("locals", locals.keys) if locals end end
show_src(frame_index: @current_frame_index, update_line: false, ignore_show_line: false, max_lines: CONFIG[:show_src_lines], **options)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 510 def show_src(frame_index: @current_frame_index, update_line: false, ignore_show_line: false, max_lines: CONFIG[:show_src_lines], **options) if frame = get_frame(frame_index) begin if ignore_show_line prev_show_line = frame.show_line frame.show_line = nil end start_line, end_line, lines = *get_src(frame, max_lines: max_lines, **options) if start_line if update_line frame.show_line = end_line end puts "[#{start_line+1}, #{end_line}] in #{frame.pretty_path}" if !update_line && max_lines != 1 puts lines[start_line...end_line] else puts "# No sourcefile available for #{frame.path}" end ensure frame.show_line = prev_show_line if prev_show_line end end end
special_local_variables(frame) { |name, send| ... }
click to toggle source
cmd: show
# File debug-1.9.1/lib/debug/thread_client.rb, line 569 def special_local_variables frame SPECIAL_LOCAL_VARS.each do |mid, name| next unless frame&.send("has_#{mid}") name = name.sub('_', '%') if frame.eval_binding.local_variable_defined?(name) yield name, frame.send(mid) end end
step_tp(iter, events = [:line, :b_return, :return]) { |tp| ... }
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 337 def step_tp iter, events = [:line, :b_return, :return] @step_tp.disable if @step_tp thread = Thread.current subsession_id = SESSION.subsession_id if SUPPORT_TARGET_THREAD @step_tp = TracePoint.new(*events){|tp| if SESSION.stop_stepping? tp.path, tp.lineno, subsession_id tp.disable next end next if !yield(tp) next if tp.path.start_with?(__dir__) next if tp.path.start_with?('<internal:trace_point>') next unless File.exist?(tp.path) if CONFIG[:skip_nosrc] loc = caller_locations(1, 1).first next if skip_location?(loc) next if iter && (iter -= 1) > 0 tp.disable suspend tp.event, tp } @step_tp.enable(target_thread: thread) else @step_tp = TracePoint.new(*events){|tp| next if thread != Thread.current if SESSION.stop_stepping? tp.path, tp.lineno, subsession_id tp.disable next end next if !yield(tp) next if tp.path.start_with?(__dir__) next if tp.path.start_with?('<internal:trace_point>') next unless File.exist?(tp.path) if CONFIG[:skip_nosrc] loc = caller_locations(1, 1).first next if skip_location?(loc) next if iter && (iter -= 1) > 0 tp.disable suspend tp.event, tp } @step_tp.enable end end
suspend(event, tp = nil, bp: nil, sig: nil, postmortem_frames: nil, replay_frames: nil, postmortem_exc: nil)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 266 def suspend event, tp = nil, bp: nil, sig: nil, postmortem_frames: nil, replay_frames: nil, postmortem_exc: nil return if management? debug_suspend(event) @current_frame_index = 0 case when postmortem_frames @target_frames = postmortem_frames @postmortem = true when replay_frames @target_frames = replay_frames else @target_frames = DEBUGGER__.capture_frames(__dir__) end cf = @target_frames.first if cf case event when :return, :b_return, :c_return cf.has_return_value = true cf.return_value = tp.return_value end if CatchBreakpoint === bp cf.has_raised_exception = true cf.raised_exception = bp.last_exc end if postmortem_exc cf.has_raised_exception = true cf.raised_exception = postmortem_exc end end if event != :pause unless bp&.skip_src show_src show_frames CONFIG[:show_frames] end set_mode :waiting if bp event! :suspend, :breakpoint, bp.key elsif sig event! :suspend, :trap, sig else event! :suspend, event end else set_mode :waiting end wait_next_action end
to_s()
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 190 def to_s str = "(#{@thread.name || @thread.status})@#{current_frame&.location || @thread.to_s}" str += " (not under control)" unless self.waiting? str end
tp_allow_reentry() { || ... }
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 386 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
truncate(string, width:)
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 690 def truncate(string, width:) if string.start_with?("#<") string[0 .. (width-5)] + '...>' else string[0 .. (width-4)] + '...' end end
type_name(obj)
click to toggle source
# File debug-1.9.1/lib/debug/server_dap.rb, line 1044 def type_name obj klass = M_CLASS.bind_call(obj) begin M_NAME.bind_call(klass) || klass.to_s rescue Exception => e "<Error: #{e.message} (#{e.backtrace.first}>" end end
value_inspect(obj, short: true)
click to toggle source
# File debug-1.9.1/lib/debug/server_dap.rb, line 778 def value_inspect obj, short: true # TODO: max length should be configuarable? str = DEBUGGER__.safe_inspect obj, short: short, max_length: MAX_LENGTH if str.encoding == Encoding::UTF_8 str.scrub else str.encode(Encoding::UTF_8, invalid: :replace, undef: :replace) end end
variable(name, obj)
click to toggle source
# File debug-1.9.1/lib/debug/server_cdp.rb, line 1275 def variable name, obj pd = propertyDescriptor name, obj case obj when Array pd[:value][:preview] = preview name, obj obj.each_with_index{|item, idx| if valuePreview = preview(idx.to_s, item) pd[:value][:preview][:properties][idx][:valuePreview] = valuePreview end } when Hash pd[:value][:preview] = preview name, obj obj.each_with_index{|item, idx| key, val = item if valuePreview = preview(key, val) pd[:value][:preview][:properties][idx][:valuePreview] = valuePreview end } end pd end
variable_(name, obj, indexedVariables: 0, namedVariables: 0)
click to toggle source
# File debug-1.9.1/lib/debug/server_dap.rb, line 1054 def variable_ name, obj, indexedVariables: 0, namedVariables: 0 if indexedVariables > 0 || namedVariables > 0 vid = @var_map.size + 1 @var_map[vid] = obj else vid = 0 end namedVariables += M_INSTANCE_VARIABLES.bind_call(obj).size if NaiveString === obj str = obj.str.dump vid = indexedVariables = namedVariables = 0 else str = value_inspect(obj) end if name { name: name, value: str, type: type_name(obj), variablesReference: vid, indexedVariables: indexedVariables, namedVariables: namedVariables, } else { result: str, type: type_name(obj), variablesReference: vid, indexedVariables: indexedVariables, namedVariables: namedVariables, } end end
wait_next_action()
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 874 def wait_next_action fiber_blocking{wait_next_action_} rescue SuspendReplay replay_suspend end
wait_next_action_()
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 880 def wait_next_action_ # assertions raise "@mode is #{@mode}" if !waiting? unless SESSION.active? pp caller set_mode :running return end while true begin set_mode :waiting if !waiting? cmds = @q_cmd.pop # pp [self, cmds: cmds] break unless cmds ensure set_mode :running end cmd, *args = *cmds case cmd when :continue break when :step step_type = args[0] iter = args[1] case step_type when :in iter = iter || 1 if @recorder&.replaying? @recorder.step_forward iter raise SuspendReplay else step_tp iter do true end break end when :next frame = @target_frames.first path = frame.location.absolute_path || "!eval:#{frame.path}" line = frame.location.lineno label = frame.location.base_label if frame.iseq frame.iseq.traceable_lines_norec(lines = {}) next_line = lines.keys.bsearch{|e| e > line} if !next_line && (last_line = frame.iseq.last_line) > line next_line = last_line end end depth = @target_frames.first.frame_depth step_tp iter do |tp| loc = caller_locations(2, 1).first loc_path = loc.absolute_path || "!eval:#{loc.path}" loc_label = loc.base_label loc_depth = DEBUGGER__.frame_depth - 3 case when loc_depth == depth && loc_label == label true when loc_depth < depth # lower stack depth true when (next_line && loc_path == path && (loc_lineno = loc.lineno) > line && loc_lineno <= next_line) # different frame (maybe block) but the line is before next_line true end end break when :finish finish_frames = (iter || 1) - 1 frame = @target_frames.first goal_depth = frame.frame_depth - finish_frames - (frame.has_return_value ? 1 : 0) step_tp nil, [:return, :b_return] do DEBUGGER__.frame_depth - 3 <= goal_depth ? true : false end break when :until location = iter&.strip frame = @target_frames.first depth = frame.frame_depth - (frame.has_return_value ? 1 : 0) target_location_label = frame.location.base_label case location when nil, /\A(?:(.+):)?(\d+)\z/ no_loc = !location file = $1 || frame.location.path line = ($2 || frame.location.lineno + 1).to_i step_tp nil, [:line, :return] do |tp| if tp.event == :line next false if no_loc && depth < DEBUGGER__.frame_depth - 3 next false unless tp.path.end_with?(file) next false unless tp.lineno >= line true else true if depth >= DEBUGGER__.frame_depth - 3 && caller_locations(2, 1).first.label == target_location_label # TODO: imcomplete condition end end else pat = location if /\A\/(.+)\/\z/ =~ pat pat = Regexp.new($1) end step_tp nil, [:call, :c_call, :return] do |tp| case tp.event when :call, :c_call true if pat === tp.callee_id.to_s else # :return, :b_return true if depth >= DEBUGGER__.frame_depth - 3 && caller_locations(2, 1).first.label == target_location_label # TODO: imcomplete condition end end end break when :back iter = iter || 1 if @recorder&.can_step_back? unless @recorder.backup_frames @recorder.backup_frames = @target_frames end @recorder.step_back iter raise SuspendReplay else puts "Can not step back more." event! :result, nil end when :reset if @recorder&.replaying? @recorder.step_reset raise SuspendReplay end else raise "unknown: #{type}" end when :eval eval_type, eval_src = *args result_type = nil case eval_type when :p result = frame_eval(eval_src) puts "=> " + color_pp(result, 2 ** 30) if alloc_path = ObjectSpace.allocation_sourcefile(result) puts "allocated at #{alloc_path}:#{ObjectSpace.allocation_sourceline(result)}" end when :pp result = frame_eval(eval_src) puts color_pp(result, SESSION.width) if alloc_path = ObjectSpace.allocation_sourcefile(result) puts "allocated at #{alloc_path}:#{ObjectSpace.allocation_sourceline(result)}" end when :call result = frame_eval(eval_src) when :irb require_relative "irb_integration" activate_irb_integration when :display, :try_display failed_results = [] eval_src.each_with_index{|src, i| result = frame_eval(src){|e| failed_results << [i, e.message] "<error: #{e.message}>" } puts "#{i}: #{src} = #{result}" } result_type = eval_type result = failed_results else raise "unknown error option: #{args.inspect}" end event! :result, result_type, result when :frame type, arg = *args case type when :up if @current_frame_index + 1 < @target_frames.size @current_frame_index += 1 show_src max_lines: 1 show_frame(@current_frame_index) end when :down if @current_frame_index > 0 @current_frame_index -= 1 show_src max_lines: 1 show_frame(@current_frame_index) end when :set if arg index = arg.to_i if index >= 0 && index < @target_frames.size @current_frame_index = index else puts "out of frame index: #{index}" end end show_src max_lines: 1 show_frame(@current_frame_index) else raise "unsupported frame operation: #{arg.inspect}" end event! :result, nil when :show type = args.shift case type when :backtrace max_lines, pattern = *args show_frames max_lines, pattern when :list show_src(update_line: true, **(args.first || {})) when :whereami show_src ignore_show_line: true show_frames CONFIG[:show_frames] when :edit show_by_editor(args.first) when :default pat = args.shift show_locals pat show_ivars pat show_consts pat, only_self: true when :locals pat = args.shift show_locals pat when :ivars pat = args.shift expr = args.shift show_ivars pat, expr when :consts pat = args.shift expr = args.shift show_consts pat, expr when :globals pat = args.shift show_globals pat when :outline show_outline args.first || 'self' else raise "unknown show param: " + [type, *args].inspect end event! :result, nil when :breakpoint case args[0] when :method bp = make_breakpoint args event! :result, :method_breakpoint, bp when :watch ivar, cond, command, path = args[1..] result = frame_eval(ivar) if @success_last_eval object = if b = current_frame.binding b.receiver else current_frame.self end bp = make_breakpoint [:watch, ivar, object, result, cond, command, path] event! :result, :watch_breakpoint, bp else event! :result, nil end end when :trace case args.shift when :object begin obj = frame_eval args.shift, re_raise: true opt = args.shift obj_inspect = DEBUGGER__.safe_inspect(obj) width = 50 if obj_inspect.length >= width obj_inspect = truncate(obj_inspect, width: width) end event! :result, :trace_pass, M_OBJECT_ID.bind_call(obj), obj_inspect, opt rescue => e puts e.message event! :result, nil end else raise "unreachable" end when :record case args[0] when nil # ok when :on # enable recording if !@recorder @recorder = Recorder.new end @recorder.enable when :off if @recorder&.enabled? @recorder.disable end else raise "unknown: #{args.inspect}" end if @recorder&.enabled? puts "Recorder for #{Thread.current}: on (#{@recorder.log.size} records)" else puts "Recorder for #{Thread.current}: off" end event! :result, nil when :quit sleep # wait for SystemExit when :dap process_dap args when :cdp process_cdp args else raise [cmd, *args].inspect end end rescue SuspendReplay, SystemExit, Interrupt raise rescue Exception => e STDERR.puts e.cause.inspect STDERR.puts e.inspect Thread.list.each{|th| STDERR.puts "@@@ #{th}" th.backtrace.each{|b| STDERR.puts " > #{b}" } } p ["DEBUGGER Exception: #{__FILE__}:#{__LINE__}", e, e.backtrace] raise ensure @returning = false end
wait_reply(event_arg)
click to toggle source
events
# File debug-1.9.1/lib/debug/thread_client.rb, line 229 def wait_reply event_arg return if management? set_mode :waiting event!(*event_arg) wait_next_action end
waiting?()
click to toggle source
# File debug-1.9.1/lib/debug/thread_client.rb, line 170 def waiting? @mode == :waiting end