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
Public Class Methods
# File lib/rubinius/debugger.rb, line 94 def self.global @global ||= new end
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
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
# File lib/rubinius/debugger.rb, line 98 def self.start global.start(1) end
Public Instance Methods
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
# 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
# File lib/rubinius/debugger.rb, line 330 def check_deferred_breakpoints @deferred_breakpoints.delete_if do |bp| bp.resolve! end end
# 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
# 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
# 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
# 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
# File lib/rubinius/debugger.rb, line 259 def frame(num) @frames[num] ||= Frame.new(self, num, @locations[num]) end
# 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
# 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
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
# 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
# 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
# File lib/rubinius/debugger.rb, line 263 def set_frame(num) @current_frame = frame(num) end
# 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
# 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
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
# 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