rjr 0.9.0 → 0.11.7

Sign up to get free protection for your applications and to get access to all the features.
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
  #