rtinspect 0.0.1 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README +84 -50
- data/TODO +18 -7
- data/VERSION +1 -0
- data/bin/rti +69 -0
- data/lib/rti/manager.rb +411 -0
- data/lib/rti/redirect.rb +102 -0
- data/lib/rti/state.rb +65 -0
- data/lib/rti/thread.rb +553 -0
- data/lib/rti/tracer.rb +71 -0
- data/lib/rtinspect.rb +76 -899
- data/test/test.rb +248 -17
- metadata +13 -6
data/lib/rti/tracer.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# Contains the RuntimeInspection::Tracer.
|
2
|
+
#
|
3
|
+
# Copyright (c) 2006-2007 Brad Robel-Forrest.
|
4
|
+
#
|
5
|
+
# This software can be distributed under the terms of the Ruby license
|
6
|
+
# as detailed in the accompanying LICENSE[link:files/LICENSE.html]
|
7
|
+
# file.
|
8
|
+
|
9
|
+
#
|
10
|
+
module RuntimeInspection
|
11
|
+
|
12
|
+
# This will help track the tracing of a particular StopPoint in a
|
13
|
+
# given thread. Having a tracer assigned to a thread (via the
|
14
|
+
# :rti_tracer element stored in the thread from
|
15
|
+
# Thread#handle_tracing) ensures that a RTIManager#bp_finish,
|
16
|
+
# RTIManager#bp_next, or RTIManager#bp_step will always occur
|
17
|
+
# within in the same thread. Note that it is still possible for
|
18
|
+
# another breakpoint to "interrupt" this tracer and stop
|
19
|
+
# processing somewhere else.
|
20
|
+
#
|
21
|
+
class Tracer
|
22
|
+
def initialize( portal, cmd )
|
23
|
+
@portal = portal
|
24
|
+
@cmd = cmd
|
25
|
+
end
|
26
|
+
|
27
|
+
# Given a generic StopPoint, adjust it where necessary if we
|
28
|
+
# are at the next real StopPoint that should be passed off to
|
29
|
+
# the client.
|
30
|
+
#
|
31
|
+
def stoppoint( sp )
|
32
|
+
case @cmd
|
33
|
+
when :step
|
34
|
+
sp.state = @portal.stoppoint.state
|
35
|
+
return sp
|
36
|
+
when :next
|
37
|
+
case sp.event
|
38
|
+
when 'line'
|
39
|
+
if( @portal.stoppoint.classname == sp.classname and
|
40
|
+
@portal.stoppoint.methodname == sp.methodname )
|
41
|
+
sp.state = @portal.stoppoint.state
|
42
|
+
return sp
|
43
|
+
end
|
44
|
+
when 'return'
|
45
|
+
if( @portal.stoppoint.classname == sp.classname and
|
46
|
+
@portal.stoppoint.methodname == sp.methodname )
|
47
|
+
@cmd = :any_line
|
48
|
+
end
|
49
|
+
end
|
50
|
+
return :continue
|
51
|
+
when :fin
|
52
|
+
if( sp.event == 'return' and
|
53
|
+
@portal.stoppoint.classname == sp.classname and
|
54
|
+
@portal.stoppoint.methodname == sp.methodname )
|
55
|
+
sp.state = @portal.stoppoint.state
|
56
|
+
return sp
|
57
|
+
end
|
58
|
+
when :any_line
|
59
|
+
if sp.event == 'line'
|
60
|
+
sp.state = @portal.stoppoint.state
|
61
|
+
return sp
|
62
|
+
else
|
63
|
+
return :continue
|
64
|
+
end
|
65
|
+
else
|
66
|
+
raise "Unknown tracer command: #{@cmd}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
data/lib/rtinspect.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# :main: README
|
2
|
-
# :title: Runtime Inspection and Debugging Thread
|
3
2
|
#
|
4
|
-
# Contains the
|
3
|
+
# Contains main entry point of the RuntimeInspection module.
|
5
4
|
#
|
6
5
|
# Copyright (c) 2006-2007 Brad Robel-Forrest.
|
7
6
|
#
|
@@ -11,940 +10,118 @@
|
|
11
10
|
|
12
11
|
require 'socket'
|
13
12
|
require 'thread'
|
14
|
-
require 'timeout'
|
15
13
|
require 'yaml'
|
16
14
|
|
17
|
-
|
18
|
-
# TCP socket.
|
19
|
-
#
|
20
|
-
class RuntimeInspectionThread < Thread
|
21
|
-
|
22
|
-
# This is the value used to distinguish data sent to the client
|
23
|
-
# from within the command evaluation from the value returned from
|
24
|
-
# the evaluation.
|
25
|
-
#
|
26
|
-
OUTPUT_MARKER = '=>'
|
27
|
-
|
28
|
-
if $DEBUG
|
29
|
-
@@dbg_handler = Proc.new {|msg| puts msg}
|
30
|
-
else
|
31
|
-
@@dbg_handler = Proc.new {}
|
32
|
-
end
|
33
|
-
|
34
|
-
@@exc_handler = Proc.new do |msg, exc|
|
35
|
-
STDERR.puts( "#{Thread.current}: Exception while #{msg}",
|
36
|
-
exc, exc.backtrace )
|
37
|
-
end
|
38
|
-
|
39
|
-
# Optionally enable your own handling of debug output. Default is
|
40
|
-
# to either suppress it or send it to $stdout if $DEBUG is enabled
|
41
|
-
# (only checked at class load time).
|
42
|
-
#
|
43
|
-
def self.dbg_handler( &block ) # :yields: msg
|
44
|
-
@@dbg_handler = block
|
45
|
-
end
|
46
|
-
|
47
|
-
# Optionally enable your own exception handler. Default is to
|
48
|
-
# write the exception and backtrace out to $stderr.
|
49
|
-
#
|
50
|
-
def self.exc_handler( &block ) # :yields: msg, exc
|
51
|
-
@@exc_handler = block
|
52
|
-
end
|
53
|
-
|
54
|
-
# Used to manage information found at each stopping point--either
|
55
|
-
# hit by a BreakPoint or because of a rti_next or rti_step
|
56
|
-
# request.
|
57
|
-
#
|
58
|
-
StopPoint = Struct.new( :classname, :methodname, :file, :line, :event,
|
59
|
-
:breakpoint )
|
60
|
-
|
61
|
-
# Used as a messaging mechanism once a StopPoint is hit to
|
62
|
-
# coordinate commands received from the client in a client thread
|
63
|
-
# (see run_client_thread) with the actual evaluation run from
|
64
|
-
# within the StopPoint's context (it's binding--see
|
65
|
-
# handle_tracing).
|
66
|
-
#
|
67
|
-
Portal = Struct.new( :stoppoint, :cmds, :out )
|
68
|
-
|
69
|
-
# Used when we want to redirect stdout or stderr for a specific
|
70
|
-
# thread. This is intelligent enough so that it reuses one if it
|
71
|
-
# is already in place from another thread.
|
72
|
-
#
|
73
|
-
class ThreadRedirect
|
15
|
+
module RuntimeInspection
|
74
16
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
# Replace the global stdout and stderr with a redirection
|
81
|
-
# object. If one is already there, then use it.
|
82
|
-
#
|
83
|
-
def self.start
|
84
|
-
MUTEX.synchronize do
|
85
|
-
if $stdout.kind_of? self
|
86
|
-
$stdout.usage += 1
|
87
|
-
else
|
88
|
-
$stdout = new( $stdout )
|
89
|
-
end
|
90
|
-
if $stderr.kind_of? self
|
91
|
-
$stderr.usage += 1
|
92
|
-
else
|
93
|
-
$stderr = new( $stderr )
|
94
|
-
end
|
95
|
-
end
|
96
|
-
return true
|
97
|
-
end
|
98
|
-
|
99
|
-
# Stop any redirection and put the original IO back in its
|
100
|
-
# place unless there are still other threads using
|
101
|
-
# redirection. Then, wait for them to stop
|
102
|
-
#
|
103
|
-
def self.stop
|
104
|
-
MUTEX.synchronize do
|
105
|
-
unless $stdout.kind_of? self
|
106
|
-
if $stdout.usage > 0
|
107
|
-
$stdout.usage -= 1
|
108
|
-
else
|
109
|
-
$stdout = $stdout.default_io
|
110
|
-
end
|
111
|
-
end
|
112
|
-
unless $stderr.kind_of? self
|
113
|
-
if $stderr.usage > 0
|
114
|
-
$stderr.usage -= 1
|
115
|
-
else
|
116
|
-
$stderr = $stderr.default_io
|
117
|
-
end
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
# Create a new redirection manager for the IO stream provided.
|
123
|
-
#
|
124
|
-
def initialize( default_io )
|
125
|
-
@default_io = default_io
|
126
|
-
@usage = 0
|
127
|
-
end
|
128
|
-
|
129
|
-
# A reference counter if other threads are also using this
|
130
|
-
# redirection manager.
|
131
|
-
#
|
132
|
-
attr_accessor :usage
|
133
|
-
|
134
|
-
# The IO to use for non-redirected output.
|
135
|
-
#
|
136
|
-
attr_reader :default_io
|
137
|
-
|
138
|
-
# Provide the output method so this object can be used as
|
139
|
-
# either stdout or stderr.
|
140
|
-
#
|
141
|
-
def write( *args )
|
142
|
-
if rio = Thread.current[:rti_redirect_io]
|
143
|
-
rio.write( *args )
|
144
|
-
else
|
145
|
-
@default_io.write( *args )
|
146
|
-
end
|
147
|
-
end
|
148
|
-
|
149
|
-
def method_missing( sym, *args, &block )
|
150
|
-
if rio = Thread.current[:rti_redirect_io]
|
151
|
-
rio.send( sym, *args, &block )
|
152
|
-
else
|
153
|
-
@default_io.send( sym, *args, &block )
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
end
|
158
|
-
|
159
|
-
# Data managed about each TCP connection. Note that these values
|
160
|
-
# can be manged from the remote session (e.g. by issuing commands
|
161
|
-
# like <em>state.use_yaml = true</em>).
|
162
|
-
#
|
163
|
-
class State < Hash
|
164
|
-
|
165
|
-
# Create a new object for tracking state information.
|
166
|
-
#
|
167
|
-
# +socket+:: The owner connection for this data.
|
168
|
-
#
|
169
|
-
# +cmd_count+:: The current evaluation number.
|
170
|
-
#
|
171
|
-
# +block_count+:: The current block number. When bigger than zero,
|
172
|
-
# multiple lines are collected until a blank line
|
173
|
-
# is encountered and then the entire block is
|
174
|
-
# evaluated. Each successive block will increase
|
175
|
-
# the number (similar to the normal _cmd_count_
|
176
|
-
# value).
|
177
|
-
#
|
178
|
-
# +block_cmd+:: The collection of the block if the +block_count+
|
179
|
-
# is larger than zero.
|
180
|
-
#
|
181
|
-
# +use_yaml+:: The output should be YAML.
|
182
|
-
#
|
183
|
-
# +eval_timeout+:: The number of seconds to allow an evaluation to
|
184
|
-
# continue. All other connections are blocked during
|
185
|
-
# this period.
|
186
|
-
#
|
187
|
-
# +safe_level+:: The level $SAFE is declared at for each evaluation.
|
188
|
-
#
|
189
|
-
def initialize( socket=Thread.current[:socket],
|
190
|
-
cmd_count=1, block_count=0, block_cmd='',
|
191
|
-
use_yaml=false, eval_timeout=60, safe_level=3 )
|
192
|
-
super()
|
193
|
-
self[:socket] = socket
|
194
|
-
self[:cmd_count] = cmd_count
|
195
|
-
self[:block_count] = block_count
|
196
|
-
self[:block_cmd] = block_cmd
|
197
|
-
self[:use_yaml] = use_yaml
|
198
|
-
self[:eval_timeout] = eval_timeout
|
199
|
-
self[:safe_level] = safe_level
|
200
|
-
end
|
201
|
-
|
202
|
-
def method_missing( sym, val=nil )
|
203
|
-
if key?( sym )
|
204
|
-
self[sym]
|
205
|
-
else
|
206
|
-
str = sym.to_s
|
207
|
-
if str[-1] == ?=
|
208
|
-
self[str.chop.to_sym] = val
|
209
|
-
else
|
210
|
-
self[sym]
|
211
|
-
end
|
212
|
-
end
|
17
|
+
class ::Thread
|
18
|
+
alias inspect_without_name inspect
|
19
|
+
def inspect
|
20
|
+
show_with_name( inspect_without_name )
|
213
21
|
end
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
class Tracer
|
218
|
-
def initialize( portal, cmd )
|
219
|
-
@portal = portal
|
220
|
-
@cmd = cmd
|
22
|
+
alias to_s_without_name to_s
|
23
|
+
def to_s
|
24
|
+
show_with_name( to_s_without_name )
|
221
25
|
end
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
return sp
|
228
|
-
when :next
|
229
|
-
case sp.event
|
230
|
-
when 'line'
|
231
|
-
if( @portal.stoppoint.classname == sp.classname and
|
232
|
-
@portal.stoppoint.methodname == sp.methodname )
|
233
|
-
sp.breakpoint = @portal.stoppoint.breakpoint
|
234
|
-
return sp
|
235
|
-
end
|
236
|
-
when 'return'
|
237
|
-
if( @portal.stoppoint.classname == sp.classname and
|
238
|
-
@portal.stoppoint.methodname == sp.methodname )
|
239
|
-
@cmd = :any_line
|
240
|
-
end
|
241
|
-
end
|
242
|
-
return :continue
|
243
|
-
when :fin
|
244
|
-
if( sp.event == 'return' and
|
245
|
-
@portal.stoppoint.classname == sp.classname and
|
246
|
-
@portal.stoppoint.methodname == sp.methodname )
|
247
|
-
sp.breakpoint = @portal.stoppoint.breakpoint
|
248
|
-
return sp
|
249
|
-
end
|
250
|
-
when :any_line
|
251
|
-
if sp.event == 'line'
|
252
|
-
sp.breakpoint = @portal.stoppoint.breakpoint
|
253
|
-
return sp
|
254
|
-
else
|
255
|
-
return :continue
|
256
|
-
end
|
26
|
+
def show_with_name( str )
|
27
|
+
if( name = self[:thread_name] or
|
28
|
+
(::Thread.main == self and name = :main) )
|
29
|
+
str.sub( /Thread:0x[[:xdigit:]]+/,
|
30
|
+
"Thread:#{object_id} #{name.inspect}" )
|
257
31
|
else
|
258
|
-
|
32
|
+
str
|
259
33
|
end
|
260
34
|
end
|
35
|
+
private :show_with_name
|
261
36
|
end
|
262
37
|
|
263
|
-
#
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
#
|
270
|
-
class RTIManager
|
271
|
-
|
272
|
-
# If this character is used as the first non-whitespace value
|
273
|
-
# in a command, the remaining data will be used to invoke an
|
274
|
-
# RTIManager method directly (outside the eval scope).
|
275
|
-
#
|
276
|
-
CMD_MARKER = ?.
|
277
|
-
|
278
|
-
# Track the list of available commands.
|
279
|
-
#
|
280
|
-
RTI_METHODS = []
|
281
|
-
|
282
|
-
# Used to collect data regarding each breakpoint. The state is
|
283
|
-
# associated with the appropriate client that is waiting for
|
284
|
-
# this breakpoint to be hit.
|
285
|
-
#
|
286
|
-
BreakPoint = Struct.new( :id, :state )
|
287
|
-
|
288
|
-
# Collect all methods that we are willing to expose to a
|
289
|
-
# caller.
|
290
|
-
#
|
291
|
-
def self.method_added( id )
|
292
|
-
name = id.id2name
|
293
|
-
RTI_METHODS << id.id2name
|
294
|
-
end
|
295
|
-
|
296
|
-
# Get a new RTIManager
|
297
|
-
#
|
298
|
-
def initialize( breakpoints, tracing_proc, state )
|
299
|
-
@breakpoints = breakpoints
|
300
|
-
@tracing_proc = tracing_proc
|
301
|
-
@state = state
|
302
|
-
end
|
303
|
-
|
304
|
-
# Get the list of RTI commands available.
|
305
|
-
#
|
306
|
-
def list
|
307
|
-
RTI_METHODS.sort
|
308
|
-
end
|
309
|
-
|
310
|
-
# Get the state information for this client.
|
311
|
-
#
|
312
|
-
def state
|
313
|
-
@state
|
314
|
-
end
|
315
|
-
|
316
|
-
# Given a name, return the real class object. Returns the first
|
317
|
-
# one found unless an explicit "path" is provided via ::
|
318
|
-
# designators.
|
319
|
-
#
|
320
|
-
def name2class( name )
|
321
|
-
if name.include?( '::' )
|
322
|
-
ObjectSpace.each_object( Class ) do |c|
|
323
|
-
return c if name == c.name
|
324
|
-
end
|
325
|
-
else
|
326
|
-
ObjectSpace.each_object( Class ) do |c|
|
327
|
-
return c if c.name.match( /#{name}$/ )
|
328
|
-
end
|
329
|
-
end
|
330
|
-
return nil
|
331
|
-
end
|
332
|
-
|
333
|
-
# Very simple helper to assist caller in retrieving a specific
|
334
|
-
# object. This will return the first one found. Anything requiring
|
335
|
-
# more complicated decision logic about what object to return
|
336
|
-
# should use ObjectSpace directly.
|
337
|
-
#
|
338
|
-
# +name+:: The object's class or name (uses #name2class for the
|
339
|
-
# latter).
|
340
|
-
#
|
341
|
-
def get_object( name )
|
342
|
-
if name.kind_of? String
|
343
|
-
return nil unless c = name2class(name)
|
344
|
-
else
|
345
|
-
c = name
|
346
|
-
end
|
347
|
-
ObjectSpace.each_object( c ) {|obj| return obj}
|
348
|
-
return nil
|
349
|
-
end
|
350
|
-
|
351
|
-
# Stop all other threads except the RuntimeInsepctionThread. Be
|
352
|
-
# careful. This will stop any new connections as well.
|
353
|
-
#
|
354
|
-
def stop_world
|
355
|
-
Thread.critical = true
|
356
|
-
end
|
357
|
-
|
358
|
-
# Start all other threads up again.
|
359
|
-
#
|
360
|
-
def start_world
|
361
|
-
Thread.critical = false
|
362
|
-
end
|
363
|
-
|
364
|
-
# Add a new BreakPoint. Execution will not stop at any BreakPoints
|
365
|
-
# until rti_bp_start is called. The argument should use one of the
|
366
|
-
# following forms:
|
367
|
-
#
|
368
|
-
# * Class#method
|
369
|
-
# * Module#method
|
370
|
-
# * method
|
371
|
-
# * file:line
|
372
|
-
#
|
373
|
-
# In the first three forms, the BreakPoint will stop on the 'call'
|
374
|
-
# event into that method.
|
375
|
-
#
|
376
|
-
def bp_add( key )
|
377
|
-
if bp = @breakpoints[key]
|
378
|
-
return "Breakpoint already held as #{bp.id} by #{bp.state.socket}"
|
379
|
-
end
|
38
|
+
# Load in the supporting files.
|
39
|
+
require "rti/state.rb"
|
40
|
+
require "rti/tracer.rb"
|
41
|
+
require "rti/manager.rb"
|
42
|
+
require "rti/redirect.rb"
|
43
|
+
require "rti/thread.rb"
|
380
44
|
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
@breakpoints[key] = BreakPoint.new( @state.breakpoint_id, @state )
|
387
|
-
|
388
|
-
ret = "Added breakpoint #{@state.breakpoint_id}"
|
389
|
-
|
390
|
-
return ret
|
391
|
-
end
|
392
|
-
|
393
|
-
# Delete a breakpoint.
|
394
|
-
#
|
395
|
-
def bp_del( id )
|
396
|
-
id = id.to_i
|
397
|
-
ret = "Unable to find breakpoint #{id}"
|
398
|
-
@breakpoints.delete_if do |key, bp|
|
399
|
-
if bp.id == id and bp.state == @state
|
400
|
-
ret = "Deleted breakpoint #{id}"
|
401
|
-
true
|
402
|
-
end
|
403
|
-
end
|
404
|
-
return ret
|
405
|
-
end
|
406
|
-
|
407
|
-
# List all the breakpoints.
|
408
|
-
#
|
409
|
-
def bp_list( show_all=false )
|
410
|
-
ordered = @breakpoints.sort do |a, b|
|
411
|
-
a[1].id <=> b[1].id
|
412
|
-
end
|
413
|
-
ordered.collect do |key, bp|
|
414
|
-
next unless show_all or bp.state == @state
|
415
|
-
if show_all and bp.state != @state
|
416
|
-
post = " (#{bp.state.socket})"
|
417
|
-
end
|
418
|
-
"#{bp.id}: #{key}#{post}"
|
419
|
-
end.compact
|
420
|
-
end
|
421
|
-
|
422
|
-
# Start tracing looking for breakpoints to stop at.
|
423
|
-
#
|
424
|
-
def bp_start
|
425
|
-
if @breakpoints.empty?
|
426
|
-
return "No breakpoints set"
|
427
|
-
end
|
428
|
-
set_trace_func( @tracing_proc )
|
429
|
-
@state.bp_portal = @state.bp_waiting.pop
|
430
|
-
return nil
|
431
|
-
end
|
432
|
-
|
433
|
-
# Continue tracing from current position waiting for next
|
434
|
-
# breakpoint to hit.
|
435
|
-
#
|
436
|
-
def bp_continue
|
437
|
-
unless @state.bp_portal
|
438
|
-
return "Not stopped at a breakpoint"
|
439
|
-
end
|
440
|
-
@state.bp_portal.cmds << :continue
|
441
|
-
@state.bp_portal = @state.bp_waiting.pop
|
442
|
-
return nil
|
443
|
-
end
|
444
|
-
|
445
|
-
def bp_fin
|
446
|
-
unless @state.bp_portal
|
447
|
-
return "Not stopped at a breakpoint"
|
448
|
-
end
|
449
|
-
@state.bp_portal.cmds << :fin
|
450
|
-
@state.bp_portal = @state.bp_waiting.pop
|
451
|
-
return nil
|
452
|
-
end
|
453
|
-
|
454
|
-
# Break execution at the next line (without going into another
|
455
|
-
# class).
|
456
|
-
#
|
457
|
-
def bp_next
|
458
|
-
unless @state.bp_portal
|
459
|
-
return "Not stopped at a breakpoint"
|
460
|
-
end
|
461
|
-
@state.bp_portal.cmds << :next
|
462
|
-
@state.bp_portal = @state.bp_waiting.pop
|
463
|
-
return nil
|
464
|
-
end
|
465
|
-
|
466
|
-
# Follow the next instruction--potentially into another class
|
467
|
-
# and/or method.
|
468
|
-
#
|
469
|
-
def bp_step
|
470
|
-
unless @state.bp_portal
|
471
|
-
return "Not stopped at a breakpoint"
|
472
|
-
end
|
473
|
-
@state.bp_portal.cmds << :step
|
474
|
-
@state.bp_portal = @state.bp_waiting.pop
|
475
|
-
return nil
|
476
|
-
end
|
477
|
-
|
478
|
-
# Stop tracing for breakpoints.
|
479
|
-
#
|
480
|
-
def bp_stop
|
481
|
-
unless @state.bp_portal
|
482
|
-
return "Not stopped at a breakpoint"
|
483
|
-
end
|
484
|
-
@state.bp_portal.cmds << :stop
|
485
|
-
@state.delete( :bp_portal )
|
486
|
-
return nil
|
487
|
-
end
|
488
|
-
|
489
|
-
end
|
490
|
-
|
491
|
-
# Create a new inspection thread.
|
492
|
-
#
|
493
|
-
# +host+:: What address to listen on.
|
494
|
-
#
|
495
|
-
# +port+:: What port to listen on.
|
496
|
-
#
|
497
|
-
# +binding+:: In what context should the evaluations be run?
|
498
|
-
#
|
499
|
-
# +prompt_name+:: What name to use in the prompt.
|
45
|
+
# This is the set of options managed by RTI. The output_marker is
|
46
|
+
# the value used to distinguish data sent to the client from
|
47
|
+
# within the command evaluation from the value returned from the
|
48
|
+
# evaluation.
|
500
49
|
#
|
501
|
-
|
502
|
-
prompt_name=File.basename($0,'.rb') )
|
503
|
-
|
504
|
-
@server = TCPServer.new( host, port )
|
505
|
-
@binding = binding
|
506
|
-
|
507
|
-
@@dbg_handler.call "Runtime inspection available at #{host}:#{port}"
|
508
|
-
|
509
|
-
@breakpoints = {}
|
510
|
-
@tracing_proc = method( :handle_tracing ).to_proc
|
511
|
-
|
512
|
-
@clients = []
|
513
|
-
@clients_mutex = Mutex.new
|
514
|
-
@dead_clients = Queue.new
|
515
|
-
|
516
|
-
run_gc_thread
|
517
|
-
|
518
|
-
super do
|
519
|
-
begin
|
520
|
-
while sockets = select( [@server] )
|
521
|
-
sockets.first.each do |socket|
|
522
|
-
socket = @server.accept
|
523
|
-
def socket.set_peerstr
|
524
|
-
unless closed?
|
525
|
-
peer = peeraddr
|
526
|
-
@peerstr = "#{peer[3]}:#{peer[1]}"
|
527
|
-
end
|
528
|
-
end
|
529
|
-
def socket.to_s
|
530
|
-
@peerstr or super
|
531
|
-
end
|
532
|
-
def socket.inspect
|
533
|
-
super.chop + " #{to_s}>"
|
534
|
-
end
|
535
|
-
|
536
|
-
socket.set_peerstr
|
537
|
-
@@dbg_handler.call "Connection established with " +
|
538
|
-
"#{socket}"
|
539
|
-
|
540
|
-
run_client_thread( prompt_name, socket )
|
541
|
-
end
|
542
|
-
end
|
543
|
-
rescue Object => e
|
544
|
-
@@exc_handler.call( "running inspection thread", e )
|
545
|
-
end
|
546
|
-
end
|
547
|
-
end
|
548
|
-
|
549
|
-
######################################################################
|
550
|
-
# Remaining functions just overrides for Thread class and then
|
551
|
-
# private stuff.
|
552
|
-
|
553
|
-
def inspect # :nodoc:
|
554
|
-
super.chop + " #{@clients.inspect}>"
|
555
|
-
end
|
556
|
-
|
557
|
-
def to_s # :nodoc:
|
558
|
-
inspect
|
559
|
-
end
|
560
|
-
|
561
|
-
def terminate # :nodoc:
|
562
|
-
super
|
563
|
-
cleanup
|
564
|
-
end
|
50
|
+
OPTS = { :output_marker => '=>' }
|
565
51
|
|
566
|
-
|
567
|
-
|
568
|
-
|
52
|
+
if $DEBUG
|
53
|
+
OPTS[:dbg_handler] = Proc.new {|msg| puts msg}
|
54
|
+
else
|
55
|
+
OPTS[:dbg_handler] = Proc.new {}
|
569
56
|
end
|
570
57
|
|
571
|
-
|
572
|
-
|
573
|
-
|
58
|
+
OPTS[:exc_handler] = Proc.new do |msg, exc|
|
59
|
+
STDERR.puts( "#{::Thread.current}: Exception while #{msg}",
|
60
|
+
exc, exc.backtrace )
|
574
61
|
end
|
575
62
|
|
576
|
-
|
577
|
-
|
578
|
-
#
|
579
|
-
# threads (see #run_client_thread).
|
63
|
+
# Optionally enable your own handling of debug output. Default is
|
64
|
+
# to either suppress it or send it to $stdout if $DEBUG is enabled
|
65
|
+
# (only checked at class load time).
|
580
66
|
#
|
581
|
-
def
|
582
|
-
|
583
|
-
loop do
|
584
|
-
th = @dead_clients.pop
|
585
|
-
if th == :quit
|
586
|
-
break
|
587
|
-
end
|
588
|
-
|
589
|
-
if th[:cleaned]
|
590
|
-
next
|
591
|
-
else
|
592
|
-
th[:cleaned] = true
|
593
|
-
end
|
594
|
-
|
595
|
-
@clients_mutex.synchronize do
|
596
|
-
@clients.delete( th )
|
597
|
-
end
|
598
|
-
|
599
|
-
@@dbg_handler.call "Cleaning up #{th[:socket]}"
|
600
|
-
|
601
|
-
state = th[:state]
|
602
|
-
bp.rti.bp_stop if state.bp_portal
|
603
|
-
@breakpoints.delete_if{|key, bp| bp.state == state}
|
604
|
-
|
605
|
-
begin
|
606
|
-
th.kill if th.alive?
|
607
|
-
th.join
|
608
|
-
rescue Object => e
|
609
|
-
@@exc_handler.call( "cleaning up thread for #{th[:socket]}",
|
610
|
-
e )
|
611
|
-
end
|
612
|
-
|
613
|
-
begin
|
614
|
-
socket = th[:socket]
|
615
|
-
unless socket.closed?
|
616
|
-
socket.close
|
617
|
-
@@dbg_handler.call "Closed connection from #{socket}"
|
618
|
-
end
|
619
|
-
rescue Object => e
|
620
|
-
@@exc_handler.call( "cleaning up socket for #{th[:socket]}",
|
621
|
-
e )
|
622
|
-
end
|
623
|
-
end
|
624
|
-
end
|
67
|
+
def self.dbg_handler( &block ) # :yields: msg
|
68
|
+
OPTS[:dbg_handler] = block
|
625
69
|
end
|
626
70
|
|
627
|
-
#
|
628
|
-
#
|
71
|
+
# Optionally enable display of any trace methods that are not seen
|
72
|
+
# as break or stop points. This will use the dbg_handler to
|
73
|
+
# output, so if that isn't set, nothing will be shown.
|
629
74
|
#
|
630
|
-
def
|
631
|
-
|
632
|
-
t = Thread.current
|
633
|
-
t[:socket] = socket
|
634
|
-
t[:state] = state = State.new
|
635
|
-
state.rti = RTIManager.new( @breakpoints, @tracing_proc, state )
|
636
|
-
|
637
|
-
loop do
|
638
|
-
begin
|
639
|
-
prompt( prompt_name, socket, state )
|
640
|
-
select( [socket] )
|
641
|
-
if handle_client( socket, state ) == :dead
|
642
|
-
break
|
643
|
-
end
|
644
|
-
rescue Object => e
|
645
|
-
@@exc_handler.call( "handling connection from #{socket}:", e )
|
646
|
-
if socket.closed?
|
647
|
-
break
|
648
|
-
else
|
649
|
-
socket.puts( "Connection handler exception:" )
|
650
|
-
socket.puts( e )
|
651
|
-
socket.puts( e.backtrace )
|
652
|
-
end
|
653
|
-
end
|
654
|
-
end
|
655
|
-
|
656
|
-
@dead_clients << t
|
657
|
-
end
|
658
|
-
|
659
|
-
def client_thread.inspect
|
660
|
-
super.chop + " #{self[:socket]}>"
|
661
|
-
end
|
662
|
-
|
663
|
-
@clients_mutex.synchronize do
|
664
|
-
@clients << client_thread
|
665
|
-
end
|
75
|
+
def self.show_trace=( val )
|
76
|
+
OPTS[:show_trace] = val
|
666
77
|
end
|
667
78
|
|
668
|
-
#
|
79
|
+
# Optionally enable your own exception handler. Default is to
|
80
|
+
# write the exception and backtrace out to $stderr.
|
669
81
|
#
|
670
|
-
def
|
671
|
-
|
672
|
-
crit = '-!!'
|
673
|
-
end
|
674
|
-
|
675
|
-
if state.block_cmd.empty?
|
676
|
-
socket.puts
|
677
|
-
|
678
|
-
if portal = state.bp_portal
|
679
|
-
sp = portal.stoppoint
|
680
|
-
if sp.breakpoint
|
681
|
-
pre = "Breakpoint #{sp.breakpoint.id}"
|
682
|
-
else
|
683
|
-
pre = "Stopped"
|
684
|
-
end
|
685
|
-
socket.puts( "#{pre} in #{sp.classname}\#" +
|
686
|
-
"#{sp.methodname} from #{sp.file}:#{sp.line} " +
|
687
|
-
"(#{sp.event})" )
|
688
|
-
end
|
689
|
-
end
|
690
|
-
|
691
|
-
socket.print( "#{prompt_name}:#{'%03d'%state.cmd_count}:" +
|
692
|
-
"#{state.block_count}#{crit}> " )
|
82
|
+
def self.exc_handler( &block ) # :yields: msg, exc
|
83
|
+
OPTS[:exc_handler] = block
|
693
84
|
end
|
694
85
|
|
695
|
-
#
|
696
|
-
# the eval returned value.
|
86
|
+
# The placeholder for an RTI thread started via on_signal.
|
697
87
|
#
|
698
|
-
|
88
|
+
@@signal_rti = nil
|
699
89
|
|
700
|
-
#
|
701
|
-
#
|
702
|
-
#
|
90
|
+
# Used to have RTI register itself on or off when the given signal
|
91
|
+
# is trapped. This way, the RTI thread is only running when needed
|
92
|
+
# during trouble-shooting or debugging. The remaining arguments
|
93
|
+
# are passed off to the #new method when the RTI thread is
|
94
|
+
# instantiated.
|
703
95
|
#
|
704
|
-
def
|
705
|
-
unless
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
state.block_count += 1
|
713
|
-
state.block_cmd = ''
|
714
|
-
else
|
715
|
-
state.block_cmd += cmd
|
716
|
-
return state.cmd_count += 1
|
717
|
-
end
|
718
|
-
end
|
719
|
-
|
720
|
-
if( cmd[0] == RTIManager::CMD_MARKER )
|
721
|
-
# Handle RTI commands (e.g. bp_add, bp_start...).
|
722
|
-
rti_cmd, *rti_args = cmd[1..-1].split( /\s+/ )
|
723
|
-
ret = EvalReturn.new( nil, state.rti.send( rti_cmd, *rti_args ))
|
724
|
-
end
|
725
|
-
|
726
|
-
unless ret
|
727
|
-
# Determine to either run the cmd locally or in a thread
|
728
|
-
# waiting at a breakpoint.
|
729
|
-
if state.bp_portal
|
730
|
-
state.bp_portal.cmds << cmd
|
731
|
-
ret = state.bp_portal.out.pop
|
732
|
-
else
|
733
|
-
ret = run_eval( state, cmd )
|
734
|
-
end
|
735
|
-
end
|
736
|
-
|
737
|
-
if out = ret.out
|
738
|
-
if out.kind_of? StringIO
|
739
|
-
socket.puts out.string unless out.length < 1
|
740
|
-
else
|
741
|
-
socket.puts out
|
742
|
-
end
|
743
|
-
end
|
744
|
-
|
745
|
-
v = ret.value
|
746
|
-
if state.use_yaml
|
747
|
-
v = v.to_yaml
|
748
|
-
elsif v.kind_of? Exception
|
749
|
-
v = "#{v}\n" + v.backtrace.join("\n")
|
750
|
-
else
|
751
|
-
v = v.inspect
|
752
|
-
end
|
753
|
-
|
754
|
-
socket.puts "#{OUTPUT_MARKER} #{v}"
|
755
|
-
|
756
|
-
state.cmd_count += 1
|
757
|
-
end
|
758
|
-
|
759
|
-
# Turned on selectively via #rti_bp_start where we are actively
|
760
|
-
# trying to find a point to stop and process commands in this
|
761
|
-
# context.
|
762
|
-
#
|
763
|
-
def handle_tracing( event, file, line, methodname, binding, classname )
|
764
|
-
sp = StopPoint.new( classname, methodname, file, line, event)
|
765
|
-
tracer = Thread.current[:rti_tracer]
|
766
|
-
|
767
|
-
if tracer
|
768
|
-
$goober.puts [:tracer, sp].inspect
|
769
|
-
$goober.flush
|
770
|
-
|
771
|
-
found = tracer.stoppoint( sp )
|
772
|
-
if found == :continue
|
773
|
-
found = nil
|
774
|
-
end
|
775
|
-
end
|
776
|
-
|
777
|
-
unless found
|
778
|
-
return unless file and line
|
779
|
-
if bp = @breakpoints["#{file}:#{line}"]
|
780
|
-
sp.breakpoint = bp
|
781
|
-
found = sp
|
782
|
-
else
|
783
|
-
# These breakpoints only stop at the initial call into
|
784
|
-
# this method.
|
785
|
-
return unless event == 'call'
|
786
|
-
if bp = @breakpoints["#{classname}##{methodname}"]
|
787
|
-
sp.breakpoint = bp
|
788
|
-
found = sp
|
789
|
-
elsif bp = @breakpoints["#{methodname}"]
|
790
|
-
sp.breakpoint = bp
|
791
|
-
found = sp
|
96
|
+
def self.on_signal( signal='USR1', *args )
|
97
|
+
unless OPTS[:signal_is_set]
|
98
|
+
OPTS[:signal_is_set] = true
|
99
|
+
Signal.trap( signal ) do
|
100
|
+
if @@signal_rti
|
101
|
+
@@signal_rti.kill
|
102
|
+
@@signal_rti = nil
|
103
|
+
OPTS[:dbg_handler].call "Stopped signal-based RTI thread"
|
792
104
|
else
|
793
|
-
|
105
|
+
@@signal_rti = Thread.new( *args )
|
106
|
+
OPTS[:dbg_handler].call "Started signal-based RTI thread"
|
794
107
|
end
|
795
108
|
end
|
796
109
|
end
|
797
|
-
|
798
|
-
if bp = found.breakpoint
|
799
|
-
# If we've found a stoppoint, we no longer need the tracer
|
800
|
-
# as it is just looking for the next stopping point.
|
801
|
-
Thread.current[:rti_tracer] = nil
|
802
|
-
portal = Portal.new( found, Queue.new, Queue.new )
|
803
|
-
bp.state.bp_waiting << portal
|
804
|
-
loop do
|
805
|
-
cmd = portal.cmds.pop
|
806
|
-
case cmd
|
807
|
-
when :step
|
808
|
-
Thread.current[:rti_tracer] = Tracer.new( portal, cmd )
|
809
|
-
break
|
810
|
-
when :next
|
811
|
-
Thread.current[:rti_tracer] = Tracer.new( portal, cmd )
|
812
|
-
break
|
813
|
-
when :fin
|
814
|
-
Thread.current[:rti_tracer] = Tracer.new( portal, cmd )
|
815
|
-
break
|
816
|
-
when :continue
|
817
|
-
break
|
818
|
-
when :stop
|
819
|
-
set_trace_func( nil )
|
820
|
-
break
|
821
|
-
else
|
822
|
-
out = run_eval( bp.state, cmd, binding )
|
823
|
-
portal.out << out
|
824
|
-
end
|
825
|
-
end
|
826
|
-
end
|
827
|
-
rescue Object => e
|
828
|
-
@@exc_handler.call( "tracing", e )
|
829
110
|
end
|
830
111
|
|
831
|
-
#
|
832
|
-
#
|
833
|
-
#
|
834
|
-
# evaluation.
|
112
|
+
# Used to manage information found at each stopping point--either
|
113
|
+
# hit by a BreakPoint or because of one of the bp_* methods in the
|
114
|
+
# RTIManager.
|
835
115
|
#
|
836
|
-
|
837
|
-
|
838
|
-
ret = EvalReturn.new
|
839
|
-
|
840
|
-
eval_args = ["$SAFE=#{safe_level};rti=Thread.current[:rti_manager];" +
|
841
|
-
cmd]
|
842
|
-
if binding
|
843
|
-
eval_args << binding
|
844
|
-
end
|
845
|
-
|
846
|
-
if $DEBUG
|
847
|
-
if binding != @binding
|
848
|
-
bindstr = " (explicit binding)"
|
849
|
-
elsif !binding
|
850
|
-
bindstr = " (without binding)"
|
851
|
-
end
|
852
|
-
@@dbg_handler.call "Executing#{bindstr} [#{safe_level}]: #{cmd}"
|
853
|
-
end
|
854
|
-
|
855
|
-
# Temporarily capture stdout/stderr into string for just the
|
856
|
-
# evaluation thread.
|
857
|
-
ret.out = StringIO.new
|
858
|
-
ThreadRedirect.start
|
859
|
-
|
860
|
-
eval_thread = Thread.new do
|
861
|
-
t = Thread.current
|
862
|
-
t[:rti_redirect_io] = ret.out
|
863
|
-
t[:rti_manager] = state.rti
|
864
|
-
begin
|
865
|
-
eval( *eval_args )
|
866
|
-
rescue Object => e
|
867
|
-
e
|
868
|
-
end
|
869
|
-
end
|
870
|
-
|
871
|
-
begin
|
872
|
-
timeout( state.eval_timeout ) do
|
873
|
-
eval_thread.join
|
874
|
-
end
|
875
|
-
rescue Object => e
|
876
|
-
ret.out.puts e, e.backtrace
|
877
|
-
# The thread could hang around forever if it isn't
|
878
|
-
# taken care of...
|
879
|
-
eval_thread.kill if eval_thread
|
880
|
-
ensure
|
881
|
-
ThreadRedirect.stop
|
882
|
-
end
|
883
|
-
|
884
|
-
ret.value = eval_thread.value
|
885
|
-
|
886
|
-
return ret
|
887
|
-
end
|
116
|
+
StopPoint = Struct.new( :classname, :methodname, :file, :line, :event,
|
117
|
+
:thread, :state, :breakpoint )
|
888
118
|
|
889
|
-
#
|
890
|
-
# the
|
119
|
+
# Used as a messaging mechanism once a StopPoint is hit to
|
120
|
+
# coordinate commands received from the client in a client thread
|
121
|
+
# (see run_client_thread) with the actual evaluation run from
|
122
|
+
# within the StopPoint's context (it's binding--see
|
123
|
+
# handle_tracing).
|
891
124
|
#
|
892
|
-
|
893
|
-
@server.close unless @server.closed?
|
894
|
-
|
895
|
-
until @clients.empty?
|
896
|
-
client = nil
|
897
|
-
@clients_mutex.synchronize do
|
898
|
-
client = @clients.pop
|
899
|
-
end
|
900
|
-
@dead_clients << client
|
901
|
-
end
|
902
|
-
|
903
|
-
# Tell the GC thread to but still give it a chance to finish
|
904
|
-
# up with all the clients we just gave it.
|
905
|
-
@dead_clients << :quit
|
906
|
-
|
907
|
-
until @dead_clients.empty?
|
908
|
-
@gc_thread.run
|
909
|
-
end
|
910
|
-
|
911
|
-
@gc_thread.kill
|
912
|
-
end
|
913
|
-
|
914
|
-
end
|
915
|
-
|
916
|
-
if $0 == __FILE__
|
917
|
-
class Foo
|
918
|
-
def bar
|
919
|
-
@a ||= 0
|
920
|
-
b = @a + 1
|
921
|
-
sleep 5
|
922
|
-
puts @a
|
923
|
-
@a += 1
|
924
|
-
end
|
925
|
-
end
|
926
|
-
|
927
|
-
def start_foo
|
928
|
-
Thread.new do
|
929
|
-
foo = Foo.new
|
930
|
-
z = 0
|
931
|
-
y = 100
|
932
|
-
loop do
|
933
|
-
z += 1
|
934
|
-
foo.bar
|
935
|
-
y += 2
|
936
|
-
end
|
937
|
-
end
|
938
|
-
end
|
939
|
-
|
940
|
-
$goober = File.open( 'goober', 'w' )
|
941
|
-
$goober.sync = false
|
942
|
-
at_exit do
|
943
|
-
$goober.flush
|
944
|
-
end
|
125
|
+
Portal = Struct.new( :stoppoint, :cmds, :out )
|
945
126
|
|
946
|
-
unless ARGV.length > 0
|
947
|
-
RuntimeInspectionThread.new.join
|
948
|
-
exit
|
949
|
-
end
|
950
127
|
end
|