class DEBUGGER__::ThreadClient

Constants

SKIP_GLOBAL_LIST
SPECIAL_LOCAL_VARS
SUPPORT_TARGET_THREAD

Attributes

id[R]
location[R]
recorder[R]
thread[R]

Public Class Methods

current() click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 21
def self.current
  if thc = Thread.current[:DEBUGGER__ThreadClient]
    thc
  else
    thc = SESSION.get_thread_client
    Thread.current[:DEBUGGER__ThreadClient] = thc
  end
end
new(id, q_evt, q_cmd, thr = Thread.current) click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 76
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
  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.4.0/lib/debug/thread_client.rb, line 178
def << req
  @q_cmd << req
end
assemble_arguments(args) click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 35
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.4.0/lib/debug/thread_client.rb, line 639
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.4.0/lib/debug/thread_client.rb, line 139
def close
  @q_cmd.close
end
current_frame() click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 434
def current_frame
  if @target_frames
    @target_frames[@current_frame_index]
  else
    nil
  end
end
deactivate() click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 96
def deactivate
  @step_tp.disable if @step_tp
end
default_frame_formatter(frame) click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 41
def default_frame_formatter frame
  call_identifier_str =
    case frame.frame_type
    when :block
      level, block_loc, args = frame.block_identifier

      if !args.empty?
        args_str = " {|#{assemble_arguments(args)}|}"
      end

      "#{colorize_blue("block")}#{args_str} in #{colorize_blue(block_loc + level)}"
    when :method
      ci, args = frame.method_identifier

      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.4.0/lib/debug/server_cdp.rb, line 860
def evaluate_result r
  v = variable nil, r
  v[:value]
end
event!(ev, *args) click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 188
def event! ev, *args
  @q_evt << [self, @output, ev, generate_info, *args]
  @output = []
end
frame_eval(src, re_raise: false) { |e| ... } click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 352
def frame_eval src, re_raise: false
  @success_last_eval = false

  b = current_frame.eval_binding

  special_local_variables current_frame do |name, var|
    b.local_variable_set(name, var) if /\%/ !~ name
  end

  result = if b
              f, _l = b.source_location
              b.eval(src, "(rdbg)/#{f}")
            else
              frame_self = current_frame.self
              instance_eval_for_cmethod(frame_self, src)
            end
  @success_last_eval = true
  result

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_str(i, frame: @target_frames[i]) click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 602
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.4.0/lib/debug/thread_client.rb, line 182
def generate_info
  return unless current_frame

  { location: current_frame.location_str, line: current_frame.location.lineno }
end
inspect() click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 143
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
instance_eval_for_cmethod(frame_self, src) click to toggle source

this method is extracted to hide frame_eval‘s local variables from C method eval’s binding

# File debug-1.4.0/lib/debug/thread_client.rb, line 343
def instance_eval_for_cmethod frame_self, src
  frame_self.instance_eval(src)
end
make_breakpoint(args) click to toggle source

cmd: breakpoint

# File debug-1.4.0/lib/debug/thread_client.rb, line 651
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, klass_name, op, method_name, cond: cond, command: cmd, path: path)
    begin
      bp.enable
    rescue Exception => e
      puts e.message
      ::DEBUGGER__::METHOD_ADDED_TRACKER.enable
    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.4.0/lib/debug/thread_client.rb, line 100
def management?
  @is_management
end
mark_as_management() click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 104
def mark_as_management
  @is_management = true
end
name() click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 135
def name
  "##{@id} #{@thread.name || @thread.backtrace.last}"
end
on_breakpoint(tp, bp) click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 216
def on_breakpoint tp, bp
  suspend tp.event, tp, bp: bp
end
on_init(name) click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 208
def on_init name
  wait_reply [:init, name]
end
on_load(iseq, eval_src) click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 204
def on_load iseq, eval_src
  wait_reply [:load, iseq, eval_src]
end
on_pause() click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 228
def on_pause
  suspend :pause
end
on_trace(trace_id, msg) click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 212
def on_trace trace_id, msg
  wait_reply [:trace, trace_id, msg]
