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.
@@ -0,0 +1,102 @@
1
+ # Contains the RuntimeInspection::ThreadRedirect.
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
+ # Used when we want to redirect stdout or stderr for a specific
13
+ # thread. This is intelligent enough so that it reuses one if it
14
+ # is already in place from another thread.
15
+ #
16
+ class ThreadRedirect
17
+
18
+ # Synchronization for ensuring we are either using the same
19
+ # redirection manager or creating only one new one.
20
+ #
21
+ MUTEX = Mutex.new
22
+
23
+ # Replace the global stdout and stderr with a redirection
24
+ # object. If one is already there, then use it.
25
+ #
26
+ def self.start
27
+ MUTEX.synchronize do
28
+ if $stdout.kind_of? self
29
+ $stdout.usage += 1
30
+ else
31
+ $stdout = new( $stdout )
32
+ end
33
+ if $stderr.kind_of? self
34
+ $stderr.usage += 1
35
+ else
36
+ $stderr = new( $stderr )
37
+ end
38
+ end
39
+ return true
40
+ end
41
+
42
+ # Stop any redirection and put the original IO back in its
43
+ # place unless there are still other threads using
44
+ # redirection.
45
+ #
46
+ def self.stop
47
+ MUTEX.synchronize do
48
+ unless $stdout.kind_of? self
49
+ if $stdout.usage > 0
50
+ $stdout.usage -= 1
51
+ else
52
+ $stdout = $stdout.default_io
53
+ end
54
+ end
55
+ unless $stderr.kind_of? self
56
+ if $stderr.usage > 0
57
+ $stderr.usage -= 1
58
+ else
59
+ $stderr = $stderr.default_io
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ # Create a new redirection manager for the IO stream provided.
66
+ #
67
+ def initialize( default_io )
68
+ @default_io = default_io
69
+ @usage = 0
70
+ end
71
+
72
+ # A reference counter if other threads are also using this
73
+ # redirection manager.
74
+ #
75
+ attr_accessor :usage
76
+
77
+ # The IO to use for non-redirected output.
78
+ #
79
+ attr_reader :default_io
80
+
81
+ # Provide the output method so this object can be used as
82
+ # either stdout or stderr.
83
+ #
84
+ def write( *args )
85
+ if rio = ::Thread.current[:rti_redirect_io]
86
+ rio.write( *args )
87
+ else
88
+ @default_io.write( *args )
89
+ end
90
+ end
91
+
92
+ def method_missing( sym, *args, &block )
93
+ if rio = Thread.current[:rti_redirect_io]
94
+ rio.send( sym, *args, &block )
95
+ else
96
+ @default_io.send( sym, *args, &block )
97
+ end
98
+ end
99
+
100
+ end
101
+
102
+ end
@@ -0,0 +1,65 @@
1
+ # Contains the RuntimeInspection::State.
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
+ # Data managed about each TCP connection. Note that these values
13
+ # can be manged from the remote session (e.g. by issuing commands
14
+ # like <em>state.use_yaml = true</em>).
15
+ #
16
+ class State < Hash
17
+
18
+ # Create a new object for tracking state information.
19
+ #
20
+ # +socket+:: The owner connection for this data.
21
+ #
22
+ # +cmd_count+:: The current evaluation number.
23
+ #
24
+ # +block_count+:: The current block number. When bigger than zero,
25
+ # multiple lines are collected until a blank line
26
+ # is encountered and then the entire block is
27
+ # evaluated. Each successive block will increase
28
+ # the number (similar to the normal _cmd_count_
29
+ # value).
30
+ #
31
+ # +block_cmd+:: The collection of the block if the +block_count+
32
+ # is larger than zero.
33
+ #
34
+ # +use_yaml+:: The output should be YAML.
35
+ #
36
+ # +safe_level+:: The level $SAFE is declared at for each evaluation.
37
+ #
38
+ def initialize( socket=Thread.current[:socket],
39
+ cmd_count=1, block_count=0, block_cmd='',
40
+ use_yaml=false, safe_level=3 )
41
+ super()
42
+ self[:socket] = socket
43
+ self[:cmd_count] = cmd_count
44
+ self[:block_count] = block_count
45
+ self[:block_cmd] = block_cmd
46
+ self[:use_yaml] = use_yaml
47
+ self[:safe_level] = safe_level
48
+ end
49
+
50
+ def method_missing( sym, val=nil )
51
+ if key?( sym )
52
+ self[sym]
53
+ else
54
+ str = sym.to_s
55
+ if str[-1] == ?=
56
+ self[str.chop.to_sym] = val
57
+ else
58
+ self[sym]
59
+ end
60
+ end
61
+ end
62
+
63
+ end
64
+
65
+ end
@@ -0,0 +1,553 @@
1
+ # Contains the RuntimeInspection::Thread.
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
+ # A thread that exposes inspection and debugging functionality
13
+ # over a TCP socket.
14
+ #
15
+ class Thread < ::Thread
16
+
17
+ # Create a new inspection thread.
18
+ #
19
+ # +host+:: What address to listen on.
20
+ #
21
+ # +port+:: What port to listen on.
22
+ #
23
+ # +binding+:: In what context should the evaluations be run?
24
+ #
25
+ # +prompt_name+:: What name to use in the prompt.
26
+ #
27
+ def initialize( host='localhost', port=56789, binding=TOPLEVEL_BINDING,
28
+ prompt_name=File.basename($0,'.rb') )
29
+
30
+ @server = TCPServer.new( host, port )
31
+ @binding = binding
32
+
33
+ OPTS[:dbg_handler].call "Runtime inspection available at " +
34
+ "#{host}:#{port}"
35
+
36
+ @breakpoints = {}
37
+ @tracing_proc = method( :handle_tracing ).to_proc
38
+
39
+ @clients = []
40
+ @clients_mutex = Mutex.new
41
+ @dead_clients = Queue.new
42
+
43
+ run_gc_thread
44
+
45
+ super do
46
+ begin
47
+ while sockets = select( [@server] )
48
+ sockets.first.each do |socket|
49
+ socket = @server.accept
50
+ def socket.set_peerstr
51
+ unless closed?
52
+ peer = peeraddr
53
+ @peerstr = "#{peer[3]}:#{peer[1]}"
54
+ end
55
+ end
56
+ socket.set_peerstr
57
+ def socket.to_s
58
+ @peerstr or super
59
+ end
60
+ def socket.inspect
61
+ super.chop + " #{to_s}>"
62
+ end
63
+
64
+ OPTS[:dbg_handler].call "Connection established " +
65
+ "with #{socket}"
66
+
67
+ run_client_thread( prompt_name, socket )
68
+ end
69
+ end
70
+ rescue Object => e
71
+ OPTS[:exc_handler].call( "running inspection thread", e )
72
+ end
73
+ end
74
+ end
75
+
76
+ #####################################################################
77
+ # Remaining functions are just overrides for ::Thread class
78
+ # and then private stuff.
79
+
80
+ def inspect # :nodoc:
81
+ super.chop + " #{@clients.inspect}>"
82
+ end
83
+
84
+ def to_s # :nodoc:
85
+ inspect
86
+ end
87
+
88
+ def terminate # :nodoc:
89
+ super
90
+ cleanup
91
+ end
92
+
93
+ def kill # :nodoc:
94
+ super
95
+ cleanup
96
+ end
97
+
98
+ def exit # :nodoc:
99
+ super
100
+ cleanup
101
+ end
102
+
103
+ private
104
+
105
+ # Establish a thread that cleans up dead or disconnected
106
+ # client threads (see #run_client_thread).
107
+ #
108
+ def run_gc_thread
109
+ @gc_thread = ::Thread.new do
110
+ ::Thread.current[:thread_name] = :rti_gc
111
+ loop do
112
+ th = @dead_clients.pop
113
+ if th == :quit
114
+ break
115
+ end
116
+
117
+ if th[:cleaned]
118
+ next
119
+ else
120
+ th[:cleaned] = true
121
+ end
122
+
123
+ @clients_mutex.synchronize do
124
+ @clients.delete( th )
125
+ end
126
+
127
+ OPTS[:dbg_handler].call "Cleaning up #{th[:socket]}"
128
+
129
+ state = th[:state]
130
+ state.rti.bp_stop if state.bp_portal
131
+
132
+ list = @breakpoints.collect do |key, bp|
133
+ if bp.state == state
134
+ bp
135
+ end
136
+ end
137
+ list.compact!
138
+ list.each do |bp|
139
+ state.rti.bp_delete( bp.id )
140
+ end
141
+
142
+ begin
143
+ th.kill if th.alive?
144
+ th.join
145
+ rescue Object => e
146
+ OPTS[:exc_handler].call( "cleaning up thread for " +
147
+ "#{th[:socket]}", e )
148
+ end
149
+
150
+ begin
151
+ socket = th[:socket]
152
+ unless socket.closed?
153
+ socket.close
154
+ OPTS[:dbg_handler].call( "Closed connection from " +
155
+ "#{socket}" )
156
+ end
157
+ rescue Object => e
158
+ OPTS[:exc_handler].call( "cleaning up socket for " +
159
+ "#{th[:socket]}", e )
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ # A new client socket has been established. Handle the
166
+ # connection in a separate thread so we don't block other
167
+ # connections.
168
+ #
169
+ def run_client_thread( prompt_name, socket )
170
+ client_thread = ::Thread.new do
171
+ t = ::Thread.current
172
+ t[:thread_name] = :rti_client
173
+ t[:socket] = socket
174
+ t[:state] = state = State.new
175
+ state.rti = RTIManager.new( @breakpoints, @tracing_proc, state )
176
+
177
+ loop do
178
+ begin
179
+ prompt( prompt_name, socket, state )
180
+ select( [socket] )
181
+ if handle_client( socket, state ) == :dead
182
+ break
183
+ end
184
+ rescue Object => e
185
+ OPTS[:exc_handler].call( "handling connection from " +
186
+ "#{socket}:", e )
187
+ if socket.closed?
188
+ break
189
+ else
190
+ socket.puts( "Connection handler exception:" )
191
+ socket.puts( e )
192
+ socket.puts( e.backtrace )
193
+ end
194
+ end
195
+ end
196
+
197
+ @dead_clients << t
198
+ end
199
+
200
+ def client_thread.inspect
201
+ super.chop + " #{self[:socket]}>"
202
+ end
203
+
204
+ @clients_mutex.synchronize do
205
+ @clients << client_thread
206
+ end
207
+ end
208
+
209
+ # Show the client a nice prompt with data about the state of
210
+ # the session.
211
+ #
212
+ def prompt( prompt_name, socket, state )
213
+ if ::Thread.critical
214
+ crit = '-!!'
215
+ end
216
+
217
+ if state.block_cmd.empty?
218
+ socket.puts
219
+
220
+ if portal = state.bp_portal
221
+ sp = portal.stoppoint
222
+ if sp.breakpoint
223
+ pre = "Breakpoint #{sp.breakpoint.id}"
224
+ else
225
+ pre = "Stopped"
226
+ end
227
+ socket.puts( "#{pre} in #{sp.classname}\#" +
228
+ "#{sp.methodname} from #{sp.file}:#{sp.line} " +
229
+ "(#{sp.event}) in #{sp.thread}" )
230
+ end
231
+ end
232
+
233
+ socket.print( "#{prompt_name}:#{'%03d'%state.cmd_count}:" +
234
+ "#{state.block_count}#{crit}> " )
235
+ end
236
+
237
+ # Used to coordinate both the redirected eval output as well
238
+ # as the eval returned value.
239
+ #
240
+ EvalReturn = Struct.new( :out, :value )
241
+
242
+ # Get the next command from the client and process it by
243
+ # either evaluating it in the default binding context or in
244
+ # the context of a breakpoint.
245
+ #
246
+ def handle_client( socket, state )
247
+ unless cmd = socket.gets
248
+ return :dead
249
+ end
250
+
251
+ if state.block_count > 0
252
+ if cmd.match( /^\s*$/ )
253
+ cmd = state.block_cmd
254
+ state.block_count += 1
255
+ state.block_cmd = ''
256
+ else
257
+ state.block_cmd += cmd
258
+ return state.cmd_count += 1
259
+ end
260
+ end
261
+
262
+ if( cmd[0] == RTIManager::CMD_MARKER )
263
+ # Handle RTI commands (e.g. bp_add, bp_start...).
264
+ rti_cmd, *rti_args = cmd[1..-1].split( /\s+/ )
265
+
266
+ direct_thread = ::Thread.new do
267
+ Thread.current[:thread_name] = :rti_direct
268
+ state.rti.send( rti_cmd, *rti_args )
269
+ end
270
+
271
+ watch_req = nil
272
+ watch_thread = run_watch_thread( ::Thread.current ) do |th, req|
273
+ watch_req = req
274
+ direct_thread.kill
275
+ end
276
+
277
+ begin
278
+ direct_thread.join
279
+ ensure
280
+ if watch_req
281
+ if watch_req == :dead_client
282
+ @dead_clients << ::Thread.current
283
+ end
284
+ else
285
+ watch_thread.kill
286
+ end
287
+ end
288
+
289
+ ret = EvalReturn.new( nil, direct_thread.value )
290
+ end
291
+
292
+ unless ret
293
+ # Determine to either run the cmd locally or in a
294
+ # thread waiting at a breakpoint.
295
+ if state.bp_portal
296
+ state.bp_portal.cmds << cmd
297
+ ret = state.bp_portal.out.pop
298
+ else
299
+ ret = run_eval( state, cmd )
300
+ end
301
+ end
302
+
303
+ if out = ret.out
304
+ if out.kind_of? StringIO
305
+ socket.puts out.string unless out.length < 1
306
+ else
307
+ socket.puts out
308
+ end
309
+ end
310
+
311
+ v = ret.value
312
+ if state.use_yaml
313
+ v = v.to_yaml
314
+ elsif v.kind_of? Exception
315
+ v = "#{v}\n" + v.backtrace.join("\n")
316
+ else
317
+ v = v.inspect
318
+ end
319
+
320
+ socket.puts "#{OPTS[:output_marker]} #{v}"
321
+
322
+ state.cmd_count += 1
323
+ end
324
+
325
+ # Turned on selectively via #rti_bp_start where we are
326
+ # actively trying to find a point to stop and process commands
327
+ # in this context.
328
+ #
329
+ def handle_tracing( event, file, line, methodname, binding, classname )
330
+ th = ::Thread.current
331
+ tracer = th[:rti_tracer]
332
+ sp = StopPoint.new( classname, methodname, file, line, event, th )
333
+
334
+ if tracer
335
+ found = tracer.stoppoint( sp )
336
+ if found == :continue
337
+ found = nil
338
+ end
339
+ end
340
+
341
+ unless found
342
+ return unless file and line
343
+ # The first delete is due to the one-time nature of
344
+ # RTIManager#bp_attach.
345
+ if( bp = @breakpoints.delete( ">#{th.object_id}" ) or
346
+ bp = @breakpoints["#{file}:#{line}"] or
347
+ bp = @breakpoints["#{file}:#{line}>#{th.object_id}"] )
348
+ sp.breakpoint = bp
349
+ sp.state = bp.state
350
+ found = sp
351
+ else
352
+ # These breakpoints only stop at the initial call
353
+ # into this method.
354
+ return unless event == 'call'
355
+ if( bp = @breakpoints["#{classname}##{methodname}"] or
356
+ bp = @breakpoints["#{classname}##{methodname}>#{th.object_id}"] )
357
+ sp.breakpoint = bp
358
+ sp.state = bp.state
359
+ found = sp
360
+ elsif( bp = @breakpoints["#{methodname}"] or
361
+ bp = @breakpoints["#{methodname}>#{th.object_id}"] )
362
+ sp.breakpoint = bp
363
+ sp.state = bp.state
364
+ found = sp
365
+ else
366
+ if OPTS[:show_trace]
367
+ OPTS[:dbg_handler].call(
368
+ "Skipping #{classname}##{methodname} " +
369
+ "at #{file}:#{line} in #{th}" )
370
+ end
371
+ return
372
+ end
373
+ end
374
+ end
375
+
376
+ # If we've found a stoppoint, we no longer need the tracer
377
+ # as it is just looking for the next stopping point.
378
+ th[:rti_tracer] = nil
379
+ portal = Portal.new( found, Queue.new, Queue.new )
380
+ found.state.bp_waiting << portal
381
+ loop do
382
+ cmd = portal.cmds.pop
383
+ case cmd
384
+ when :step
385
+ th[:rti_tracer] = Tracer.new( portal, cmd )
386
+ break
387
+ when :next
388
+ th[:rti_tracer] = Tracer.new( portal, cmd )
389
+ break
390
+ when :fin
391
+ th[:rti_tracer] = Tracer.new( portal, cmd )
392
+ break
393
+ when :continue
394
+ break
395
+ when :stop
396
+ set_trace_func( nil )
397
+ break
398
+ else
399
+ out = run_eval( found.state, cmd, binding )
400
+ portal.out << out
401
+ end
402
+ end
403
+ rescue Object => e
404
+ OPTS[:exc_handler].call( "tracing", e )
405
+ end
406
+
407
+ # Run a safe evaluation of a client's command. This redirect's
408
+ # any output from the command's evaluation. The return will be
409
+ # both the output from the evaluation as well as the value of
410
+ # the evaluation.
411
+ #
412
+ def run_eval( state, cmd, binding=@binding, safe_level=nil )
413
+ safe_level = state.safe_level unless safe_level
414
+ ret = EvalReturn.new
415
+
416
+ eval_args = ["$SAFE=#{safe_level};" +
417
+ "rti=::Thread.current[:rti_manager];" + cmd]
418
+ if binding
419
+ eval_args << binding
420
+ end
421
+
422
+ if $DEBUG
423
+ if binding != @binding
424
+ bindstr = " (explicit binding)"
425
+ elsif !binding
426
+ bindstr = " (without binding)"
427
+ end
428
+ OPTS[:dbg_handler].call( "Executing#{bindstr} " +
429
+ "[#{safe_level}]: #{cmd}" )
430
+ end
431
+
432
+ # Temporarily capture stdout/stderr into string for just
433
+ # the evaluation thread.
434
+ ret.out = StringIO.new
435
+ ThreadRedirect.start
436
+
437
+ eval_thread = ::Thread.new do
438
+ t = ::Thread.current
439
+ t[:thread_name] = :rti_eval
440
+ t[:rti_redirect_io] = ret.out
441
+ t[:rti_manager] = state.rti
442
+ begin
443
+ eval( *eval_args )
444
+ rescue Object => e
445
+ e
446
+ end
447
+ end
448
+
449
+ watch_req = nil
450
+ watch_thread = run_watch_thread( ::Thread.current ) do |th, req|
451
+ watch_req = req
452
+ eval_thread.kill
453
+ end
454
+
455
+ begin
456
+ eval_thread.join
457
+ rescue Object => e
458
+ ret.out.puts e, e.backtrace
459
+ # The thread could hang around forever if it isn't
460
+ # taken care of...
461
+ eval_thread.kill
462
+ ensure
463
+ ThreadRedirect.stop
464
+ if watch_req
465
+ if watch_req == :dead_client
466
+ @dead_clients << ::Thread.current
467
+ end
468
+ # nothing more--let the watch thread stop on its own
469
+ else
470
+ watch_thread.kill
471
+ end
472
+ end
473
+
474
+ ret.value = eval_thread.value
475
+
476
+ return ret
477
+ end
478
+
479
+ # Here, we do not protect this with a timeout as the amount of
480
+ # time to wait is indeterminate (i.e. in some cases, one may
481
+ # wait a long time for a breakpoint to hit based on how
482
+ # complicated the setup is to get the codepath in to that
483
+ # state). However, we must detect when a client disconnects
484
+ # while this thread waits around trying to find a breakpoint,
485
+ # otherwise, this thread will hang around forever.
486
+ #
487
+ def run_watch_thread( client_thread )
488
+ ::Thread.new do
489
+ ::Thread.current[:thread_name] = :rti_watch
490
+ socket = client_thread[:socket]
491
+ loop do
492
+ IO.select( [socket] )
493
+ if socket.eof?
494
+ yield( client_thread, :dead_client )
495
+ break
496
+ else
497
+ req = socket.read_nonblock( 100 )
498
+ chars = req.unpack( "C*" )
499
+ # There has *got* to be a better way...
500
+ case chars
501
+ when [4]
502
+ # This is the ^d so the client can
503
+ # interrupt a command without killing the
504
+ # connection.
505
+ yield( client_thread, :interrupt )
506
+ break
507
+ when [0xff, 0xf4, 0xff, 0xfd, 0x6]
508
+ # When we capture ^c it appears that
509
+ # nothing else is ever displayed by the
510
+ # telnet client for some strange
511
+ # reason. So, we terminate the connection.
512
+ yield( client_thread, :dead_client )
513
+ break
514
+ else
515
+ hexstr = chars.collect{|c| c.to_s(16)}.join
516
+ OPTS[:dbg_handler].call( "Ignoring #{req} " +
517
+ "(#{hexstr})" )
518
+ socket.puts "Unknown request (#{req.chomp}). " +
519
+ "Use control-d to interrupt the process."
520
+ end
521
+ end
522
+ end
523
+ end
524
+ end
525
+
526
+ # Close down all our threads and clean stuff up in
527
+ # anticipation of the main thread going away.
528
+ #
529
+ def cleanup
530
+ @server.close unless @server.closed?
531
+
532
+ until @clients.empty?
533
+ client = nil
534
+ @clients_mutex.synchronize do
535
+ client = @clients.pop
536
+ end
537
+ @dead_clients << client
538
+ end
539
+
540
+ # Tell the GC thread to but still give it a chance to
541
+ # finish up with all the clients we just gave it.
542
+ @dead_clients << :quit
543
+
544
+ until @dead_clients.empty?
545
+ @gc_thread.run
546
+ end
547
+
548
+ @gc_thread.kill
549
+ end
550
+
551
+ end
552
+
553
+ end