What is this?

Are you a Ruby enthusiast pining for the days of yore when you could fire up a debugger to walk through and learn a new piece of code? Do you ever find yourself desperately wishing you could attach to a Ruby process and find out what values are hanging around in a method call that doesn’t seem to be working properly? Or, perhaps you’re somebody that is tired of the “add a puts call - run the process - test the problem” cycle? If any of this sounds familiar then making use of a RuntimeInspectionThread is for you.

An RTI thread exposes remote access into a running Ruby process. Once instantiated, you can telnet in and issue strings that will be passed off to eval with the results printed back to the telnet session. The code evaluation can either occur in a generic binding as defined by the main thread’s instantiation or the evaluation can be run from within a given breakpoint–not unlike using the Ruby debugger. Thus, you have the full power of inspection as well as the ability to break into a thread and carefully step through logic, inspecting local variables and such along the way. RTI even provides some helpful lookup methods that can be accessed to obtain and inspect various objects instantiated in the process you are trying to learn and/or debug.

Also note that there are other ways to help you work with and debug Ruby processes.

  • Use GDB to attach to a process and issue Ruby commands. This is very useful in those cases where Ruby itself is spinning, if a C extension is having problems, or if you’d just like to see where the Ruby or C call stack is at a given point in time. There are some very handy helpers for making this level of interaction easier.

  • Statically insert breakpoints in your code using ruby-breakpoint and IRB.

General Inspection Example

The following is an example session from the client’s perspective to give you an very simple sampling of some of the capabilities of an RTI thread’s inspection abilities.

$ telnet localhost 56789
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

myapp:001:0> local_variables
=> ["rti"]

myapp:002:0> rti.state
=> {:socket=>#<TCPSocket:0xb7cb92d0 127.0.0.1:48720>, :rti=>#<RuntimeInspectionThread::RTIManager:0xb7cb5f18 @tracing_proc=#<Proc:[email protected]:511>, @breakpoints={}, @state={...}>, :cmd_count=>2, :safe_level=>3, :block_count=>0, :block_cmd=>"", :use_yaml=>false, :eval_timeout=>60}

myapp:003:0> rti.state.use_yaml = true
=> --- true

myapp:004:0> rti.state
=> --- &id001 !map:RuntimeInspectionThread::State
:socket: !ruby/object:TCPSocket
  peerstr: 127.0.0.1:48720
:rti: !ruby/object:RuntimeInspectionThread::RTIManager
  breakpoints: {}

  state: *id001
  tracing_proc: !ruby/object:Proc {}

:cmd_count: 4
:safe_level: 3
:block_count: 0
:block_cmd: ""
:use_yaml: true
:eval_timeout: 60

myapp:005:0> rti.state.block_count = 1
=> --- 1

myapp:006:1> 2.times do
myapp:007:1> |i|
myapp:008:1> p i
myapp:009:1> end
myapp:010:1>
0
1
=> --- 2

myapp:011:2> exit
myapp:012:2>
=> --- !ruby/exception:SystemExit
message: "(eval):1:in `exit': exit"

myapp:013:3> ^]

telnet> quit
Connection closed.

Note, after the block mode was enabled, an empty line was necessary for the evaluation of the code to happen. The final exit command is actually going to try to call exit in the remote process. Because we run with a default $SAFE level of 3, this action is denied and an exception is raised, caught, and shown to the remote client.

There are two numbers shown in the prompt. The first is simply the “line” number of the current command. It is an ever increasing value for every input received from the client. The second is the block count. Normally, this is zero indicating each line is a full command to evaluate. However, when the block count is non-zero, this second value increases each time a full block is evaluated.

Here’s what happened on the server’s side (note, it’s running with debug enabled so we can see the details):

$ ruby -d -Ilib test/myapp.rb
Runtime inspection available at localhost:56789
Connection established with 127.0.0.1:48720
Executing [3]: local_variables
Executing [3]: rti.state
Executing [3]: rti.state.use_yaml = true
Executing [3]: rti.state
Executing [3]: rti.state.block_count = 1
Executing [3]: 2.times do
|i|
p i
end
Executing [3]: exit
Cleaning up 127.0.0.1:48720
Closed connection from 127.0.0.1:48720

Breakpoint Example

Stopping another thread’s execution can serve to be very useful to track how the program operates as well as to inspect any local values for erroneous data. From the client’s perspective, the following is what might be expected.

$ telnet localhost 56789
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

myapp:001:0> .bp_add Foo#bar
=> "Added breakpoint 1"

myapp:002:0> start_foo
=> #<Thread:0xb7d22974 sleep>

myapp:003:0> .bp_start
=> nil

Breakpoint 1 in Foo#bar from myapp.rb:10 (call)
myapp:004:0> local_variables
=> ["b", "rti"]

Breakpoint 1 in Foo#bar from myapp.rb:10 (call)
myapp:005:0> b
=> nil

Breakpoint 1 in Foo#bar from myapp.rb:10 (call)
myapp:006:0> b = 1003
=> 1003