end
on_trap(sig) click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 220
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.4.0/lib/debug/thread_client.rb, line 630
def outline_method(o, klass, obj)
  singleton_class = begin obj.singleton_class; rescue TypeError; 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
process_cdp(args) click to toggle source
# File debug-1.4.0/lib/debug/server_cdp.rb, line 641
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 path.match /<internal:(.*)>/
          path = $1
        end

        {
          callFrameId: SecureRandom.hex(16),
          functionName: frame.name,
          functionLocation: {
            scriptId: path,
            lineNumber: 0
          },
          location: {
            scriptId: path,
            lineNumber: frame.location.lineno - 1 # The line number is 0-based.
          },
          url: "http://debuggee#{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! :cdp_result, :backtrace, req, result
  when :evaluate
    res = {}
    fid, expr = args
    frame = @target_frames[fid]
    message = nil

    if frame && (b = frame.binding)
      b = b.dup
      special_local_variables current_frame do |name, var|
        b.local_variable_set(name, var) if /\%/ !~name
      end

      result = nil

      case req.dig('params', 'objectGroup')
      when 'popover'
        case expr
        # Chrome doesn't read instance variables
        when /\A\$\S/
          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-zA-Z]*)/
          unless result = search_const(b, $1)
            message = "Error: Not defined constant: #{expr.inspect}"
          end
        else
          begin
            # try to check local variables
            b.local_variable_defined?(expr) or raise NameError
            result = b.local_variable_get(expr)
          rescue NameError
            # try to check method
            if b.receiver.respond_to? expr, include_all: true
              result = b.receiver.method(expr)
            else
              message = "Error: Can not evaluate: #{expr.inspect}"
            end
          end
        end
      else
        begin
          orig_stdout = $stdout
          $stdout = StringIO.new
          result = current_frame.binding.eval(expr.to_s, '(DEBUG CONSOLE)')
        rescue Exception => e
          result = e
          b = result.backtrace.map{|e| "    #{e}\n"}
          line = b.first.match('.*:(\d+):in .*')[1].to_i
          res[:exceptionDetails] = {
            exceptionId: 1,
            text: 'Uncaught',
            lineNumber: line - 1,
            columnNumber: 0,
            exception: evaluate_result(result),
          }
        ensure
          output = $stdout.string
          $stdout = orig_stdout
        end
      end
    else
      result = Exception.new("Error: Can not evaluate on this frame")
    end

    res[:result] = evaluate_result(result)
    event! :cdp_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! :cdp_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 = [
          property('#length', obj.length),
          property('#encoding', obj.encoding)
        ]
      when Class, Module
        result = obj.instance_variables.map{|iv|
          variable(iv, obj.instance_variable_get(iv))
        }
        prop = [property('%ancestors', obj.ancestors[1..])]
      when Range
        prop = [
          property('#begin', obj.begin),
          property('#end', obj.end),
        ]
      end

      result += obj.instance_variables.map{|iv|
        variable(iv, obj.instance_variable_get(iv))
      }
      prop += [property('#class', obj.class)]
    end
    event! :cdp_result, :properties, req, result: result, internalProperties: prop
  end
