123456789_123456789_123456789_123456789_123456789_

Class: DEBUGGER__::ThreadClient

Relationships & Source Files
Namespace Children
Classes:
Exceptions:
Super Chains via Extension / Inclusion / Inheritance
Instance Chain:
Inherits: Object
Defined in: lib/debug/server_cdp.rb,
lib/debug/server_dap.rb,
lib/debug/thread_client.rb

Constant Summary

Class Method Summary

Instance Attribute Summary

Instance Method Summary

SkipPathHelper - Included

Color - Included

#color_pp

See additional method definition at line 50.

#colored_inspect,
#colorize

See additional method definition at line 36.

#colorize_blue,
#colorize_code

See additional method definition at line 79.

#colorize_cyan, #colorize_dim, #colorize_magenta,
#irb_colorize

See additional method definition at line 27.

#with_inspection_error_guard

Constructor Details

.new(id, q_evt, q_cmd, thr = Thread.current) ⇒ ThreadClient

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 103

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

Class Method Details

.current

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 42

def self.current
  if thc = Thread.current[:DEBUGGER__ThreadClient]
    thc
  else
    thc = SESSION.get_thread_client
    Thread.current[:DEBUGGER__ThreadClient] = thc
  end
end

Instance Attribute Details

#check_bp_fulfillment_map (readonly)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 54

attr_reader :thread, :id, :recorder, :check_bp_fulfillment_map

#id (readonly)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 54

attr_reader :thread, :id, :recorder, :check_bp_fulfillment_map

#management?Boolean (readonly)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 130

def management?
  @is_management
end

#recorder (readonly)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 54

attr_reader :thread, :id, :recorder, :check_bp_fulfillment_map

#running?Boolean (readonly)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 158

def running?
  @mode == :running
end

#thread (readonly)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 54

attr_reader :thread, :id, :recorder, :check_bp_fulfillment_map

#waiting?Boolean (readonly)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 162

def waiting?
  @mode == :waiting
end

Instance Method Details

#<<(req)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 202

def << req
  debug_cmd(req)
  @q_cmd << req
end

#assemble_arguments(args)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 60

def assemble_arguments(args)
  args.map do |arg|
    "#{colorize_cyan(arg[:name])}=#{arg[:value]}"
  end.join(", ")
end

#class_method_map(classes)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 728

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

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 170

def close
  @q_cmd.close
end

#collect_locals(frame)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 514

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) ⇒ Boolean

TODO: support non-ASCII Constant name

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 741

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

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 502

def current_frame
  get_frame(@current_frame_index)
end

#deactivate

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 126

def deactivate
  @step_tp.disable if @step_tp
end

#debug_cmd(cmds)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 1125

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)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 1112

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)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 1119

def debug_mode(old_mode, new_mode)
  DEBUGGER__.debug{
    "#{inspect} changes mode (#{old_mode} -> #{new_mode})"
  }
end

#debug_suspend(event)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 1133

def debug_suspend(event)
  DEBUGGER__.debug{
    "#{inspect} is suspended for #{event.inspect}"
  }
end

#default_frame_formatter(frame)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 66

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)

See additional method definition at file lib/debug/server_cdp.rb line 1016.

[ GitHub ]

  
# File 'lib/debug/server_dap.rb', line 929

def evaluate_result r
  v = variable nil, r
  v[:value]
end

#event!(ev, *args)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 213

def event! ev, *args
  debug_event(ev, args)
  @q_evt << [self, @output, ev, generate_info, *args]
  @output = []
end

#frame_eval(src, re_raise: false)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 414

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 = frame_eval_core(src, b)

  @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)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 387

def frame_eval_core src, b
  saved_target_frames = @target_frames
  saved_current_frame_index = @current_frame_index

  if b
    f, _l = b.source_location

    tp_allow_reentry do
      b.eval(src, "(rdbg)/#{f}")
    end
  else
    frame_self = current_frame.self

    tp_allow_reentry do
      frame_self.instance_eval(src)
    end
  end
ensure
  @target_frames = saved_target_frames
  @current_frame_index = saved_current_frame_index
end

#frame_str(i, frame: )

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 684

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

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 207

def generate_info
  return unless current_frame

  { location: current_frame.location_str, line: current_frame.location.lineno }
end

#get_frame(index)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 506

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)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 442

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

    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 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

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 174

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)

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 1021

def internalProperty name, obj
  v = variable name, obj
  v.delete :configurable
  v.delete :enumerable
  v
end

#location

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 56

def location
  current_frame&.location
end

#make_breakpoint(args)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 757

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 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

#mark_as_management

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 134

def mark_as_management
  @is_management = true
end

#name

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 166

def name
  "##{@id} #{@thread.name || @thread.backtrace.last}"
end

#on_breakpoint(tp, bp)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 242

def on_breakpoint tp, bp
  suspend tp.event, tp, bp: bp
end

#on_init(name)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 234

def on_init name
  wait_reply [:init, name]
end

#on_load(iseq, eval_src)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 230

def on_load iseq, eval_src
  wait_reply [:load, iseq, eval_src]
end

#on_pause

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 254

def on_pause
  suspend :pause
end

#on_trace(trace_id, msg)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 238

def on_trace trace_id, msg
  wait_reply [:trace, trace_id, msg]
end

#on_trap(sig)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 246

def on_trap sig
  if waiting?
    # raise Interrupt
  else
    suspend :trap, sig: sig
  end
end

#outline_method(o, klass, obj)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 714

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)

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 1092

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)

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 1053

