From: Florian Gross Date: 2004-11-18T09:43:17+09:00 Subject: Re: debugging rails in an IDE? --------------010708010102070104070006 Content-Type: text/plain; charset=us-ascii; format=flowed Content-Transfer-Encoding: 7bit Pete Johnson wrote: > I'm coming from a Windows C++ background where I can attach an IDE > debugger to a process on a web server, and so catch and debug errors in > web server extensions. > > Is there a way to use one of the Ruby IDE debuggers with Rails? I feel > I'd be much more productive this way, but can't work out how I can > grab the process and attach to an IDE debugger. > > Should I be using WEbrick rather than apache, and run the whole thing > under a debugger? This is where I get completely lost... The latest versions of breakpoint.rb contain facilities for DRb debugging which allows you to set a breakpoint in your web framework and attach an irb session from within a terminal. I have attached the most up-to-date version to this email. There's still a few issues (I'd like to have a way of executing pieces of code from within the irb session on the client-side and the whole assume() naming controversy) to be resolved until this can be merged into dev-utils but I'm pretty sure they can be resolved soon. See the documentation (inside source, can be extracted with RDoc) for how to use this. If you have an specific questions or suggestions I'd love to hear about them. License is the same as Ruby in case it matters. --------------010708010102070104070006 Content-Type: text/plain; name="breakpoint_client.rb" Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="breakpoint_client.rb" require 'breakpoint' require 'optparse' options = { :ClientURI => nil, :ServerURI => "druby://localhost:42531", :RetryDelay => 10 } ARGV.options do |opts| script_name = File.basename($0) opts.banner = [ "Usage: ruby #{script_name} [options] [server uri]", "", "This tool lets you connect to a breakpoint service ", "which was started via Breakpoint.activate_drb.", "", "The server uri defaults to druby://localhost:42531" ].join("\n") opts.separator "" opts.on("-c", "--client-uri=uri", "Run the client on the specified uri.", "This can be used to specify the port", "that the client uses to allow for back", "connections from the server.", "Default: Find a good URI automatically.", "Example: -c druby://localhost:12345" ) { |options[:ClientURI]| } opts.on("-s", "--server-uri=uri", "Connect to the server specified at the", "specified uri.", "Default: druby://localhost:42531" ) { |options[:ServerURI]| } opts.on("-R", "--retry-delay=delay", Integer, "Automatically try to reconnect to the", "server after delay seconds when the", "connection failed or timed out.", "A value of 0 disables automatical", "reconnecting completely.", "Default: 10" ) { |options[:RetryDelay]| } opts.separator "" opts.on("-h", "--help", "Show this help message." ) { puts opts; exit } opts.parse! end options[:ServerURI] = ARGV[0] if ARGV[0] DRb.start_service(options[:ClientURI]) begin service = DRbObject.new(nil, options[:ServerURI]) begin service.register_handler do |workspace, message| puts message IRB.start(nil, nil, workspace) end service.register_exit_handler do # Need to delay this because server expects a response Thread.new do puts "Received shut down signal." sleep 1 exit! end nil end sleep ensure service.unregister_handler service.unregister_exit_handler end rescue Exception => error if options[:RetryDelay] > 0 then puts "No connection to breakpoint service at #{options[:ServerURI]}:", " (#{error})", " Reconnecting in #{options[:RetryDelay]} seconds..." sleep options[:RetryDelay] retry else raise end end --------------010708010102070104070006 Content-Type: text/plain; name="binding_of_caller.rb" Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="binding_of_caller.rb" begin require 'simplecc' rescue LoadError def Continuation.create(*args, &block) cc = nil; result = callcc {|c| cc = c; block.call(cc) if block and args.empty?} result ||= args return *[cc, *result] end end # This method returns the binding of the method that called your # method. It will raise an Exception when you're not inside a method. # # It's used like this: # def inc_counter(amount = 1) # Binding.of_caller do |binding| # # Create a lambda that will increase the variable 'counter' # # in the caller of this method when called. # inc = eval("lambda { |arg| counter += arg }", binding) # # We can refer to amount from inside this block safely. # inc.call(amount) # end # # No other statements can go here. Put them inside the block. # end # counter = 0 # 2.times { inc_counter } # counter # => 2 # # Binding.of_caller must be the last statement in the method. # This means that you will have to put everything you want to # do after the call to Binding.of_caller into the block of it. # This should be no problem however, because Ruby has closures. # If you don't do this an Exception will be raised. Because of # the way that Binding.of_caller is implemented it has to be # done this way. def Binding.of_caller(&block) old_critical = Thread.critical Thread.critical = true count = 0 cc, result, error, extra_data = Continuation.create(nil, nil) error.call if error tracer = lambda do |*args| type, context, extra_data = args[0], args[4], args if type == "return" count += 1 # First this method and then calling one will return -- # the trace event of the second event gets the context # of the method which called the method that called this # method. if count == 2 # It would be nice if we could restore the trace_func # that was set before we swapped in our own one, but # this is impossible without overloading set_trace_func # in current Ruby. set_trace_func(nil) cc.call(eval("binding", context), nil, extra_data) end elsif type != "line" set_trace_func(nil) error_msg = "Binding.of_caller used in non-method context or " + "trailing statements of method using it aren't in the block." cc.call(nil, lambda { raise(ArgumentError, error_msg) }, nil) end end unless result set_trace_func(tracer) return nil else Thread.critical = old_critical case block.arity when 1 then yield(result) else yield(result, extra_data) end end end --------------010708010102070104070006 Content-Type: text/plain; name="breakpoint.rb" Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="breakpoint.rb" require 'irb' require 'binding_of_caller' require 'drb' module Breakpoint extend self # This will pop up an interactive ruby session at a # pre-defined break point in a Ruby application. In # this session you can examine the environment of # the break point. # # You can get a list of variables in the context using # local_variables via +local_variables+. You can then # examine their values by typing their names. # # You can have a look at the call stack via +caller+. # # The source code around the location where the breakpoint # was executed can be examined via +source_lines+. Its # argument specifies how much lines of context to display. # The default amount of context is 5 lines. Note that # the call to +source_lines+ can raise an exception when # it isn't able to read in the source code. # # breakpoints can also return a value. They will execute # a supplied block for getting a default return value. # A custom value can be returned from the session by doing # +throw(:debug_return, value)+. # # You can also give names to break points which will be # used in the message that is displayed upon execution # of them. # # Here's a sample of how breakpoints should be placed: # # class Person # def initialize(name, age) # @name, @age = name, age # breakpoint("Person#initialize") # end # # attr_reader :age # def name # breakpoint("Person#name") { @name } # end # end # # person = Person.new("Random Person", 23) # puts "Name: #{person.name}" # # And here is a sample debug session: # # Executing break point "Person#initialize" at file.rb:4 in `initialize' # irb(#):001:0> local_variables # => ["name", "age", "_", "__"] # irb(#):002:0> [name, age] # => ["Random Person", 23] # irb(#):003:0> [@name, @age] # => ["Random Person", 23] # irb(#):004:0> self # => # # irb(#):005:0> @age += 1; self # => # # irb(#):006:0> exit # Executing break point "Person#name" at file.rb:9 in `name' # irb(#):001:0> throw(:debug_return, "Overriden name") # Name: Overriden name def breakpoint(id = nil, context = nil, &block) callstack = caller callstack.slice!(0, 3) if callstack.first["breakpoint"] file, line, method = *callstack.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/).captures message = "Executing break point " + (id ? "#{id.inspect} " : "") + "at #{file}:#{line}" + (method ? " in `#{method}'" : "") if context then return handle_breakpoint(context, message, file, line, &block) end Binding.of_caller do |binding_context| handle_breakpoint(binding_context, message, file, line, &block) end end def handle_breakpoint(context, message, file = "", line = "", &block) # :nodoc: catch(:debug_return) do |value| eval(%{ def self.source_lines(context = 5, return_line_numbers = false) lines = File.readlines(#{file.inspect}).map { |line| line.chomp } break_line = #{line} start_line = [break_line - context, 1].max end_line = break_line + context result = lines[(start_line - 1) .. (end_line - 1)] if return_line_numbers then return [start_line, break_line, result] else return result end end }, context) rescue nil if not use_drb? then puts message IRB.start(nil, IRB::WorkSpace.new(context)) else @drb_service.add_breakpoint(context, message) end block.call if block end end private :handle_breakpoint # This asserts that the block evaluates to true. # If it doesn't evaluate to true a breakpoint will # automatically be created at that execution point. # # You can disable assert checking by setting # Breakpoint.optimize_asserts to true before # loading the breakpoint.rb library. (It will still # be enabled when Ruby is run via the -d argument.) # # Example: # person_name = "Foobar" # assert { not person_name.nil? } def assert(context = nil, &condition) return if Breakpoint.optimize_asserts and not $DEBUG return if yield callstack = caller callstack.slice!(0, 3) if callstack.first["assert"] file, line, method = *callstack.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/).captures message = "Assert failed at #{file}:#{line}#{" in `#{method}'" if method}. " + "Executing implicit breakpoint." if context then return handle_breakpoint(context, message, file, line) end Binding.of_caller do |context| handle_breakpoint(context, message, file, line) end end # Whether asserts should be ignored if not in debug mode. # Debug mode can be enabled by running ruby with the -d # switch or by setting $DEBUG to true. attr_accessor :optimize_asserts self.optimize_asserts = false class DRbService include DRbUndumped def initialize @handler = nil @at_exit = lambda { } IRB.instance_eval { @CONF[:RC] = true } IRB.run_config at_exit { @at_exit.call } end def add_breakpoint(context, message) workspace = IRB::WorkSpace.new(context) workspace.extend(DRbUndumped) until @handler; end @handler.call(workspace, message) end def register_handler(&block) @handler = block end def unregister_handler @handler = nil end def register_exit_handler(&block) @at_exit = block end def unregister_exit_handler @at_exit = lambda { } end end # Will run Breakpoint in DRb mode. This will spawn a server # that can be attached to via the breakpoint-client command # whenever a breakpoint is executed. This is useful when you # are debugging CGI applications or other applications where # you can't access debug sessions via the standard input and # output of your application. # # You can specify an URI where the DRb server will run at. # This way you can specify the port the server runs on. The # default URI is druby://localhost:42531. # # Please note that breakpoints will be skipped silently in # case the DRb server can not spawned. (This can happen if # the port is already used by another instance of your # application on CGI or another application.) # # Also note that by default this is not secure. You can # however specify a list of allowed hosts. But that will # still not protect you from somebody reading the data # as it goes through the net. # # A good approach for getting security is setting up an SSH # tunnel between the DRb service and the client. This is # usually done like this: # # $ ssh -L20000:127.0.0.1:20000 -R10000:127.0.0.1:10000 example.com # (This will connect port 20000 at the client side to port # 20000 at the server side, and port 10000 at the server # side to port 10000 at the client side.) # # After you have done that you should specify "localhost" # as the second argument to the +activate_drb()+ call. # # Running through such a SSH proxy will also let you use # breakpoint.rb in case you are behind a firewall. # # Detailed information about running DRb through firewalls is # available at http://www.rubygarden.org/ruby?DrbTutorial def activate_drb(uri = 'druby://localhost:42531', allowed_hosts = nil) if allowed_hosts then acl = ["deny all"] Array(allowed_hosts).each do |host| acl << "allow #{host}" end DRb.install_acl(ACL.new(acl.join("\n"))) end @use_drb = true @drb_service = DRbService.new DRb.start_service(uri, @drb_service) end def use_drb? @use_drb == true end end module IRB def IRB.start(ap_path = nil, main_context = nil, workspace = nil) $0 = File::basename(ap_path, ".rb") if ap_path # suppress some warnings about redefined constants old_verbose, $VERBOSE = $VERBOSE, nil IRB.setup(ap_path) $VERBOSE = old_verbose if @CONF[:SCRIPT] then irb = Irb.new(main_context, @CONF[:SCRIPT]) else irb = Irb.new(main_context) end if workspace then irb.context.workspace = workspace end @CONF[:IRB_RC].call(irb.context) if @CONF[:IRB_RC] @CONF[:MAIN_CONTEXT] = irb.context trap("SIGINT") do irb.signal_handle end catch(:IRB_EXIT) do irb.eval_input end end class << self; alias :old_CurrentContext :CurrentContext; end def IRB.CurrentContext if old_CurrentContext.nil? and Breakpoint.use_drb? then result = Object.new def result.last_value; end return result else old_CurrentContext end end class Context alias :old_evaluate :evaluate def evaluate(line, line_no) if line.chomp == "exit" then exit else old_evaluate(line, line_no) end end end class WorkSpace alias :old_evaluate :evaluate def evaluate(*args) if Breakpoint.use_drb? then result = old_evaluate(*args) unless [true, false, nil].include?(result) result.extend(DRbUndumped) rescue nil end return result else old_evaluate(*args) end end end end class DRb::DRbObject undef :inspect end def breakpoint(id = nil, &block) Binding.of_caller do |context| Breakpoint.breakpoint(id, context, &block) end end def assert(&block) Binding.of_caller do |context| Breakpoint.assert(context, &block) end end --------------010708010102070104070006--