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/redirect.rb
ADDED
@@ -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
|
data/lib/rti/state.rb
ADDED
@@ -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
|
data/lib/rti/thread.rb
ADDED
@@ -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
|