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
This commit is contained in:
David Heinemeier Hansson 2005-01-24 13:41:57 +00:00
parent 981d86cbb9
commit 505e2d99da
6 changed files with 840 additions and 837 deletions

@ -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
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

File diff suppressed because it is too large Load Diff

@ -1,5 +1,7 @@
*SVN*
* Added Florian Gross' latest version of Breakpointer and friends that fixes a variaty of bugs #441 [Florian Gross]
* Fixed skeleton Rakefile to work with sqlite3 out of the box #521 [rasputnik]
* Fixed that script/breakpointer didn't get the Ruby path rewritten as the other scripts #523 [brandt@kurowski.net]

@ -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
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

@ -21,6 +21,9 @@
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 @@ def breakpoint(id = nil, context = nil, &block)
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 @@ def eval(code)
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 @@ def source_lines(context = 5, return_line_numbers = false)
# 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 @@ def handle_breakpoint(context, message, file = "", line = "", &block) # :nodoc:
# 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 @@ def collision
@collision_handler.call
end
def ping; end
def ping() end
def add_breakpoint(context, message)
workspace = IRB::WorkSpace.new(context)
@ -290,31 +302,7 @@ def add_breakpoint(context, message)
@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 @@ def unregister_collision_handler
#
# 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 @@ def activate_drb(uri = nil, allowed_hosts = ['localhost', '127.0.0.1', '::1'], i
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 @@ def deactivate_drb #:nodoc:
# 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 @@ def self.start(ap_path = nil, main_context = nil, workspace = nil)
@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 @@ def result.last_value; end
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 @@ def evaluate(line, line_no)
end
end
class WorkSpace#:nodoc:
class WorkSpace
alias :old_evaluate :evaluate
def evaluate(*args)
@ -493,7 +486,7 @@ def evaluate(*args)
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 @@ def self.eval(code, context, *more)
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
end

@ -2,17 +2,18 @@
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 @@
"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 @@
"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 "Waiting for initial breakpoint..."
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
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
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
puts " Reconnecting in #{options[:RetryDelay]} seconds..." if options[:Verbose]
sleep options[:RetryDelay]
sleep Options[:RetryDelay]
retry
else
raise
end
end
end
end