def preview_ value, hash, overflow
  {
    type: value[:type],
    subtype: value[:subtype],
    description: value[:description],
    overflow: overflow,
    properties: hash.map{|k, v|
      pd = propertyDescriptor k, v
      {
        name: pd[:name],
        type: pd[:value][:type],
        value: pd[:value][:description]
      }
    }
  }
end

#process_cdp(args)

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 775

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! :cdp_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/
          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 b.receiver.respond_to? expr, include_all: true
              result = b.receiver.method(expr)
            else
              message = "Error: Can not evaluate: #{expr.inspect}"
            end
          end
        end
      when 'console', 'watch-group'
        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"}
          frames = [
            {
              columnNumber: 0,
              functionName: 'eval',
              lineNumber: 0,
              url: ''
            }
          ]
          e.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
          res[:exceptionDetails] = {
            exceptionId: 1,
            text: 'Uncaught',
            lineNumber: 0,
            columnNumber: 0,
            exception: evaluate_result(result),
            stackTrace: {
              callFrames: frames
            }
          }
        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! :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 = [
          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 += obj.instance_variables.map{|iv|
        variable(iv, obj.instance_variable_get(iv))
      }
      prop += [internalProperty('#class', obj.class)]
    end
    event! :cdp_result, :properties, req, result: result, internalProperties: prop
  end
end

#process_dap(args)

[ GitHub ]

  
# File 'lib/debug/server_dap.rb', line 693

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
      break if (levels -= 1) < 0

      path = frame.realpath || frame.path
      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! :dap_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! :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 = get_frame(fid)
    vars = collect_locals(frame).map do |var, val|
      variable(var, val)
    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(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)
          ]
        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 += M_INSTANCE_VARIABLES.bind_call(obj).map{|iv|
          variable(iv, M_INSTANCE_VARIABLE_GET.bind_call(obj, iv))
        }
        vars.unshift variable('#class', M_CLASS.bind_call(obj))
      end
    end
    event! :dap_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'
        begin
          result = b.eval(expr.to_s, '(DEBUG CONSOLE)')
        rescue Exception => e
          result = e
        end

      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/
          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! :dap_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! :dap_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
    raise "Unknown req: #{args.inspect}"
  end
end

#propertyDescriptor(name, obj)

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 1116

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)

[ GitHub ]

  
# File 'lib/debug/server_cdp.rb', line 1028

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] = obj.class
  end
  prop
end

#puts(str = '')

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 188

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)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 598

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

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 313

def replay_suspend
  # @recorder.current_position
  suspend :replay, replay_frames: @recorder.current_frame
end

#search_const(b, expr)

See additional method definition at file lib/debug/server_cdp.rb line 999.

[ GitHub ]

  
# File 'lib/debug/server_dap.rb', line 912

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)
           mod = mod.const_get(c)
         else
           false
         end
       }
      # if-body
      return mod
    end
  }
  false
end

#set_mode(mode)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 138

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)

cmd: show edit

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 633

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}"
      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)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 558

def show_consts pat, only_self: false
  if s = current_frame&.self
    cs = {}
    if M_KIND_OF_P.bind_call(s, Module)
      cs[s] = :self
    else
      s = M_CLASS.bind_call(s)
      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)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 680

def show_frame i=0
  puts frame_str(i)
end

#show_frames(max = nil, pattern = nil)

cmd: show frames

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 657

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)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 589

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)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 549

def show_ivars pat
  if s = current_frame&.self
    M_INSTANCE_VARIABLES.bind_call(s).sort.each{|iv|
      value = M_INSTANCE_VARIABLE_GET.bind_call(s, iv)
      puts_variable_info iv, value, pat
    }
  end
end

#show_locals(pat)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 543

def show_locals pat
  collect_locals(current_frame).each do |var, val|
    puts_variable_info(var, val, pat)
  end
end

#show_outline(expr)

cmd: show outline

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 693

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, max_lines: , **options)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 485

def show_src(frame_index: @current_frame_index, update_line: false, max_lines: CONFIG[:show_src_lines], **options)
  if frame = get_frame(frame_index)
    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
  end
end

#special_local_variables(frame)

cmd: show

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 535

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])

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 327

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)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 258

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
    show_src
    show_frames CONFIG[:show_frames]

    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

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 182

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

See additional method definition at line 369.

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 382

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:)

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 623

def truncate(string, width:)
  if string.start_with?("#<")
    string[0 .. (width-5)] + '...>'
  else
    string[0 .. (width-4)] + '...'
  end
end

#value_inspect(obj)

[ GitHub ]

  
# File 'lib/debug/server_dap.rb', line 688

def value_inspect obj
  # TODO: max length should be configuarable?
  DEBUGGER__.safe_inspect obj, short: true, max_length: 4 * 1024
end

#variable(name, obj)

See additional method definition at file lib/debug/server_cdp.rb line 1070.

[ GitHub ]

  
# File 'lib/debug/server_dap.rb', line 956

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)

[ GitHub ]

  
# File 'lib/debug/server_dap.rb', line 937

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

  ivnum = M_INSTANCE_VARIABLES.bind_call(obj).size

  { name: name,
    value: value_inspect(obj),
    type: (klass = M_CLASS.bind_call(obj)).name || klass.to_s,
    variablesReference: vid,
    indexedVariables: indexedVariables,
    namedVariables: namedVariables + ivnum,
  }
end

#wait_next_action

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 796

def wait_next_action
  wait_next_action_
rescue SuspendReplay
  replay_suspend
end

#wait_next_action_

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 802

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 = 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 :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)

events

[ GitHub ]

  
# File 'lib/debug/thread_client.rb', line 221

def wait_reply event_arg
  return if management?

  set_mode :waiting

  event!(*event_arg)
  wait_next_action
end