548 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Ruby
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			548 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Ruby
		
	
	
		
			Executable File
		
	
	
| # The Breakpoint library provides the convenience of
 | |
| # being able to inspect and modify state, diagnose
 | |
| # bugs all via IRB by simply setting breakpoints in
 | |
| # your applications by the call of a method.
 | |
| #
 | |
| # This library was written and is supported by me,
 | |
| # Florian Gross. I can be reached at flgr@ccan.de
 | |
| # and enjoy getting feedback about my libraries.
 | |
| #
 | |
| # The whole library (including breakpoint_client.rb
 | |
| # and binding_of_caller.rb) is licensed under the
 | |
| # same license that Ruby uses. (Which is currently
 | |
| # either the GNU General Public License or a custom
 | |
| # one that allows for commercial usage.) If you for
 | |
| # some good reason need to use this under another
 | |
| # license please contact me.
 | |
| 
 | |
| require 'irb'
 | |
| require 'caller'
 | |
| require 'drb'
 | |
| require 'drb/acl'
 | |
| require 'thread'
 | |
| 
 | |
| module Breakpoint
 | |
|   id = %q$Id: breakpoint.rb 52 2005-02-26 19:43:19Z flgr $
 | |
|   current_version = id.split(" ")[2]
 | |
|   unless defined?(Version)
 | |
|     # The Version of ruby-breakpoint you are using as String of the
 | |
|     # 1.2.3 form where the digits stand for release, major and minor
 | |
|     # version respectively.
 | |
|     Version = "0.5.0"
 | |
|   end
 | |
| 
 | |