Breakpoint 1 in Foo#bar from myapp.rb:10 (call)
myapp:007:0> @a
=> 3

Breakpoint 1 in Foo#bar from myapp.rb:10 (call)
myapp:008:0> @a = 11
=> 11

Breakpoint 1 in Foo#bar from myapp.rb:10 (call)
myapp:009:0> .bp_next
=> nil

Breakpoint 1 in Foo#bar from myapp.rb:11 (line)
myapp:010:0> .bp_continue
=> nil

Breakpoint 1 in Foo#bar from myapp.rb:10 (call)
myapp:011:0> b
=> nil

Breakpoint 1 in Foo#bar from myapp.rb:10 (call)
myapp:012:0> p @a
12
=> nil

Breakpoint 1 in Foo#bar from myapp.rb:10 (call)
myapp:013:0> .bp_stop
=> nil

myapp:014:0> ^]

telnet> quit
Connection closed.

Note that values can be altered in addition to examined. The “period” marker is used to denote actions that can be executed directly in the RTI object itself, instead of running through an eval. This makes some commands easier to work with without having to escape much of the data to pass through. For example, the breakpoint could have been added via a command like “rti.bp_add ‘Foo#bar’” to achieve the same effect.

And from the server’s perspective (running in debug mode to see the details), after the call to start_foo is made, the Foo#bar object begins printing out the value of its member variable, @a. Notice that the value jumps from 2 to 11 after the breakpoint is hit and the client has adjusted the value.

$ ruby -d -Ilib test/myapp.rb
Runtime inspection available at localhost:56789
Connection established with 127.0.0.1:40202
Executing [3]: start_foo
0
1
2
Executing (explicit binding) [3]: local_variables
Executing (explicit binding) [3]: b
Executing (explicit binding) [3]: b = 1003
Executing (explicit binding) [3]: @a
Executing (explicit binding) [3]: @a = 11
11
Executing (explicit binding) [3]: b
Executing (explicit binding) [3]: p @a
12
Cleaning up 127.0.0.1:40202
Closed connection from 127.0.0.1:40202
13
14

Finding and Inspecting Other Objects Example

It may be unnecessary to halt a thread in a breakpoint. In many cases, simple examination of a particular object is needed. The following is a straightforward case that looks for a specific object and inspects it using methods added to the object dynamically.

$ telnet localhost 56789
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

myapp:001:0> start_foo
=> #<Thread:0xb7d11d04 sleep>

myapp:002:0> f = rti.get_object('Foo')
=> #<Foo:0xb7d116b0 @a=2>

myapp:003:0> rti.state.block_count = 1
=> 1

myapp:004:1> def f.seeit
myapp:005:1> @a
myapp:006:1> end
myapp:007:1>
=> nil

myapp:008:2> f.seeit
myapp:009:2>
=> 11

myapp:010:3> ^]

telnet> quit
Connection closed.

No surprises from the server…

$ ruby -d -Ilib test/myapp.rb
Runtime inspection available at localhost:56789
Connection established with 127.0.0.1:45689
Executing [3]: start_foo
0
1
Executing [3]: f = rti.get_object('Foo')
2
Executing [3]: rti.state.block_count = 1
3
4
5
6
7
8
9
Executing [3]: def f.seeit
@a
end
10
Executing [3]: f.seeit
11
12
13
14
Cleaning up 127.0.0.1:45689
Closed connection from 127.0.0.1:45689

Inserting RTI Dynamically

Now, imagine that you have a Ruby process running that did not require or load RTI on its own. Here’s how to insert it live without stopping and restarting the process. First, we start up a process without RTI…

$ ruby -d -Ilib test/myapp.rb noload
Running as 26061

…then we use the rti helper script to load in RTI…

$ bin/rti -load lib 26061
Loaded lib/rtinspect.rb in process 26061

…which does this in the myapp.rb process…

Running as 26061
true
Runtime inspection available at localhost:56789
#<RuntimeInspection::Thread:0xb79f9874 sleep []>

…and now we telnet to RTI just like the previous examples…

$ telnet localhost 56789
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

myapp:001:0> local_variables
=> ["a", "b", "rti"]

myapp:002:0>

RTI Use

RTI can be distributed via the same kind of terms as the Ruby license.

There are a few simple ways of enabling an RTI thread:

  1. Require the class and enable the thread via a signal.

  2. Require the class and always have the thread running and available.

  3. Run the script in attach mode whereby it leverages GDB to attach to a running process to load itself and start an RTI thread.

Tips and Tricks

  • The default binding used in normal (non-breakpoint) evals is kept around. Thus, you can use this to store locals across eval calls.

  • The state information is available as a local to each eval call as simply “rti_state”. This means you can do things like “rti_state.use_yaml = true” to adjust various runtime options of the client state or store information to keep across eval calls during a breakpoint eval.

Notes/Caveats

  • If two clients are debugging the same thread, only one will get access to it at a time.

  • Only one thread may have request a given breakpoint (i.e. two threads can not both try to stop at Foo#bar).