class Rubinius::Debugger

The Rubinius reference debugger.

This debugger is wired into the debugging APIs provided by Rubinius. It serves as a simple, builtin debugger that others can use as an example for how to build a better debugger.

Constants

ROOT_DIR

Find the source for the kernel.

VERSION

Attributes

breakpoints[R]
current_frame[R]
locations[R]
user_variables[R]
variables[R]

Public Class Methods

global() click to toggle source
# File lib/rubinius/debugger.rb, line 94
def self.global
  @global ||= new
end
here() click to toggle source

This is simplest API point. This starts up the debugger in the caller of this method to begin debugging.

# File lib/rubinius/debugger.rb, line 105
def self.here
  global.start(1)
end
new() click to toggle source

Create a new debugger object. The debugger starts up a thread which is where the command line interface executes from. Other threads that you wish to debug are told that their debugging thread is the debugger thread. This is how the debugger is handed control of execution.

# File lib/rubinius/debugger.rb, line 28
def initialize
  @file_lines = Hash.new do |hash, path|
    if File.exists? path
      hash[path] = File.readlines(path)
    else
      ab_path = File.join(@root_dir, path)
      if File.exists? ab_path
        hash[path] = File.readlines(ab_path)
      else
        hash[path] = []
      end
    end
  end

  @thread = nil
  @frames = []

  @variables = {
    :show_ip              => false,
    :show_bytecode        => false,
    :highlight            => false,
    :list_command_history => {
      :path        => nil,
      :center_line => nil
    }
  }

  @loaded_hook = proc { |file|
    check_deferred_breakpoints
  }

  @added_hook = proc { |mod, name, exec|
    check_deferred_breakpoints
  }

  # Use a few Rubinius specific hooks to trigger checking
  # for deferred breakpoints.

  Rubinius::CodeLoader.loaded_hook.add @loaded_hook
  Rubinius.add_method_hook.add @added_hook

  @deferred_breakpoints = []

  @user_variables = 0

  @breakpoints = []

  @history_path = File.expand_path("~/.rbx_debug")

  if File.exists?(@history_path)
    File.readlines(@history_path).each do |line|
      Readline::HISTORY << line.strip
    end
    @history_io = File.new(@history_path, "a")
  else
    @history_io = File.new(@history_path, "w")
  end

  @history_io.sync = true

  @root_dir = ROOT_DIR
end
start() click to toggle source
# File lib/rubinius/debugger.rb, line 98
def self.start
  global.start(1)
end

Public Instance Methods

accept_commands() click to toggle source

Get a command from the user to run using readline

# File lib/rubinius/debugger.rb, line 185
def accept_commands
  command_list_code = []
  cmd = Readline.readline "debug> "

  if cmd.nil?
    # ^D was entered
    cmd = "quit"
  elsif cmd.empty?
    cmd = @last_command
  else
    @last_command = cmd
    Readline::HISTORY << cmd
  end

  command, args = cmd.to_s.strip.split(/\s+/, 2)

  runner = Command.commands.find { |k| k.match?(command) }

  if runner
    if runner == Command::CommandsList
      bp_id = (args || @breakpoints.size).to_i

      if @breakpoints.empty?
        puts "No breakpoint set"
        return
      elsif bp_id > @breakpoints.size || bp_id < 1
        puts "Invalid breakpoint number."
        return
      end

      puts "Type commands for breakpoint ##{bp_id}, one per line."
      puts "End with a line saying just 'END'."
      code = Readline.readline "> "
      while code != 'END'
        command_list_code << code
        code = Readline.readline "> "
      end
      args = {
        :bp_id => bp_id,
        :code  => command_list_code.empty? ? nil : command_list_code.join(";")
      }
    end

    runner.new(self).run args
  else
    puts "Unrecognized command: #{command}"
    return
  end

  # Save it to the history.
  @history_io.puts cmd
  unless command_list_code.empty?
    command_list_code << "END"
    @history_io.puts command_list_code.join("\n")
  end
end
add_deferred_breakpoint(klass_name, which, name, line) click to toggle source
# File lib/rubinius/debugger.rb, line 323
def add_deferred_breakpoint(klass_name, which, name, line)
  dbp = DeferredBreakPoint.new(self, @current_frame, klass_name, which, name,
                              line, @deferred_breakpoints)
  @deferred_breakpoints << dbp
  @breakpoints << dbp
end
check_deferred_breakpoints() click to toggle source
# File lib/rubinius/debugger.rb, line 330
def check_deferred_breakpoints
  @deferred_breakpoints.delete_if do |bp|
    bp.resolve!
  end
