Best way to debug ruby+fastcgi?

J

J. D.

What is the best way to debug fastcgi scripts?

Do you use Ruby? Expect? Or just deal with 'incomplete header'
messages in the Apache logs?
 
E

Eric Hodel

What is the best way to debug fastcgi scripts?

Do you use Ruby? Expect? Or just deal with 'incomplete header'
messages in the Apache logs?

1) Run the equivalent as a regular CGI, make sure it works.
2) Swear
3) Play with rescue and printing to STDERR until you find the error.
 
D

David Garamond

J. D. said:
What is the best way to debug fastcgi scripts?

Do you use Ruby? Expect? Or just deal with 'incomplete header'
messages in the Apache logs?

Argh, that error message always makes me mad. I'm sure Apache can be
configured so we can see the script's STDERR on some logs, but so far I
didn't know how.

Sometimes switching to CGI doesn't help, as you need to know what went
wrong on _that particular request_...
 
F

Florian Gross

J. D. said:
What is the best way to debug fastcgi scripts?

Remote breakpoints might be a good option here -- they let you connect
an irb shell to any given point of your application meaning you can just
use Ruby's regular introspection facilities to have a look at local
variables, instance variables, call traces, methods and modify Object
altogether. Attached to this mail you will find the latest mostly stable
version which will turn into a release soon. If everything goes well
this should be merged back into dev-utils and be available in Ruby On
Rails with a few nice goodies (a link to launch a breakpoint from the
error page you get in your browser when an uncaught exception of your
application occurs) very soon.


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

# 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 (e-mail address removed)
# 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 'binding_of_caller'
require 'drb'
require 'drb/acl'

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(#<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

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 = 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)
if args.empty?
result = eval("#{method}")
else
result = eval("#{method}(*Marshal.load(#{Marshal.dump(args).inspect}))")
end

unless [true, false, nil].include?(result)
result.extend(DRbUndumped) rescue nil
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
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
}, 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 :eek:ptimize_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.call
end

def ping; end

def add_breakpoint(context, message)
workspace = IRB::WorkSpace.new(context)
workspace.extend(DRbUndumped)

sleep(0.5) until @handler

@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
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 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
def activate_drb(uri = 'druby://localhost:42531',
allowed_hosts = '127.0.0.1')

return false if @use_drb

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
DRb.start_service(uri, @drb_service)
rescue Errno::EADDRINUSE
# 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

return true
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
irb.signal_handle
end

catch:)IRB_EXIT) do
irb.eval_input
end
ensure
trap("SIGINT", old_sigint)
end

class << self
alias :eek:ld_CurrentContext :CurrentContext
remove_method :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 :eek:ld_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 :eek:ld_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
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
undef :inspect
undef :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

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_eval_handler do |code|
result = eval(code, TOPLEVEL_BINDING)
result.extend(DRb::DRbUndumped) rescue nil
result
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
end

service.register_handler do |workspace, message|
puts message
IRB.start(nil, nil, workspace)
end

loop do
begin
service.ping
rescue DRb::DRbConnError => error
puts "Server exited. Exiting..."
exit!
end

sleep(0.5)
end
ensure
service.unregister_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
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,766
Messages
2,569,569
Members
45,043
Latest member
CannalabsCBDReview

Latest Threads

Top