rjr 0.9.0 → 0.11.7

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/bin/rjr-client ADDED
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/ruby
2
+ # RJR client
3
+ #
4
+ # Copyright (C) 2013 Mohammed Morsi <mo@morsi.org>
5
+ # Licensed under the Apache License, Version 2.0
6
+
7
+ require 'rubygems'
8
+ require 'optparse'
9
+ require 'rjr'
10
+
11
+ RJR::Logger.log_level = ::Logger::DEBUG
12
+
13
+ ##########################################################
14
+
15
+
16
+ MAX_MESSAGES = 5 # only used when generating random number of messages
17
+ MAX_INTERVAL = 20 # only used when generating random interval
18
+ MAX_DISCONNECT = 30 # only used when generating random disconnect
19
+
20
+ config = { :mode => :msg,
21
+ :node_id => 'rjr_test_client',
22
+ :transport => :amqp,
23
+ :broker => 'localhost',
24
+ :dst => 'rjr_test_server-queue',
25
+ :num_msg => 1,
26
+ :msg_id => :rand,
27
+ :block => false,
28
+ :interval => 0,
29
+ :disconnect => false}
30
+
31
+ optparse = OptionParser.new do |opts|
32
+ opts.on('-h', '--help', 'Display this help screen') do
33
+ puts opts
34
+ exit
35
+ end
36
+
37
+ opts.on('-m', '--mode mode', 'Mode of operation, default "msg" to send message, may be "rand" to send sequences of random bytes') do |mode|
38
+ config[:mode] = mode.intern
39
+ end
40
+
41
+ opts.on('-i', '--id ID', 'Node ID to assign to client') do |id|
42
+ config[:node_id] = id
43
+ end
44
+
45
+ opts.on('-t', '--transport type', 'Type of transport to connect to server') do |t|
46
+ config[:transport] = t.intern
47
+ end
48
+
49
+ opts.on('-b', '--broker broker', 'AMQP Broker (only needed if transport is amqp)') do |b|
50
+ config[:broker] = b
51
+ end
52
+
53
+ opts.on('--dst dst', 'Destination which to connect to/send messages to') do |dst|
54
+ config[:dst] = dst
55
+ end
56
+
57
+ opts.on('-r', '--[no-]return', 'Boolean indicating if client should block/wait for server messages (default false)') do |ret|
58
+ config[:block] = !ret
59
+ end
60
+
61
+ opts.on('-n', '--num number_of_messages', 'Number of messages to send to server (may be :rand or indefinite)') do |n|
62
+ config[:num_msg] = case n.to_s.intern
63
+ when :rand then rand(MAX_MESSAGES)
64
+ when :indefinite then 1000000000 # XXX 'indefinite ;-)'
65
+ else n.to_i
66
+ end
67
+ end
68
+
69
+ opts.on('--message ID', 'Message to send to server (rand to select random)') do |mid|
70
+ config[:msg_id] = mid
71
+ end
72
+
73
+ opts.on('--interval seconds', 'Number of seconds after which to wait between requests (or rand)') do |s|
74
+ config[:interval] = s.to_s.intern
75
+ end
76
+
77
+ opts.on('-d', '--disconnect seconds', OptionParser::DecimalNumeric, 'Number of seconds after which to disconnect client (or rand)') do |d|
78
+ config[:disconnect] = d.to_s.intern
79
+ end
80
+ end
81
+
82
+ optparse.parse!
83
+
84
+ if config[:num_msg] < 1
85
+ puts "At least one message must be specified"
86
+ exit 1
87
+ end
88
+
89
+ ##########################################################
90
+
91
+ NODES = {config[:transport] => {:node_id => config[:node_id],
92
+ :broker => config[:broker], # XXX only needed for amqp
93
+ :keep_alive => true} } # conditionally set keep alive?
94
+
95
+ cdir = File.dirname(__FILE__)
96
+ client_path = File.join(ENV['RJR_LOAD_PATH'] || File.join(cdir, '..', 'tests', 'harness'), "client")
97
+
98
+ require_path client_path
99
+
100
+ ##########################################################
101
+
102
+ node = RJR::EasyNode.new(NODES)
103
+
104
+ if config[:disconnect]
105
+ disconnect_thread = Thread.new {
106
+ seconds = config[:disconnect] == :rand ? rand(MAX_DISCONNECT) : config[:disconnect].to_s.to_i
107
+ sleep seconds
108
+ # node.halt ?
109
+ exit 1
110
+ }
111
+ end
112
+
113
+ # invoke request(s)
114
+ 0.upto(config[:num_msg]-1) { |i|
115
+ # TODO implement mode == :rand
116
+
117
+ # grab message (or rand message)
118
+ msg = (config[:msg_id] == :rand ? RJR::Definitions.rand_msg(config[:transport]) :
119
+ RJR::Definitions.rjr_message(config[:msg_id]))
120
+
121
+ if msg.nil?
122
+ puts "Invalid message id"
123
+ exit 1
124
+ end
125
+
126
+ # manipulate msg params
127
+ parmas = []
128
+ params = msg[:params].collect { |p|
129
+ if p.is_a?(String)
130
+ p.gsub('<CLIENT_ID>', config[:node_id]) # TODO other substitutions
131
+ elsif p.is_a?(Proc)
132
+ p.call
133
+ else
134
+ p
135
+ end
136
+ } if msg[:params]
137
+
138
+ # invoke request
139
+ res = node.invoke_request(config[:dst], msg[:method], *params)
140
+
141
+ # verify and output result
142
+ ress = (msg[:result].nil? ? "" : (msg[:result].call(res) ? "passed" : "failed"))
143
+ RJR::Logger.info "#{msg[:method]} result #{res} #{ress}"
144
+
145
+ if msg[:interval] && i < (config[:num_msg] - 1)
146
+ sleep(msg[:interval] == :rand ? rand(MAX_INTERVAL) : msg[:interval].to_s.to_i)
147
+ end
148
+ }
149
+
150
+ node.join if config[:block]
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/ruby
2
+ # Launches a series of clients
3
+ #
4
+ # Copyright (C) 2013 Mohammed Morsi <mo@morsi.org>
5
+ # Licensed under the Apache License, Version 2.0
6
+
7
+ ID_OFFSET = ARGV.shift.to_i || 100
8
+
9
+ NUM_CLIENTS = 5
10
+ NUM_MSGS = 20 # per client
11
+ NODE_ID = 'rjr_test_launcher-'
12
+ MSG_IDS = ['stress', 'stress_callback']
13
+ TRANSPORTS = {#:amqp => 'rjr_test_server-queue',
14
+ #:tcp => 'jsonrpc://localhost:8181',
15
+ :ws => 'jsonrpc://localhost:8080',
16
+ #:www => 'http://localhost:8888'
17
+ }
18
+ BROKER = 'localhost' # only used for amqp
19
+ MSG_INTERVAL= 3
20
+
21
+ CLIENT = File.join(File.dirname(__FILE__), 'rjr-client')
22
+
23
+ threads = []
24
+
25
+ 0.upto(NUM_CLIENTS) { |i|
26
+ transport = TRANSPORTS.keys[rand(TRANSPORTS.keys.size)]
27
+ dst = TRANSPORTS[transport]
28
+ mode = rand(2) == 0 ? :msg : :rand
29
+ node_id = NODE_ID + (i + ID_OFFSET).to_s
30
+ msg_id = MSG_IDS[rand(MSG_IDS.size)]
31
+
32
+ threads <<
33
+ Thread.new{
34
+ system("#{CLIENT} -m #{mode} -t #{transport} -i #{node_id} -b #{BROKER} --dst #{dst} -n #{NUM_MSGS} --message #{msg_id} --interval #{MSG_INTERVAL}")
35
+ }
36
+ }
37
+
38
+ threads.each { |t| t.join }
data/bin/rjr-server CHANGED
@@ -1,52 +1,74 @@
1
1
  #!/usr/bin/ruby