end
process_dap(args) click to toggle source
# File debug-1.4.0/lib/debug/server_dap.rb, line 588
def process_dap args
  # pp tc: self, args: args
  type = args.shift
  req = args.shift

  case type
  when :backtrace
    event! :dap_result, :backtrace, req, {
      stackFrames: @target_frames.map{|frame|
        path = frame.realpath || frame.path
        ref = frame.file_lines unless path && File.exist?(path)

        {
          # id: ??? # filled by SESSION
          name: frame.name,
          line: frame.location.lineno,
          column: 1,
          source: {
            name: File.basename(frame.path),
            path: path,
            sourceReference: ref,
          },
        }
      }
    }
  when :scopes
    fid = args.shift
    frame = @target_frames[fid]

    lnum =
      if frame.binding
        frame.binding.local_variables.size
      elsif vars = frame.local_variables
        vars.size
      else
        0
      end

    event! :dap_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: global_variables.size,
      indexedVariables: 0,
      expensive: false,
    }]
  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.push variable(name, val)
      end
    end
    event! :dap_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(DEBUGGER__.safe_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)
          ]
        when Class, Module
          vars = obj.instance_variables.map{|iv|
            variable(iv, obj.instance_variable_get(iv))
          }
          vars.unshift variable('%ancestors', obj.ancestors[1..])
        when Range
          vars = [
            variable('#begin', obj.begin),
            variable('#end', obj.end),
          ]
        end

        vars += obj.instance_variables.map{|iv|
          variable(iv, obj.instance_variable_get(iv))
        }
        vars.unshift variable('#class', obj.class)
      end
    end
    event! :dap_result, :variable, req, variables: (vars || []), tid: self.id

  when :evaluate
    fid, expr, context = args
    frame = @target_frames[fid]
    message = nil

    if frame && (b = frame.binding)
      b = b.dup
      special_local_variables current_frame do |name, var|
        b.local_variable_set(name, var) if /\%/ !~ name
      end

      case context
      when 'repl', 'watch'
        begin
          result = b.eval(expr.to_s, '(DEBUG CONSOLE)')
        rescue Exception => e
          result = e
        end

      when 'hover'
        case expr
        when /\A\@\S/
          begin
            (r = b.receiver).instance_variable_defined?(expr) or raise(NameError)
            result = r.instance_variable_get(expr)
          rescue NameError
            message = "Error: Not defined instance variable: #{expr.inspect}"
          end
        when /\A\$\S/
          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]/
          unless result = search_const(b, expr)
            message = "Error: Not defined constants: #{expr.inspect}"
          end
        else
          begin
            # try to check local variables
            b.local_variable_defined?(expr) or raise NameError
            result = b.local_variable_get(expr)
          rescue NameError
            # try to check method
            if b.receiver.respond_to? expr, include_all: true
              result = b.receiver.method(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! :dap_result, :evaluate, req, message: message, tid: self.id, **evaluate_result(result)

  when :completions
    fid, text = args
    frame = @target_frames[fid]

    if (b = frame&.binding) && word = text&.split(/[\s\{]/)&.last
      words = IRB::InputCompletor::retrieve_completion_data(word, bind: b).compact
    end

    event! :dap_result, :completions, req, targets: (words || []).map{|phrase|
      if /\b([_a-zA-Z]\w*[!\?]?)\z/ =~ phrase
        w = $1
      else
        w = phrase
      end

      begin
        if b&.local_variable_defined?(w)
          v = b.local_variable_get(w)
          phrase += " (variable:#{DEBUGGER__.safe_inspect(v)})"
        end
      rescue NameError
      end

      {
        label: phrase,
        text: w,
      }
    }

  else
    raise "Unknown req: #{args.inspect}"
  end
end
property(name, obj) click to toggle source
# File debug-1.4.0/lib/debug/server_cdp.rb, line 865
def property name, obj
  v = variable name, obj
  v.delete :configurable
  v.delete :enumerable
  v
end
puts(str = '') click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 164
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.4.0/lib/debug/thread_client.rb, line 516
def puts_variable_info label, obj, pat
  return if pat && pat !~ label

  begin
    inspected = obj.inspect
  rescue Exception => e
    inspected = e.inspect
  end
  mono_info = "#{label} = #{inspected}"

  w = SESSION::width

  if mono_info.length >= w
    info = truncate(mono_info, width: w)
  else
    valstr = colored_inspect(obj, width: 2 ** 30)
    valstr = inspected if valstr.lines.size > 1
    info = "#{colorize_cyan(label)} = #{valstr}"
  end

  puts info
end
replay_suspend() click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 287
def replay_suspend
  # @recorder.current_position
  suspend :replay, replay_frames: @recorder.current_frame
end
running?() click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 127
def running?
  @mode == :running
end
search_const(b, expr) click to toggle source
# File debug-1.4.0/lib/debug/server_cdp.rb, line 843
def search_const b, expr
  cs = expr.split('::')
  [Object, *b.eval('Module.nesting')].reverse_each{|mod|
    if cs.all?{|c|
         if mod.const_defined?(c)
           mod = mod.const_get(c)
         else
           false
         end
       }
      # if-body
      return mod
    end
  }
  false
end
set_mode(mode) click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 108
def set_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.4.0/lib/debug/thread_client.rb, line 547
def show_by_editor path = nil
  unless path
    if @target_frames && frame = @target_frames[@current_frame_index]
      path = 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}"
      system(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, only_self: false) click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 476
def show_consts pat, only_self: false
  if s = current_frame&.self
    cs = {}
    if s.kind_of? Module
      cs[s] = :self
    else
      s = s.class
      cs[s] = :self unless only_self
    end

    unless only_self
      s.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, _|
      c.constants(false).sort.each{|name|
        next if names.has_key? name
        names[name] = nil
        value = c.const_get(name)
        puts_variable_info name, value, pat
      }
    }
  end
