123456789_123456789_123456789_123456789_123456789_

Class: DEBUGGER__::Session

Relationships & Source Files
Super Chains via Extension / Inclusion / Inheritance
Instance Chain:
Inherits: Object
Defined in: lib/debug/server_cdp.rb,
lib/debug/irb_integration.rb,
lib/debug/server_dap.rb,
lib/debug/session.rb

Constant Summary

GlobalVariablesHelper - Included

SKIP_GLOBAL_LIST

Class Method Summary

Instance Attribute Summary

Instance Method Summary

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

GlobalVariablesHelper - Included

Constructor Details

.newSession

[ GitHub ]

  
# File 'lib/debug/session.rb', line 98

def initialize
  @ui = nil
  @sr = SourceRepository.new
  @bps = {} # bp.key => bp
            #   [file, line] => LineBreakpoint
            #   "Error" => CatchBreakpoint
            #   "Foo#bar" => MethodBreakpoint
            #   [:watch, ivar] => WatchIVarBreakpoint
            #   [:check, expr] => CheckBreakpoint
  #
  @tracers = {}
  @th_clients = {} # {Thread => ThreadClient}
  @q_evt = Queue.new
  @displays = []
  @tc = nil
  @tc_id = 0
  @preset_command = nil
  @postmortem_hook = nil
  @postmortem = false
  @intercept_trap_sigint = false
  @intercepted_sigint_cmd = 'DEFAULT'
  @process_group = ProcessGroup.new
  @subsession_stack = []
  @subsession_id = 0

  @frame_map = {} # for DAP: {id => [threadId, frame_depth]} and CDP: {id => frame_depth}
  @var_map   = {1 => [:globals], } # {id => ...} for DAP
  @src_map   = {} # {id => src}

  @scr_id_map = {} # for CDP
  @obj_map = {} # { object_id => ... } for CDP

  @tp_thread_begin = nil
  @tp_thread_end = nil

  @commands = {}
  @unsafe_context = false

  @has_keep_script_lines = defined?(RubyVM.keep_script_lines)

  @tp_load_script = TracePoint.new(:script_compiled){|tp|
    eval_script = tp.eval_script unless @has_keep_script_lines
    ThreadClient.current.on_load tp.instruction_sequence, eval_script
  }
  @tp_load_script.enable

  @thread_stopper = thread_stopper
  self.postmortem = CONFIG[:postmortem]

  register_default_command
end

Class Method Details

.activate_method_added_trackers

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1847

def self.activate_method_added_trackers
  METHOD_ADDED_TRACKERS.each do |m, tp|
    tp.enable(target: m) unless tp.enabled?
  rescue ArgumentError
    DEBUGGER__.warn "Methods defined under #{m.owner} can not track by the debugger."
  end
end

.create_method_added_tracker(mod, method_added_id, method_accessor = :method)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1840

def self.create_method_added_tracker mod, method_added_id, method_accessor = :method
  m = mod.__send__(method_accessor, method_added_id)
  METHOD_ADDED_TRACKERS[m] = TracePoint.new(:call) do |tp|
    SESSION.method_added tp
  end
end

.deactivate_method_added_trackers

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1855

def self.deactivate_method_added_trackers
  METHOD_ADDED_TRACKERS.each do |m, tp|
    tp.disable if tp.enabled?
  end
end

Instance Attribute Details

#active?Boolean (readonly)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 150

def active?
  !@q_evt.closed?
end

#in_subsession?Boolean (readonly)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1741

def in_subsession?
  !@subsession_stack.empty?
end

#intercept_trap_sigint?Boolean (readonly)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1982

def intercept_trap_sigint?
  @intercept_trap_sigint
end

#intercepted_sigint_cmd (readonly)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 94

attr_reader :intercepted_sigint_cmd, :process_group, :subsession_id

#postmortem=(is_enable) (writeonly)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1927

def postmortem=(is_enable)
  if is_enable
    unless @postmortem_hook
      @postmortem_hook = TracePoint.new(:raise){|tp|
        exc = tp.raised_exception
        frames = DEBUGGER__.capture_frames(__dir__)
        exc.instance_variable_set(:@__debugger_postmortem_frames, frames)
      }
      at_exit{
        @postmortem_hook.disable
        if CONFIG[:postmortem] && (exc = $!) != nil
          exc = exc.cause while exc.cause

          begin
            @ui.puts "Enter postmortem mode with #{exc.inspect}"
            @ui.puts exc.backtrace.map{|e| '  ' + e}
            @ui.puts "\n"

            enter_postmortem_session exc
          rescue SystemExit
            exit!
          rescue Exception => e
            @ui = STDERR unless @ui
            @ui.puts "Error while postmortem console: #{e.inspect}"
          end
        end
      }
    end

    if !@postmortem_hook.enabled?
      @postmortem_hook.enable
    end
  else
    if @postmortem_hook && @postmortem_hook.enabled?
      @postmortem_hook.disable
    end
  end
end

#process_group (readonly)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 94

attr_reader :intercepted_sigint_cmd, :process_group, :subsession_id

#remote?Boolean (readonly)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 154

def remote?
  @ui.remote?
end

#subsession_id (readonly)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 94

attr_reader :intercepted_sigint_cmd, :process_group, :subsession_id

Instance Method Details

#activate(ui = nil, on_fork: false)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 168

def activate ui = nil, on_fork: false
  @ui = ui if ui

  @tp_thread_begin&.disable
  @tp_thread_end&.disable
  @tp_thread_begin = nil
  @tp_thread_end = nil
  @ui.activate self, on_fork: on_fork

  q = Queue.new
  first_q = Queue.new
  @session_server = Thread.new do
    # make sure `@session_server` is assigned
    first_q.pop; first_q = nil

    Thread.current.name = 'DEBUGGER__::SESSION@server'
    Thread.current.abort_on_exception = true

    # Thread management
    setup_threads
    thc = get_thread_client Thread.current
    thc.mark_as_management

    if @ui.respond_to?(:reader_thread) && thc = get_thread_client(@ui.reader_thread)
      thc.mark_as_management
    end

    @tp_thread_begin = TracePoint.new(:thread_begin) do |tp|
      get_thread_client
    end
    @tp_thread_begin.enable

    @tp_thread_end = TracePoint.new(:thread_end) do |tp|
      @th_clients.delete(Thread.current)
    end
    @tp_thread_end.enable

    # session start
    q << true
    session_server_main
  end
  first_q << :ok

  q.pop

  # For activating irb:rdbg with startup config like `RUBY_DEBUG_IRB_CONSOLE=1`
  # Because in that case the `Config#if_updated` callback would not be triggered
  if CONFIG[:irb_console] && !CONFIG[:open]
    activate_irb_integration
  end
end

#activate_irb_integration

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1881

def activate_irb_integration
  require_relative "irb_integration"
  thc = get_thread_client(@session_server)
  thc.activate_irb_integration
end

#add_bp(bp)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1398

def add_bp bp
  # don't repeat commands that add breakpoints
  if @bps.has_key? bp.key
    if bp.duplicable?
      bp
    else
      @ui.puts "duplicated breakpoint: #{bp}"
      bp.disable
      nil
    end
  else
    @bps[bp.key] = bp
  end
end

#add_catch_breakpoint(pat, cond: nil)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1498

def add_catch_breakpoint pat, cond: nil
  bp = CatchBreakpoint.new(pat, cond: cond)
  add_bp bp
end

#add_check_breakpoint(cond, path, command)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1503

def add_check_breakpoint cond, path, command
  bp = CheckBreakpoint.new(cond: cond, path: path, command: command)
  add_bp bp
end

#add_iseq_breakpoint(iseq, **kw)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1545

def add_iseq_breakpoint iseq, **kw
  bp = ISeqBreakpoint.new(iseq, [:line], **kw)
  add_bp bp
end

#add_line_breakpoint(file, line, **kw)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1508

def add_line_breakpoint file, line, **kw
  file = resolve_path(file)
  bp = LineBreakpoint.new(file, line, **kw)

  add_bp bp
rescue Errno::ENOENT => e
  @ui.puts e.message
end

#add_preset_commands(name, cmds, kick: true, continue: true)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 374