|   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(#<Person:0x292fbe8>):001:0> local_variables
 | |
|   #   => ["name", "age", "_", "__"]
 | |
|   #   irb(#<Person:0x292fbe8>):002:0> [name, age]
 | |
|   #   => ["Random Person", 23]
 | |
|   #   irb(#<Person:0x292fbe8>):003:0> [@name, @age]
 | |
|   #   => ["Random Person", 23]
 | |
|   #   irb(#<Person:0x292fbe8>):004:0> self
 | |
|   #   => #<Person:0x292fbe8 @age=23, @name="Random Person">
 | |
|   #   irb(#<Person:0x292fbe8>):005:0> @age += 1; self
 | |
|   #   => #<Person:0x292fbe8 @age=24, @name="Random Person">
 | |
|   #   irb(#<Person:0x292fbe8>):006:0> exit
 | |
|   #   Executing break point "Person#name" at file.rb:9 in `name'
 | |
|   #   irb(#<Person:0x292fbe8>):001:0> throw(:debug_return, "Overriden name")
 | |
|   #   Name: Overriden name
 | |
|   #
 | |
|   # Breakpoint sessions will automatically have a few
 | |
|   # convenience methods available. See Breakpoint::CommandBundle
 | |
|   # for a list of them.
 | |
|   #
 | |
|   # Breakpoints can also be used remotely over sockets.
 | |
|   # This is implemented by running part of the IRB session
 | |
|   # in the application and part of it in a special client.
 | |
|   # You have to call Breakpoint.activate_drb to enable
 | |
|   # support for remote breakpoints and then run
 | |
|   # breakpoint_client.rb which is distributed with this
 | |
|   # library. See the documentation of Breakpoint.activate_drb
 | |
|   # for details.
 | |
|   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
 | |
| 
 | |
|   # These commands are automatically available in all breakpoint shells.
 | |
|   module CommandBundle
 | |
|     # Proxy to a Breakpoint client. Lets you directly execute code
 | |
|     # in the context of the client.
 | |
|     class Client
 | |
|       def initialize(eval_handler) # :nodoc:
 | |
|         eval_handler.untaint
 | |
|         @eval_handler = eval_handler
 | |
|       end
 | |
| 
 | |
|       instance_methods.each do |method|
 | |
|         next if method[/^__.+__$/]
 | |
|         undef_method method
 | |
|       end
 | |
| 
 | |
|       # Executes the specified code at the client.
 | |
|       def eval(code)
 | |
|         @eval_handler.call(code)
 | |
|       end
 | |
| 
 | |
|       # Will execute the specified statement at the client.
 | |
|       def method_missing(method, *args, &block)
 | |
|         if args.empty? and not block
 | |
|           result = eval "#{method}"
 | |
|         else
 | |
|           # This is a bit ugly. The alternative would be using an
 | |
|           # eval context instead of an eval handler for executing
 | |
|           # the code at the client. The problem with that approach
 | |
|           # is that we would have to handle special expressions
 | |
|           # like "self", "nil" or constants ourself which is hard.
 | |
|           remote = eval %{
 | |
|             result = lambda { |block, *args| #{method}(*args, &block) }
 | |
|             def result.call_with_block(*args, &block)
 | |
|               call(block, *args)
 | |
|             end
 | |
|             result
 | |
|           }
 | |
|           remote.call_with_block(*args, &block)
 | |
|         end
 | |
| 
 | |
|         return result
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     # Returns the source code surrounding the location where the
 | |
|     # breakpoint was issued.
 | |
|     def source_lines(context = 5, return_line_numbers = false)
 | |
|       lines = File.readlines(@__bp_file).map { |line| line.chomp }
 | |
| 
 | |
|       break_line = @__bp_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
 | |
| 
 | |
|     # Lets an object that will forward method calls to the breakpoint
 | |
|     # client. This is useful for outputting longer things at the client
 | |
|     # and so on. You can for example do these things:
 | |
|     #
 | |
|     #   client.puts "Hello" # outputs "Hello" at client console
 | |
|     #   # outputs "Hello" into the file temp.txt at the client
 | |
|     #   client.File.open("temp.txt", "w") { |f| f.puts "Hello" } 
 | |
|     def client()
 | |
|       if Breakpoint.use_drb? then
 | |
|         sleep(0.5) until Breakpoint.drb_service.eval_handler
 | |
|         Client.new(Breakpoint.drb_service.eval_handler)
 | |
|       else
 | |
|         Client.new(lambda { |code| eval(code, TOPLEVEL_BINDING) })
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   def handle_breakpoint(context, message, file = "", line = "", &block) # :nodoc:
 | |
|     catch(:debug_return) do |value|
 | |
|       eval(%{
 | |
|         @__bp_file = #{file.inspect}
 | |
|         @__bp_line = #{line}
 | |
|         extend Breakpoint::CommandBundle
 | |
|         extend DRbUndumped if self
 | |
|       }, 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
 | |
| 
 | |
|   # These exceptions will be raised on failed asserts
 | |
|   # if Breakpoint.asserts_cause_exceptions is set to
 | |
|   # true.
 | |
|   class FailedAssertError < RuntimeError
 | |
|   end
 | |
| 
 | |
|   # 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 in production
 | |
|   # code by setting Breakpoint.optimize_asserts to
 | |
|   # true. (It will still be enabled when Ruby is run
 | |
|   # via the -d argument.)
 | |
|   #
 | |
|   # Example:
 | |
|   #   person_name = "Foobar"
 | |
|   #   assert { not person_name.nil? }
 | |
|   #
 | |
|   # Note: If you want to use this method from an
 | |
|   # unit test, you will have to call it by its full
 | |
|   # name, Breakpoint.assert.
 | |
|   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}."
 | |
| 
 | |
|     if Breakpoint.asserts_cause_exceptions and not $DEBUG then
 | |
|       raise(Breakpoint::FailedAssertError, message)
 | |
|     end
 | |
| 
 | |
|     message += " 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
 | |
| 
 | |
|   # Whether an Exception should be raised on failed asserts
 | |
|   # in non-$DEBUG code or not. By default this is disabled.
 | |
|   attr_accessor :asserts_cause_exceptions
 | |
|   self.asserts_cause_exceptions = false
 | |
|   @use_drb = false
 | |
| 
 | |
|   attr_reader :drb_service # :nodoc:
 | |
| 
 | |
|   class DRbService # :nodoc:
 | |
|     include DRbUndumped
 | |
| 
 | |
|     def initialize
 | |
|       @handler = @eval_handler = @collision_handler = nil
 | |
| 
 | |
|       IRB.instance_eval { @CONF[:RC] = true }
 | |
|       IRB.run_config
 | |
|     end
 | |
| 
 | |
|     def collision
 | |
|       sleep(0.5) until @collision_handler
 | |
| 
 | |
|       @collision_handler.untaint
 | |
| 
 | |
|       @collision_handler.call
 | |
|     end
 | |
| 
 | |
|     def ping() end
 | |
| 
 | |
|     def add_breakpoint(context, message)
 | |
|       workspace = IRB::WorkSpace.new(context)
 | |
|       workspace.extend(DRbUndumped)
 | |
| 
 | |
|       sleep(0.5) until @handler
 | |
| 
 | |
|       @handler.untaint
 | |
|       @handler.call(workspace, message)
 | |
|     rescue Errno::ECONNREFUSED, DRb::DRbConnError
 | |
|       raise if Breakpoint.use_drb? 
 | |
|     end
 | |
| 
 | |
|     attr_accessor :handler, :eval_handler, :collision_handler
 | |
|   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 will only allow access
 | |
|   # from localhost. You can however specify a list of
 | |
|   # allowed hosts or nil (to allow access from everywhere).
 | |
|   # But that will still not protect you from somebody
 | |
|   # reading the data as it goes through the net.
 | |
|   #
 | |
|   # A good approach for getting security and remote access
 | |
|   # 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 that do this on the server side: (the code being debugged)
 | |
|   # Breakpoint.activate_drb("druby://127.0.0.1:20000", "localhost")
 | |
|   #
 | |
|   # And at the client side:
 | |
|   # ruby breakpoint_client.rb -c druby://127.0.0.1:10000 -s druby://127.0.0.1:20000
 | |
|   #
 | |
|   # 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
 | |
|   #
 | |
|   # == Security considerations
 | |
|   # Usually you will be fine when using the default druby:// URI and the default
 | |
|   # access control list. However, if you are sitting on a machine where there are
 | |
|   # local users that you likely can not trust (this is the case for example on
 | |
|   # most web hosts which have multiple users sitting on the same physical machine)
 | |
|   # you will be better off by doing client/server communication through a unix
 | |
|   # socket. This can be accomplished by calling with a drbunix:/ style URI, e.g.
 | |
|   # <code>Breakpoint.activate_drb('drbunix:/tmp/breakpoint_server')</code>. This
 | |
|   # will only work on Unix based platforms.
 | |
|   def activate_drb(uri = nil, allowed_hosts = ['localhost', '127.0.0.1', '::1'],
 | |
|     ignore_collisions = false)
 | |
| 
 | |
|     return false if @use_drb
 | |
| 
 | |
|     uri ||= 'druby://localhost:42531'
 | |
| 
 | |
|     if allowed_hosts then
 | |
|       acl = ["deny", "all"]
 | |
| 
 | |
|       Array(allowed_hosts).each do |host|
 | |
|         acl += ["allow", host]
 | |
|       end
 | |
| 
 | |
|       DRb.install_acl(ACL.new(acl))
 | |
|     end
 | |
| 
 | |
|     @use_drb = true
 | |
|     @drb_service = DRbService.new
 | |
|     did_collision = false
 | |
|     begin
 | |
|       @service = DRb.start_service(uri, @drb_service)
 | |
|     rescue Errno::EADDRINUSE
 | |
|       if ignore_collisions then
 | |
|         nil
 | |
|       else
 | |
|         # The port is already occupied by another
 | |
|         # Breakpoint service. We will try to tell
 | |
|         # the old service that we want its port.
 | |
|         # It will then forward that request to the
 | |
|         # user and retry.
 | |
|         unless did_collision then
 | |
|           DRbObject.new(nil, uri).collision
 | |
|           did_collision = true
 | |
|         end
 | |
|         sleep(10)
 | |
|         retry
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     return true
 | |
|   end
 | |
| 
 | |
|   # Deactivates a running Breakpoint service.
 | |
|   def deactivate_drb
 | |
|     Thread.exclusive do
 | |
|       @service.stop_service unless @service.nil?
 | |
|       @service = nil
 | |
|       @use_drb = false
 | |
|       @drb_service = nil
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   # Returns true when Breakpoints are used over DRb.
 | |
|   # Breakpoint.activate_drb causes this to be true.
 | |
|   def use_drb?
 | |
|     @use_drb == true
 | |
|   end
 | |
| end
 | |
| 
 | |
| module IRB # :nodoc:
 | |
|   class << self; remove_method :start; end
 | |
|   def self.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
 | |
| 
 | |
|     old_sigint = trap("SIGINT") do
 | |
|       begin
 | |
|         irb.signal_handle
 | |
|       rescue RubyLex::TerminateLineInput
 | |
|         # ignored
 | |
|       end
 | |
|     end
 | |
|     
 | |
|     catch(:IRB_EXIT) do
 | |
|       irb.eval_input
 | |
|     end
 | |
|   ensure
 | |
|     trap("SIGINT", old_sigint)
 | |
|   end
 | |
| 
 | |
|   class << self
 | |
|     alias :old_CurrentContext :CurrentContext
 | |
|     remove_method :CurrentContext
 | |
|     remove_method :parse_opts
 | |
|   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
 | |
|   def IRB.parse_opts() end
 | |
| 
 | |
|   class Context # :nodoc:
 | |
|     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 # :nodoc:
 | |
|     alias :old_evaluate :evaluate
 | |
| 
 | |
|     def evaluate(*args)
 | |
|       if Breakpoint.use_drb? then
 | |
|         result = old_evaluate(*args)
 | |
|         if args[0] != :no_proxy and
 | |
|           not [true, false, nil].include?(result)
 | |
|         then
 | |
|           result.extend(DRbUndumped) rescue nil
 | |
|         end
 | |
|         return result
 | |
|       else
 | |
|         old_evaluate(*args)
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   module InputCompletor # :nodoc:
 | |
|     def self.eval(code, context, *more)
 | |
|       # Big hack, this assumes that InputCompletor
 | |
|       # will only call eval() when it wants code
 | |
|       # to be executed in the IRB context.
 | |
|       IRB.conf[:MAIN_CONTEXT].workspace.evaluate(:no_proxy, code, *more)
 | |
|     end
 | |
|   end
 | |
| end
 | |
| 
 | |
| module DRb # :nodoc:
 | |
|   class DRbObject # :nodoc:
 | |
|     undef :inspect if method_defined?(:inspect)
 | |
|     undef :clone if method_defined?(:clone)
 | |
|   end
 | |
| end
 | |
| 
 | |
| # See Breakpoint.breakpoint
 | |
| def breakpoint(id = nil, &block)
 | |
|   Binding.of_caller do |context|
 | |
|     Breakpoint.breakpoint(id, context, &block)
 | |
|   end
 | |
| end
 | |
| 
 | |
| # See Breakpoint.assert
 | |
| def assert(&block)
 | |
|   Binding.of_caller do |context|
 | |
|     Breakpoint.assert(context, &block)
 | |
|   end
 | |
| end
 |