2
- # A rjr server executable
3
- # Executable to launch registered rjr methods
2
+ # RJR server
4
3
  #
5
- # Flags:
6
- # -h --help
7
- #
8
- # Copyright (C) 2011-2012 Mohammed Morsi <mo@morsi.org>
4
+ # Copyright (C) 2013 Mohammed Morsi <mo@morsi.org>
9
5
  # Licensed under the Apache License, Version 2.0
10
6
 
11
7
  require 'rubygems'
12
8
  require 'optparse'
13
9
  require 'rjr'
10
+ require 'stringio'
14
11
 
15
- ######################
12
+ require 'rjr/inspect'
16
13
 
14
+ ##########################################################
17
15
 
18
- def main()
19
- # setup cmd line options
20
- opts = OptionParser.new do |opts|
21
- opts.on("-h", "--help", "Print help message") do
22
- puts opts
23
- exit
24
- end
25
- end
16
+ config = { :node_id => 'rjr_test_server',
17
+ :broker => 'localhost',
18
+ :host => 'localhost',
19
+ :tcp_port => 8181,
20
+ :www_port => 8888,
21
+ :ws_port => 8080 }
26
22
 
27
- # parse cmd line
28
- begin
29
- opts.parse!(ARGV)
30
- rescue OptionParser::InvalidOption
23
+ optparse = OptionParser.new do |opts|
24
+ opts.on('-h', '--help', 'Display this help screen') do
31
25
  puts opts