end
show_frame(i=0) click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 598
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.4.0/lib/debug/thread_client.rb, line 571
def show_frames max = nil, pattern = nil
  if @target_frames && (max ||= @target_frames.size) > 0
    frames = []
    @target_frames.each_with_index{|f, i|
      next if pattern && !(f.name.match?(pattern) || f.location_str.match?(pattern))
      next if CONFIG[:skip_path] && CONFIG[:skip_path].any?{|pat|
        case pat
        when String
          f.location_str.start_with?(pat)
        when Regexp
          f.location_str.match?(pat)
        end
      }

      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.4.0/lib/debug/thread_client.rb, line 507
def show_globals pat
  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) click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 467
def show_ivars pat
  if s = current_frame&.self
    s.instance_variables.sort.each{|iv|
      value = s.instance_variable_get(iv)
      puts_variable_info iv, value, pat
    }
  end
end
show_locals(pat) click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 452
def show_locals pat
  if s = current_frame&.self
    puts_variable_info '%self', s, pat
  end
  special_local_variables current_frame do |name, val|
    puts_variable_info name, val, pat
  end

  if vars = current_frame&.local_variables
    vars.each{|var, val|
      puts_variable_info var, val, pat
    }
  end
end
show_outline(expr) click to toggle source

cmd: show outline

# File debug-1.4.0/lib/debug/thread_client.rb, line 611
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  = (obj.class == Class || obj.class == Module ? obj : obj.class)

    o.dump("constants", obj.constants) if obj.respond_to?(:constants)
    outline_method(o, klass, obj)
    o.dump("instance variables", obj.instance_variables)
    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, max_lines: CONFIG[:show_src_lines] || 10, start_line: nil, end_line: nil, dir: +1) click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 383
def show_src(frame_index: @current_frame_index,
             update_line: false,
             max_lines: CONFIG[:show_src_lines] || 10,
             start_line: nil,
             end_line: nil,
             dir: +1)
  if @target_frames && frame = @target_frames[frame_index]
    if file_lines = frame.file_lines
      frame_line = frame.location.lineno - 1

      lines = file_lines.map.with_index do |e, i|
        cur = i == frame_line ? '=>' : '  '
        line = colorize_dim('%4d|' % (i+1))
        "#{cur}#{line} #{e}"
      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 update_line
        frame.show_line = end_line
      end

      if start_line != end_line && max_lines
        puts "[#{start_line+1}, #{end_line}] in #{frame.pretty_path}" if !update_line && max_lines != 1
        puts lines[start_line ... end_line]
      end
    else # no file lines
      puts "# No sourcefile available for #{frame.path}"
    end
  end
rescue Exception => e
  p e
  pp e.backtrace
  exit!
end
special_local_variables(frame) { |name, send| ... } click to toggle source

cmd: show

