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.
Files changed (7) hide show
  1. data/COPYING +340 -0
  2. data/LICENSE +53 -0
  3. data/README +331 -0
  4. data/TODO +36 -0
  5. data/lib/rtinspect.rb +950 -0
  6. data/test/test.rb +162 -0
  7. 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