rtinspect 0.0.1
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/COPYING +340 -0
- data/LICENSE +53 -0
- data/README +331 -0
- data/TODO +36 -0
- data/lib/rtinspect.rb +950 -0
- data/test/test.rb +162 -0
- metadata +54 -0
data/lib/rtinspect.rb
ADDED
@@ -0,0 +1,950 @@
|
|
1
|
+
# :main: README
|
2
|
+
# :title: Runtime Inspection and Debugging Thread
|
3
|
+
#
|
4
|
+
# Contains the RuntimeInspectionThread.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2006-2007 Brad Robel-Forrest.
|
7
|
+
#
|
8
|
+
# This software can be distributed under the terms of the Ruby license
|
9
|
+
# as detailed in the accompanying LICENSE[link:files/LICENSE.html]
|
10
|
+
# file.
|
11
|
+
|
12
|
+
require 'socket'
|
13
|
+
require 'thread'
|
14
|
+
require 'timeout'
|
15
|
+
require 'yaml'
|
16
|
+
|
17
|
+
# A thread that exposes inspection and debugging functionality over a
|
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
|
74
|
+
|
75
|
+
# Synchronization for ensuring we are either using the same
|
76
|
+
# redirection manager or creating only one new one.
|
77
|
+
#
|
78
|
+
MUTEX = Mutex.new
|
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
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
|
217
|
+
class Tracer
|
218
|
+
def initialize( portal, cmd )
|
219
|
+
@portal = portal
|
220
|
+
@cmd = cmd
|
221
|
+
end
|
222
|
+
|
223
|
+
def stoppoint( sp )
|
224
|
+
case @cmd
|
225
|
+
when :step
|
226
|
+
sp.breakpoint = @portal.stoppoint.breakpoint
|
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
|
257
|
+
else
|
258
|
+
raise "Unknown tracer command: #{@cmd}"
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# This will be the list of all public instance methods that start
|
264
|
+
# with 'rti_'. These methods are callable from a client's
|
265
|
+
# connection to invoke various helpers remotely (e.g. manipulating
|
266
|
+
# breakpoints as well as other helpful functions). The first
|
267
|
+
# argument to these will always be the state. Remaining arguments
|
268
|
+
# will be whatever strings were obtained from the client.
|
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
|
380
|
+
|
381
|
+
@state.bp_waiting ||= Queue.new
|
382
|
+
|
383
|
+
@state.breakpoint_id ||= 0
|
384
|
+
@state.breakpoint_id += 1
|
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.
|
500
|
+
#
|
501
|
+
def initialize( host='localhost', port=56789, binding=TOPLEVEL_BINDING,
|
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
|
565
|
+
|
566
|
+
def kill # :nodoc:
|
567
|
+
super
|
568
|
+
cleanup
|
569
|
+
end
|
570
|
+
|
571
|
+
def exit # :nodoc:
|
572
|
+
super
|
573
|
+
cleanup
|
574
|
+
end
|
575
|
+
|
576
|
+
private
|
577
|
+
|
578
|
+
# Establish a thread that cleans up dead or disconnected client
|
579
|
+
# threads (see #run_client_thread).
|
580
|
+
#
|
581
|
+
def run_gc_thread
|
582
|
+
@gc_thread = Thread.new do
|
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
|
625
|
+
end
|
626
|
+
|
627
|
+
# A new client socket has been established. Handle the connection
|
628
|
+
# in a separate thread so we don't block other connections.
|
629
|
+
#
|
630
|
+
def run_client_thread( prompt_name, socket )
|
631
|
+
client_thread = Thread.new do
|
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
|
666
|
+
end
|
667
|
+
|
668
|
+
# Show the client a nice prompt with data about the state of the session.
|
669
|
+
#
|
670
|
+
def prompt( prompt_name, socket, state )
|
671
|
+
if Thread.critical
|
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}> " )
|
693
|
+
end
|
694
|
+
|
695
|
+
# Used to coordinate both the redirected eval output as well as
|
696
|
+
# the eval returned value.
|
697
|
+
#
|
698
|
+
EvalReturn = Struct.new( :out, :value )
|
699
|
+
|
700
|
+
# Get the next command from the client and process it by either
|
701
|
+
# evaluating it in the default binding context or in the context
|
702
|
+
# of a breakpoint.
|
703
|
+
#
|
704
|
+
def handle_client( socket, state )
|
705
|
+
unless cmd = socket.gets
|
706
|
+
return :dead
|
707
|
+
end
|
708
|
+
|
709
|
+
if state.block_count > 0
|
710
|
+
if cmd.match( /^\s*$/ )
|
711
|
+
cmd = state.block_cmd
|
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
|
792
|
+
else
|
793
|
+
return
|
794
|
+
end
|
795
|
+
end
|
796
|
+
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
|
+
end
|
830
|
+
|
831
|
+
# Run a safe evaluation of a client's command. This redirect's any
|
832
|
+
# output from the command's evaluation. The return will be both
|
833
|
+
# the output from the evaluation as well as the value of the
|
834
|
+
# evaluation.
|
835
|
+
#
|
836
|
+
def run_eval( state, cmd, binding=@binding, safe_level=nil )
|
837
|
+
safe_level = state.safe_level unless safe_level
|
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
|
888
|
+
|
889
|
+
# Close down all our threads and clean stuff up in anticipation of
|
890
|
+
# the main thread going away.
|
891
|
+
#
|
892
|
+
def cleanup
|
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
|
945
|
+
|
946
|
+
unless ARGV.length > 0
|
947
|
+
RuntimeInspectionThread.new.join
|
948
|
+
exit
|
949
|
+
end
|
950
|
+
end
|