end
decode_one() click to toggle source
# File lib/rubinius/debugger.rb, line 406
def decode_one
  ip = @current_frame.ip

  meth = @current_frame.method
  decoder = Rubinius::InstructionDecoder.new(meth.iseq)
  partial = decoder.decode_between(ip, ip+1)

  partial.each do |ins|
    op = ins.shift

    ins.each_index do |i|
      case op.args[i]
      when :literal
        ins[i] = meth.literals[ins[i]].inspect
      when :local
        if meth.local_names
          ins[i] = meth.local_names[ins[i]]
        end
      end
    end

    display "ip #{ip} = #{op.opcode} #{ins.join(', ')}"
  end
end
delete_breakpoint(i) click to toggle source
# File lib/rubinius/debugger.rb, line 310
def delete_breakpoint(i)
  bp = @breakpoints[i-1]

  unless bp
    error "Unknown breakpoint '#{i}'"
    return
  end

  bp.delete!

  @breakpoints[i-1] = nil
end
each_frame(start=0) { |frame(idx)| ... } click to toggle source
# File lib/rubinius/debugger.rb, line 267
def each_frame(start=0)
  start = start.number if start.kind_of?(Frame)

  start.upto(@locations.size-1) do |idx|
    yield frame(idx)
  end
end
eval_code(args, pretty = false) click to toggle source
# File lib/rubinius/debugger.rb, line 242
def eval_code(args, pretty = false)
  obj = @current_frame.run(args)

  idx = @user_variables
  @user_variables += 1

  str = "$d#{idx}"
  Rubinius::Globals[str.to_sym] = obj

  if pretty
    require 'pp'
    puts "#{str} = #{obj.pretty_inspect}\n"
  else
    puts "#{str} = #{obj.inspect}\n"
  end
end
frame(num) click to toggle source
# File lib/rubinius/debugger.rb, line 259
def frame(num)
  @frames[num] ||= Frame.new(self, num, @locations[num])
end
list_code_around_line(path, center_line, lines_to_show) click to toggle source
# File lib/rubinius/debugger.rb, line 360
def list_code_around_line(path, center_line, lines_to_show)
  lines_around = lines_to_show / 2
  start_line   = center_line - lines_around
  end_line     = center_line + lines_around

  list_code_range(path, start_line, end_line, center_line)
end
list_code_range(path, start_line, end_line, center_line) click to toggle source
# File lib/rubinius/debugger.rb, line 368
def list_code_range(path, start_line, end_line, center_line)
  if !File.exists?(path) && !File.exists?(File.join(@root_dir, path))
    error "Cannot find file #{path}"
    return
  end

  if start_line > @file_lines[path].size
    error "Line number #{@file_lines[path].size + 1} out of range: #{path} has #{@file_lines[path].size} lines."
    return
  end

  start_line = 1 if start_line < 1
  end_line   = @file_lines[path].size if end_line > @file_lines[path].size

  @variables[:list_command_history][:path]        = path
  @variables[:list_command_history][:center_line] = center_line

  (start_line).upto(end_line) do |i|
    show_code(i, path)
  end
end
listen(step_into=false) click to toggle source

Stop and wait for a debuggee thread to send us info about stoping at a breakpoint.

# File lib/rubinius/debugger.rb, line 135
def listen(step_into=false)
  while true
    if @channel
      if step_into
        @channel << :step
      else
        @channel << true
      end
    end

    # Wait for someone to stop
    bp, thr, chan, locs = @local_channel.receive

    # Uncache all frames since we stopped at a new place
    @frames = []

    @locations = locs
    @breakpoint = bp
    @debuggee_thread = thr
    @channel = chan

    @current_frame = frame(0)

    if bp
      # Only break out if the hit was valid
      if bp.hit!(locs.first)
        if bp.has_condition?
          break if @current_frame.run(bp.condition)
        else
          break
        end
      end
    else
      break
    end
  end

  puts
  info "Breakpoint: #{@current_frame.describe}"
  show_code
  eval_code(@breakpoint.commands) if @breakpoint && @breakpoint.has_commands?

  if @variables[:show_bytecode]
    decode_one
  end

