rjr 0.5.3
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +661 -0
- data/README.rdoc +73 -0
- data/Rakefile +64 -0
- data/bin/rjr-server +51 -0
- data/lib/rjr/amqp_node.rb +164 -0
- data/lib/rjr/common.rb +60 -0
- data/lib/rjr/dispatcher.rb +169 -0
- data/lib/rjr/errors.rb +21 -0
- data/lib/rjr/local_node.rb +65 -0
- data/lib/rjr/message.rb +146 -0
- data/lib/rjr/multi_node.rb +35 -0
- data/lib/rjr/node.rb +81 -0
- data/lib/rjr/semaphore.rb +58 -0
- data/lib/rjr/tcp_node.rb +1 -0
- data/lib/rjr/thread_pool.rb +165 -0
- data/lib/rjr/udp_node.rb +1 -0
- data/lib/rjr/web_node.rb +112 -0
- data/lib/rjr/web_socket.rb +589 -0
- data/lib/rjr/ws_node.rb +100 -0
- data/lib/rjr.rb +20 -0
- metadata +103 -0
data/README.rdoc
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
== RJR - Ruby Json Rpc Library
|
2
|
+
|
3
|
+
Copyright (C) 2012 Mo Morsi <mo@morsi.org>
|
4
|
+
|
5
|
+
RJR is made available under the GNU AFFERO GENERAL PUBLIC LICENSE
|
6
|
+
as published by the Free Software Foundation, either version 3
|
7
|
+
of the License, or (at your option) any later version.
|
8
|
+
|
9
|
+
=== Intro
|
10
|
+
To install rjr simply run:
|
11
|
+
gem install rjr
|
12
|
+
|
13
|
+
Source code is available via:
|
14
|
+
git clone http://github.com/movitto/rjr
|
15
|
+
|
16
|
+
=== Using
|
17
|
+
|
18
|
+
Simply require rubygems and the rjr library
|
19
|
+
|
20
|
+
require 'rubygems'
|
21
|
+
require 'rjr'
|
22
|
+
|
23
|
+
server.rb:
|
24
|
+
|
25
|
+
# define a rpc method called 'hello' which takes
|
26
|
+
# one argument and returns it in upper case
|
27
|
+
RJR::Dispatcher.add_handler("hello") { |arg|
|
28
|
+
arg.upcase
|
29
|
+
}
|
30
|
+
|
31
|
+
# listen for this method via amqp, websockets, http, and via local calls
|
32
|
+
amqp_node = RJR::AMQPNode.new :node_id => 'server', :broker => 'localhost'
|
33
|
+
ws_node = RJR::WSNode.new :node_id => 'server', :host => 'localhost', :port => 8080
|
34
|
+
www_node = RJR::WebNode.new :node_id => 'server', :host => 'localhost', :port => 8888
|
35
|
+
local_node = RJR::LocalNode.new :node_id => 'server'
|
36
|
+
|
37
|
+
# start the server and block
|
38
|
+
multi_node = RJR::MultiNode.new :nodes => [amqp_node, ws_node, www_node, local_node]
|
39
|
+
multi_node.listen
|
40
|
+
multi_node.join
|
41
|
+
|
42
|
+
|
43
|
+
amqp_client.rb:
|
44
|
+
|
45
|
+
# invoke the method over amqp
|
46
|
+
amqp_node = RJR::AMQPNode.new :node_id => 'client', :broker => 'localhost'
|
47
|
+
puts amqp_node.invoke_request('server-queue', 'hello', 'world')
|
48
|
+
|
49
|
+
|
50
|
+
ws_client.js:
|
51
|
+
|
52
|
+
// use the js client to invoke the method via a websocket
|
53
|
+
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
|
54
|
+
<script type="text/javascript" src="site/json.js" />
|
55
|
+
<script type="text/javascript" src="site/jrw.js" />
|
56
|
+
<script type="text/javascript">
|
57
|
+
var node = new WSNode('127.0.0.1', '8080');
|
58
|
+
node.onopen = function(){
|
59
|
+
node.invoke_request('hello', 'rjr');
|
60
|
+
};
|
61
|
+
node.onsuccess = function(result){
|
62
|
+
alert(result);
|
63
|
+
};
|
64
|
+
node.open();
|
65
|
+
</script>
|
66
|
+
|
67
|
+
Generate documentation via
|
68
|
+
rake rdoc
|
69
|
+
|
70
|
+
Also see specs for detailed usage.
|
71
|
+
|
72
|
+
=== Authors
|
73
|
+
Mo Morsi <mo@morsi.org>
|
data/Rakefile
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# rjr project Rakefile
|
2
|
+
#
|
3
|
+
# Copyright (C) 2010 Mohammed Morsi <movitto@yahoo.com>
|
4
|
+
# Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
|
5
|
+
|
6
|
+
require 'rdoc/task'
|
7
|
+
require "rspec/core/rake_task"
|
8
|
+
require 'rubygems/package_task'
|
9
|
+
|
10
|
+
|
11
|
+
GEM_NAME="rjr"
|
12
|
+
PKG_VERSION='0.5.3'
|
13
|
+
|
14
|
+
desc "Run all specs"
|
15
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
16
|
+
spec.pattern = 'specs/**/*_spec.rb'
|
17
|
+
spec.rspec_opts = ['--backtrace']
|
18
|
+
end
|
19
|
+
|
20
|
+
desc "run javascript tests"
|
21
|
+
task :test_js do
|
22
|
+
ENV['RUBYLIB'] = "lib"
|
23
|
+
puts "Launching js test runner"
|
24
|
+
system("tests/js/runner")
|
25
|
+
end
|
26
|
+
|
27
|
+
desc "run integration/stress tests"
|
28
|
+
task :integration do
|
29
|
+
ENV['RUBYLIB'] = "lib"
|
30
|
+
puts "Launching integration test runner"
|
31
|
+
system("tests/integration/runner")
|
32
|
+
end
|
33
|
+
|
34
|
+
Rake::RDocTask.new do |rd|
|
35
|
+
rd.main = "README.rdoc"
|
36
|
+
rd.rdoc_dir = "doc/site/api"
|
37
|
+
rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
|
38
|
+
end
|
39
|
+
|
40
|
+
PKG_FILES = FileList['lib/**/*.rb',
|
41
|
+
'LICENSE', 'Rakefile', 'README.rdoc', 'spec/**/*.rb' ]
|
42
|
+
|
43
|
+
SPEC = Gem::Specification.new do |s|
|
44
|
+
s.name = GEM_NAME
|
45
|
+
s.version = PKG_VERSION
|
46
|
+
s.files = PKG_FILES
|
47
|
+
s.executables << 'rjr-server'
|
48
|
+
|
49
|
+
s.required_ruby_version = '>= 1.8.1'
|
50
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.3.3")
|
51
|
+
s.add_development_dependency('rspec', '~> 1.3.0')
|
52
|
+
|
53
|
+
s.author = "Mohammed Morsi"
|
54
|
+
s.email = "mo@morsi.org"
|
55
|
+
s.date = %q{2012-04-25}
|
56
|
+
s.description = %q{Ruby Json Rpc library}
|
57
|
+
s.summary = %q{JSON RPC server and client library over amqp, websockets}
|
58
|
+
s.homepage = %q{http://morsi.org/projects/rjr}
|
59
|
+
end
|
60
|
+
|
61
|
+
Gem::PackageTask.new(SPEC) do |pkg|
|
62
|
+
pkg.need_tar = true
|
63
|
+
pkg.need_zip = true
|
64
|
+
end
|
data/bin/rjr-server
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# A rjr server executable
|
3
|
+
# Executable to launch registered rjr methods
|
4
|
+
#
|
5
|
+
# Flags:
|
6
|
+
# -h --help
|
7
|
+
#
|
8
|
+
# Copyright (C) 2011 Mohammed Morsi <mo@morsi.org>
|
9
|
+
# Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
|
10
|
+
|
11
|
+
require 'rubygems'
|
12
|
+
require 'optparse'
|
13
|
+
require 'rjr'
|
14
|
+
|
15
|
+
######################
|
16
|
+
|
17
|
+
|
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
|
26
|
+
|
27
|
+
# parse cmd line
|
28
|
+
begin
|
29
|
+
opts.parse!(ARGV)
|
30
|
+
rescue OptionParser::InvalidOption
|
31
|
+
puts opts
|
32
|
+
exit
|
33
|
+
end
|
34
|
+
|
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
|
38
|
+
|
39
|
+
RJR::Dispatcher.add_handler('hello') { |msg|
|
40
|
+
#raise Exception.new("foobar")
|
41
|
+
puts "hello #{msg}"
|
42
|
+
"world"
|
43
|
+
}
|
44
|
+
|
45
|
+
rjr_node = RJR::MultiNode.new :nodes => [amqp_node, ws_node, www_node]
|
46
|
+
|
47
|
+
rjr_node.listen
|
48
|
+
rjr_node.terminate # TODO run in signal handler
|
49
|
+
end
|
50
|
+
|
51
|
+
main()
|
@@ -0,0 +1,164 @@
|
|
1
|
+
# RJR AMQP Endpoint
|
2
|
+
#
|
3
|
+
# Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
|
4
|
+
# Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
|
5
|
+
|
6
|
+
# establish client connection w/ specified args and invoke block w/
|
7
|
+
# newly created client, returning it after block terminates
|
8
|
+
|
9
|
+
require 'amqp'
|
10
|
+
require 'thread'
|
11
|
+
require 'rjr/node'
|
12
|
+
require 'rjr/message'
|
13
|
+
|
14
|
+
module RJR
|
15
|
+
|
16
|
+
# AMQP client node callback interface,
|
17
|
+
# send data back to client via AMQP.
|
18
|
+
class AMQPNodeCallback
|
19
|
+
def initialize(args = {})
|
20
|
+
@exchange = args[:exchange]
|
21
|
+
@exchange_lock = args[:exchange_lock]
|
22
|
+
@destination = args[:destination]
|
23
|
+
@message_headers = args[:headers]
|
24
|
+
@disconnected = false
|
25
|
+
|
26
|
+
@exchange_lock.synchronize{
|
27
|
+
# FIXME should disconnect all callbacks on_return
|
28
|
+
@exchange.on_return do |basic_return, metadata, payload|
|
29
|
+
puts "#{payload} was returned! reply_code = #{basic_return.reply_code}, reply_text = #{basic_return.reply_text}"
|
30
|
+
@disconnected = true
|
31
|
+
end
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def invoke(callback_method, *data)
|
36
|
+
msg = RequestMessage.new :method => callback_method, :args => data, :headers => @message_headers
|
37
|
+
raise RJR::Errors::ConnectionError.new("client unreachable") if @disconnected
|
38
|
+
@exchange_lock.synchronize{
|
39
|
+
@exchange.publish(msg.to_s, :routing_key => @destination, :mandatory => true)
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# AMQP node definition, listen for and invoke json-rpc requests over AMQP
|
45
|
+
class AMQPNode < RJR::Node
|
46
|
+
RJR_NODE_TYPE = :amqp
|
47
|
+
|
48
|
+
|
49
|
+
private
|
50
|
+
def handle_message(metadata, msg)
|
51
|
+
if RequestMessage.is_request_message?(msg)
|
52
|
+
reply_to = metadata.reply_to
|
53
|
+
|
54
|
+
# TODO should delete handler threads as they complete & should handle timeout
|
55
|
+
@thread_pool << ThreadPoolJob.new { handle_request(reply_to, msg) }
|
56
|
+
|
57
|
+
elsif ResponseMessage.is_response_message?(msg)
|
58
|
+
# TODO test message, make sure it is a response message
|
59
|
+
msg = ResponseMessage.new(:message => msg, :headers => @message_headers)
|
60
|
+
lock = @message_locks[msg.msg_id]
|
61
|
+
if lock
|
62
|
+
headers = @message_headers.merge(msg.headers)
|
63
|
+
res = Dispatcher.handle_response(msg.result)
|
64
|
+
lock << res
|
65
|
+
lock[0].synchronize { lock[1].signal }
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def handle_request(reply_to, message)
|
72
|
+
msg = RequestMessage.new(:message => message, :headers => @message_headers)
|
73
|
+
headers = @message_headers.merge(msg.headers) # append request message headers
|
74
|
+
result = Dispatcher.dispatch_request(msg.jr_method,
|
75
|
+
:method_args => msg.jr_args,
|
76
|
+
:headers => headers,
|
77
|
+
:rjr_node_id => @node_id,
|
78
|
+
:rjr_node_type => RJR_NODE_TYPE,
|
79
|
+
:rjr_callback =>
|
80
|
+
AMQPNodeCallback.new(:exchange => @exchange,
|
81
|
+
:exchange_lock => @exchange_lock,
|
82
|
+
:destination => reply_to,
|
83
|
+
:headers => headers))
|
84
|
+
response = ResponseMessage.new(:id => msg.msg_id, :result => result, :headers => headers)
|
85
|
+
@exchange_lock.synchronize{
|
86
|
+
@exchange.publish(response.to_s, :routing_key => reply_to)
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
public
|
91
|
+
|
92
|
+
# initialize the node w/ the specified params
|
93
|
+
def initialize(args = {})
|
94
|
+
super(args)
|
95
|
+
@broker = args[:broker]
|
96
|
+
|
97
|
+
# tuple of message ids to locks/condition variables for the responses
|
98
|
+
# of those messages with optional result response
|
99
|
+
@message_locks = {}
|
100
|
+
end
|
101
|
+
|
102
|
+
# Initialize the amqp subsystem
|
103
|
+
def init_node
|
104
|
+
@conn = AMQP.connect(:host => @broker)
|
105
|
+
@conn.on_tcp_connection_failure { puts "OTCF #{@node_id}" }
|
106
|
+
|
107
|
+
### connect to qpid broker
|
108
|
+
@channel = AMQP::Channel.new(@conn)
|
109
|
+
|
110
|
+
# qpid constructs that will be created for node
|
111
|
+
@queue_name = "#{@node_id.to_s}-queue"
|
112
|
+
@queue = @channel.queue(@queue_name, :auto_delete => true)
|
113
|
+
@exchange = @channel.default_exchange
|
114
|
+
@exchange_lock = Mutex.new
|
115
|
+
end
|
116
|
+
|
117
|
+
# Instruct Node to start listening for and dispatching rpc requests
|
118
|
+
def listen
|
119
|
+
em_run do
|
120
|
+
init_node
|
121
|
+
|
122
|
+
# start receiving messages
|
123
|
+
@queue.subscribe do |metadata, msg|
|
124
|
+
handle_message(metadata, msg)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Instructs node to send rpc request, and wait for / return response
|
130
|
+
def invoke_request(routing_key, rpc_method, *args)
|
131
|
+
req_mutex = Mutex.new
|
132
|
+
req_cv = ConditionVariable.new
|
133
|
+
|
134
|
+
message = RequestMessage.new :method => rpc_method,
|
135
|
+
:args => args,
|
136
|
+
:headers => @message_headers
|
137
|
+
em_run do
|
138
|
+
init_node
|
139
|
+
|
140
|
+
@message_locks[message.msg_id] = [req_mutex, req_cv]
|
141
|
+
|
142
|
+
# begin listening for result
|
143
|
+
@queue.subscribe do |metadata, msg|
|
144
|
+
handle_message(metadata, msg)
|
145
|
+
end
|
146
|
+
|
147
|
+
@exchange_lock.synchronize{
|
148
|
+
@exchange.publish(message.to_s, :routing_key => routing_key, :reply_to => @queue_name)
|
149
|
+
}
|
150
|
+
end
|
151
|
+
|
152
|
+
## wait for result
|
153
|
+
# TODO - make this optional, eg a non-blocking operation mode
|
154
|
+
# (allowing event handler registration to be run on success / fail / etc)
|
155
|
+
req_mutex.synchronize { req_cv.wait(req_mutex) }
|
156
|
+
result = @message_locks[message.msg_id][2]
|
157
|
+
@message_locks.delete(message.msg_id)
|
158
|
+
self.stop
|
159
|
+
self.join unless self.em_running?
|
160
|
+
return result
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
end
|
data/lib/rjr/common.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# RJR Utility Methods
|
2
|
+
#
|
3
|
+
# Copyright (C) 2011 Mohammed Morsi <mo@morsi.org>
|
4
|
+
# Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
|
5
|
+
|
6
|
+
require 'logger'
|
7
|
+
|
8
|
+
module RJR
|
9
|
+
|
10
|
+
# Logger helper class
|
11
|
+
class Logger
|
12
|
+
private
|
13
|
+
def self._instantiate_logger
|
14
|
+
unless defined? @@logger
|
15
|
+
@@logger = ::Logger.new(STDOUT)
|
16
|
+
@@logger.level = ::Logger::FATAL
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
public
|
21
|
+
|
22
|
+
def self.method_missing(method_id, *args)
|
23
|
+
_instantiate_logger
|
24
|
+
@@logger.send(method_id, args)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.logger
|
28
|
+
_instantiate_logger
|
29
|
+
@@logger
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.log_level=(level)
|
33
|
+
_instantiate_logger
|
34
|
+
@@logger.level = level
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end # module RJR
|
39
|
+
|
40
|
+
# http://blog.jayfields.com/2006/09/ruby-instanceexec-aka-instanceeval.html
|
41
|
+
class Object
|
42
|
+
module InstanceExecHelper; end
|
43
|
+
include InstanceExecHelper
|
44
|
+
def instance_exec(*args, &block)
|
45
|
+
begin
|
46
|
+
old_critical, Thread.critical = Thread.critical, true
|
47
|
+
n = 0
|
48
|
+
n += 1 while respond_to?(mname="__instance_exec#{n}")
|
49
|
+
InstanceExecHelper.module_eval{ define_method(mname, &block) }
|
50
|
+
ensure
|
51
|
+
Thread.critical = old_critical
|
52
|
+
end
|
53
|
+
begin
|
54
|
+
ret = send(mname, *args)
|
55
|
+
ensure
|
56
|
+
InstanceExecHelper.module_eval{ remove_method(mname) } rescue nil
|
57
|
+
end
|
58
|
+
ret
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# RJR Request / Response Dispatcher
|
2
|
+
#
|
3
|
+
# Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
|
4
|
+
# Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
|
5
|
+
|
6
|
+
# establish client connection w/ specified args and invoke block w/
|
7
|
+
# newly created client, returning it after block terminates
|
8
|
+
|
9
|
+
require 'rjr/common'
|
10
|
+
|
11
|
+
module RJR
|
12
|
+
|
13
|
+
class Request
|
14
|
+
attr_accessor :method
|
15
|
+
attr_accessor :method_args
|
16
|
+
attr_accessor :headers
|
17
|
+
attr_accessor :rjr_callback
|
18
|
+
attr_accessor :rjr_node_type
|
19
|
+
attr_accessor :rjr_node_id
|
20
|
+
|
21
|
+
attr_accessor :handler
|
22
|
+
|
23
|
+
def initialize(args = {})
|
24
|
+
@method = args[:method]
|
25
|
+
@method_args = args[:method_args]
|
26
|
+
@headers = args[:headers]
|
27
|
+
@rjr_callback = args[:rjr_callback]
|
28
|
+
@rjr_node_id = args[:rjr_node_id]
|
29
|
+
@rjr_node_type = args[:rjr_node_type]
|
30
|
+
@handler = args[:handler]
|
31
|
+
end
|
32
|
+
|
33
|
+
def handle
|
34
|
+
RJR::Logger.info "Dispatching '#{@method}' request with parameters (#{@method_args.join(',')}) on #{@rjr_node_type}-node(#{@rjr_node_id})"
|
35
|
+
retval = instance_exec(*@method_args, &@handler)
|
36
|
+
RJR::Logger.info "#{@method} request with parameters (#{@method_args.join(',')}) returning #{retval}"
|
37
|
+
return retval
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Result
|
42
|
+
attr_accessor :success
|
43
|
+
attr_accessor :failed
|
44
|
+
attr_accessor :result
|
45
|
+
attr_accessor :error_code
|
46
|
+
attr_accessor :error_msg
|
47
|
+
attr_accessor :error_class
|
48
|
+
|
49
|
+
def initialize(args = {})
|
50
|
+
@result = nil
|
51
|
+
@error_code = nil
|
52
|
+
@error_message = nil
|
53
|
+
@error_class = nil
|
54
|
+
|
55
|
+
if args.has_key?(:result)
|
56
|
+
@success = true
|
57
|
+
@failed = false
|
58
|
+
@result = args[:result]
|
59
|
+
|
60
|
+
elsif args.has_key?(:error_code)
|
61
|
+
@success = false
|
62
|
+
@failed = true
|
63
|
+
@error_code = args[:error_code]
|
64
|
+
@error_msg = args[:error_msg]
|
65
|
+
@error_class = args[:error_class]
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def ==(other)
|
71
|
+
@success == other.success &&
|
72
|
+
@failed == other.failed &&
|
73
|
+
@result == other.result &&
|
74
|
+
@error_code == other.error_code &&
|
75
|
+
@error_msg == other.error_msg &&
|
76
|
+
@error_class == other.error_class
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_s
|
80
|
+
"#{@success} #{@result} #{@error_code} #{@error_msg} #{@error_class}"
|
81
|
+
end
|
82
|
+
|
83
|
+
######### Specific request types
|
84
|
+
|
85
|
+
def self.invalid_request
|
86
|
+
return Result.new(:error_code => -32600,
|
87
|
+
:error_msg => ' Invalid Request')
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.method_not_found(name)
|
91
|
+
return Result.new(:error_code => -32602,
|
92
|
+
:error_msg => "Method '#{name}' not found")
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
class Handler
|
98
|
+
attr_accessor :method_name
|
99
|
+
attr_accessor :handler_proc
|
100
|
+
|
101
|
+
def initialize(args = {})
|
102
|
+
@method_name = args[:method]
|
103
|
+
@handler_proc = args[:handler]
|
104
|
+
end
|
105
|
+
|
106
|
+
def handle(args = {})
|
107
|
+
return Result.method_not_found(args[:missing_name]) if @method_name.nil?
|
108
|
+
|
109
|
+
begin
|
110
|
+
request = Request.new args.merge(:method => @method_name,
|
111
|
+
:handler => @handler_proc)
|
112
|
+
retval = request.handle
|
113
|
+
return Result.new(:result => retval)
|
114
|
+
|
115
|
+
rescue Exception => e
|
116
|
+
RJR::Logger.warn "Exception Raised in #{method_name} handler #{e}"
|
117
|
+
e.backtrace.each { |b| RJR::Logger.warn b }
|
118
|
+
# TODO store exception class to be raised later
|
119
|
+
|
120
|
+
return Result.new(:error_code => -32000,
|
121
|
+
:error_msg => e.to_s,
|
122
|
+
:error_class => e.class)
|
123
|
+
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class Dispatcher
|
129
|
+
# clear handlers
|
130
|
+
def self.init_handlers
|
131
|
+
@@handlers = {}
|
132
|
+
end
|
133
|
+
|
134
|
+
# register a handler to the specified method
|
135
|
+
def self.add_handler(method_name, args = {}, &handler)
|
136
|
+
@@handlers ||= {}
|
137
|
+
@@handlers[method_name] = Handler.new args.merge(:method => method_name,
|
138
|
+
:handler => handler)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Helper to handle request messages
|
142
|
+
def self.dispatch_request(method_name, args = {})
|
143
|
+
@@handlers ||= {}
|
144
|
+
handler = @@handlers[method_name]
|
145
|
+
|
146
|
+
if handler.nil?
|
147
|
+
@@generic_handler ||= Handler.new :method => nil
|
148
|
+
return @@generic_handler.handle(args.merge(:missing_name => method_name))
|
149
|
+
end
|
150
|
+
|
151
|
+
return handler.handle args
|
152
|
+
end
|
153
|
+
|
154
|
+
# Helper to handle response messages
|
155
|
+
def self.handle_response(result)
|
156
|
+
unless result.success
|
157
|
+
#if result.error_class
|
158
|
+
# TODO needs to be constantized first (see TODO in lib/rjr/message)
|
159
|
+
# raise result.error_class.new(result.error_msg) unless result.success
|
160
|
+
#else
|
161
|
+
raise Exception, result.error_msg
|
162
|
+
#end
|
163
|
+
end
|
164
|
+
return result.result
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
end # module RJR
|
data/lib/rjr/errors.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# RJR Errors
|
2
|
+
#
|
3
|
+
# Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
|
4
|
+
# Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
|
5
|
+
|
6
|
+
# establish client connection w/ specified args and invoke block w/
|
7
|
+
# newly created client, returning it after block terminates
|
8
|
+
|
9
|
+
module RJR
|
10
|
+
module Errors
|
11
|
+
|
12
|
+
def self.const_missing(error_name) # :nodoc:
|
13
|
+
if error_name.to_s =~ /Error\z/
|
14
|
+
const_set(error_name, Class.new(RuntimeError))
|
15
|
+
else
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# RJR Local Endpoint
|
2
|
+
#
|
3
|
+
# Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
|
4
|
+
# Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
|
5
|
+
|
6
|
+
# establish client connection w/ specified args and invoke block w/
|
7
|
+
# newly created client, returning it after block terminates
|
8
|
+
|
9
|
+
require 'rjr/node'
|
10
|
+
require 'rjr/message'
|
11
|
+
|
12
|
+
module RJR
|
13
|
+
|
14
|
+
# Local client node callback interface,
|
15
|
+
# send data back to client via local handlers
|
16
|
+
class LocalNodeCallback
|
17
|
+
def initialize(args = {})
|
18
|
+
@node = args[:node]
|
19
|
+
end
|
20
|
+
|
21
|
+
def invoke(callback_method, *data)
|
22
|
+
@node.invoke_request(callback_method, *data)
|
23
|
+
# TODO support local_node 'disconnections'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Local node definition, listen for and invoke json-rpc
|
28
|
+
# requests via local handlers
|
29
|
+
class LocalNode < RJR::Node
|
30
|
+
RJR_NODE_TYPE = :local
|
31
|
+
|
32
|
+
# allow clients to override the node type for the local node
|
33
|
+
attr_accessor :node_type
|
34
|
+
|
35
|
+
# initialize the node w/ the specified params
|
36
|
+
def initialize(args = {})
|
37
|
+
super(args)
|
38
|
+
@node_type = RJR_NODE_TYPE
|
39
|
+
end
|
40
|
+
|
41
|
+
# Instruct Node to start listening for and dispatching rpc requests
|
42
|
+
def listen
|
43
|
+
em_run do
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Instructs node to send rpc request, and wait for / return response
|
48
|
+
def invoke_request(rpc_method, *args)
|
49
|
+
0.upto(args.size).each { |i| args[i] = args[i].to_s if args[i].is_a?(Symbol) }
|
50
|
+
message = RequestMessage.new :method => rpc_method,
|
51
|
+
:args => args,
|
52
|
+
:headers => @message_headers
|
53
|
+
result = Dispatcher.dispatch_request(rpc_method,
|
54
|
+
:method_args => args,
|
55
|
+
:headers => @message_headers,
|
56
|
+
:rjr_node_id => @node_id,
|
57
|
+
:rjr_node_type => @node_type,
|
58
|
+
:rjr_callback =>
|
59
|
+
LocalNodeCallback.new(:node => self,
|
60
|
+
:headers => @message_headers))
|
61
|
+
return Dispatcher.handle_response(result)
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|