32
26
  exit
33
27
  end
34
28
 
35
- amqp_node = RJR::AMQPNode.new :node_id => 'rjr', :broker => 'localhost'
36
- ws_node = RJR::WSNode.new :node_id => 'rjr', :host => 'localhost', :port => 8080
37
- www_node = RJR::WebNode.new :node_id => 'rjr', :host => 'localhost', :port => 8888
29
+ opts.on('-i', '--id ID', 'Node ID to assign to server') do |id|
30
+ config[:node_id] = id
31
+ end
38
32
 
39
- RJR::Dispatcher.add_handler('hello') { |msg|
40
- #raise Exception.new("foobar")
41
- puts "hello #{msg}"
42
- "world"
43
- }
33
+ opts.on('-b', '--broker broker', 'AMQP Broker') do |b|
34
+ config[:broker] = b
35
+ end
44
36
 
45
- rjr_node = RJR::MultiNode.new :nodes => [amqp_node, ws_node, www_node]
37
+ opts.on('-h', '--host host', 'Host (or ip) which to listen on') do |h|
38
+ config[:host] = host
39
+ end
40
+
41
+ opts.on('--tcp port', 'TCP Port to listen on') do |p|
42
+ config[:tcp_port] = p
43
+ end
44
+
45
+ opts.on('--www port', 'Port to listen on for www requests') do |p|
46
+ config[:www_port] = p
47
+ end
48
+
49
+ opts.on('--ws port', 'Websocket Port to listen on') do |p|
50
+ config[:ws_port] = p
51
+ end
46
52
 
47
- rjr_node.listen
48
- rjr_node.join
49
- rjr_node.stop # TODO run in signal handler
50
53
  end
51
54
 
52
- main()
55
+ optparse.parse!
56
+
57
+ ##########################################################
58
+
59
+ NODES = {:amqp => {:node_id => config[:node_id], :broker => config[:broker]},
60
+ :ws => {:node_id => config[:node_id], :host => config[:host], :port => config[:ws_port]},
61
+ :www => {:node_id => config[:node_id], :host => config[:host], :port => config[:www_port]},
62
+ :tcp => {:node_id => config[:node_id], :host => config[:host], :port => config[:tcp_port]}}
63
+
64
+ cdir = File.dirname(__FILE__)
65
+ server_path = File.join(ENV['RJR_LOAD_PATH'] || File.join(cdir, '..', 'tests', 'harness'), 'server')
66
+ require_path server_path
67
+
68
+ ##########################################################
69
+
70
+ $messages = StringIO.new
71
+
72
+ RJR::Logger.log_level = ::Logger::DEBUG
73
+ RJR::Logger.log_to $messages
74
+ RJR::EasyNode.new(NODES).stop_on("INT").listen.join
data/lib/rjr/amqp_node.rb CHANGED
@@ -5,10 +5,23 @@
5
5
  # Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
6
6
  # Licensed under the Apache License, Version 2.0
7
7
 
8
+ skip_module = false
9
+ begin
8
10
  require 'amqp'
9
- require 'thread'
11
+ rescue LoadError
12
+ skip_module = true
13
+ end
14
+
15
+ if skip_module
16
+ # TODO output: "amqp gem could not be loaded, skipping amqp node definition"
17
+ require 'rjr/missing_node'
18
+ RJR::AMQPNode = RJR::MissingNode
19
+
20
+ else
10
21
  require 'rjr/node'
11
22
  require 'rjr/message'
23
+ require 'rjr/dispatcher'
24
+ require 'rjr/thread_pool2'
12
25
 
13
26
  module RJR
14
27
 