# File debug-1.4.0/lib/debug/thread_client.rb, line 444
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]) { |event| ... } click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 301
def step_tp iter, events = [:line, :b_return, :return]
  @step_tp.disable if @step_tp

  thread = Thread.current

  if SUPPORT_TARGET_THREAD
    @step_tp = TracePoint.new(*events){|tp|
      next if SESSION.break_at? tp.path, tp.lineno
      next if !yield(tp.event)
      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
      next if SESSION.break_at? tp.path, tp.lineno
      next if !yield(tp.event)
      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.4.0/lib/debug/thread_client.rb, line 232
def suspend event, tp = nil, bp: nil, sig: nil, postmortem_frames: nil, replay_frames: nil, postmortem_exc: nil
  return if management?

  @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
    @location = cf.location
    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
    show_src
    show_frames CONFIG[:show_frames] || 2

    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.4.0/lib/debug/thread_client.rb, line 151
def to_s
  loc = current_frame&.location

  if loc
    str = "(#{@thread.name || @thread.status})@#{loc}"
  else
    str = "(#{@thread.name || @thread.status})@#{@thread.to_s}"
  end

  str += " (not under control)" unless self.waiting?
  str
end
truncate(string, width:) click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 539
def truncate(string, width:)
  str = string[0 .. (width-4)] + '...'
  str += ">" if str.start_with?("#<")
  str
end
variable(name, obj) click to toggle source
# File debug-1.4.0/lib/debug/server_cdp.rb, line 896
def variable name, obj
  case obj
  when Array
    variable_ name, obj, 'object', description: "Array(#{obj.size})", subtype: 'array'
  when Hash
    variable_ name, obj, 'object', description: "Hash(#{obj.size})", subtype: 'map'
  when String
    variable_ name, obj, 'string', description: obj
  when Class, Module, Struct, Range, Time, Method
    variable_ name, obj, 'object'
  when TrueClass, FalseClass
    variable_ name, obj, 'boolean'
  when Symbol
    variable_ name, obj, 'symbol'
  when Integer, Float
    variable_ name, obj, 'number'
  when Exception
    bt = nil
    if log = obj.backtrace
      bt = log.map{|e| "    #{e}\n"}.join
    end
    variable_ name, obj, 'object', description: "#{obj.inspect}\n#{bt}", subtype: 'error'
  else
    variable_ name, obj, 'undefined'
  end
end
variable_(name, obj, type, description: obj.inspect, subtype: nil) click to toggle source
# File debug-1.4.0/lib/debug/server_cdp.rb, line 872
def variable_ name, obj, type, description: obj.inspect, subtype: 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] = obj.class
  end
  prop
end
wait_next_action() click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 675
def wait_next_action
  wait_next_action_
rescue SuspendReplay
  replay_suspend
end
wait_next_action_() click to toggle source
# File debug-1.4.0/lib/debug/thread_client.rb, line 681
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
        if @recorder&.replaying?
          @recorder.step_forward
          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

        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
          loc = caller_locations(2, 1).first
          loc_path = loc.absolute_path || "!eval:#{loc.path}"

          # same stack depth
          (DEBUGGER__.frame_depth - 3 <= depth) ||

          # different frame
          (next_line && loc_path == path &&
           (loc_lineno = loc.lineno) > line &&
           loc_lineno <= next_line)
        end
        break

      when :finish
        finish_frames = (iter || 1) - 1
        goal_depth = @target_frames.first.frame_depth - finish_frames

        step_tp nil, [:return, :b_return] do
          DEBUGGER__.frame_depth - 3 <= goal_depth ? true : false
        end
        break

      when :back
        if @recorder&.can_step_back?
          unless @recorder.backup_frames
            @recorder.backup_frames = @target_frames
          end
          @recorder.step_back
          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
        begin
          result = frame_eval('binding.irb')
        ensure
          # workaround: https://github.com/ruby/debug/issues/308
          Reline.prompt_proc = nil if defined? Reline
        end
      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 :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
        show_ivars pat

      when :consts
        pat = args.shift
        show_consts pat

      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 = obj.inspect

          width = 50

          if obj_inspect.length >= width
            obj_inspect = truncate(obj_inspect, width: width)
          end

          event! :result, :trace_pass, obj.object_id, 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
          @recorder.enable
        end
      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 :dap
      process_dap args
    when :cdp
      process_cdp args
    else
      raise [cmd, *args].inspect
    end
  end

rescue SuspendReplay, SystemExit, Interrupt
  raise
rescue Exception => e
  pp ["DEBUGGER Exception: #{__FILE__}:#{__LINE__}", e, e.backtrace]
  raise
end
wait_reply(event_arg) click to toggle source

events

# File debug-1.4.0/lib/debug/thread_client.rb, line 195
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.4.0/lib/debug/thread_client.rb, line 131
def waiting?
  @mode == :waiting
end