arachni-rpc-em 0.1

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.
@@ -0,0 +1,113 @@
1
+ =begin
2
+ Arachni-RPC
3
+ Copyright (c) 2011 Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
4
+
5
+ This is free software; you can copy and distribute and modify
6
+ this program under the term of the GPL v2.0 License
7
+ (See LICENSE file for details)
8
+
9
+ =end
10
+
11
+ # require 'arachni/rpc'
12
+ require File.join( File.expand_path( File.dirname( __FILE__ ) ), '../lib/arachni/rpc/', 'em' )
13
+
14
+ #
15
+ # You don't *need* to stick the whole thing inside an ::EM.run block,
16
+ # the system will manage EM on its own...however you *can* if you want to
17
+ # or if you're working inside a framework that already runs EventMachine.
18
+ ::EM.run do
19
+
20
+ # connect to the server
21
+ client = Arachni::RPC::EM::Client.new(
22
+ :host => 'localhost',
23
+ :port => 7332,
24
+
25
+ # optional authentication token, if it doesn't match the one
26
+ # set on the server-side you'll be getting exceptions.
27
+ :token => 'superdupersecret',
28
+
29
+ # :keep_alive => false,
30
+
31
+ # optional serializer (defaults to YAML)
32
+ # see the 'serializer' method at:
33
+ # http://eventmachine.rubyforge.org/EventMachine/Protocols/ObjectProtocol.html#M000369
34
+ :serializer => Marshal
35
+ )
36
+
37
+ bench = Arachni::RPC::RemoteObjectMapper.new( client, 'bench' )
38
+
39
+ #
40
+ # There's one downside though, if you want to run this thing inside an
41
+ # ::EM.run block: you'll have to wrap all sync calls in a ::Arachni::RPC::EM::EM::Synchrony.run block.
42
+ #
43
+ # Like so:
44
+ ::Arachni::RPC::EM::Synchrony.run do
45
+ p bench.foo( 'First sync call in individual Synchrony block.' )
46
+ # => "First sync call in individual Synchrony block."
47
+ end
48
+
49
+ # you can use it again individually
50
+ ::Arachni::RPC::EM::Synchrony.run do
51
+ p bench.foo( 'Second sync call in individual Synchrony block.' )
52
+ # => "Second sync call in individual Synchrony block."
53
+ end
54
+
55
+ # or just wrap lots of calls in it
56
+ ::Arachni::RPC::EM::Synchrony.run do
57
+ p bench.foo( 'Third sync call in individual Synchrony block.' )
58
+ # => "Third sync call in individual Synchrony block."
59
+
60
+ p bench.foo( '--> And this one is in the same block as well.' )
61
+ # => "--> And this one is in the same block as well."
62
+
63
+ p bench.async_foo( 'This is a sync call to an async remote method.' )
64
+ # => "This is a sync call to an async remote method."
65
+ end
66
+
67
+ # async calls are the same
68
+ bench.foo( 'This is an async call... business as usual. :)' ) {
69
+ |res|
70
+ p res
71
+ # => "This is an async call... business as usual. :)"
72
+ }
73
+
74
+ bench.async_foo( 'This is an async call to an async remote method.' ) {
75
+ |res|
76
+ p res
77
+ # => "This is an async call to an async remote method."
78
+ }
79
+
80
+
81
+ #
82
+ # The system uses 2 methods to make calls appear sync:
83
+ # - Threads (when the code is *not* run directly inside the Reactor's thread, see example.rb)
84
+ # - Fibers (when the code *is* run inside the Reactor's thread, like right here)
85
+ #
86
+ # For performance reasons callbacks are ::EM.defer'ed.
87
+ #
88
+ # This means that they're already in their own thread so you don't need
89
+ # ::Arachni::RPC::EM::EM::Synchrony.run to perform sync calls from inside callbacks.
90
+ #
91
+ bench.async_foo( 'Coo-coo' ) {
92
+ |res|
93
+
94
+ p res
95
+ # => "Coo-coo"
96
+
97
+ p bench.async_foo( 'Coo-coo 2' )
98
+ # => "Coo-coo 2"
99
+
100
+ bench.async_foo( 'Coo-coo 3' ) {
101
+ |res|
102
+
103
+ p res
104
+ # => "Coo-coo 3"
105
+
106
+ p bench.foo( 'Coo-coo 4' )
107
+ # => "Coo-coo 4"
108
+ }
109
+
110
+ }
111
+
112
+
113
+ end
@@ -0,0 +1,181 @@
1
+ =begin
2
+ Arachni-RPC
3
+ Copyright (c) 2011 Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
4
+
5
+ This is free software; you can copy and distribute and modify
6
+ this program under the term of the GPL v2.0 License
7
+ (See LICENSE file for details)
8
+
9
+ =end
10
+
11
+ cwd = File.expand_path( File.dirname( __FILE__ ) )
12
+ require File.join( cwd, '../lib/arachni/rpc/', 'em' )
13
+
14
+
15
+ # connect to the server
16
+ client = Arachni::RPC::EM::Client.new(
17
+ :host => 'localhost',
18
+ :port => 7332,
19
+
20
+ # optional authentication token, if it doesn't match the one
21
+ # set on the server-side you'll be getting exceptions.
22
+ :token => 'superdupersecret',
23
+
24
+ # optional serializer (defaults to YAML)
25
+ # see the 'serializer' method at:
26
+ # http://eventmachine.rubyforge.org/EventMachine/Protocols/ObjectProtocol.html#M000369
27
+ :serializer => Marshal,
28
+
29
+ #
30
+ # Connection keep alive is set to true by default, this means that
31
+ # a single connection will be maintained and all calls will pass
32
+ # through it.
33
+ # This bypasses a bug in EventMachine and allows you to perform thousands
34
+ # of calls without issue.
35
+ #
36
+ # However, you are responsible for closing the connection when you're done.
37
+ #
38
+ # If keep alive is set to false then each call will go through its own connection
39
+ # and the responsibility for closing that connection falls on Arachni-RPC.
40
+ #
41
+ # Unfortunately, if you try to make a greater number of calls than your system's
42
+ # maximum open file descriptors limit EventMachine will freak-out.
43
+ #
44
+ :keep_alive => false,
45
+
46
+ #
47
+ # In order to enable peer verification one must first provide
48
+ # the following:
49
+ #
50
+ # SSL CA certificate
51
+ # :ssl_ca => cwd + '/../spec/pems/cacert.pem',
52
+ # SSL private key
53
+ # :ssl_pkey => cwd + '/../spec/pems/client/key.pem',
54
+ # SSL certificate
55
+ # :ssl_cert => cwd + '/../spec/pems/client/cert.pem'
56
+ )
57
+
58
+ # Make things easy on the eyes using the mapper, it allows you to do this:
59
+ #
60
+ # res = bench.foo( arg )
61
+ #
62
+ # Instead of:
63
+ #
64
+ # res = client.call( 'bench.foo', arg )
65
+ #
66
+ bench = Arachni::RPC::RemoteObjectMapper.new( client, 'bench' )
67
+
68
+ #
69
+ # In order to perform an asynchronous call you will need to provide a block,
70
+ # even if it is an empty one.
71
+ #
72
+ bench.foo( 'This is an async call to "bench.foo".' ) {
73
+ |res|
74
+
75
+ p res
76
+ # => "This is an async call to \"bench.foo\"."
77
+
78
+ # did something RPC related go wrong?
79
+ # p res.rpc_exception?
80
+ # => false
81
+
82
+ # did something go wrong on the server-side?
83
+ # p res.rpc_remote_exception?
84
+ # => false
85
+
86
+ # did the connection die abruptly?
87
+ # p res.rpc_connection_error?
88
+ # => false
89
+
90
+ # did we call an object for which there is no handler on the server-side?
91
+ # p res.rpc_invalid_object_error?
92
+ # => false
93
+
94
+ # did we call a server-side method that isn't existent or public?
95
+ # p res.rpc_invalid_method_error?
96
+ # => false
97
+
98
+ # was there an authentication token mismatch?
99
+ # p res.rpc_invalid_token_error?
100
+ # => false
101
+ }
102
+
103
+
104
+
105
+ #
106
+ # On the server-side this is an async method but works just like everything else here.
107
+ #
108
+ # You'll need to kind-of specify the async methods on the server-side,
109
+ # check the server example file for more info.
110
+ #
111
+ bench.async_foo( 'This is an async call to "bench.async_foo".' ) {
112
+ |res|
113
+ p res
114
+ # => "This is an async call to \"bench.async_foo\"."
115
+ }
116
+
117
+ p bench.async_foo( 'This is a sync call to "bench.async_foo".' )
118
+ # => "This is a sync call to \"bench.async_foo\"."
119
+
120
+
121
+ #
122
+ # To perform a sync (blocking) call do the usual stuff.
123
+ #
124
+ # This is thread safe so if you'd rather use Threads instead of async calls
125
+ # for that extra performance kick you go ahead and do your thing now...
126
+ #
127
+ p bench.foo( 'This is a sync call to "bench.foo".' )
128
+ # => "This is a sync call to \"bench.foo\"."
129
+
130
+
131
+ #
132
+ # When you are performing a synchronous call and things go wrong
133
+ # an exception will be thrown.
134
+ #
135
+ # Exceptions on the server-side unrelated to the RPC system will be forwarded.
136
+ #
137
+
138
+ #
139
+ # Non-existent object.
140
+ #
141
+ blah = Arachni::RPC::RemoteObjectMapper.new( client, 'blah' )
142
+ begin
143
+ p blah.something
144
+ rescue Exception => e
145
+ p e # => #<Arachni::RPC::EM::Exceptions::InvalidObject: Trying to access non-existent object 'blah'.>
146
+ end
147
+
148
+ #
149
+ # Non-existent or non-public method.
150
+ #
151
+ begin
152
+ p bench.fdoo
153
+ rescue Exception => e
154
+ p e # => #<Arachni::RPC::EM::Exceptions::InvalidMethod: Trying to access non-public method 'fdoo'.>
155
+ end
156
+
157
+ #
158
+ # When you are performing an asynchronous call and things go wrong
159
+ # an exception will be returned.
160
+ #
161
+ # It will *NOT* be thrown!
162
+ # It will be *RETURNED*!
163
+ #
164
+ blah.something {
165
+ |res|
166
+ p res # => #<Arachni::RPC::EM::Exceptions::InvalidObject: Trying to access non-existent object 'blah'.>
167
+
168
+ # RPC Exception helper methods have been added to all Ruby objects (except BasicObject)
169
+ # so they'll always be there when you need them.
170
+
171
+ # p res.rpc_exception? # => true
172
+ # p res.rpc_invalid_object_error? # => true
173
+ }
174
+
175
+ #
176
+ # We don't know when async calls will return so we wait forever.
177
+ #
178
+ # Call ::EM.stop to break-out.
179
+ #
180
+ Arachni::RPC::EM.block!
181
+
@@ -0,0 +1,80 @@
1
+ =begin
2
+ Arachni-RPC
3
+ Copyright (c) 2011 Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
4
+
5
+ This is free software; you can copy and distribute and modify
6
+ this program under the term of the GPL v2.0 License
7
+ (See LICENSE file for details)
8
+
9
+ =end
10
+
11
+ cwd = File.expand_path( File.dirname( __FILE__ ) )
12
+ require File.join( cwd, '../lib/arachni/rpc/', 'em' )
13
+
14
+ class Parent
15
+ def foo( arg )
16
+ return arg
17
+ end
18
+ end
19
+
20
+ class Bench < Parent
21
+
22
+ # in order to make inherited methods accessible you've got to explicitly
23
+ # make them public
24
+ private :foo
25
+ public :foo
26
+
27
+ #
28
+ # Uses EventMachine to call the block asynchronously
29
+ #
30
+ def async_foo( arg, &block )
31
+ ::EM.schedule {
32
+ ::EM.defer {
33
+ block.call( arg ) if block_given?
34
+ }
35
+ }
36
+ end
37
+
38
+ end
39
+
40
+ server = Arachni::RPC::EM::Server.new(
41
+ :host => 'localhost',
42
+ :port => 7332,
43
+
44
+ # optional authentication token, if it doesn't match the one
45
+ # set on the client-side the client won't be able to do anything
46
+ # and keep getting exceptions.
47
+ :token => 'superdupersecret',
48
+
49
+ # optional serializer (defaults to YAML)
50
+ # see the 'serializer' method at:
51
+ # http://eventmachine.rubyforge.org/EventMachine/Protocols/ObjectProtocol.html#M000369
52
+ :serializer => Marshal,
53
+
54
+ # :ssl_ca => cwd + '/../spec/pems/cacert.pem',
55
+ # :ssl_pkey => cwd + '/../spec/pems/server/key.pem',
56
+ # :ssl_cert => cwd + '/../spec/pems/server/cert.pem'
57
+ )
58
+
59
+ #
60
+ # This is a way for you to identify methods that pass their result to a block
61
+ # instead of simply returning them (which is the most usual operation of async methods.
62
+ #
63
+ # So no need to change your coding convetions to fit the RPC stuff,
64
+ # you can just decide dynamically based on a plethora of data which Ruby provides
65
+ # by its 'Method' class.
66
+ #
67
+ server.add_async_check {
68
+ |method|
69
+ #
70
+ # Must return 'true' for async and 'false' for sync.
71
+ #
72
+ # Very simple check here...
73
+ #
74
+ 'async' == method.name.to_s.split( '_' )[0]
75
+ }
76
+
77
+ server.add_handler( 'bench', Bench.new )
78
+
79
+ # this will block forever, call server.shutdown to kill the server.
80
+ server.run
@@ -0,0 +1,26 @@
1
+ =begin
2
+ Arachni
3
+ Copyright (c) 2010-2011 Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
4
+
5
+ This is free software; you can copy and distribute and modify
6
+ this program under the term of the GPL v2.0 License
7
+ (See LICENSE file for details)
8
+
9
+ =end
10
+
11
+ require 'eventmachine'
12
+ require 'socket'
13
+ require 'logger'
14
+ require 'fiber'
15
+
16
+ require 'arachni/rpc'
17
+
18
+ require 'yaml'
19
+ YAML::ENGINE.yamler = 'syck'
20
+
21
+ require File.join( File.expand_path( File.dirname( __FILE__ ) ), 'em', 'connection_utilities' )
22
+ require File.join( File.expand_path( File.dirname( __FILE__ ) ), 'em', 'ssl' )
23
+ require File.join( File.expand_path( File.dirname( __FILE__ ) ), 'em', 'protocol' )
24
+ require File.join( File.expand_path( File.dirname( __FILE__ ) ), 'em', 'server' )
25
+ require File.join( File.expand_path( File.dirname( __FILE__ ) ), 'em', 'client' )
26
+ require File.join( File.expand_path( File.dirname( __FILE__ ) ), 'em', 'em' )
@@ -0,0 +1,280 @@
1
+ =begin
2
+ Arachni-RPC
3
+ Copyright (c) 2011 Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
4
+
5
+ This is free software; you can copy and distribute and modify
6
+ this program under the term of the GPL v2.0 License
7
+ (See LICENSE file for details)
8
+
9
+ =end
10
+
11
+ module Arachni
12
+ module RPC
13
+ module EM
14
+
15
+ #
16
+ # Simple EventMachine-based RPC client.
17
+ #
18
+ # It's capable of:
19
+ # - performing and handling a few thousands requests per second (depending on call size, network conditions and the like)
20
+ # - TLS encryption
21
+ # - asynchronous and synchronous requests
22
+ # - handling remote asynchronous calls that require a block
23
+ #
24
+ # @author: Tasos "Zapotek" Laskos
25
+ # <tasos.laskos@gmail.com>
26
+ # <zapotek@segfault.gr>
27
+ # @version: 0.1
28
+ #
29
+ class Client
30
+
31
+ include ::Arachni::RPC::Exceptions
32
+
33
+ #
34
+ # Handles EventMachine's connection and RPC related stuff.
35
+ #
36
+ # It's responsible for TLS, storing and calling callbacks as well as
37
+ # serializing, transmitting and receiving objects.
38
+ #
39
+ # @author: Tasos "Zapotek" Laskos
40
+ # <tasos.laskos@gmail.com>
41
+ # <zapotek@segfault.gr>
42
+ # @version: 0.1
43
+ #
44
+ class Handler < EventMachine::Connection
45
+ include ::Arachni::RPC::EM::Protocol
46
+ include ::Arachni::RPC::EM::ConnectionUtilities
47
+
48
+ def initialize( opts )
49
+ @opts = opts
50
+ @status = :idle
51
+
52
+ @request = nil
53
+ assume_client_role!
54
+ end
55
+
56
+ def post_init
57
+ @status = :active
58
+ start_ssl
59
+ end
60
+
61
+ def unbind( reason )
62
+ end_ssl
63
+
64
+ if @request && @request.callback && @status != :done
65
+ e = Arachni::RPC::Exceptions::ConnectionError.new( "Connection closed [#{reason}]" )
66
+ @request.callback.call( e )
67
+ end
68
+
69
+ @status = :closed
70
+ end
71
+
72
+ def connection_completed
73
+ @status = :established
74
+ end
75
+
76
+ def status
77
+ @status
78
+ end
79
+
80
+ #
81
+ # Used to handle responses.
82
+ #
83
+ # @param [Arachni::RPC::EM::Response] res
84
+ #
85
+ def receive_response( res )
86
+
87
+ if exception?( res )
88
+ res.obj = Arachni::RPC::Exceptions.from_response( res )
89
+ end
90
+
91
+ if cb = @request.callback
92
+
93
+ callback = Proc.new {
94
+ |obj|
95
+ cb.call( obj )
96
+
97
+ @status = :done
98
+ close_connection
99
+ }
100
+
101
+ if @request.defer?
102
+ # the callback might block a bit so tell EM to put it in a thread
103
+ ::EM.defer {
104
+ callback.call( res.obj )
105
+ }
106
+ else
107
+ callback.call( res.obj )
108
+ end
109
+ end
110
+ end
111
+
112
+ # @param [Arachni::RPC::EM::Response] res
113
+ def exception?( res )
114
+ res.obj.is_a?( Hash ) && res.obj['exception'] ? true : false
115
+ end
116
+
117
+ #
118
+ # Sends the request.
119
+ #
120
+ # @param [Arachni::RPC::EM::Request] req
121
+ #
122
+ def send_request( req )
123
+ @status = :pending
124
+ @request = req
125
+ super( req )
126
+ end
127
+ end
128
+
129
+ #
130
+ # Options hash
131
+ #
132
+ # @return [Hash]
133
+ #
134
+ attr_reader :opts
135
+
136
+ #
137
+ # Starts EventMachine and connects to the remote server.
138
+ #
139
+ # opts example:
140
+ #
141
+ # {
142
+ # :host => 'localhost',
143
+ # :port => 7331,
144
+ #
145
+ # # optional authentication token, if it doesn't match the one
146
+ # # set on the server-side you'll be getting exceptions.
147
+ # :token => 'superdupersecret',
148
+ #
149
+ # # optional serializer (defaults to YAML)
150
+ # # see the 'serializer' method at:
151
+ # # http://eventmachine.rubyforge.org/EventMachine/Protocols/ObjectProtocol.html#M000369
152
+ # :serializer => Marshal,
153
+ #
154
+ # #
155
+ # # In order to enable peer verification one must first provide
156
+ # # the following:
157
+ # #
158
+ # # SSL CA certificate
159
+ # :ssl_ca => cwd + '/../spec/pems/cacert.pem',
160
+ # # SSL private key
161
+ # :ssl_pkey => cwd + '/../spec/pems/client/key.pem',
162
+ # # SSL certificate
163
+ # :ssl_cert => cwd + '/../spec/pems/client/cert.pem'
164
+ # }
165
+ #
166
+ # @param [Hash] opts
167
+ #
168
+ def initialize( opts )
169
+
170
+ begin
171
+ @opts = opts.merge( :role => :client )
172
+ @token = @opts[:token]
173
+
174
+ @host, @port = @opts[:host], @opts[:port]
175
+
176
+ Arachni::RPC::EM.ensure_em_running!
177
+ rescue EventMachine::ConnectionError => e
178
+ exc = ConnectionError.new( e.to_s + " for '#{@k}'." )
179
+ exc.set_backtrace( e.backtrace )
180
+ raise exc
181
+ end
182
+ end
183
+
184
+ #
185
+ # Calls a remote method and grabs the result.
186
+ #
187
+ # There are 2 ways to perform a call, async (non-blocking) and sync (blocking).
188
+ #
189
+ # To perform an async call you need to provide a block which will be passed
190
+ # the return value once the method has finished executing.
191
+ #
192
+ # server.call( 'handler.method', arg1, arg2 ){
193
+ # |res|
194
+ # do_stuff( res )
195
+ # }
196
+ #
197
+ #
198
+ # To perform a sync (blocking) call do not pass a block, the value will be
199
+ # returned as usual.
200
+ #
201
+ # res = server.call( 'handler.method', arg1, arg2 )
202
+ #
203
+ # @param [String] msg in the form of <i>handler.method</i>
204
+ # @param [Array] args collection of argumenta to be passed to the method
205
+ # @param [Proc] &block
206
+ #
207
+ def call( msg, *args, &block )
208
+
209
+ req = Request.new(
210
+ :message => msg,
211
+ :args => args,
212
+ :callback => block,
213
+ :token => @token
214
+ )
215
+
216
+ if block_given?
217
+ call_async( req )
218
+ else
219
+ return call_sync( req )
220
+ end
221
+ end
222
+
223
+ private
224
+
225
+ def connect
226
+ ::EM.connect( @host, @port, Handler, @opts )
227
+ end
228
+
229
+ def call_async( req, &block )
230
+ ::EM.schedule {
231
+ req.callback = block if block_given?
232
+ connect.send_request( req )
233
+ }
234
+ end
235
+
236
+ def call_sync( req )
237
+
238
+ ret = nil
239
+ # if we're in the Reactor thread use a Fiber and if we're not
240
+ # use a Thread
241
+ if !::EM::reactor_thread?
242
+ t = Thread.current
243
+ call_async( req ) {
244
+ |obj|
245
+ t.wakeup
246
+ ret = obj
247
+ }
248
+ sleep
249
+ else
250
+ # Fibers do not work across threads so don't defer the callback
251
+ # once the Handler gets to it
252
+ req.do_not_defer!
253
+
254
+ f = Fiber.current
255
+ call_async( req ) {
256
+ |obj|
257
+ f.resume( obj )
258
+ }
259
+
260
+ begin
261
+ ret = Fiber.yield
262
+ rescue FiberError => e
263
+ msg = e.to_s + "\n"
264
+ msg += '(Consider wrapping your sync code in a' +
265
+ ' "::Arachni::RPC::EM::Synchrony.run" ' +
266
+ 'block when your app is running inside the Reactor\'s thread)'
267
+
268
+ raise( msg )
269
+ end
270
+ end
271
+
272
+ raise ret if ret.is_a?( Exception )
273
+ return ret
274
+ end
275
+
276
+ end
277
+
278
+ end
279
+ end
280
+ end