dcsimg

Hijack: Living on the Edge of (Ruby and) Rails, Part 4

Hijack can debug most any running Ruby process. Here's a hands-on introduction.

The previous three installments of this series looked at new features in Edge Rails and other very recent releases of the popular Web development framework. Tomorrow’s installment looks at some next-generation gems and next week’s regular column looks at nested forms, an addition sure to please every Rails developer, from newbie to master.

Today’s installment is something of a spur, because I had intended to write about a different topic. However, today’s topic is very clever, certainly on the edge—a first alpha—and useful nonetheless.

Debug Anything with Hijack

I’m a big fan of irb and the Ruby debugger, and use both to explore code and find and fix bugs in Ruby and Rails projects. For a Ruby script, install the ruby-debug gem and then invoke the script with the debugger, rdebug.

$ sudo gem install ruby-debug
Successfully installed ruby-debug-0.10.3
1 gem installed
Installing ri documentation for ruby-debug-0.10.3...
Installing RDoc documentation for ruby-debug-0.10.3...
$ which rdebug
/usr/bin/rdebug
$ rdebug some_script.rb

It’s also very easy to debug a Rails application: Sprinkle the statement…

debugger

… liberally throughout your code and invoke the server with…

$ ./script/server --debugger

Whenever your Rails application hits a debugger statement, ZAP!, the application suspends, presenting a prompt where you can poke and prod variables, instances, and classes, and step through statements. After you address the defect, either remove the debugger statements or omit the --debugger option. In the latter case, debugger statements are simply ignored (although the server reminds you of that with some vociferousness.)

Real world scenarios tend to be a little more challening. What happens when a bug appears after a certain period or occurs only intermittently? Short of divine intervention, your logs, the application database, and anecdotal evidence may be your only clues to the culprit.

But like Prometheus, who stole fire from Zeus and returned it to man, developer Ian Leitch has granted we mere mortals a little bit of omniscience. Leitch’s Hijack can attach to almost any running Ruby process, interrupt its execution, and let your peer in as if you were truly all-powerful. You can swap in code on-the-fly, too, to add a debugger call wherever you need it.

Hijack works with common scripts and can attach to the Mongrel daemon and Thin, too. However, it does not yet work with the Mongrel script (./script/server) included with every Rails project, nor with the Rails script runner. Leitch has plans to address these shortcomings in the near future. As he said in an email to me, “Hijack was little more than an experiment up until a few days ago when someone mentioned it on Twitter.”

As a demonstration, let’s attach to a small Rails application and use the available techniques to usurp execution and debug the code. The test machine is a MacBook running Mac OS X Leopard, Edge Rails, and the GNU Debugger, Apple version gdb-966.

Using Hijack with Debugger Statements

Using Hijack is simple. First, install it. Hijack adds a gem and a command-line utility called hijack.

$ gem sources -a http://gems.github.com
$ sudo gem install ileitch-hijack
$ which hijack
/usr/bin/hijack

For this initial example, I’ve left a breakpoint in one of my Rails validators. Typically, mongrel_rails ignores such a statement. However, once I hijack the process, the application goes into limbo when it meanders across line 1.

def validate_novelty
  debugger

  self.errors.add_to_base( 'This location is already defined' ) unless Location.
  find(:first, :conditions =>
    { :group_id => group.id, :room => room, :aisle => aisle, :bin => bin } ).nil?
end

To continue, install the ruby-debug gem (if you do not yet have it) and launch your application with mongrel_rails start.

$ sudo gem install ruby-debug
$ cd ~/Projects/warehouse
$ mongrel_rails start
** Starting Mongrel listening at 0.0.0.0:3000
** Starting Rails with development environment...
** Rails loaded.
** Loading any Rails specific GemPlugins
** Signals ready.  TERM => stop.  USR2 => restart.  INT => stop (no restart).
** Rails signals registered.  HUP => reload (without restart).  It might not work well.
** Mongrel 1.1.5 available at 0.0.0.0:3000
** Use CTRL-C to stop.

Next, attach to the Mongrel process. Use ps or jobs to find its process ID and invoke Hijack.

