rjr 0.5.3
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/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
|