@@ -31,8 +44,8 @@ class AMQPNodeCallback
31
44
 
32
45
  # Implementation of {RJR::NodeCallback#invoke}
33
46
  def invoke(callback_method, *data)
34
- msg = RequestMessage.new :method => callback_method, :args => data, :headers => @message_headers
35
- @node.publish msg.to_s, :routing_key => @destination, :mandatory => true
47
+ msg = NotificationMessage.new :method => callback_method, :args => data, :headers => @message_headers
48
+ @node.publish(msg.to_s, :routing_key => @destination, :mandatory => true) { }
36
49
  end
37
50
  end
38
51
 
@@ -66,7 +79,11 @@ class AMQPNode < RJR::Node
66
79
  def handle_message(metadata, msg)
67
80
  if RequestMessage.is_request_message?(msg)
68
81
  reply_to = metadata.reply_to
69
- @thread_pool << ThreadPoolJob.new { handle_request(reply_to, msg) }
82
+ ThreadPool2Manager << ThreadPool2Job.new { handle_request(reply_to, msg, false) }
83
+
84
+ elsif NotificationMessage.is_notification_message?(msg)
85
+ reply_to = metadata.reply_to
86
+ ThreadPool2Manager << ThreadPool2Job.new { handle_request(reply_to, msg, true) }
70
87
 
71
88
  elsif ResponseMessage.is_response_message?(msg)
72
89
  handle_response(msg)
@@ -75,8 +92,9 @@ class AMQPNode < RJR::Node
75
92
  end
76
93
 
77
94
  # Internal helper, handle request message pulled off queue
78
- def handle_request(reply_to, message)
79
- msg = RequestMessage.new(:message => message, :headers => @message_headers)
95
+ def handle_request(reply_to, message, notification=false)
96
+ msg = notification ? NotificationMessage.new(:message => message, :headers => @message_headers) :
97
+ RequestMessage.new(:message => message, :headers => @message_headers)
80
98
  headers = @message_headers.merge(msg.headers) # append request message headers
81
99
  result = Dispatcher.dispatch_request(msg.jr_method,
82
100
  :method_args => msg.jr_args,
@@ -91,8 +109,10 @@ class AMQPNode < RJR::Node
91
109
  :exchange => @exchange,
92
110
  :destination => reply_to,
93
111
  :headers => headers))
94
- response = ResponseMessage.new(:id => msg.msg_id, :result => result, :headers => headers)
95
- publish response.to_s, :routing_key => reply_to
112
+ unless notification
113
+ response = ResponseMessage.new(:id => msg.msg_id, :result => result, :headers => headers)
114
+ publish(response.to_s, :routing_key => reply_to) { }
115
+ end
96
116
  end
97
117
 
98
118
  # Internal helper, handle response message pulled off queue
@@ -102,7 +122,7 @@ class AMQPNode < RJR::Node
102
122
  begin
103
123
  res = Dispatcher.handle_response(msg.result)
104
124
  rescue Exception => e
105
- err = e
125
+ err = e.to_s
106
126
  end
107
127
 