$ ps ux | grep Mongrel
7563 s003  S+     0:04.37 /System/Library/Frameworks/Ruby.framework/
  Versions/1.8/usr/bin/ruby /usr/bin/mongrel_rails start
$ hijack 7563
=> Hijacked 7563 (/usr/bin/mongrel_rails) (ruby 1.8.6 [universal-darwin9.0])
>>

When the Hijack prompt appears, type two commands: hijack_debug_mode followed by hijack_debug_start. (I omitted the former several times in preparing this post, with unpredictable results.)

>> hijack_debug_mode
=> true
>> hijack_debug_start
Connected.

According to Leitch, the two commands break the hijacking process into two steps intentionally. The first command, hijack_debug_mode, requires ruby-debug and then stops by design so you can insert your breakpoint(s). The next command, hijack_debug_start, connects hijack to the application. (Specifically, hijack connects to a remote debugger which proxies for the application.) debugger calls fire only when the application is connected.

At this point, you are connected and ready to debug. Whenever the application hits the breakpoint, Hijack drops to the debugger. Examine variables and look at backtraces as needed; when finished type continue to resume normal processing.

Connected.
(rdb:20) p self
#<Location id: nil, group_id: 1, room: "sds", aisle: "dsds", bin: "dsdsd", created_at: nil, updated_at: nil>
(rdb:20) continue

Under the hood, Hijack uses the GNU Debugger, or gdb, to inject code that starts a Distributed Ruby (DRb) server. Once DRb is up and running, gdb is no longer needed and Hijack communicates directly with DRb. You must run Hijack on the same machine as the process being scrutinized; you must also run Hijack as the same user as the target process. Both limitations protect your processes from, well, being hijacked for nefarious purposes.

Hijack Running Code

In addition to affecting the execution of a running process, you can also change its code on-the-fly. You can replace both class methods and instance methods.

To replace code, start your server and hijack and type hijack_debug_mode. At the prompt, enter any code you want to “inject”. The general template to replace a class method is:

Klass.class_eval do
  class << self
    def some_class_method
      ...
      debugger
      ...
    end
  end
end

class_eval evaluates the block within the context of the named class, here Klass. The meta-class class << self affects the class methods for Klass. Hence, you can override any class method simply be redefining it. Leitch provides this example.

Start your Rails application and start Hijack. Type hijack_debug_mode and then paste the following code at the prompt.

ActionController::Dispatcher.class_eval do
  class << self
    def dispatch_with_debugger(cgi, session_options, output)
      debugger
      dispatch_without_debugger(cgi, session_options, output)
    end

    alias_method :dispatch_without_debugger, :dispatch
    alias_method :dispatch, :dispatch_with_debugger
  end
end

Now, if you point your browser to the application, it suspends execution within the dispatch method, which is a reasonable starting point to debug most anything.

The general template to replace instance methods looks like this:

Qlass.class_eval do
  def some_instance_method
    ...
    debugger
    ...
  end
end

Returning to my first example, if debugger wasn't already present, I could inject what I wanted with Hijack.

$ hijack 9377
=> Hijacked 9377 (/usr/bin/mongrel_rails) (ruby 1.8.6 [universal-darwin9.0])
>> hijack_debug_mode
=> true
>> Location.class_eval do
?>   def validate_novelty
>>     debugger
>>
?>     self.errors.add_to_base( 'This location is already defined' ) unless Location.
?>       find(:first, :conditions => { :group_id => group.id, :room => room, :aisle => aisle, :bin => bin } ).nil?
>>   end
>> end
=> nil
>> hijack_debug_start
Connected.

When I use the application to subsequently invoke the validator validate_novelty, Hijack drops to the debugger prompt.

(rdb:8)

Grand Theft Ruby

Hijack is in a nascent stage, but already has proven to be a very valuable tool. Expect lots of new features in the next release: pulling people from cars, shooting at cops, and outrunning John Law. Just kidding. This isn't Grand Theft Ruby. Still, the name is catchy.

My thanks to Ian Leitch for midnight tech support.

Happy tinkering.

Fatal error: Call to undefined function aa_author_bios() in /opt/apache/dms/b2b/linux-mag.com/site/www/htdocs/wp-content/themes/linuxmag/single.php on line 62