From 505e2d99da9f0e199dd0bf248fc1db77145730ba Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 24 Jan 2005 13:41:57 +0000 Subject: Added Florian Gross' latest version of Breakpointer and friends that fixes a variaty of bugs #441 [Florian Gross] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@492 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- railties/lib/binding_of_caller.rb | 168 +++++++++++++++---------------- railties/lib/breakpoint.rb | 89 ++++++++-------- railties/lib/breakpoint_client.rb | 207 +++++++++++++++++++++----------------- 3 files changed, 238 insertions(+), 226 deletions(-) (limited to 'railties/lib') diff --git a/railties/lib/binding_of_caller.rb b/railties/lib/binding_of_caller.rb index 759e760016..d18fbdef3d 100644 --- a/railties/lib/binding_of_caller.rb +++ b/railties/lib/binding_of_caller.rb @@ -1,85 +1,83 @@ -begin - require 'simplecc' -rescue LoadError - class Continuation #:nodoc: - def self.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 -end - -class Binding #:nodoc: - # 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 self.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" then - nil - elsif type == "c-return" and extra_data[3] == :set_trace_func then - nil - else - 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 -end \ No newline at end of file +begin + require 'simplecc' +rescue LoadError + class Continuation; end # :nodoc: # for RDoc + def Continuation.create(*args, &block) # :nodoc: + cc = nil; result = callcc {|c| cc = c; block.call(cc) if block and args.empty?} + result ||= args + return *[cc, *result] + end +end + +class Binding; end # for RDoc +# 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" then + nil + elsif type == "c-return" and extra_data[3] == :set_trace_func then + nil + else + 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 diff --git a/railties/lib/breakpoint.rb b/railties/lib/breakpoint.rb index 6de2d64a7e..1923d49733 100644 --- a/railties/lib/breakpoint.rb +++ b/railties/lib/breakpoint.rb @@ -21,6 +21,9 @@ require 'drb' require 'drb/acl' module Breakpoint + id = %q$Id: breakpoint.rb 41 2005-01-22 20:22:10Z flgr $ + Version = id.split(" ")[2].to_i + extend self # This will pop up an interactive ruby session at a @@ -114,10 +117,10 @@ module Breakpoint end end - module CommandBundle #:nodoc: + module CommandBundle # Proxy to a Breakpoint client. Lets you directly execute code # in the context of the client. - class Client#:nodoc: + class Client def initialize(eval_handler) # :nodoc: @eval_handler = eval_handler end @@ -133,15 +136,23 @@ module Breakpoint end # Will execute the specified statement at the client. - def method_missing(method, *args) - if args.empty? - result = eval("#{method}") + def method_missing(method, *args, &block) + if args.empty? and not block + result = eval "#{method}" else - result = eval("#{method}(*Marshal.load(#{Marshal.dump(args).inspect}))") - end - - unless [true, false, nil].include?(result) - result.extend(DRbUndumped) if result + # 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 @@ -175,6 +186,7 @@ module Breakpoint # 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) }) @@ -205,7 +217,7 @@ module Breakpoint # These exceptions will be raised on failed asserts # if Breakpoint.asserts_cause_exceptions is set to # true. - class FailedAssertError < RuntimeError#:nodoc: + class FailedAssertError < RuntimeError end # This asserts that the block evaluates to true. @@ -279,7 +291,7 @@ module Breakpoint @collision_handler.call end - def ping; end + def ping() end def add_breakpoint(context, message) workspace = IRB::WorkSpace.new(context) @@ -290,31 +302,7 @@ module Breakpoint @handler.call(workspace, message) end - def register_handler(&block) - @handler = block - end - - def unregister_handler - @handler = nil - end - - attr_reader :eval_handler - - def register_eval_handler(&block) - @eval_handler = block - end - - def unregister_eval_handler - @eval_handler = lambda { } - end - - def register_collision_handler(&block) - @collision_handler = block - end - - def unregister_collision_handler - @collision_handler = lambda { } - end + attr_accessor :handler, :eval_handler, :collision_handler end # Will run Breakpoint in DRb mode. This will spawn a server @@ -359,7 +347,8 @@ module Breakpoint # # Detailed information about running DRb through firewalls is # available at http://www.rubygarden.org/ruby?DrbTutorial - def activate_drb(uri = nil, allowed_hosts = ['localhost', '127.0.0.1', '::1'], ignore_collisions = false) #:nodoc: + def activate_drb(uri = nil, allowed_hosts = ['localhost', '127.0.0.1', '::1'], + ignore_collisions = false) return false if @use_drb @@ -402,7 +391,7 @@ module Breakpoint end # Deactivates a running Breakpoint service. - def deactivate_drb #:nodoc: + def deactivate_drb @service.stop_service unless @service.nil? @service = nil @use_drb = false @@ -411,7 +400,7 @@ module Breakpoint # Returns true when Breakpoints are used over DRb. # Breakpoint.activate_drb causes this to be true. - def use_drb? #:nodoc: + def use_drb? @use_drb == true end end @@ -440,7 +429,11 @@ module IRB # :nodoc: @CONF[:MAIN_CONTEXT] = irb.context old_sigint = trap("SIGINT") do - irb.signal_handle + begin + irb.signal_handle + rescue RubyLex::TerminateLineInput + # ignored + end end catch(:IRB_EXIT) do @@ -464,7 +457,7 @@ module IRB # :nodoc: end end - class Context#:nodoc: + class Context alias :old_evaluate :evaluate def evaluate(line, line_no) if line.chomp == "exit" then @@ -475,7 +468,7 @@ module IRB # :nodoc: end end - class WorkSpace#:nodoc: + class WorkSpace alias :old_evaluate :evaluate def evaluate(*args) @@ -493,7 +486,7 @@ module IRB # :nodoc: end end - module InputCompletor#:nodoc: + module InputCompletor def self.eval(code, context, *more) # Big hack, this assumes that InputCompletor # will only call eval() when it wants code @@ -504,9 +497,9 @@ module IRB # :nodoc: end module DRb # :nodoc: - class DRbObject#:nodoc: - undef :inspect - undef :clone + class DRbObject + undef :inspect if method_defined?(:inspect) + undef :clone if method_defined?(:clone) end end @@ -522,4 +515,4 @@ def assert(&block) Binding.of_caller do |context| Breakpoint.assert(context, &block) end -end \ No newline at end of file +end diff --git a/railties/lib/breakpoint_client.rb b/railties/lib/breakpoint_client.rb index fa93c11f3e..0abcb6824e 100644 --- a/railties/lib/breakpoint_client.rb +++ b/railties/lib/breakpoint_client.rb @@ -2,17 +2,18 @@ require 'breakpoint' require 'optparse' require 'timeout' -options = { +Options = { :ClientURI => nil, :ServerURI => "druby://localhost:42531", - :RetryDelay => 1, + :RetryDelay => 3, + :Permanent => true, :Verbose => false } ARGV.options do |opts| script_name = File.basename($0) opts.banner = [ - "Usage: ruby #{script_name} [options] [server uri]", + "Usage: ruby #{script_name} [Options] [server uri]", "", "This tool lets you connect to a breakpoint service ", "which was started via Breakpoint.activate_drb.", @@ -29,18 +30,13 @@ ARGV.options do |opts| "connections from the server.", "Default: Find a good URI automatically.", "Example: -c druby://localhost:12345" - ) { |options[:ClientURI]| } + ) { |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("-v", "--verbose", - "Report all connections and disconnections", - "Default: false" - ) { |options[:Verbose]| } + ) { |Options[:ServerURI]| } opts.on("-R", "--retry-delay=delay", Integer, "Automatically try to reconnect to the", @@ -49,124 +45,149 @@ ARGV.options do |opts| "A value of 0 disables automatical", "reconnecting completely.", "Default: 10" - ) { |options[:RetryDelay]| } + ) { |Options[:RetryDelay]| } + + opts.on("-P", "--[no-]permanent", + "Run the breakpoint client in permanent mode.", + "This means that the client will keep continue", + "running even after the server has closed the", + "connection. Useful for example in Rails." + ) { |Options[:Permanent]| } + + opts.on("-V", "--[no-]verbose", + "Run the breakpoint client in verbose mode.", + "Will produce more messages, for example between", + "individual breakpoints. This might help in seeing", + "that the breakpoint client is still alive, but adds", + "quite a bit of clutter." + ) { |Options[:Verbose]| } opts.separator "" opts.on("-h", "--help", "Show this help message." ) { puts opts; exit } + opts.on("-v", "--version", + "Display the version information." + ) do + id = %q$Id: breakpoint_client.rb 40 2005-01-22 20:05:00Z flgr $ + puts id.sub("Id: ", "") + puts "(Breakpoint::Version = #{Breakpoint::Version})" + exit + end opts.parse! end -options[:ServerURI] = ARGV[0] if ARGV[0] +Options[:ServerURI] = ARGV[0] if ARGV[0] -$running = true +module Handlers + extend self -trap("INT"){$running = false} + def breakpoint_handler(workspace, message) + puts message + IRB.start(nil, nil, workspace) + + puts "" + if Options[:Verbose] then + puts "Resumed execution. Waiting for next breakpoint...", "" + end + end + + def eval_handler(code) + result = eval(code, TOPLEVEL_BINDING) + if result then + DRbObject.new(result) + else + result + end + end -puts "Waiting for initial breakpoint..." + def collision_handler() + msg = [ + " *** Breakpoint service collision ***", + " Another Breakpoint service tried to use the", + " port already occupied by this one. It will", + " keep waiting until this Breakpoint service", + " is shut down.", + " ", + " If you are using the Breakpoint library for", + " debugging a Rails or other CGI application", + " this likely means that this Breakpoint", + " session belongs to an earlier, outdated", + " request and should be shut down via 'exit'." + ].join("\n") + + if RUBY_PLATFORM["win"] then + # This sucks. Sorry, I'm not doing this because + # I like funky message boxes -- I need to do this + # because on Windows I have no way of displaying + # my notification via puts() when gets() is still + # being performed on STDIN. I have not found a + # better solution. + begin + require 'tk' + root = TkRoot.new { withdraw } + Tk.messageBox('message' => msg, 'type' => 'ok') + root.destroy + rescue Exception + puts "", msg, "" + end + else + puts "", msg, "" + end + end +end + +# Used for checking whether we are currently in the reconnecting loop. +reconnecting = false loop do - DRb.start_service(options[:ClientURI]) + DRb.start_service(Options[:ClientURI]) begin - service = DRbObject.new(nil, options[:ServerURI]) + service = DRbObject.new(nil, Options[:ServerURI]) begin - timeout(10) { service.ping } - rescue Timeout::Error, DRb::DRbConnError - if options[:Verbose] - puts "", - " *** Breakpoint service didn't respond to ping request ***", - " This likely happened because of a misconfigured ACL (see the", - " documentation of Breakpoint.activate_drb, note that by default", - " you can only connect to a remote Breakpoint service via a SSH", - " tunnel), but might also be caused by an extremely slow connection.", - "" - end - raise - end + service.eval_handler = Handlers.method(:eval_handler) + service.collision_handler = Handlers.method(:collision_handler) + service.handler = Handlers.method(:breakpoint_handler) - begin - service.register_eval_handler do |code| - result = eval(code, TOPLEVEL_BINDING) - if result - DRbObject.new(result) - else - result - end - end - - service.register_collision_handler do - msg = [ - " *** Breakpoint service collision ***", - " Another Breakpoint service tried to use the", - " port already occupied by this one. It will", - " keep waiting until this Breakpoint service", - " is shut down.", - " ", - " If you are using the Breakpoint library for", - " debugging a Rails or other CGI application", - " this likely means that this Breakpoint", - " session belongs to an earlier, outdated", - " request and should be shut down via 'exit'." - ].join("\n") - - if RUBY_PLATFORM["win"] then - # This sucks. Sorry, I'm not doing this because - # I like funky message boxes -- I need to do this - # because on Windows I have no way of displaying - # my notification via puts() when gets() is still - # being performed on STDIN. I have not found a - # better solution. - begin - require 'tk' - root = TkRoot.new { withdraw } - Tk.messageBox('message' => msg, 'type' => 'ok') - root.destroy - rescue Exception - puts "", msg, "" - end - else - puts "", msg, "" - end + reconnecting = false + if Options[:Verbose] then + puts "Connection established. Waiting for breakpoint...", "" end - service.register_handler do |workspace, message| - puts message - IRB.start(nil, nil, workspace) - puts "", "Resumed execution. Waiting for next breakpoint...", "" - end - - puts "Connection established. Waiting for breakpoint...", "" if options[:Verbose] - - while $running + loop do begin service.ping rescue DRb::DRbConnError => error - puts "Server exited. Closing connection..." if options[:Verbose] + puts "Server exited. Closing connection...", "" + exit! unless Options[:Permanent] break end sleep(0.5) end ensure - service.unregister_handler + service.eval_handler = nil + service.collision_handler = nil + service.handler = nil end rescue Exception => error - break unless $running - if options[:RetryDelay] > 0 then - puts "No connection to breakpoint service at #{options[:ServerURI]}:", " (#{error.inspect})" if options[:Verbose] - error.backtrace if $DEBUG - - puts " Reconnecting in #{options[:RetryDelay]} seconds..." if options[:Verbose] - - sleep options[:RetryDelay] + if Options[:RetryDelay] > 0 then + if not reconnecting then + reconnecting = true + puts "No connection to breakpoint service at #{Options[:ServerURI]} " + + "(#{error.class})" + puts error.backtrace if $DEBUG + puts "Tries to connect will be made every #{Options[:RetryDelay]} seconds..." + end + + sleep Options[:RetryDelay] retry else raise end end -end \ No newline at end of file +end -- cgit v1.2.3