def add_preset_commands name, cmds, kick: true, continue: true
  cs = cmds.map{|c|
    c.each_line.map{|line|
      line = line.strip.gsub(/\A\s*\#.*/, '').strip
      line unless line.empty?
    }.compact
  }.flatten.compact

  if @preset_command && !@preset_command.commands.empty?
    @preset_command.commands += cs
  else
    @preset_command = PresetCommands.new(cs, name, continue)
  end

  ThreadClient.current.on_init name if kick
end

#add_tracer(tracer)

tracers

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1552

def add_tracer tracer
  if @tracers[tracer.key]&.enabled?
    tracer.disable
    @ui.puts "Duplicated tracer: #{tracer}"
  else
    @tracers[tracer.key] = tracer
    @ui.puts "Enable #{tracer}"
  end
end

#after_fork_parent

[ GitHub ]

  
# File 'lib/debug/session.rb', line 2017

def after_fork_parent
  @ui.after_fork_parent
end

#ask(msg, default = 'Y')

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1340

def ask msg, default = 'Y'
  opts = '[y/n]'.tr(default.downcase, default)
  input = @ui.ask("#{msg} #{opts} ")
  input = default if input.empty?
  case input
  when 'y', 'Y'
    true
  else
    false
  end
end

#ask_thread_client(th) (private)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1641

private def ask_thread_client th
  # TODO: Ractor support
  q2 = Queue.new
  # tc, output, ev, @internal_info, *ev_args = evt
  @q_evt << [nil, [], :thread_begin, nil, th, q2]
  q2.pop

  @th_clients[th] or raise "unexpected error"
end

#before_fork(need_lock = true)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 2011

def before_fork need_lock = true
  if need_lock
    @process_group.multi_process!
  end
end

#bp_index(specific_bp_key)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1375

def bp_index specific_bp_key
  iterate_bps do |key, bp, i|
    if key == specific_bp_key
      return [bp, i]
    end
  end
  nil
end

#cancel_auto_continue

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1288

def cancel_auto_continue
  if @preset_command&.auto_continue
    @preset_command.auto_continue = false
  end
end

#capture_exception_frames(*exclude_path)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1897

def capture_exception_frames *exclude_path
  postmortem_hook = TracePoint.new(:raise){|tp|
    exc = tp.raised_exception
    frames = DEBUGGER__.capture_frames(__dir__)

    exclude_path.each{|ex|
      if Regexp === ex
        frames.delete_if{|e| ex =~ e.path}
      else
        frames.delete_if{|e| e.path.start_with? ex.to_s}
      end
    }
    exc.instance_variable_set(:@__debugger_postmortem_frames, frames)
  }
  postmortem_hook.enable

  begin
    yield
    nil
  rescue Exception => e
    if e.instance_variable_defined? :@__debugger_postmortem_frames
      e
    else
      raise
    end
  ensure
    postmortem_hook.disable
  end
end

#check_postmortem

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1869

def check_postmortem
  if @postmortem
    raise PostmortemError, "Can not use this command on postmortem mode."
  end
end

#check_unsafe

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1875

def check_unsafe
  if @unsafe_context
    raise RuntimeError, "#{@repl_prev_line.dump} is not allowed on unsafe context."
  end
end

#clean_bps

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1392

def clean_bps
  @bps.delete_if{|_k, bp|
    bp.deleted?
  }
end

#clear_all_breakpoints

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1541

def clear_all_breakpoints
  clear_breakpoints{true}
end

#clear_breakpoints(&condition)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1517

def clear_breakpoints(&condition)
  @bps.delete_if do |k, bp|
    if condition.call(k, bp)
      bp.delete
      true
    end
  end
end

#clear_catch_breakpoints(*exception_names)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1535

def clear_catch_breakpoints *exception_names
  clear_breakpoints do |k, bp|
    bp.is_a?(CatchBreakpoint) && exception_names.include?(k[1])
  end
end

#clear_line_breakpoints(path)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1526

def clear_line_breakpoints path
  path = resolve_path(path)
  clear_breakpoints do |k, bp|
    bp.is_a?(LineBreakpoint) && bp.path_is?(path)
  end
rescue Errno::ENOENT
  # just ignore
end

#config_command(arg)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1254

def config_command arg
  case arg
  when nil
    CONFIG_SET.each do |k, _|
      config_show k
    end

  when /\Aunset\s(.)\z/
    if CONFIG_SET[key = $1.to_sym]
      CONFIG[key] = nil
    end
    config_show key

  when /\A(\w)\s*=\s*(.)\z/
    config_set $1, $2

  when /\A\s*set\s(\w)\s(.)\z/
    config_set $1, $2

  when /\A(\w)\s*<<\s*(.)\z/
    config_set $1, $2, append: true

  when /\A\s*append\s(\w)\s(.)\z/
    config_set $1, $2, append: true

  when /\A(\w+)\z/
    config_show $1

  else
    @ui.puts "Can not parse parameters: #{arg}"
  end
end

#config_set(key, val, append: false)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1238

def config_set key, val, append: false
  if CONFIG_SET[key = key.to_sym]
    begin
      if append
        CONFIG.append_config(key, val)
      else
        CONFIG[key] = val
      end
    rescue => e
      @ui.puts e.message
    end
  end

  config_show key
end

#config_show(key)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1214

def config_show key
  key = key.to_sym
  config_detail = CONFIG_SET[key]

  if config_detail
    v = CONFIG[key]
    kv = "#{key} = #{v.inspect}"
    desc = config_detail[1]

    if config_default = config_detail[3]
      desc += " (default: #{config_default})"
    end

    line = "%-34s \# %s" % [kv, desc]
    if line.size > SESSION.width
      @ui.puts "\# #{desc}\n#{kv}"
    else
      @ui.puts line
    end
  else
    @ui.puts "Unknown configuration: #{key}. 'config' shows all configurations."
  end
end

#create_thread_client(th) (private)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1635

private def create_thread_client th
  # TODO: Ractor support
  raise "Only session_server can create thread_client" unless Thread.current == @session_server
  @th_clients[th] = ThreadClient.new((@tc_id += 1), @q_evt, Queue.new, th)
end

#deactivate

[ GitHub ]

  
# File 'lib/debug/session.rb', line 220

def deactivate
  get_thread_client.deactivate
  @thread_stopper.disable
  @tp_load_script.disable
  @tp_thread_begin.disable
  @tp_thread_end.disable
  @bps.each_value{|bp| bp.disable}
  @th_clients.each_value{|thc| thc.close}
  @tracers.values.each{|t| t.disable}
  @q_evt.close
  @ui&.deactivate
  @ui = nil
end

#deactivate_irb_integration

[ GitHub ]

  
# File 'lib/debug/irb_integration.rb', line 29

def deactivate_irb_integration
  Reline.completion_proc = nil
  Reline.output_modifier_proc = nil
  Reline.autocompletion = false
  Reline.dig_perfect_match_proc = nil
  reset_ui UI_LocalConsole.new
end

#delete_bp(arg = nil)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1413

def delete_bp arg = nil
  case arg
  when nil
    @bps.each{|key, bp| bp.delete}
    @bps.clear
  else
    del_bp = nil
    iterate_bps{|key, bp, i| del_bp = bp if i == arg}
    if del_bp
      del_bp.delete
      @bps.delete del_bp.key
      return [arg, del_bp]
    end
  end
end

#enter_postmortem_session(exc)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1887

def enter_postmortem_session exc
  return unless exc.instance_variable_defined? :@__debugger_postmortem_frames

  frames = exc.instance_variable_get(:@__debugger_postmortem_frames)
  @postmortem = true
  ThreadClient.current.suspend :postmortem, postmortem_frames: frames, postmortem_exc: exc
ensure
  @postmortem = false
end

#enter_subsession (private)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1709

private def enter_subsession
  @subsession_id += 1
  if !@subsession_stack.empty?
    DEBUGGER__.debug{ "Enter subsession (nested #{@subsession_stack.size})" }
  else
    DEBUGGER__.debug{ "Enter subsession" }
    stop_all_threads
    @process_group.lock
  end

  @subsession_stack << true
end

#extend_feature(session: nil, thread_client: nil, ui: nil)

experimental API

[ GitHub ]

  
# File 'lib/debug/session.rb', line 2022

def extend_feature session: nil, thread_client: nil, ui: nil
  Session.include session if session
  ThreadClient.include thread_client if thread_client
  @ui.extend ui if ui
end

#fail_response(req, **kw)

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

[ GitHub ]

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

def fail_response req, **result
  @ui.respond_fail req, **result
  return :retry
end

#find_waiting_tc(id)

[ GitHub ]

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

def find_waiting_tc id
  @th_clients.each{|th, tc|
    return tc if tc.id == id && tc.waiting?
  }
  return nil
end

#get_thread_client(th = Thread.current)

can be called by other threads

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1652

def get_thread_client th = Thread.current
  if @th_clients.has_key? th
    @th_clients[th]
  else
    if Thread.current == @session_server
      create_thread_client th
    else
      ask_thread_client th
    end
  end
end

#get_type(obj)

FIXME: unify this method with ThreadClient#propertyDescriptor.

[ GitHub ]

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

def get_type obj
  case obj
  when Array
    ['object', 'array']
  when Hash
    ['object', 'map']
  when String
    ['string']
  when TrueClass, FalseClass
    ['boolean']
  when Symbol
    ['symbol']
  when Integer, Float
    ['number']
  when Exception
    ['object', 'error']
  else
    ['object']
  end
end

#inspect

[ GitHub ]

  
# File 'lib/debug/session.rb', line 399

def inspect
  "DEBUGGER__::SESSION"
end

#intercept_trap_sigint(flag, &b) (readonly)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1986

def intercept_trap_sigint flag, &b
  prev = @intercept_trap_sigint
  @intercept_trap_sigint = flag
  yield
ensure
  @intercept_trap_sigint = prev
end

#intercept_trap_sigint_end

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1999

def intercept_trap_sigint_end
  @intercept_trap_sigint = false
  prev, @intercepted_sigint_cmd = @intercepted_sigint_cmd, nil
  prev
end

#intercept_trap_sigint_start(prev)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1994

def intercept_trap_sigint_start prev
  @intercept_trap_sigint = true
  @intercepted_sigint_cmd = prev
end

#iterate_bps

breakpoint management

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1354

def iterate_bps
  deleted_bps = []
  i = 0
  @bps.each{|key, bp|
    if !bp.deleted?
      yield key, bp, i
      i += 1
    else
      deleted_bps << bp
    end
  }
ensure
  deleted_bps.each{|bp| @bps.delete bp}
end

#leave_subsession(type) (private)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1722

private def leave_subsession type
  raise '[BUG] leave_subsession: not entered' if @subsession_stack.empty?
  @subsession_stack.pop

  if @subsession_stack.empty?
    DEBUGGER__.debug{ "Leave subsession" }
    @process_group.unlock
    restart_all_threads
  else
    DEBUGGER__.debug{ "Leave subsession (nested #{@subsession_stack.size})" }
  end

  request_tc type if type
  @tc = nil
rescue Exception => e
  STDERR.puts PP.pp([e, e.backtrace], ''.dup)
  raise
end

#managed_thread_clients

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1596

def managed_thread_clients
  thcs, _unmanaged_ths = update_thread_list
  thcs
end

#method_added(tp)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1798

def method_added tp
  b = tp.binding

  if var_name = b.local_variables.first
    mid = b.local_variable_get(var_name)
    resolved = true

    @bps.each{|k, bp|
      case bp
      when MethodBreakpoint
        if bp.method.nil?
          if bp.sig_method_name == mid.to_s
            bp.try_enable(added: true)
          end
        end

        resolved = false if !bp.enabled?
      end
    }

    if resolved
      Session.deactivate_method_added_trackers
    end

    case mid
    when :method_added, :singleton_method_added
      Session.create_method_added_tracker(tp.self, mid)
      Session.activate_method_added_trackers unless resolved
    end
  end
end

#on_load(iseq, src)

event

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1747

def on_load iseq, src
  DEBUGGER__.info "Load #{iseq.absolute_path || iseq.path}"

  file_path, reloaded = @sr.add(iseq, src)
  @ui.event :load, file_path, reloaded

  # check breakpoints
  if file_path
    @bps.find_all do |_key, bp|
      LineBreakpoint === bp && bp.path_is?(file_path) && (iseq.first_lineno..iseq.last_line).cover?(bp.line)
    end.each do |_key, bp|
      if !bp.iseq
        bp.try_activate iseq
      elsif reloaded
        @bps.delete bp.key # to allow duplicate

        # When we delete a breakpoint from the @bps hash, we also need to deactivate it or else its tracepoint event
        # will continue to be enabled and we'll suspend on ghost breakpoints
        bp.delete

        nbp = LineBreakpoint.copy(bp, iseq)
        add_bp nbp
      end
    end
  else # !file_path => file_path is not existing
    @bps.find_all do |_key, bp|
      LineBreakpoint === bp && !bp.iseq && DEBUGGER__.compare_path(bp.path, (iseq.absolute_path || iseq.path))
    end.each do |_key, bp|
      bp.try_activate iseq
    end
  end
end

#on_thread_begin(th)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1627

def on_thread_begin th
  if @th_clients.has_key? th
    # TODO: NG?
  else
    create_thread_client th
  end
end

#parse_break(type, arg) (private)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1431

private def parse_break type, arg
  mode = :sig
  expr = Hash.new{|h, k| h[k] = []}
  arg.split(' ').each{|w|
    if BREAK_KEYWORDS.any?{|pat| w == pat}
      mode = w[0..-2].to_sym
    else
      expr[mode] << w
    end
  }
  expr.default_proc = nil
  expr = expr.transform_values{|v| v.join(' ')}

  if (path = expr[:path]) && path =~ /\A\/(.*)\/\z/
    expr[:path] = Regexp.compile($1)
  end

  if expr[:do] || expr[:pre]
    check_unsafe
    expr[:cmd] = [type, expr[:pre], expr[:do]]
  end

  expr
end

#pop_event

[ GitHub ]

  
# File 'lib/debug/session.rb', line 249

def pop_event
  @q_evt.pop
end

#process_command(line)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1145

def process_command line
  if line.empty?
    if @repl_prev_line
      line = @repl_prev_line
    else
      return :retry
    end
  else
    @repl_prev_line = line
  end

  /([^\s])(?:\s(.+))?/ =~ line
  cmd_name, cmd_arg = $1, $2

  if cmd = @commands[cmd_name]
    check_postmortem      if !cmd.postmortem
    check_unsafe          if cmd.unsafe
    cancel_auto_continue  if cmd.cancel_auto_continue
    @repl_prev_line = nil if !cmd.repeat

    cmd.block.call(cmd_arg)
  else
    @repl_prev_line = nil
    check_unsafe

    request_eval :pp, line
  end

rescue Interrupt
  return :retry
rescue SystemExit
  raise
rescue PostmortemError => e
  @ui.puts e.message
  return :retry
rescue Exception => e
  @ui.puts "[REPL ERROR] #{e.inspect}"
  @ui.puts e.backtrace.map{|e| '  ' + e}
  return :retry
end

#process_event(evt)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 274

def process_event evt
  # variable `@internal_info` is only used for test
  tc, output, ev, @internal_info, *ev_args = evt

  output.each{|str| @ui.puts str} if ev != :suspend

  # special event, tc is nil
  # and we don't want to set @tc to the newly created thread's ThreadClient
  if ev == :thread_begin
    th = ev_args.shift
    q = ev_args.shift
    on_thread_begin th
    q << true

    return
  end

  @tc = tc

  case ev
  when :init
    enter_subsession
    wait_command_loop
  when :load
    iseq, src = ev_args
    on_load iseq, src
    request_tc :continue

  when :trace
    trace_id, msg = ev_args
    if t = @tracers.values.find{|t| t.object_id == trace_id}
      t.puts msg
    end
    request_tc :continue

  when :suspend
    enter_subsession if ev_args.first != :replay
    output.each{|str| @ui.puts str} unless @ui.ignore_output_on_suspend?

    case ev_args.first
    when :breakpoint
      bp, i = bp_index ev_args[1]
      clean_bps unless bp
      @ui.event :suspend_bp, i, bp, @tc.id
    when :trap
      @ui.event :suspend_trap, sig = ev_args[1], @tc.id

      if sig == :SIGINT && (@intercepted_sigint_cmd.kind_of?(Proc) || @intercepted_sigint_cmd.kind_of?(String))
        @ui.puts "#{@intercepted_sigint_cmd.inspect} is registered as SIGINT handler."
        @ui.puts "`sigint` command execute it."
      end
    else
      @ui.event :suspended, @tc.id
    end

    if @displays.empty?
      wait_command_loop
    else
      request_eval :display, @displays
    end
  when :result
    raise "[BUG] not in subsession" if @subsession_stack.empty?

    case ev_args.first
    when :try_display
      failed_results = ev_args[1]
      if failed_results.size > 0
        i, _msg = failed_results.last
        if i+1 == @displays.size
          @ui.puts "canceled: #{@displays.pop}"
        end
      end

      stop_all_threads
    when :method_breakpoint, :watch_breakpoint
      bp = ev_args[1]
      if bp
        add_bp(bp)
        show_bps bp
      else
        # can't make a bp
      end
    when :trace_pass
      obj_id = ev_args[1]
      obj_inspect = ev_args[2]
      opt = ev_args[3]
      add_tracer ObjectTracer.new(@ui, obj_id, obj_inspect, **opt)
      stop_all_threads
    else
      stop_all_threads
    end

    wait_command_loop

  when :protocol_result
    process_protocol_result ev_args
    wait_command_loop
  end
end

#process_info

[ GitHub ]

  
# File 'lib/debug/session.rb', line 2005

def process_info
  if @process_group.multi?
    "#{$0}\##{Process.pid}"
  end
end

#process_protocol_request(req)

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

[ GitHub ]

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

def process_protocol_request req
  case req['method']
  when 'Debugger.stepOver', 'Debugger.stepInto', 'Debugger.stepOut', 'Debugger.resume', 'Debugger.enable'
    request_tc [:cdp, :backtrace, req]
  when 'Debugger.evaluateOnCallFrame'
    frame_id = req.dig('params', 'callFrameId')
    group = req.dig('params', 'objectGroup')
    if fid = @frame_map[frame_id]
      expr = req.dig('params', 'expression')
      request_tc [:cdp, :evaluate, req, fid, expr, group]
    else
      fail_response req,
                    code: INVALID_PARAMS,
                    message: "'callFrameId' is an invalid"
    end
  when 'Runtime.getProperties', 'Runtime.getExceptionDetails'
    oid = req.dig('params', 'objectId') || req.dig('params', 'errorObjectId')
    if ref = @obj_map[oid]
      case ref[0]
      when 'local'
        frame_id = ref[1]
        fid = @frame_map[frame_id]
        request_tc [:cdp, :scope, req, fid]
      when 'global'
        vars = safe_global_variables.sort.map do |name|
          begin
            gv = eval(name.to_s)
          rescue Errno::ENOENT
            gv = nil
          end
          prop = {
            name: name,
            value: {
              description: gv.inspect
            },
            configurable: true,
            enumerable: true
          }
          type, subtype = get_type(gv)
          prop[:value][:type] = type
          prop[:value][:subtype] = subtype if subtype
          prop
        end

        @ui.respond req, result: vars
        return :retry
      when 'properties'
        request_tc [:cdp, :properties, req, oid]
      when 'exception'
        request_tc [:cdp, :exception, req, oid]
      when 'script'
        # TODO: Support script and global types
        @ui.respond req, result: []
        return :retry
      else
        raise "Unknown type: #{ref.inspect}"
      end
    else
      fail_response req,
                    code: INVALID_PARAMS,
                    message: "'objectId' is an invalid"
    end
  when 'Debugger.getScriptSource'
    s_id = req.dig('params', 'scriptId')
    if src = @src_map[s_id]
      @ui.respond req, scriptSource: src
    else
      fail_response req,
                    code: INVALID_PARAMS,
                    message: "'scriptId' is an invalid"
    end
    return :retry
  when 'Debugger.getPossibleBreakpoints'
    s_id = req.dig('params', 'start', 'scriptId')
    if src = @src_map[s_id]
      lineno = req.dig('params', 'start', 'lineNumber')
      end_line = src.lines.count
      lineno = end_line  if lineno > end_line
      @ui.respond req,
                  locations: [{
                    scriptId: s_id,
                    lineNumber: lineno
                  }]
    else
      fail_response req,
                    code: INVALID_PARAMS,
                    message: "'scriptId' is an invalid"
    end
    return :retry
  when 'Debugger.setBreakpointByUrl'
    path = req.dig('params', 'scriptId')
    if s_id = @scr_id_map[path]
      lineno = req.dig('params', 'lineNumber')
      b_id = req.dig('params', 'breakpointId')
      @ui.respond req,
                  breakpointId: b_id,
                  locations: [{
                      scriptId: s_id,
                      lineNumber: lineno
                  }]
    else
      fail_response req,
                    code: INTERNAL_ERROR,
                    message: 'The target script is not found...'
    end
    return :retry
  end
end

#process_protocol_result(args)

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

[ GitHub ]

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

def process_protocol_result args
  type, req, result = args

  case type
  when :backtrace
    result[:callFrames].each.with_index do |frame, i|
      frame_id = frame[:callFrameId]
      @frame_map[frame_id] = i
      path = frame[:url]
      unless s_id = @scr_id_map[path]
        s_id = (@scr_id_map.size + 1).to_s
        @scr_id_map[path] = s_id
        lineno = 0
        src = ''
        if path && File.exist?(path)
          src = File.read(path)
          @src_map[s_id] = src
          lineno = src.lines.count
        end
        @ui.fire_event 'Debugger.scriptParsed',
                      scriptId: s_id,
                      url: path,
                      startLine: 0,
                      startColumn: 0,
                      endLine: lineno,
                      endColumn: 0,
                      executionContextId: 1,
                      hash: src.hash.inspect
      end
      frame[:location][:scriptId] = s_id
      frame[:functionLocation][:scriptId] = s_id

      frame[:scopeChain].each {|s|
        oid = s.dig(:object, :objectId)
        @obj_map[oid] = [s[:type], frame_id]
      }
    end

    if oid = result.dig(:data, :objectId)
      @obj_map[oid] = ['properties']
    end
    @ui.fire_event 'Debugger.paused', **result
  when :evaluate
    message = result.delete :message
    if message
      fail_response req,
                    code: INVALID_PARAMS,
                    message: message
    else
      src = req.dig('params', 'expression')
      s_id = (@src_map.size + 1).to_s
      @src_map[s_id] = src
      lineno = src.lines.count
      @ui.fire_event 'Debugger.scriptParsed',
                        scriptId: s_id,
                        url: '',
                        startLine: 0,
                        startColumn: 0,
                        endLine: lineno,
                        endColumn: 0,
                        executionContextId: 1,
                        hash: src.hash.inspect
      if exc = result.dig(:response, :exceptionDetails)
        exc[:stackTrace][:callFrames].each{|frame|
          if frame[:url].empty?
            frame[:scriptId] = s_id
          else
            path = frame[:url]
            unless s_id = @scr_id_map[path]
              s_id = (@scr_id_map.size + 1).to_s
              @scr_id_map[path] = s_id
            end
            frame[:scriptId] = s_id
          end
        }
        if oid = exc[:exception][:objectId]
          @obj_map[oid] = ['exception']
        end
      end
      rs = result.dig(:response, :result)
      [rs].each{|obj|
        if oid = obj[:objectId]
          @obj_map[oid] = ['properties']
        end
      }
      @ui.respond req, **result[:response]

      out = result[:output]
      if out && !out.empty?
        @ui.fire_event 'Runtime.consoleAPICalled',
                        type: 'log',
                        args: [
                          type: out.class,
                          value: out
                        ],
                        executionContextId: 1, # Change this number if something goes wrong.
                        timestamp: Time.now.to_f
      end
    end
  when :scope
    result.each{|obj|
      if oid = obj.dig(:value, :objectId)
        @obj_map[oid] = ['properties']
      end
    }
    @ui.respond req, result: result
  when :properties
    result.each_value{|v|
      v.each{|obj|
        if oid = obj.dig(:value, :objectId)
          @obj_map[oid] = ['properties']
        end
      }
    }
    @ui.respond req, **result
  when :exception
    @ui.respond req, **result
  end
end

#prompt

[ GitHub ]

  
# File 'lib/debug/session.rb', line 417

def prompt
  if @postmortem
    '(rdbg:postmortem) '
  elsif @process_group.multi?
    "(rdbg@#{process_info}) "
  else
    '(rdbg) '
  end
end

#register_command(*names, repeat: false, unsafe: true, cancel_auto_continue: false, postmortem: true, &b) (private)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 458

private def register_command *names,
                             repeat: false, unsafe: true, cancel_auto_continue: false, postmortem: true,
                             &b
  cmd = SessionCommand.new(b, repeat, unsafe, cancel_auto_continue, postmortem)

  names.each{|name|
    @commands[name] = cmd
  }
end

#register_default_command

[ GitHub ]

  
# File 'lib/debug/session.rb', line 468

def register_default_command
  ### Control flow

  # * `s[tep]`
  #   * Step in. Resume the program until next breakable point.
  # * `s[tep] <n>`
  #   * Step in, resume the program at `<n>`th breakable point.
  register_command 's', 'step',
                   repeat: true,
                   cancel_auto_continue: true,
                   postmortem: false do |arg|
    step_command :in, arg
  end

  # * `n[ext]`
  #   * Step over. Resume the program until next line.
  # * `n[ext] <n>`
  #   * Step over, same as `step <n>`.
  register_command 'n', 'next',
                   repeat: true,
                   cancel_auto_continue: true,
                   postmortem: false do |arg|
    step_command :next, arg
  end

  # * `fin[ish]`
  #   * Finish this frame. Resume the program until the current frame is finished.
  # * `fin[ish] <n>`
  #   * Finish `<n>`th frames.
  register_command 'fin', 'finish',
                   repeat: true,
                   cancel_auto_continue: true,
                   postmortem: false do |arg|
    if arg&.to_i == 0
      raise 'finish command with 0 does not make sense.'
    end

    step_command :finish, arg
  end

  # * `u[ntil]`
  #   * Similar to `next` command, but only stop later lines or the end of the current frame.
  #   * Similar to gdb's `advance` command.
  # * `u[ntil] <[file:]line>`
  #   * Run til the program reaches given location or the end of the current frame.
  # * `u[ntil] <name>`
  #   * Run til the program invokes a method `<name>`. `<name>` can be a regexp with `/name/`.
  register_command 'u', 'until',
                   repeat: true,
                   cancel_auto_continue: true,
                   postmortem: false do |arg|

    step_command :until, arg
  end

  # * `c` or `cont` or `continue`
  #   * Resume the program.
  register_command 'c', 'cont', 'continue',
                   repeat: true,
                   cancel_auto_continue: true do |arg|
    leave_subsession :continue
  end

  # * `q[uit]` or `Ctrl-D`
  #   * Finish debugger (with the debuggee process on non-remote debugging).
  register_command 'q', 'quit' do |arg|
    if ask 'Really quit?'
      @ui.quit arg.to_i do
        request_tc :quit
      end
      leave_subsession :continue
    else
      next :retry
    end
  end

  # * `q[uit]!`
  #   * Same as q[uit] but without the confirmation prompt.
  register_command 'q!', 'quit!', unsafe: false do |arg|
    @ui.quit arg.to_i do
      request_tc :quit
    end
    leave_subsession :continue
  end

  # * `kill`
  #   * Stop the debuggee process with `Kernel#exit!`.
  register_command 'kill' do |arg|
    if ask 'Really kill?'
      exit! (arg || 1).to_i
    else
      next :retry
    end
  end

  # * `kill!`
  #   * Same as kill but without the confirmation prompt.
  register_command 'kill!', unsafe: false do |arg|
    exit! (arg || 1).to_i
  end

  # * `sigint`
  #   * Execute SIGINT handler registered by the debuggee.
  #   * Note that this command should be used just after stop by `SIGINT`.
  register_command 'sigint' do
    begin
      case cmd = @intercepted_sigint_cmd
      when nil, 'IGNORE', :IGNORE, 'DEFAULT', :DEFAULT
        # ignore
      when String
        eval(cmd)
      when Proc
        cmd.call
      end

      leave_subsession :continue

    rescue Exception => e
      @ui.puts "Exception: #{e}"
      @ui.puts e.backtrace.map{|line| "  #{e}"}
      next :retry
    end
  end

  ### Breakpoint

  # * `b[reak]`
  #   * Show all breakpoints.
  # * `b[reak] <line>`
  #   * Set breakpoint on `<line>` at the current frame's file.
  # * `b[reak] <file>:<line>` or `<file> <line>`
  #   * Set breakpoint on `<file>:<line>`.
  # * `b[reak] <class>#<name>`
  #    * Set breakpoint on the method `<class>#<name>`.
  # * `b[reak] <expr>.<name>`
  #    * Set breakpoint on the method `<expr>.<name>`.
  # * `b[reak] ... if: <expr>`
  #   * break if `<expr>` is true at specified location.
  # * `b[reak] ... pre: <command>`
  #   * break and run `<command>` before stopping.
  # * `b[reak] ... do: <command>`
  #   * break and run `<command>`, and continue.
  # * `b[reak] ... path: <path>`
  #   * break if the path matches to `<path>`. `<path>` can be a regexp with `/regexp/`.
  # * `b[reak] if: <expr>`
  #   * break if: `<expr>` is true at any lines.
  #   * Note that this feature is super slow.
  register_command 'b', 'break', postmortem: false, unsafe: false do |arg|
    if arg == nil
      show_bps
      next :retry
    else
      case bp = repl_add_breakpoint(arg)
      when :noretry
      when nil
        next :retry
      else
        show_bps bp
        next :retry
      end
    end
  end

  # * `catch <Error>`
  #   * Set breakpoint on raising `<Error>`.
  # * `catch ... if: <expr>`
  #   * stops only if `<expr>` is true as well.
  # * `catch ... pre: <command>`
  #   * runs `<command>` before stopping.
  # * `catch ... do: <command>`
  #   * stops and run `<command>`, and continue.
  # * `catch ... path: <path>`
  #   * stops if the exception is raised from a `<path>`. `<path>` can be a regexp with `/regexp/`.
  register_command 'catch', postmortem: false, unsafe: false do |arg|
    if arg
      bp = repl_add_catch_breakpoint arg
      show_bps bp if bp
    else
      show_bps
    end

    :retry
  end

  # * `watch @ivar`
  #   * Stop the execution when the result of current scope's `@ivar` is changed.
  #   * Note that this feature is super slow.
  # * `watch ... if: <expr>`
  #   * stops only if `<expr>` is true as well.
  # * `watch ... pre: <command>`
  #   * runs `<command>` before stopping.
  # * `watch ... do: <command>`
  #   * stops and run `<command>`, and continue.
  # * `watch ... path: <path>`
  #   * stops if the path matches `<path>`. `<path>` can be a regexp with `/regexp/`.
  register_command 'wat', 'watch', postmortem: false, unsafe: false do |arg|
    if arg && arg.match?(/\A@\w+/)
      repl_add_watch_breakpoint(arg)
    else
      show_bps
      :retry
    end
  end

  # * `del[ete]`
  #   * delete all breakpoints.
  # * `del[ete] <bpnum>`
  #   * delete specified breakpoint.
  register_command 'del', 'delete', postmortem: false, unsafe: false do |arg|
    case arg
    when nil
      show_bps
      if ask "Remove all breakpoints?", 'N'
        delete_bp
      end
    when /\d+/
      bp = delete_bp arg.to_i
    else
      nil
    end
    @ui.puts "deleted: \##{bp[0]} #{bp[1]}" if bp
    :retry
  end

  ### Information

  # * `bt` or `backtrace`
  #   * Show backtrace (frame) information.
  # * `bt <num>` or `backtrace <num>`
  #   * Only shows first `<num>` frames.
  # * `bt /regexp/` or `backtrace /regexp/`
  #   * Only shows frames with method name or location info that matches `/regexp/`.
  # * `bt <num> /regexp/` or `backtrace <num> /regexp/`
  #   * Only shows first `<num>` frames with method name or location info that matches `/regexp/`.
  register_command 'bt', 'backtrace', unsafe: false do |arg|
    case arg
    when /\A(\d+)\z/
      request_tc_with_restarted_threads [:show, :backtrace, arg.to_i, nil]
    when /\A\/(.*)\/\z/
      pattern = $1
      request_tc_with_restarted_threads [:show, :backtrace, nil, Regexp.compile(pattern)]
    when /\A(\d)\s\/(.*)\/\z/
      max, pattern = $1, $2
      request_tc_with_restarted_threads [:show, :backtrace, max.to_i, Regexp.compile(pattern)]
    else
      request_tc_with_restarted_threads [:show, :backtrace, nil, nil]
    end
  end

  # * `l[ist]`
  #   * Show current frame's source code.
  #   * Next `list` command shows the successor lines.
  # * `l[ist] -`
  #   * Show predecessor lines as opposed to the `list` command.
  # * `l[ist] <start>` or `l[ist] <start>-<end>`
  #   * Show current frame's source code from the line <start> to <end> if given.
  register_command 'l', 'list', repeat: true, unsafe: false do |arg|
    case arg ? arg.strip : nil
    when /\A(\d+)\z/
      request_tc [:show, :list, {start_line: arg.to_i - 1}]
    when /\A-\z/
      request_tc [:show, :list, {dir: -1}]
    when /\A(\d)-(\d)\z/
      request_tc [:show, :list, {start_line: $1.to_i - 1, end_line: $2.to_i}]
    when nil
      request_tc [:show, :list]
    else
      @ui.puts "Can not handle list argument: #{arg}"
      :retry
    end
  end

  # * `whereami`
  #   * Show the current frame with source code.
  register_command 'whereami', unsafe: false do
    request_tc [:show, :whereami]
  end

  # * `edit`
  #   * Open the current file on the editor (use `EDITOR` environment variable).
  #   * Note that edited file will not be reloaded.
  # * `edit <file>`
  #   * Open <file> on the editor.
  register_command 'edit' do |arg|
    if @ui.remote?
      @ui.puts "not supported on the remote console."
      next :retry
    end

    begin
      arg = resolve_path(arg) if arg
    rescue Errno::ENOENT
      @ui.puts "not found: #{arg}"
      next :retry
    end

    request_tc [:show, :edit, arg]
  end

  info_subcommands = nil
  info_subcommands_abbrev = nil

  # * `i[nfo]`
  #   * Show information about current frame (local/instance variables and defined constants).
  # * `i[nfo]` <subcommand>
  #   * `info` has the following sub-commands.
  #   * Sub-commands can be specified with few letters which is unambiguous, like `l` for 'locals'.
  # * `i[nfo] l or locals or local_variables`
  #   * Show information about the current frame (local variables)
  #   * It includes `self` as `%self` and a return value as `_return`.
  # * `i[nfo] i or ivars or instance_variables`
  #   * Show information about instance variables about `self`.
  #   * `info ivars <expr>` shows the instance variables of the result of `<expr>`.
  # * `i[nfo] c or consts or constants`
  #   * Show information about accessible constants except toplevel constants.
  #   * `info consts <expr>` shows the constants of a class/module of the result of `<expr>`
  # * `i[nfo] g or globals or global_variables`
  #   * Show information about global variables
  # * `i[nfo] th or threads`
  #   * Show all threads (same as `th[read]`).
  # * `i[nfo] b or breakpoints or w or watchpoints`
  #   * Show all breakpoints and watchpoints.
  # * `i[nfo] ... /regexp/`
  #   * Filter the output with `/regexp/`.
  register_command 'i', 'info', unsafe: false do |arg|
    if /\/(.+)\/\z/ =~ arg
      pat = Regexp.compile($1)
      sub = $~.pre_match.strip
    else
      sub = arg
    end

    if /\A(.?)\b(.)/ =~ sub
      sub = $1
      opt = $2.strip
      opt = nil if opt.empty?
    end

    if sub && !info_subcommands
      info_subcommands = {
        locals: %w[ locals local_variables ],
        ivars:  %w[ ivars instance_variables ],
        consts: %w[ consts constants ],
        globals:%w[ globals global_variables ],
        threads:%w[ threads ],
        breaks: %w[ breakpoints ],
        watchs: %w[ watchpoints ],
      }

      require_relative 'abbrev_command'
      info_subcommands_abbrev = AbbrevCommand.new(info_subcommands)
    end

    if sub
      sub = info_subcommands_abbrev.search sub, :unknown do |candidates|
        # note: unreached now
        @ui.puts "Ambiguous command '#{sub}': #{candidates.join(' ')}"
      end
    end

    case sub
    when nil
      request_tc_with_restarted_threads [:show, :default, pat] # something useful
    when :locals
      request_tc_with_restarted_threads [:show, :locals, pat]
    when :ivars
      request_tc_with_restarted_threads [:show, :ivars, pat, opt]
    when :consts
      request_tc_with_restarted_threads [:show, :consts, pat, opt]
    when :globals
      request_tc_with_restarted_threads [:show, :globals, pat]
    when :threads
      thread_list
      :retry
    when :breaks, :watchs
      show_bps
      :retry
    else
      @ui.puts "unrecognized argument for info command: #{arg}"
      show_help 'info'
      :retry
    end
  end

  # * `o[utline]` or `ls`
  #   * Show you available methods, constants, local variables, and instance variables in the current scope.
  # * `o[utline] <expr>` or `ls <expr>`
  #   * Show you available methods and instance variables of the given object.
  #   * If the object is a class/module, it also lists its constants.
  register_command 'outline', 'o', 'ls', unsafe: false do |arg|
    request_tc_with_restarted_threads [:show, :outline, arg]
  end

  # * `display`
  #   * Show display setting.
  # * `display <expr>`
  #   * Show the result of `<expr>` at every suspended timing.
  register_command 'display', postmortem: false do |arg|
    if arg && !arg.empty?
      @displays << arg
      request_eval :try_display, @displays
    else
      request_eval :display, @displays
    end
  end

  # * `undisplay`
  #   * Remove all display settings.
  # * `undisplay <displaynum>`
  #   * Remove a specified display setting.
  register_command 'undisplay', postmortem: false, unsafe: false do |arg|
    case arg
    when /(\d+)/
      if @displays[n = $1.to_i]
        @displays.delete_at n
      end
      request_eval :display, @displays
    when nil
      if ask "clear all?", 'N'
        @displays.clear
      end
      :retry
    end
  end

  ### Frame control

  # * `f[rame]`
  #   * Show the current frame.
  # * `f[rame] <framenum>`
  #   * Specify a current frame. Evaluation are run on specified frame.
  register_command 'frame', 'f', unsafe: false do |arg|
    request_tc [:frame, :set, arg]
  end

  # * `up`
  #   * Specify the upper frame.
  register_command 'up', repeat: true, unsafe: false do |arg|
    request_tc [:frame, :up]
  end

  # * `down`
  #   * Specify the lower frame.
  register_command 'down', repeat: true, unsafe: false do |arg|
    request_tc [:frame, :down]
  end

  ### Evaluate

  # * `p <expr>`
  #   * Evaluate like `p <expr>` on the current frame.
  register_command 'p' do |arg|
    request_eval :p, arg.to_s
  end

  # * `pp <expr>`
  #   * Evaluate like `pp <expr>` on the current frame.
  register_command 'pp' do |arg|
    request_eval :pp, arg.to_s
  end

  # * `eval <expr>`
  #   * Evaluate `<expr>` on the current frame.
  register_command 'eval', 'call' do |arg|
    if arg == nil || arg.empty?
      show_help 'eval'
      @ui.puts "\nTo evaluate the variable `#{cmd}`, use `pp #{cmd}` instead."
      :retry
    else
      request_eval :call, arg
    end
  end

  # * `irb`
  #   * Activate and switch to `irb:rdbg` console
  register_command 'irb' do |arg|
    if @ui.remote?
      @ui.puts "\nIRB is not supported on the remote console."
    else
      config_set :irb_console, true
    end

    :retry
  end

  ### Trace
  # * `trace`
  #   * Show available tracers list.
  # * `trace line`
  #   * Add a line tracer. It indicates line events.
  # * `trace call`
  #   * Add a call tracer. It indicate call/return events.
  # * `trace exception`
  #   * Add an exception tracer. It indicates raising exceptions.
  # * `trace object <expr>`
  #   * Add an object tracer. It indicates that an object by `<expr>` is passed as a parameter or a receiver on method call.
  # * `trace ... /regexp/`
  #   * Indicates only matched events to `/regexp/`.
  # * `trace ... into: <file>`
  #   * Save trace information into: `<file>`.
  # * `trace off <num>`
  #   * Disable tracer specified by `<num>` (use `trace` command to check the numbers).
  # * `trace off [line|call|pass]`
  #   * Disable all tracers. If `<type>` is provided, disable specified type tracers.
  register_command 'trace', postmortem: false, unsafe: false do |arg|
    if (re = /\sinto:\s*(.)/) =~ arg
      into = $1
      arg.sub!(re, '')
    end

    if (re = /\s\/(.+)\/\z/) =~ arg
      pattern = $1
      arg.sub!(re, '')
    end

    case arg
    when nil
      @ui.puts 'Tracers:'
      @tracers.values.each_with_index{|t, i|
        @ui.puts "* \##{i} #{t}"
      }
      @ui.puts
      :retry

    when /\Aline\z/
      add_tracer LineTracer.new(@ui, pattern: pattern, into: into)
      :retry

    when /\Acall\z/
      add_tracer CallTracer.new(@ui, pattern: pattern, into: into)
      :retry

    when /\Aexception\z/
      add_tracer ExceptionTracer.new(@ui, pattern: pattern, into: into)
      :retry

    when /\Aobject\s(.)/
      request_tc_with_restarted_threads [:trace, :object, $1.strip, {pattern: pattern, into: into}]

    when /\Aoff\s(\d)\z/
      if t = @tracers.values[$1.to_i]
        t.disable
        @ui.puts "Disable #{t.to_s}"
      else
        @ui.puts "Unmatched: #{$1}"
      end
      :retry

    when /\Aoff(\s+(line|call|exception|object))?\z/
      @tracers.values.each{|t|
        if $2.nil? || t.type == $2
          t.disable
          @ui.puts "Disable #{t.to_s}"
        end
      }
      :retry

    else
      @ui.puts "Unknown trace option: #{arg.inspect}"
      :retry
    end
  end

  # Record
  # * `record`
  #   * Show recording status.
  # * `record [on|off]`
  #   * Start/Stop recording.
  # * `step back`
  #   * Start replay. Step back with the last execution log.
  #   * `s[tep]` does stepping forward with the last log.
  # * `step reset`
  #   * Stop replay .
  register_command 'record', postmortem: false, unsafe: false do |arg|
    case arg
    when nil, 'on', 'off'
      request_tc [:record, arg&.to_sym]
    else
      @ui.puts "unknown command: #{arg}"
      :retry
    end
  end

  ### Thread control

  # * `th[read]`
  #   * Show all threads.
  # * `th[read] <thnum>`
  #   * Switch thread specified by `<thnum>`.
  register_command 'th', 'thread', unsafe: false do |arg|
    case arg
    when nil, 'list', 'l'
      thread_list
    when /(\d+)/
      switch_thread $1.to_i
    else
      @ui.puts "unknown thread command: #{arg}"
    end
    :retry
  end

  ### Configuration
  # * `config`
  #   * Show all configuration with description.
  # * `config <name>`
  #   * Show current configuration of <name>.
  # * `config set <name> <val>` or `config <name> = <val>`
  #   * Set <name> to <val>.
  # * `config append <name> <val>` or `config <name> << <val>`
  #   * Append `<val>` to `<name>` if it is an array.
  # * `config unset <name>`
  #   * Set <name> to default.
  register_command 'config', unsafe: false do |arg|
    config_command arg
    :retry
  end

  # * `source <file>`
  #   * Evaluate lines in `<file>` as debug commands.
  register_command 'source' do |arg|
    if arg
      begin
        cmds = File.readlines(path = File.expand_path(arg))
        add_preset_commands path, cmds, kick: true, continue: false
      rescue Errno::ENOENT
        @ui.puts "File not found: #{arg}"
      end
    else
      show_help 'source'
    end
    :retry
  end

  # * `open`
  #   * open debuggee port on UNIX domain socket and wait for attaching.
  #   * Note that `open` command is EXPERIMENTAL.
  # * `open [<host>:]<port>`
  #   * open debuggee port on TCP/IP with given `[<host>:]<port>` and wait for attaching.
  # * `open vscode`
  #   * open debuggee port for VSCode and launch VSCode if available.
  # * `open chrome`
  #   * open debuggee port for Chrome and wait for attaching.
  register_command 'open' do |arg|
    case arg&.downcase
    when '', nil
      ::DEBUGGER__.open nonstop: true
    when /\A(\d+)z/
      ::DEBUGGER__.open_tcp host: nil, port: $1.to_i, nonstop: true
    when /\A(.):(\d)\z/
      ::DEBUGGER__.open_tcp host: $1, port: $2.to_i, nonstop: true
    when 'tcp'
      ::DEBUGGER__.open_tcp host: CONFIG[:host], port: (CONFIG[:port] || 0), nonstop: true
    when 'vscode'
      CONFIG[:open] = 'vscode'
      ::DEBUGGER__.open nonstop: true
    when 'chrome', 'cdp'
      CONFIG[:open] = 'chrome'
      ::DEBUGGER__.open_tcp host: CONFIG[:host], port: (CONFIG[:port] || 0), nonstop: true
    else
      raise "Unknown arg: #{arg}"
    end

    :retry
  end

  ### Help

  # * `h[elp]`
  #   * Show help for all commands.
  # * `h[elp] <command>`
  #   * Show help for the given command.
  register_command 'h', 'help', '?', unsafe: false do |arg|
    show_help arg
    :retry
  end
end

#register_var(v, tid)

[ GitHub ]

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

def register_var v, tid
  if (tl_vid = v[:variablesReference]) > 0
    vid = @var_map.size + 1
    @var_map[vid] = [:variable, tid, tl_vid]
    v[:variablesReference] = vid
  end
end

#register_vars(vars, tid)

[ GitHub ]

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

def register_vars vars, tid
  raise tid.inspect unless tid.kind_of?(Integer)
  vars.each{|v|
    register_var v, tid
  }
end

#rehash_bps

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1384

def rehash_bps
  bps = @bps.values
  @bps.clear
  bps.each{|bp|
    add_bp bp
  }
end

#repl_add_breakpoint(arg)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1456

def repl_add_breakpoint arg
  expr = parse_break 'break', arg.strip
  cond = expr[:if]
  cmd  = expr[:cmd]
  path = expr[:path]

  case expr[:sig]
  when /\A(\d+)\z/
    add_line_breakpoint @tc.location.path, $1.to_i, cond: cond, command: cmd
  when /\A(.)[:\s](\d+)\z/
    add_line_breakpoint $1, $2.to_i, cond: cond, command: cmd
  when /\A(.)([\.\#])(.)\z/
    request_tc [:breakpoint, :method, $1, $2, $3, cond, cmd, path]
    return :noretry
  when nil
    add_check_breakpoint cond, path, cmd
  else
    @ui.puts "Unknown breakpoint format: #{arg}"
    @ui.puts
    show_help 'b'
  end
end

#repl_add_catch_breakpoint(arg)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1479

def repl_add_catch_breakpoint arg
  expr = parse_break 'catch', arg.strip
  cond = expr[:if]
  cmd  = expr[:cmd]
  path = expr[:path]

  bp = CatchBreakpoint.new(expr[:sig], cond: cond, command: cmd, path: path)
  add_bp bp
end

#repl_add_watch_breakpoint(arg)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1489

def repl_add_watch_breakpoint arg
  expr = parse_break 'watch', arg.strip
  cond = expr[:if]
  cmd  = expr[:cmd]
  path = Regexp.compile(expr[:path]) if expr[:path]

  request_tc [:breakpoint, :watch, expr[:sig], cond, cmd, path]
end

#request_eval(type, src)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 270

def request_eval type, src
  request_tc_with_restarted_threads [:eval, type, src]
end

#request_tc(req)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 261

def request_tc(req)
  @tc << req
end

#request_tc_with_restarted_threads(req)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 265

def request_tc_with_restarted_threads(req)
  restart_all_threads
  request_tc(req)
end

#reset_ui(ui)

[ GitHub ]

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

def reset_ui ui
  @ui.deactivate
  @ui = ui

  # activate new ui
  @tp_thread_begin.disable
  @tp_thread_end.disable
  @ui.activate self
  if @ui.respond_to?(:reader_thread) && thc = get_thread_client(@ui.reader_thread)
    thc.mark_as_management
  end
  @tp_thread_begin.enable
  @tp_thread_end.enable
end

#resolve_path(file)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1780

def resolve_path file
  File.realpath(File.expand_path(file))
rescue Errno::ENOENT
  case file
  when '-e', '-'
    return file
  else
    $LOAD_PATH.each do |lp|
      libpath = File.join(lp, file)
      return File.realpath(libpath)
    rescue Errno::ENOENT
      # next
    end
  end

  raise
end

#restart_all_threads (private)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1699

private def restart_all_threads
  stopper = @thread_stopper
  stopper.disable if stopper.enabled?

  waiting_thread_clients.each{|tc|
    next if @tc == tc
    tc << :continue
  }
end

#running_thread_clients_count (private)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1664

private def running_thread_clients_count
  @th_clients.count{|th, tc|
    next if tc.management?
    next unless tc.running?
    true
  }
end

#save_int_trap(cmd)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1977

def save_int_trap cmd
  prev, @intercepted_sigint_cmd = @intercepted_sigint_cmd, cmd
  prev
end

#session_server_main

[ GitHub ]

  
# File 'lib/debug/session.rb', line 253

def session_server_main
  while evt = pop_event
    process_event evt
  end
ensure
  deactivate
end

#set_no_sigint_hook(old, new)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1966

def set_no_sigint_hook old, new
  return unless old != new
  return unless @ui.respond_to? :activate_sigint

  if old # no -> yes
    @ui.activate_sigint
  else
    @ui.deactivate_sigint
  end
end

#setup_threads

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1614

def setup_threads
  prev_clients = @th_clients
  @th_clients = {}

  Thread.list.each{|th|
    if tc = prev_clients[th]
      @th_clients[th] = tc
    else
      create_thread_client(th)
    end
  }
end

#show_bps(specific_bp = nil)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1369

def show_bps specific_bp = nil
  iterate_bps do |key, bp, i|
    @ui.puts "#%d %s" % [i, bp.to_s] if !specific_bp || bp == specific_bp
  end
end

#show_help(arg = nil)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1294

def show_help arg = nil
  instructions = (DEBUGGER__.commands.keys + DEBUGGER__.commands.values).uniq
  print_instructions = proc do |desc|
    desc.split("\n").each do |line|
      next if line.start_with?(" ") # workaround for step back
      formatted_line = line.gsub(/[\[\]\*]/, "").strip
      instructions.each do |inst|
        if formatted_line.start_with?("`#{inst}")
          desc.sub!(line, colorize(line, [:CYAN, :BOLD]))
        end
      end
    end
    @ui.puts desc
  end

  print_category = proc do |cat|
    @ui.puts "\n"
    @ui.puts colorize("### #{cat}", [:GREEN, :BOLD])
    @ui.puts "\n"
  end

  DEBUGGER__.helps.each { |cat, cs|
    # categories
    if arg.nil?
      print_category.call(cat)
    else
      cs.each { |ws, _|
        if ws.include?(arg)
          print_category.call(cat)
          break
        end
      }
    end

    # instructions
    cs.each { |ws, desc|
      if arg.nil? || ws.include?(arg)
        print_instructions.call(desc.dup)
        return if arg
      end
    }
  }

  @ui.puts "not found: #{arg}" if arg
end

#source(iseq)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 391

def source iseq
  if !CONFIG[:no_color]
    @sr.get_colored(iseq)
  else
    @sr.get(iseq)
  end
end

#step_command(type, arg)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1186

def step_command type, arg
  if type == :until
    leave_subsession [:step, type, arg]
    return
  end

  case arg
  when nil, /\A\d+\z/
    if type == :in && @tc.recorder&.replaying?
      request_tc [:step, type, arg&.to_i]
    else
      leave_subsession [:step, type, arg&.to_i]
    end
  when /\A(back)\z/, /\A(back)\s(\d)\z/, /\A(reset)\z/
    if type != :in
      @ui.puts "only `step #{arg}` is supported."
      :retry
    else
      type = $1.to_sym
      iter = $2&.to_i
      request_tc [:step, type, iter]
    end
  else
    @ui.puts "Unknown option: #{arg}"
    :retry
  end
end

#stop_all_threads (private)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1692

private def stop_all_threads
  return if running_thread_clients_count == 0

  stopper = @thread_stopper
  stopper.enable unless stopper.enabled?
end

#stop_stepping?(file, line, subsession_id = nil) ⇒ Boolean

[ GitHub ]

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

def stop_stepping? file, line, subsession_id = nil
  if @bps.has_key? [file, line]
    true
  elsif subsession_id && @subsession_id != subsession_id
    true
  else
    false
  end
end

#switch_thread(n)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1601

def switch_thread n
  thcs, _unmanaged_ths = update_thread_list

  if tc = thcs[n]
    if tc.waiting?
      @tc = tc
    else
      @ui.puts "#{tc.thread} is not controllable yet."
    end
  end
  thread_list
end

#thread_list

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1582

def thread_list
  thcs, unmanaged_ths = update_thread_list
  thcs.each_with_index{|thc, i|
    @ui.puts "#{@tc == thc ? "--> " : "    "}\##{i} #{thc}"
  }

  if !unmanaged_ths.empty?
    @ui.puts "The following threads are not managed yet by the debugger:"
    unmanaged_ths.each{|th|
      @ui.puts "     " + th.to_s
    }
  end
end

#thread_stopper (private)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1680

private def thread_stopper
  TracePoint.new(:line) do
    # run on each thread
    tc = ThreadClient.current
    next if tc.management?
    next unless tc.running?
    next if tc == @tc

    tc.on_pause
  end
end

#update_thread_list

threads

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1564

def update_thread_list
  list = Thread.list
  thcs = []
  unmanaged = []

  list.each{|th|
    if thc = @th_clients[th]
      if !thc.management?
        thcs << thc
      end
    else
      unmanaged << th
    end
  }

  return thcs.sort_by{|thc| thc.id}, unmanaged
end

#wait_command

[ GitHub ]

  
# File 'lib/debug/session.rb', line 427

def wait_command
  if @preset_command
    if @preset_command.commands.empty?
      if @preset_command.auto_continue
        @preset_command = nil

        leave_subsession :continue
        return
      else
        @preset_command = nil
        return :retry
      end
    else
      line = @preset_command.commands.shift
      @ui.puts "(rdbg:#{@preset_command.source}) #{line}"
    end
  else
    @ui.puts "INTERNAL_INFO: #{JSON.generate(@internal_info)}" if ENV['RUBY_DEBUG_TEST_UI'] == 'terminal'
    line = @ui.readline prompt
  end

  case line
  when String
    process_command line
  when Hash
    process_protocol_request line # defined in server.rb
  else
    raise "unexpected input: #{line.inspect}"
  end
end

#wait_command_loop

[ GitHub ]

  
# File 'lib/debug/session.rb', line 403

def wait_command_loop
  loop do
    case wait_command
    when :retry
      # nothing
    else
      break
    end
  rescue Interrupt
    @ui.puts "\n^C"
    retry
  end
end

#waiting_thread_clients (private)

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1672

private def waiting_thread_clients
  @th_clients.map{|th, tc|
    next if tc.management?
    next unless tc.waiting?
    tc
  }.compact
end

#width

[ GitHub ]

  
# File 'lib/debug/session.rb', line 1865

def width
  @ui.width
end