end
send_between(exec, start, fin) click to toggle source
# File lib/rubinius/debugger.rb, line 336
def send_between(exec, start, fin)
  ss   = Rubinius::InstructionSet.opcodes_map[:send_stack]
  sm   = Rubinius::InstructionSet.opcodes_map[:send_method]
  sb   = Rubinius::InstructionSet.opcodes_map[:send_stack_with_block]

  iseq = exec.iseq

  fin = iseq.size if fin < 0

  i = start
  while i < fin
    op = iseq[i]
    case op
    when ss, sm, sb
      return exec.literals[iseq[i + 1]]
    else
      op = Rubinius::InstructionSet[op]
      i += (op.arg_count + 1)
    end
  end

  return nil
end
set_breakpoint_method(descriptor, method, line=nil, condition=nil, commands=nil, index=nil) click to toggle source
# File lib/rubinius/debugger.rb, line 275
def set_breakpoint_method(descriptor, method, line=nil, condition=nil, commands=nil, index=nil)
  exec = method.executable

  unless exec.kind_of?(Rubinius::CompiledCode)
    error "Unsupported method type: #{exec.class}"
    return
  end

  if line
    ip = exec.first_ip_on_line(line)

    if !ip
      error "Unknown line '#{line}' in method '#{method.name}'"
      return
    end
  else
    line = exec.first_line
    ip = 0
  end

  bp = BreakPoint.new(descriptor, exec, ip, line, condition)
  bp.activate

  if index
    bp.set_commands(commands)
    @breakpoints[index] = bp
    info "Set breakpoint #{index+1}: #{bp.location}"
  else
    @breakpoints << bp
    info "Set breakpoint #{@breakpoints.size}: #{bp.location}"
  end

  return bp
end
set_frame(num) click to toggle source
# File lib/rubinius/debugger.rb, line 263
def set_frame(num)
  @current_frame = frame(num)
end
show_bytecode(line=@current_frame.line) click to toggle source
# File lib/rubinius/debugger.rb, line 431
def show_bytecode(line=@current_frame.line)
  meth = @current_frame.method
  start = meth.first_ip_on_line(line)
  fin = meth.first_ip_on_line(line+1)

  if !fin
    fin = meth.iseq.size
  end

  section "Bytecode between #{start} and #{fin-1} for line #{line}"

  decoder = Rubinius::InstructionDecoder.new(meth.iseq)
  partial = decoder.decode_between(start, fin)

  ip = start

  partial.each do |ins|
    op = ins.shift

    ins.each_index do |i|
      case op.args[i]
      when :literal
        ins[i] = meth.literals[ins[i]].inspect
      when :local
        if meth.local_names
          ins[i] = meth.local_names[ins[i]]
        end
      end
    end

    info " %4d: #{op.opcode} #{ins.join(', ')}" % ip

    ip += (ins.size + 1)
  end
end
show_code(line = @current_frame.line, path = @current_frame.method.active_path) click to toggle source
# File lib/rubinius/debugger.rb, line 390
def show_code(line = @current_frame.line,
              path = @current_frame.method.active_path)
  if str = @file_lines[path][line - 1]
    if @variables[:highlight]
      if fin = @current_frame.method.first_ip_on_line(line + 1)
        if name = send_between(@current_frame.method, @current_frame.ip, fin)
          str = str.gsub name.to_s, "\033[0;4m#{name}\033[0m"
        end
      end
    end
    info "#{line}: #{str}"
  else
    show_bytecode(line)
  end
end
start(offset=0) click to toggle source

Startup the debugger, skipping back offset frames. This lets you start the debugger straight into callers method.

# File lib/rubinius/debugger.rb, line 112
def start(offset=0)
  spinup_thread

  # Feed info to the debugger thread!
  locs = Rubinius::VM.backtrace(offset + 1)

  method = Rubinius::CompiledCode.of_sender

  bp = BreakPoint.new "<start>", method, 0, 0
  channel = Rubinius::Channel.new

  @local_channel.send Rubinius::Tuple[bp, Thread.current, channel, locs]

  # wait for the debugger to release us
  channel.receive

  Thread.current.set_debugger_thread @thread
  self
end

Private Instance Methods

spinup_thread() click to toggle source
# File lib/rubinius/debugger.rb, line 467
def spinup_thread
  return if @thread

  @local_channel = Rubinius::Channel.new

  @thread = Thread.new do
    begin
      listen
    rescue Exception => e
      e.render("Listening")
      break
    end

    while true
      begin
        accept_commands
      rescue Exception => e
        begin
          e.render "Error in debugger"
        rescue Exception => e2
          puts "Error rendering backtrace in debugger!"
        end
      end
    end
  end

  @thread.setup_control!(@local_channel)
end