108
128
  @response_lock.synchronize{
@@ -114,11 +134,20 @@ class AMQPNode < RJR::Node
114
134
  end
115
135
 
116
136
  # Initialize the amqp subsystem
117
- def init_node
118
- return unless @conn.nil? || !@conn.connected?
119
- @conn = AMQP.connect(:host => @broker)
137
+ def init_node(&on_init)
138
+ if !@conn.nil? && @conn.connected?
139
+ on_init.call
140
+ return
141
+ end
142
+
143
+ super
144
+ @conn = AMQP.connect(:host => @broker) do |*args|
145
+ on_init.call
146
+ end
120
147
  @conn.on_tcp_connection_failure { puts "OTCF #{@node_id}" }
121
148
 
149
+ # TODO move the rest into connect callback ?
150
+
122
151
  ### connect to qpid broker
123
152
  @channel = AMQP::Channel.new(@conn)
124
153
 
@@ -143,18 +172,15 @@ class AMQPNode < RJR::Node
143
172
  res = nil
144
173
  while res.nil?
145
174
  @response_lock.synchronize{
146
- @response_cv.wait @response_lock
147
175
  # FIXME throw err if more than 1 match found
148
176
  res = @responses.select { |response| message.msg_id == response.first }.first
149
- unless res.nil?
177
+ if !res.nil?
150
178
  @responses.delete(res)
179
+
151
180
  else
152
- # we can't just go back to waiting for message here, need to give
153
- # other nodes a chance to check it first
154
181
  @response_cv.signal
155
- @response_check_cv.wait @response_lock
182
+ @response_cv.wait @response_lock
156
183
  end
157
- @response_check_cv.signal
158
184
  }
159
185
  end
160
186
  return res
@@ -194,7 +220,6 @@ class AMQPNode < RJR::Node
194
220
  @connection_event_handlers = {:closed => [], :error => []}
195
221
  @response_lock = Mutex.new
196
222
  @response_cv = ConditionVariable.new
197
- @response_check_cv = ConditionVariable.new
198
223
  @responses = []
199
224
  @amqp_lock = Mutex.new
200
225
  end
@@ -202,10 +227,12 @@ class AMQPNode < RJR::Node
202
227
  # Publish a message using the amqp exchange (*do* *not* *use*).
203
228
  #
204
229
  # XXX hack should be private, declared publically so as to be able to be used by {RJR::AMQPNodeCallback}
205
- def publish(*args)
230
+ def publish(*args, &on_publish)
206
231
  @amqp_lock.synchronize {
207
232
  #raise RJR::Errors::ConnectionError.new("client unreachable") if @disconnected
208
- @exchange.publish *args
233
+ @exchange.publish *args do |*cargs|
234
+ on_publish.call
235
+ end
209
236
  }
210
237
  nil
211
238
  end
@@ -225,16 +252,20 @@ class AMQPNode < RJR::Node
225
252
  # Implementation of {RJR::Node#listen}
226
253
  def listen
227
254
  em_run do
228
- init_node
229
-
230
- # start receiving messages
231
- subscribe { |metadata, msg|
232
- handle_message(metadata, msg)
255
+ init_node {
256
+ # start receiving messages
257
+ subscribe { |metadata, msg|
258
+ handle_message(metadata, msg)
259
+ }
233
260
  }
234
261
  end
235
262
  end
236
263
 
237
- # Instructs node to send rpc request, and wait for and return response
264
+ # Instructs node to send rpc request, and wait for and return response.
265
+ #
266
+ # Do not invoke directly from em event loop or callback as will block the message
267
+ # subscription used to receive responses
268
+ #
238
269
  # @param [String] routing_key destination queue to send request to
239
270
  # @param [String] rpc_method json-rpc method to invoke on destination
240
271
  # @param [Array] args array of arguments to convert to json and invoke remote method wtih
@@ -245,26 +276,53 @@ class AMQPNode < RJR::Node
245
276
  :args => args,
246
277
  :headers => @message_headers
247
278
  em_run do
248
- init_node
279
+ init_node {
280
+ # begin listening for result
281
+ subscribe { |metadata, msg|
282
+ handle_message(metadata, msg)
283
+ }
249
284
 
250
- # begin listening for result
251
- subscribe { |metadata, msg|
252
- handle_message(metadata, msg)
285
+ publish(message.to_s, :routing_key => routing_key, :reply_to => @queue_name) { }
253
286
  }
254
-
255
- publish message.to_s, :routing_key => routing_key, :reply_to => @queue_name
256
287
  end
257
288
 
258
- # TODO optional timeout for response ?
289
+ # TODO optional timeout for response
259
290
  result = wait_for_result(message)
260
- #self.stop
261
- #self.join unless self.em_running?
262
291
 
263
292
  if result.size > 2
264
- raise result[2]
293
+ raise Exception, result[2]
265
294
  end
266
295
  return result[1]
267
296
  end
268
297
 
298
+ # FIXME add method to instruct node to send rpc request, and immediately
299
+ # return / ignoring response & also add method to collect response
300
+ # at a later time
301
+
302
+ # Instructs node to send rpc notification (immadiately returns / no response is generated)
303
+ #
304
+ # @param [String] routing_key destination queue to send request to
305
+ # @param [String] rpc_method json-rpc method to invoke on destination
306
+ # @param [Array] args array of arguments to convert to json and invoke remote method wtih
307
+ def send_notification(routing_key, rpc_method, *args)
308
+ # will block until message is published
309
+ published_l = Mutex.new
310
+ published_c = ConditionVariable.new
311
+
312
+ message = NotificationMessage.new :method => rpc_method,
313
+ :args => args,
314
+ :headers => @message_headers
315
+ em_run do
316
+ init_node {
317
+ publish(message.to_s, :routing_key => routing_key, :reply_to => @queue_name){
318
+ published_l.synchronize { published_c.signal }
319
+ }
320
+ }
321
+ end
322
+ published_l.synchronize { published_c.wait published_l }
323
+ nil
324
+ end
325
+
326
+ end
269
327
  end
270
328
  end
data/lib/rjr/common.rb CHANGED
@@ -1,4 +1,4 @@
1
- # RJR Utility Methods
1
+ # Low level RJR Utility Methods
2
2
  #
3
3
  # Assortment of helper methods and methods that don't fit elsewhere
4
4
  #
@@ -14,17 +14,20 @@ module RJR
14
14
  # Encapsulates the standard ruby logger in a thread safe manner. Dispatches
15
15
  # class methods to an internally tracked logger to provide global access.
16
16
  #
17
+ # TODO handle logging errors (log size too big, logrotate, etc)
18
+ #
17
19
  # @example
18
20
  # RJR::Logger.info 'my message'
19
21
  # RJR::Logger.warn 'my warning'
20
22
  class Logger
21
23
  private
22
24
  def self._instantiate_logger
23
- unless defined? @@logger
25
+ if @logger.nil?
24
26
  #STDOUT.sync = true
25
- @@logger = ::Logger.new(STDOUT)
26
- @@logger.level = ::Logger::FATAL
27
- @@logger_mutex = Mutex.new
27
+ output = @log_to || ENV['RJR_LOG'] || STDOUT
28
+ @logger = ::Logger.new(output)
29
+ @logger.level = @log_level || ::Logger::FATAL
30
+ @logger_mutex = Mutex.new
28
31
  end
29
32
  end
30
33
 
@@ -32,32 +35,79 @@ class Logger
32
35
 
33
36
  def self.method_missing(method_id, *args)
34
37
  _instantiate_logger
35
- @@logger_mutex.synchronize {
38
+ @logger_mutex.synchronize {
36
39
  if args.first.is_a?(Array)
37
40
  args.first.each{ |a|
38
- @@logger.send(method_id, a)
41
+ @logger.send(method_id, a)
39
42
  }
40
43
  else
41
- @@logger.send(method_id, args)
44
+ @logger.send(method_id, args)
42
45
  end
43
46
  }
44
47
  end
45
48
 
46
49
  def self.logger
47
50
  _instantiate_logger
48
- @@logger
51
+ @logger
52
+ end
53
+
54
+ # Set log destination
55
+ # @param dst destination which to log to (file name, STDOUT, etc)
56
+ def self.log_to(dst)
57
+ @log_to = dst
58
+ @logger = nil
59
+ _instantiate_logger
49
60
  end
50
61
 
51
62
  # Set log level.
52
63
  # @param level one of the standard rails log levels (default fatal)
53
64
  def self.log_level=(level)
54
65
  _instantiate_logger
55
- @@logger.level = level
66
+ if level.is_a?(String)
67
+ level = case level
68
+ when 'debug' then
69
+ ::Logger::DEBUG
70
+ when 'info' then
71
+ ::Logger::INFO
72
+ when 'warn' then
73
+ ::Logger::WARN
74
+ when 'error' then
75
+ ::Logger::ERROR
76
+ when 'fatal' then
77
+ ::Logger::FATAL
78
+ end
79
+ end
80
+ @log_level = level
81
+ @logger.level = level
82
+ end
83
+
84
+ # Return true if log level is set to debug, else false
85
+ def self.debug?
86
+ @log_level == ::Logger::DEBUG
56
87
  end
57
88
  end
58
89
 
59
90
  end # module RJR
60
91
 
92
+ class Object
93
+ def eigenclass
94
+ class << self
95
+ self
96
+ end
97
+ end
98
+ end
99
+
100
+ module Kernel
101
+ def require_path(path)
102
+ path.split(':').all? { |dir|
103
+ # TODO also all .so files? allow user to specify suffix or omit?
104
+ Dir.glob(File.join(dir, '*.rb')).all? { |rb|
105
+ require rb
106
+ }
107
+ }
108
+ end
109
+ end
110
+
61
111
  if RUBY_VERSION < "1.9"
62
112
  # We extend object in ruby 1.9 to define 'instance_exec'
63
113
  #