simplerpc 0.2.0c → 0.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fa356f3746316c99d38412a6b6442338df636c06
4
- data.tar.gz: 6113c09fea5d80ccc07f1c740f1be02a46d21ba1
3
+ metadata.gz: fe0eb8578d0b1f0f41f18696580bf8b8bb26439d
4
+ data.tar.gz: ab97b64f7519ea0b18e9f04c7b7fc52a6206917d
5
5
  SHA512:
6
- metadata.gz: 5caa01c23100bc3cf6d118668ef6ece8bb1452bc86f4f5ae0d7abfc70aed4cac0f80891b30ab623b3a021cfbf7e9b8625c6f6b3fa3eaeb291828a5cc2eeff07f
7
- data.tar.gz: 33830456c160bfe693bb02b79577d001c23ddab85e0244bf49d476c2270a2b5c3db224e4e804197a42ae53b8fb68716424710cdd28b96a63083ebe68ceedd92e
6
+ metadata.gz: 0152af8e2cff5b277fdb40f02fc18732b14c6a249e33840587a682fc9a7ae9a5867301394ec049eacbed133f321f3d1a1877d982dddf7f9071839c91c5ceed6d
7
+ data.tar.gz: 6d5ba75c705eb414ae87482f144f638001623b7d6250aa9dec2ee3addc156e862f58ea200ec1d376d818dc41da9374578991d03330016cffd546a5617b591508
@@ -1,12 +1,26 @@
1
1
  require 'socket' # Sockets are in standard library
2
2
  require 'simplerpc/socket_protocol'
3
3
 
4
- module SimpleRPC
4
+ # rubocop:disable LineLength
5
5
 
6
- # Exception thrown when the client fails to connect.
6
+
7
+
8
+ module SimpleRPC
9
+
10
+ # Exception thrown when the client fails to connect.
7
11
  class AuthenticationError < StandardError
8
12
  end
9
13
 
14
+ # Thrown when the server raises an exception.
15
+ #
16
+ # The message is set to the server's exception class.
17
+ class RemoteException < Exception
18
+ end
19
+
20
+ # The superclass of a proxy object
21
+ class RemoteObject < BasicObject
22
+ end
23
+
10
24
  # The SimpleRPC client connects to a server, either persistently on on-demand, and makes
11
25
  # calls to its proxy object.
12
26
  #
@@ -14,13 +28,28 @@ module SimpleRPC
14
28
  # object, i.e.:
15
29
  #
16
30
  # require 'simplerpc/client'
31
+ #
32
+ # # Connect
17
33
  # c = SimpleRPC::Client.new(:hostname => '127.0.0.1', :port => 27045)
18
- # c.length # 2
19
- # c.call(:dup) # ["thing", "thing2"]
20
- # c.close
21
- #
34
+ #
35
+ # # Make some calls directly
36
+ # c.length # 2
37
+ # c.call(:dup) # ["thing", "thing2"]
38
+ # c.call(:class) # Array
39
+ #
40
+ # # Get a proxy object
41
+ # p = c.get_proxy
42
+ # c.persist # always-on mode with 1 connection
43
+ # p.dup # ["thing", "thing2"]
44
+ # p.length # 2
45
+ # p.class # Array
46
+ # p.each{|x| puts x} # outputs "thing\nthing2\n"
47
+ #
48
+ # # Disconnect from always-on mode
49
+ # c.disconnect
50
+ #
22
51
  # == Making Requests
23
- #
52
+ #
24
53
  # Requests can be made on the client object as if it were local, and these will be
25
54
  # proxied to the server. For methods that are clobbered locally (for example '.class',
26
55
  # which will return 'SimpleRPC::Client', you may use #call to send this without local
@@ -29,18 +58,56 @@ module SimpleRPC
29
58
  # c.class # SimpleRPC::Client
30
59
  # c.call(:class) # Array
31
60
  #
32
- # The client will throw network errors upon failure, (ERRno::... ), so be sure to catch
33
- # these in your application.
61
+ # === Proxy Objects
62
+ #
63
+ # Calling #get_proxy will return a dynamically-constructed object that lacks any methods
64
+ # other than remote ones---this means it will be almost indistinguishable from a local
65
+ # object:
66
+ #
67
+ # c.class # Array
68
+ # c.dup # ['thing', 'thing2']
69
+ #
70
+ # This is an exceptionally seamless way of interacting, but you must retain the original
71
+ # client connection in order to call Client#disconnect or use always-on mode.
72
+ #
73
+ # == Blocks
74
+ #
75
+ # Blocks are supported and run on the client-side. A server object may yield any
76
+ # number of times. Note that if the client is single-threaded, it is not possible
77
+ # to call further calls when inside the block (if :threading is on this is perfectly
78
+ # acceptable).
79
+ #
80
+ # == Exceptions
81
+ #
82
+ # Remote exceptions fired by the server during a call are wrapped in RemoteException.
83
+ #
84
+ # Network errors are exposed directly. The server will not close a pipe during
85
+ # an operation, so if using connect-on-demand you should only observe
86
+ # Errno::ECONNREFUSED exceptions. If using a persistent connection pool,
87
+ # you will encounter either Errno::ECONNREFUSED, Errno::ECONNRESET or EOFError as
88
+ # the serialiser attempts to read from the closed socket.
89
+ #
90
+ # == Thread Safety
91
+ #
92
+ # Clients are thread-safe and will block when controlling the always-on connection
93
+ # with #persist and #close.
94
+ #
95
+ # If :threaded is true, clients will support multiple connections to the server. If
96
+ # used in always-on mode, this means it will maintain one re-usable connection, and only
97
+ # spawn new ones if requested.
34
98
  #
35
99
  # == Modes
36
100
  #
37
- # It is possible to use the client in two modes: _always-on_ and _connect-on-demand_.
38
- # The former of these maintains a single socket to the server, and all requests are
39
- # sent over this. Call #connect and #disconnect to control this connection.
101
+ # It is possible to use the client in two modes: _always-on_ and _connect-on-demand_,
102
+ # controlled by calling #connect and #disconnect.
103
+ #
104
+ # Always-on mode maintains a pool of connections to the server, and requests
105
+ # are preferentially sent over these (note that if you have threading off, it makes
106
+ # no sense to allocate more than one entry in the pool)
40
107
  #
41
- # The latter establishes a connection when necessary. This mode is used whenever the
42
- # client is not connected, so is a fallback if always-on fails. There is a small
43
- # performance hit to reconnecting each time.
108
+ # connect-on-demand creates a connection when necessary. This mode is used whenever the
109
+ # client is not connected. There is a small performance hit to reconnecting each time,
110
+ # especially if you are using authentication.
44
111
  #
45
112
  # == Serialisation Formats
46
113
  #
@@ -50,7 +117,7 @@ module SimpleRPC
50
117
  # The serialiser also supports MessagePack (the msgpack gem), and this yields a small
51
118
  # performance increase at the expense of generality (restrictions on data type).
52
119
  #
53
- # Note that JSON and YAML, though they support reading and writing to sockets, do not
120
+ # Note that JSON and YAML, though they support reading and writing to sockets, do not
54
121
  # properly terminate their reads and cause the system to hang. These methods are
55
122
  # both slow and limited by comparison anyway, and algorithms needed to support their
56
123
  # use require relatively large memory usage. They may be supported in later versions.
@@ -67,7 +134,7 @@ module SimpleRPC
67
134
  # speed) so the results of using mismatched configurations are undefined.
68
135
  #
69
136
  # The auth process is simple and not particularly secure, but is designed to deter casual
70
- # connections and attacks. It uses a password that is sent encrypted against a salt sent
137
+ # connections and attacks. It uses a password that is sent encrypted against a salt sent
71
138
  # by the server to prevent replay attacks. If you want more reliable security, use an SSH tunnel.
72
139
  #
73
140
  # The performance impact of auth is small, and takes about the same time as a simple
@@ -75,9 +142,9 @@ module SimpleRPC
75
142
  #
76
143
  class Client
77
144
 
78
- attr_reader :hostname, :port
79
- attr_accessor :serialiser, :timeout, :fast_auth
80
- attr_writer :password, :secret
145
+ attr_reader :hostname, :port, :threaded
146
+ attr_accessor :serialiser, :timeout, :fast_auth
147
+ attr_writer :password, :secret
81
148
 
82
149
  # Create a new client for the network.
83
150
  # Takes an options hash, in which :port is required:
@@ -88,172 +155,330 @@ module SimpleRPC
88
155
  # I recommend using MessagePack if this is not fast enough
89
156
  # [:timeout] Socket timeout in seconds.
90
157
  # [:password] The password clients need to connect
91
- # [:secret] The encryption key used during password authentication.
158
+ # [:secret] The encryption key used during password authentication.
92
159
  # Should be some long random string that matches the server's.
93
160
  # [:fast_auth] Use a slightly faster auth system that is incapable of knowing if it has failed or not.
94
161
  # By default this is off.
162
+ # [:threaded] Support multiple connections to the server (default is on)
163
+ # If off, threaded requests will queue in the client.
95
164
  #
96
165
  def initialize(opts = {})
97
166
 
98
167
  # Connection details
99
168
  @hostname = opts[:hostname] || '127.0.0.1'
100
169
  @port = opts[:port]
101
- raise "Port required" if not @port
170
+ raise 'Port required' unless @port
102
171
  @timeout = opts[:timeout]
103
172
 
173
+ # Support multiple connections at once?
174
+ @threaded = !(opts[:threaded] == false)
175
+
104
176
  # Serialiser.
105
- @serialiser = opts[:serialiser] || Marshal
177
+ @serialiser = opts[:serialiser] || Marshal
106
178
 
107
179
  # Auth system
108
- if opts[:password] and opts[:secret] then
180
+ if opts[:password] && opts[:secret]
109
181
  require 'simplerpc/encryption'
110
182
  @password = opts[:password]
111
183
  @secret = opts[:secret]
112
-
184
+
113
185
  # Check for return from auth?
114
186
  @fast_auth = (opts[:fast_auth] == true)
115
187
  end
116
188
 
117
- @m = Mutex.new
189
+ # Threading uses @pool, single thread uses @s and @mutex
190
+ if @threaded
191
+ @pool_mutex = Mutex.new # Controls edits to the pool
192
+ @pool = {} # List of available sockets with
193
+ # accompanying mutices
194
+ else
195
+ @mutex = Mutex.new
196
+ @s = nil
197
+ end
118
198
  end
119
199
 
120
- # Connect to the server.
200
+ # Connect to the remote server and return two things:
201
+ #
202
+ # * A proxy object for communicating with the server
203
+ # * The client itself, for controlling the connection
121
204
  #
122
- # Returns true if connected, or false if not.
205
+ # All options are the same as #new
123
206
  #
124
- # Note that this is only needed if using the client in always-on mode.
125
- def connect
126
- @m.synchronize{
127
- _connect
128
- }
207
+ def self.new_proxy(opts = {})
208
+ client = self.new(opts)
209
+ proxy = client.get_proxy
210
+
211
+ return proxy, client
129
212
  end
130
213
 
131
- # Disconnect from the server.
132
- def close
133
- @m.synchronize{
134
- _disconnect
135
- }
214
+ # -------------------------------------------------------------------------
215
+ # Persistent connection management
216
+ #
217
+
218
+ # Tell the client how many connections to persist.
219
+ #
220
+ # If the client is single-threaded, this can either be 1 or 0.
221
+ # If the client is multi-threaded, it can be any positive integer
222
+ # value (or 0).
223
+ #
224
+ # #persist(0) is equivalent to #disconnect.
225
+ def persist(pool_size = 1)
226
+
227
+ # Check the pool size is positive
228
+ raise 'Invalid pool size requested' if pool_size < 0
229
+
230
+ # If not threaded, check pool size is valid and connect/disconnect
231
+ # single socket
232
+ unless @threaded
233
+ raise 'Threading is disabled: pool size must be 1' if pool_size > 1
234
+
235
+ # Set socket up
236
+ @mutex.synchronize do
237
+ if pool_size == 0
238
+ _disconnect(@s)
239
+ @s = nil
240
+ else
241
+ @s = _connect
242
+ end
243
+ end
244
+
245
+ return
246
+ end
247
+
248
+ # If threaded, create a pool of sockets instead
249
+ @pool_mutex.synchronize do
250
+
251
+ # Resize the pool
252
+ if pool_size > @pool.length
253
+
254
+ # Allocate more pool space by simply
255
+ # connecting more sockets
256
+ (pool_size - @pool.length).times { @pool[_connect] = Mutex.new }
257
+
258
+ else
259
+
260
+ # remove from the pool by trying to remove available
261
+ # sockets over and over until they are gone.
262
+ #
263
+ # This has the effect of waiting for clients to be done
264
+ # with the socket, without hanging on any one mutex.
265
+ while @pool.length > pool_size do
266
+
267
+ # Go through and remove from the pool if unused.
268
+ @pool.each do |s, m|
269
+ if @pool.length > pool_size && m.try_lock
270
+ _disconnect(s)
271
+ @pool.delete(s)
272
+ end
273
+ end
274
+
275
+ # Since we're spinning, delay for a while
276
+ sleep(0.05)
277
+ end
278
+ end
279
+ end
136
280
  end
137
281
 
138
- # Alias for close
139
- alias :disconnect :close
282
+ # Close all persistent connections to the server.
283
+ def disconnect
284
+ persist(0)
285
+ end
140
286
 
141
- # Is the client currently connected?
287
+ # Is this client maintaining any persistent connections?
288
+ #
289
+ # Returns true/false if the client is single-threaded,
290
+ # or the number of active connections if the client is multi-threaded
142
291
  def connected?
143
- @m.synchronize{
144
- _connected?
145
- }
292
+
293
+ # If not threaded, simply check socket
294
+ @mutex.synchronize { return _connected?(@s) } unless @threaded
295
+
296
+ # if threaded, return pool length
297
+ @pool_mutex.synchronize { return (@pool.length) }
146
298
  end
147
299
 
300
+ # -------------------------------------------------------------------------
301
+ # Call handling
302
+ #
303
+
148
304
  # Call a method that is otherwise clobbered
149
305
  # by the client object, e.g.:
150
306
  #
151
307
  # client.call(:dup) # return a copy of the server object
152
308
  #
153
- def call(m, *args)
154
- method_missing(m, *args)
309
+ def call(m, *args, &block)
310
+ method_missing(m, *args, &block)
155
311
  end
156
312
 
157
313
  # Calls RPC on the remote object.
158
314
  #
159
- # You should not need to call this directly.
315
+ # You should not need to call this directly (though you are welcome to).
160
316
  #
161
317
  def method_missing(m, *args, &block)
162
318
 
319
+ # Records the server's return values.
163
320
  result = nil
164
321
  success = true
165
322
 
166
- @m.synchronize{
167
- already_connected = _connected?
168
- if not already_connected
169
- raise Errno::ECONNREFUSED, "Failed to connect" if not _connect
170
- end
323
+ # Get a socket preferentially from the pool,
324
+ # and do the actual work
325
+ _get_socket() do |s, persist|
326
+
171
327
  # send method name and arity
172
- _send([m, args, already_connected])
328
+ SocketProtocol::Stream.send(s, [m, args, block_given?, persist], @serialiser, @timeout)
173
329
 
174
- # call with args
175
- success, result = _recv
330
+ # Call with args
331
+ success, result = SocketProtocol::Stream.recv(s, @serialiser, @timeout)
176
332
 
177
- # Then d/c
178
- _disconnect if not already_connected
179
- }
180
-
181
- # puts "[c] /calling #{m}..."
333
+ # Check if we should yield
334
+ while success == SocketProtocol::REQUEST_YIELD do
335
+ block_result = yield(*result)
336
+ SocketProtocol::Stream.send(s, block_result, @serialiser, @timeout)
337
+ success, result = SocketProtocol::Stream.recv(s, @serialiser, @timeout)
338
+ end
339
+
340
+ end
341
+
182
342
  # If it didn't succeed, treat the payload as an exception
183
- raise result if not success
343
+ raise RemoteException.new(result) unless success == SocketProtocol::REQUEST_SUCCESS
184
344
  return result
185
-
186
- # rescue StandardError => e
187
- # $stderr.puts "-> #{e}, #{e.backtrace.join("--")}"
188
- # case e
189
- # when Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::ETIMEDOUT
190
- # c.close
191
- # else
192
- # raise e
193
- # end
194
345
  end
195
346
 
196
- private
347
+ # Returns a proxy object that is all but indistinguishable
348
+ # from the remote object.
349
+ #
350
+ # This allows you to pass the object around whilst retaining control
351
+ # over the RPC client (i.e. calling persist/disconnect).
352
+ #
353
+ # The class returned extends BasicObject and is thus able to pass
354
+ # all calls through to the server.
355
+ #
356
+ def get_proxy
197
357
 
358
+ # Construct a new class as a subclass of RemoteObject
359
+ cls = Class.new(RemoteObject) do
198
360
 
199
- # -------------------------------------------------------------------------
200
- # Send/Receive
201
- #
361
+ # Accept the originating client
362
+ def initialize(client)
363
+ @client = client
364
+ end
202
365
 
203
- # Receive data from the server
204
- def _recv
205
- SocketProtocol::Stream.recv( @s, @serialiser, @timeout )
206
- end
366
+ # And handle method_missing by calling the client
367
+ def method_missing(m, *args, &block)
368
+ @client.call(m, *args, &block)
369
+ end
370
+ end
207
371
 
208
- # Send data to the server
209
- def _send(obj)
210
- SocketProtocol::Stream.send( @s, obj, @serialiser, @timeout )
372
+ # Return a new class linked to us
373
+ return cls.new(self)
211
374
  end
212
375
 
376
+
377
+ # ---------------------------------------------------------------------------
378
+ private
379
+
213
380
  # -------------------------------------------------------------------------
214
381
  # Socket management
215
382
  #
216
383
 
217
- # Connect to the server
384
+ # Connect to the server and return a socket
218
385
  def _connect
219
386
  # Connect to the host
220
- @s = Socket.tcp( @hostname, @port, nil, nil, :connect_timeout => @timeout )
221
-
387
+ s = Socket.tcp(@hostname, @port, nil, nil, connect_timeout: @timeout)
388
+
222
389
  # Disable Nagle's algorithm
223
- @s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
224
-
390
+ s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
391
+
225
392
  # if auth is required
226
- if @password and @secret
227
- salt = SocketProtocol::Simple.recv( @s, @timeout )
228
- challenge = Encryption.encrypt( @password, @secret, salt )
229
- SocketProtocol::Simple.send( @s, challenge, @timeout )
230
- if not @fast_auth
231
- if SocketProtocol::Simple.recv( @s, @timeout ) != SocketProtocol::AUTH_SUCCESS
232
- @s.close
233
- raise AuthenticationError, "Authentication failed"
393
+ if @password && @secret
394
+ salt = SocketProtocol::Simple.recv(s, @timeout)
395
+ challenge = Encryption.encrypt(@password, @secret, salt)
396
+ SocketProtocol::Simple.send(s, challenge, @timeout)
397
+
398
+ # Check return if not @fast_auth
399
+ unless @fast_auth
400
+ unless SocketProtocol::Simple.recv(s, @timeout) == SocketProtocol::AUTH_SUCCESS
401
+ s.close
402
+ raise AuthenticationError, 'Authentication failed'
234
403
  end
235
404
  end
236
405
  end
237
406
 
238
407
  # Check and raise
239
- return _connected?
408
+ return s
240
409
  end
241
410
 
242
- # Disconnect from the server
243
- def _disconnect
244
- return if not _connected?
411
+ # Get a socket from the reusable pool if possible,
412
+ # else spawn a new one.
413
+ #
414
+ # Blocks if threading is off and the persistent socket
415
+ # is in use.
416
+ def _get_socket
417
+
418
+ # If not threaded, try using @s and block on @mutex
419
+ unless @threaded
420
+ # Try to load from pool
421
+ if @s
422
+ # Persistent connection
423
+ @mutex.synchronize do
424
+
425
+ # Keepalive for pool sockets
426
+ unless _connected?(@s)
427
+ raise Errno::ECONNREFUSED, 'Failed to connect' unless (@s = _connect)
428
+ end
429
+
430
+ yield(@s, true)
431
+ end
432
+ else
433
+ # On-demand connection
434
+ @mutex.synchronize { yield(_connect, false) }
435
+ end
436
+ return
437
+ end
438
+
439
+ # If threaded, try using the pool and use try_lock instead,
440
+ # then fall back to using a new connection
441
+
442
+ # Look through the pool to find a suitable socket
443
+ @pool.each do |s, m|
444
+
445
+ # If not threaded, block.
446
+ if s && m && m.try_lock
447
+ begin
448
+
449
+ # Keepalive for pool sockets
450
+ unless _connected?(s)
451
+ raise Errno::ECONNREFUSED, 'Failed to connect' unless (s = _connect)
452
+ end
453
+
454
+ # Increase count of active connections and yield
455
+ yield(s, true)
456
+ ensure
457
+ m.unlock
458
+ end
459
+ return
460
+ end
461
+ end
462
+
463
+ # Else use a temporary one...
464
+ s = _connect
465
+ yield(s, false)
466
+ _disconnect(s)
467
+ end
468
+
469
+ # Disconnect a socket from the server
470
+ def _disconnect(s)
471
+ return unless _connected?(s)
245
472
 
246
473
  # Then underlying socket
247
- @s.close if @s and not @s.closed?
248
- @s = nil
474
+ s.close if s && !s.closed?
249
475
  end
250
476
 
251
- # Non-mutexed check for connectedness
252
- def _connected?
253
- @s and not @s.closed?
477
+ # Thread-unsafe check for connectedness
478
+ def _connected?(s)
479
+ s && !s.closed?
254
480
  end
255
481
 
256
482
  end
257
483
 
258
484
  end
259
-
@@ -15,30 +15,30 @@ module SimpleRPC
15
15
  CIPHER_STRENGTH = 256
16
16
 
17
17
  # Encrypt data
18
- def self.encrypt( password, secret, salt )
18
+ def self.encrypt(password, secret, salt)
19
19
  # Encrypt with salted key
20
- cipher = OpenSSL::Cipher::AES.new( CIPHER_STRENGTH, :CBC )
20
+ cipher = OpenSSL::Cipher::AES.new(CIPHER_STRENGTH, :CBC)
21
21
  cipher.encrypt
22
- cipher.key = salt_key( salt, secret )
22
+ cipher.key = salt_key(salt, secret)
23
23
  return cipher.update(password) + cipher.final
24
- rescue StandardError => e
24
+ rescue StandardError
25
25
  return nil # Don't allow anyone to deliberately cause lockups
26
26
  end
27
27
 
28
28
  # Decrypt data
29
- def self.decrypt( raw, secret, salt )
29
+ def self.decrypt(raw, secret, salt)
30
30
  # Decrypt raw input
31
- decipher = OpenSSL::Cipher::AES.new( CIPHER_STRENGTH, :CBC )
31
+ decipher = OpenSSL::Cipher::AES.new(CIPHER_STRENGTH, :CBC)
32
32
  decipher.decrypt
33
- decipher.key = salt_key( salt, secret )
33
+ decipher.key = salt_key(salt, secret)
34
34
  return decipher.update(raw) + decipher.final
35
- rescue StandardError => e
35
+ rescue StandardError
36
36
  return nil # Don't allow anyone to deliberately cause lockups
37
37
  end
38
38
 
39
39
  # Salt a key by simply adding the two
40
40
  # together
41
- def self.salt_key( salt, key )
41
+ def self.salt_key(salt, key)
42
42
  return salt + key
43
43
  end
44
44
 
@@ -1,20 +1,41 @@
1
1
 
2
-
3
2
  require 'socket' # Get sockets from stdlib
4
3
  require 'simplerpc/socket_protocol'
5
4
 
6
- module SimpleRPC
5
+ # rubocop:disable LineLength
6
+
7
+
7
8
 
9
+ module SimpleRPC
10
+
11
+ # Thrown when #listen is called but the server
12
+ # is already listening on the port given
13
+ class AlreadyListeningError < Exception
14
+ end
8
15
 
9
16
  # SimpleRPC's server. This wraps an object and exposes its methods to the network.
10
17
  #
11
18
  # i.e.:
12
- #
19
+ #
13
20
  # require 'simplerpc/server'
21
+ #
22
+ # # Expose the Array api on port 27045
14
23
  # s = SimpleRPC::Server.new( ["thing", "thing2"], :port => 27045 )
24
+ #
25
+ # # Listen in a thread so we can shut down later
15
26
  # Thread.new(){ s.listen }
16
27
  # sleep(10)
17
- # s.close # Thread-safe
28
+ #
29
+ # # Tell the server to exit cleanly
30
+ # s.close
31
+ #
32
+ # == Thread Safety
33
+ #
34
+ # The server is thread-safe, and will not interrupt any clients when #close is called
35
+ # (instead it will wait for requests to finish, then shut down).
36
+ #
37
+ # If :threaded is set to true, the server will be able to make many simultaneous calls
38
+ # to the object being proxied.
18
39
  #
19
40
  # == Controlling a Server
20
41
  #
@@ -24,7 +45,7 @@ module SimpleRPC
24
45
  #
25
46
  # 1. The current client requests will end
26
47
  # 2. The socket will close
27
- # 3. #listen and #close will return (almost) simultaneously
48
+ # 3. #listen and #close will return (almost) simultaneously
28
49
  #
29
50
  # == Serialisation Formats
30
51
  #
@@ -34,7 +55,7 @@ module SimpleRPC
34
55
  # The serialiser also supports MessagePack (the msgpack gem), and this yields a small
35
56
  # performance increase at the expense of generality (restrictions on data type).
36
57
  #
37
- # Note that JSON and YAML, though they support reading and writing to sockets, do not
58
+ # Note that JSON and YAML, though they support reading and writing to sockets, do not
38
59
  # properly terminate their reads and cause the system to hang. These methods are
39
60
  # both slow and limited by comparison anyway, and algorithms needed to support their
40
61
  # use require relatively large memory usage. They may be supported in later versions.
@@ -47,7 +68,7 @@ module SimpleRPC
47
68
  # speed) so the results of using mismatched configurations are undefined.
48
69
  #
49
70
  # The auth process is simple and not particularly secure, but is designed to deter casual
50
- # connections and attacks. It uses a password that is sent encrypted against a salt sent
71
+ # connections and attacks. It uses a password that is sent encrypted against a salt sent
51
72
  # by the server to prevent replay attacks. If you want more reliable security, use an SSH tunnel.
52
73
  #
53
74
  # The performance impact of auth is small, and takes about the same time as a simple
@@ -58,24 +79,24 @@ module SimpleRPC
58
79
  attr_reader :hostname, :port, :obj, :threaded
59
80
  attr_accessor :verbose_errors, :serialiser, :timeout, :fast_auth
60
81
  attr_writer :password, :secret
61
-
82
+
62
83
  # Create a new server for a given proxy object.
63
84
  #
64
- # The single required parameter, obj, is an object you wish to expose to the
85
+ # The single required parameter, obj, is an object you wish to expose to the
65
86
  # network. This is the API that will respond to RPCs.
66
87
  #
67
88
  # Takes an option hash with options:
68
89
  #
69
90
  # [:port] The port on which to listen, or 0 for the OS to set it
70
91
  # [:hostname] The hostname of the interface to listen on (omit for all interfaces)
71
- # [:serialiser] A class supporting #load(IO) and #dump(obj, IO) for serialisation.
92
+ # [:serialiser] A class supporting #load(IO) and #dump(obj, IO) for serialisation.
72
93
  # Defaults to Marshal. I recommend using MessagePack if this is not
73
94
  # fast enough. Note that JSON/YAML do not work as they don't send
74
95
  # terminating characters over the socket.
75
96
  # [:verbose_errors] Report all socket errors from clients (by default these will be quashed).
76
97
  # [:timeout] Socket timeout in seconds. Default is infinite (nil)
77
98
  # [:threaded] Accept more than one client at once? Note that proxy object should be thread-safe for this
78
- # Default is single-threaded mode.
99
+ # Default is on.
79
100
  # [:password] The password clients need to connect
80
101
  # [:secret] The encryption key used during password authentication. Should be some long random string.
81
102
  # [:salt_size] The size of the string used as a nonce during password auth. Defaults to 10 chars
@@ -102,7 +123,7 @@ module SimpleRPC
102
123
  @timeout = opts[:timeout]
103
124
 
104
125
  # Auth
105
- if opts[:password] and opts[:secret] then
126
+ if opts[:password] && opts[:secret]
106
127
  require 'simplerpc/encryption'
107
128
  @password = opts[:password]
108
129
  @secret = opts[:secret]
@@ -110,87 +131,92 @@ module SimpleRPC
110
131
  end
111
132
 
112
133
  # Threaded or not?
113
- @threaded = (opts[:threaded] == true)
114
- if(@threaded)
134
+ @threaded = !(opts[:threaded] == false)
135
+ if @threaded
115
136
  @clients = {}
116
- @m = Mutex.new
137
+ @mc = Mutex.new # Client list mutex
117
138
  end
139
+
140
+ # Listener mutex
141
+ @ml = Mutex.new
118
142
  end
119
143
 
120
144
  # Start listening forever.
121
145
  #
122
146
  # Use threads and .close to stop the server.
123
147
  #
148
+ # Throws AlreadyListeningError when the server is already
149
+ # busy listening for connections
124
150
  def listen
151
+ raise 'Server is already listening' unless @ml.try_lock
152
+
125
153
  # Listen on one interface only if hostname given
126
- create_server_socket if not @s or @s.closed?
154
+ s = create_server_socket
127
155
 
128
156
  # Handle clients
129
- loop{
130
-
131
- begin
132
- # Accept in an interruptable manner
133
- if( c = interruptable_accept )
134
- if @threaded
135
-
136
- # Create the thread
137
- id = rand.hash
138
- thread = Thread.new(id, @m, c){|id, m, c|
139
- handle_client(c)
140
-
141
- m.synchronize{
142
- @clients.delete(id)
143
- }
144
- }
145
-
146
- # Add to the client list
147
- @m.synchronize{
148
- @clients[id] = thread
149
- }
150
- else
151
- # Handle client
152
- handle_client(c)
157
+ loop do
158
+
159
+ # Accept in an interruptable manner
160
+ if (c = interruptable_accept(s))
161
+ # Threaded
162
+ if @threaded
163
+
164
+ # Create the id
165
+ id = rand.hash
166
+
167
+ # Add to the client list
168
+ @mc.synchronize do
169
+ @clients[id] = Thread.new() do
170
+ # puts "[#{@clients.length+1}->#{id}"
171
+ begin
172
+ handle_client(c)
173
+ ensure
174
+ # Remove self from list
175
+ @mc.synchronize { @clients.delete(id) }
176
+ # puts "[#{@clients.length}<-#{id}"
177
+ end
178
+ end
179
+ @clients[id].abort_on_exception = true
153
180
  end
181
+
182
+ # Single-threaded
183
+ else
184
+ # Handle client
185
+ handle_client(c)
154
186
  end
155
- rescue StandardError => e
156
- raise e if @verbose_errors
157
187
  end
158
188
 
159
189
  break if @close
160
- }
190
+ end
161
191
 
162
192
  # Wait for threads to end
163
- if @threaded then
164
- @clients.each{|id, thread|
165
- thread.join
166
- }
167
- end
193
+ @clients.each {|id, thread| thread.join } if @threaded
168
194
 
169
195
  # Close socket
170
- close_server_sockets
171
-
172
- # Finally, say we've closed
173
- @close = false if @close
196
+ @close = false if @close # say we've closed
197
+ @ml.unlock
174
198
  end
175
199
 
176
- # Return the number of active clients.
177
- def active_clients
178
- @m.synchronize{
179
- @clients.length
180
- }
200
+ # Return the number of active client threads.
201
+ #
202
+ # Returns 0 if :threaded is set to false.
203
+ def active_client_threads
204
+ # If threaded return a count from the clients list
205
+ return @mc.synchronize { @clients.length } if @threaded
206
+
207
+ # Else return 0 if not threaded
208
+ return 0
181
209
  end
182
210
 
183
211
  # Close the server object nicely,
184
212
  # waiting on threads if necessary
185
213
  def close
186
214
  # Ask the loop to close
187
- @close = true
188
215
  @close_in.putc 'x' # Tell select to close
189
216
 
190
- # Wait for loop to end
191
- while(@close)
192
- sleep(0.1)
193
- end
217
+ # Wait for loop to end
218
+ @ml.lock
219
+ @ml.unlock
194
220
  end
195
221
 
196
222
  private
@@ -198,23 +224,25 @@ module SimpleRPC
198
224
  # -------------------------------------------------------------------------
199
225
  # Client Management
200
226
  #
201
-
202
- # Accept with the ability for other
227
+
228
+ # Accept with the ability for other
203
229
  # threads to call close
204
- def interruptable_accept
205
- c = IO.select([@s, @close_out], nil, nil)
206
-
230
+ def interruptable_accept(s)
231
+ c = IO.select([s, @close_out], nil, nil)
232
+
207
233
  # puts "--> #{c}"
208
234
 
209
- return nil if not c
210
- if(c[0][0] == @close_out)
235
+ return nil unless c
236
+ if c[0][0] == @close_out
211
237
  # @close is set, so consume from socket
212
238
  # and return nil
213
239
  @close_out.getc
214
- return nil
240
+ @close = true
241
+ s.close # close server socket
242
+ return nil
215
243
  end
216
- return @s.accept if( not @close and c )
217
- rescue IOError => e
244
+ return s.accept if !@close && c
245
+ rescue IOError
218
246
  # cover 'closed stream' errors
219
247
  return nil
220
248
  end
@@ -225,108 +253,102 @@ module SimpleRPC
225
253
  c.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
226
254
 
227
255
  # Encrypted password auth
228
- if @password and @secret
256
+ if @password && @secret
229
257
  # Send challenge
230
258
  # XXX: this is notably not crytographically random,
231
259
  # but it's better than nothing against replay attacks
232
- salt = Random.new.bytes( @salt_size )
233
- SocketProtocol::Simple.send( c, salt, @timeout )
260
+ salt = Random.new.bytes(@salt_size)
261
+ SocketProtocol::Simple.send(c, salt, @timeout)
234
262
 
235
263
  # Receive encrypted challenge
236
- raw = SocketProtocol::Simple.recv( c, @timeout )
264
+ raw = SocketProtocol::Simple.recv(c, @timeout)
237
265
 
238
266
  # D/c if failed
239
- if Encryption.decrypt( raw, @secret, salt) != @password
240
- SocketProtocol::Simple.send( c, SocketProtocol::AUTH_FAIL, @timeout ) if not @fast_auth
241
- c.close
267
+ unless Encryption.decrypt(raw, @secret, salt) == @password
268
+ SocketProtocol::Simple.send(c, SocketProtocol::AUTH_FAIL, @timeout) unless @fast_auth
242
269
  return
243
270
  end
244
- SocketProtocol::Simple.send( c, SocketProtocol::AUTH_SUCCESS, @timeout ) if not @fast_auth
271
+ SocketProtocol::Simple.send(c, SocketProtocol::AUTH_SUCCESS, @timeout) unless @fast_auth
245
272
  end
246
273
 
247
274
  # Handle requests
248
275
  persist = true
249
- while(not @close and persist) do
276
+ while !@close && persist do
250
277
 
251
- m, args, persist = recv(c)
252
- # puts "Method: #{m}, args: #{args}, persist: #{persist}"
278
+ # Note, when clients d/c this throws EOFError
279
+ m, args, remote_block_given, persist = SocketProtocol::Stream.recv(c, @serialiser, @timeout)
280
+ # puts "Method: #{m}, args: #{args}, block?: #{remote_block_given}, persist: #{persist}"
253
281
 
254
- if(m and args) then
282
+ if m && args
255
283
 
256
284
  # Record success status
257
285
  result = nil
258
- success = true
286
+ success = SocketProtocol::REQUEST_SUCCESS
259
287
 
260
288
  # Try to make the call, catching exceptions
261
289
  begin
262
- result = @obj.send(m, *args)
290
+
291
+ if remote_block_given
292
+ # Proxy with a block that sends back to the client
293
+ result = @obj.send(m, *args) do |*yield_args|
294
+ SocketProtocol::Stream.send(c, [SocketProtocol::REQUEST_YIELD, yield_args], @serialiser, @timeout)
295
+ SocketProtocol::Stream.recv(c, @serialiser, @timeout)
296
+ end
297
+
298
+ else
299
+ # Proxy without block for correct exceptions
300
+ result = @obj.send(m, *args)
301
+ end
302
+
263
303
  rescue StandardError => se
264
304
  result = se
265
- success = false
305
+ success = SocketProtocol::REQUEST_FAIL
266
306
  end
267
307
 
268
308
  # Send over the result
269
309
  # puts "[s] sending result..."
270
- send(c, [success, result] )
310
+ SocketProtocol::Stream.send(c, [success, result], @serialiser, @timeout)
271
311
  else
272
312
  persist = false
273
313
  end
274
314
 
275
315
  end
276
-
277
- # Close
278
- c.close
316
+
279
317
  rescue StandardError => e
280
318
  case e
281
- when Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::ETIMEDOUT
282
- c.close
319
+ when EOFError
320
+ return
321
+ when Errno::EPIPE, Errno::ECONNRESET,
322
+ Errno::ECONNABORTED, Errno::ETIMEDOUT
323
+ raise e if @verbose_errors
283
324
  else
284
- raise e
325
+ raise e if @verbose_errors
285
326
  end
286
- end
287
-
288
- # -------------------------------------------------------------------------
289
- # Send/Receive
290
- #
291
-
292
- # Receive data from a client
293
- def recv(c)
294
- SocketProtocol::Stream.recv( c, @serialiser, @timeout )
295
- end
296
-
297
- # Send data to a client
298
- def send(c, obj)
299
- SocketProtocol::Stream.send( c, obj, @serialiser, @timeout )
327
+ ensure
328
+ # Always ensure we close the socket
329
+ c.close
300
330
  end
301
331
 
302
332
  # -------------------------------------------------------------------------
303
333
  # Socket Management
304
334
  #
305
335
 
306
- # Creates a new socket with the given timeout
336
+ # Creates a new socket
337
+ # and returns it
307
338
  def create_server_socket
308
- if @hostname
309
- @s = TCPServer.open( @hostname, @port )
339
+ if @hostname
340
+ s = TCPServer.open(@hostname, @port)
310
341
  else
311
- @s = TCPServer.open( @port )
342
+ s = TCPServer.open(@port)
312
343
  end
313
- @port = @s.addr[1]
314
-
315
- @s.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
316
- end
344
+ @port = s.addr[1]
317
345
 
318
- # Close the server socket
319
- def close_server_sockets
320
- return if not @s
346
+ s.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
321
347
 
322
- # Close underlying socket
323
- @s.close if @s and not @s.closed?
324
- @s = nil
348
+ return s
325
349
  end
326
350
 
327
-
328
351
  end
329
352
 
330
-
331
353
  end
332
354
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  module SimpleRPC
4
4
 
5
-
6
5
  # SocketProtocol defines common low-level aspects of data transfer
7
6
  # between client and server.
8
7
  #
@@ -17,6 +16,15 @@ module SimpleRPC
17
16
  # Sent when auth fails
18
17
  AUTH_FAIL = 'F'
19
18
 
19
+ # The request succeeded
20
+ REQUEST_SUCCESS = 0
21
+
22
+ # The request failed and threw something
23
+ REQUEST_FAIL = 1
24
+
25
+ # The request is yielding data to a block
26
+ REQUEST_YIELD = 2
27
+
20
28
  # Send objects by streaming them through a socket using
21
29
  # a serialiser such as Marshal.
22
30
  #
@@ -30,20 +38,19 @@ module SimpleRPC
30
38
  module Stream
31
39
 
32
40
  # Send using a serialiser writing through the socket
33
- def self.send( s, obj, serialiser, timeout=nil )
34
- raise Errno::ETIMEDOUT if not IO.select([], [s], [], timeout)
35
- return serialiser.dump( obj, s )
41
+ def self.send(s, obj, serialiser, timeout = nil)
42
+ raise Errno::ETIMEDOUT unless IO.select([], [s], [], timeout)
43
+ return serialiser.dump(obj, s)
36
44
  end
37
45
 
38
46
  # Recieve using a serialiser reading from the socket
39
- def self.recv( s, serialiser, timeout=nil )
40
- raise Errno::ETIMEDOUT if not IO.select([s], [], [], timeout)
41
- return serialiser.load( s )
47
+ def self.recv(s, serialiser, timeout = nil)
48
+ raise Errno::ETIMEDOUT unless IO.select([s], [], [], timeout)
49
+ return serialiser.load(s)
42
50
  end
43
51
 
44
52
  end
45
53
 
46
-
47
54
  # Sends string buffers back and forth using a simple protocol.
48
55
  #
49
56
  # This method is significantly slower, but significantly more secure,
@@ -52,38 +59,38 @@ module SimpleRPC
52
59
  module Simple
53
60
 
54
61
  # Send a buffer
55
- def self.send( s, buf, timeout=nil )
62
+ def self.send(s, buf, timeout = nil)
56
63
  # Dump into buffer
57
64
  buflen = buf.length
58
65
 
59
66
  # Send buffer length
60
- raise Errno::ETIMEDOUT if not IO.select([], [s], [], timeout)
67
+ raise Errno::ETIMEDOUT unless IO.select([], [s], [], timeout)
61
68
  s.puts(buflen)
62
69
 
63
70
  # Send buffer
64
71
  sent = 0
65
- while(sent < buflen and (x = IO.select([], [s], [], timeout))) do
66
- sent += s.write( buf[sent..-1] )
72
+ while sent < buflen && (x = IO.select([], [s], [], timeout)) do
73
+ sent += s.write(buf[sent..-1])
67
74
  end
68
- raise Errno::ETIMEDOUT if not x
75
+ raise Errno::ETIMEDOUT unless x
69
76
 
70
77
  end
71
78
 
72
79
  # Receive a buffer
73
- def self.recv( s, timeout=nil )
74
- raise Errno::ETIMEDOUT if not IO.select([s], [], [], timeout)
80
+ def self.recv(s, timeout = nil)
81
+ raise Errno::ETIMEDOUT unless IO.select([s], [], [], timeout)
75
82
  buflen = s.gets.to_s.chomp.to_i
76
83
 
77
84
  return nil if buflen <= 0
78
85
 
79
- buf = ""
86
+ buf = ''
80
87
  recieved = 0
81
- while( recieved < buflen and (x = IO.select([s], [], [], timeout)) ) do
82
- str = s.read( buflen - recieved )
88
+ while recieved < buflen && (x = IO.select([s], [], [], timeout)) do
89
+ str = s.read(buflen - recieved)
83
90
  buf += str
84
91
  recieved += str.length
85
92
  end
86
- raise Errno::ETIMEDOUT if not x
93
+ raise Errno::ETIMEDOUT unless x
87
94
 
88
95
  return buf
89
96
  end
data/lib/simplerpc.rb CHANGED
@@ -1,7 +1,6 @@
1
1
 
2
2
  require 'simplerpc/server'
3
3
  require 'simplerpc/client'
4
- require 'simplerpc/serialiser'
5
4
 
6
5
  # SimpleRPC is a very simple RPC library for ruby, designed to be as fast as possible whilst still
7
6
  # retaining a simple API.
@@ -16,6 +15,6 @@ require 'simplerpc/serialiser'
16
15
  # and including it includes all other project files
17
16
  module SimpleRPC
18
17
 
19
- VERSION = "0.2.0b"
18
+ VERSION = '0.2.0'
20
19
 
21
20
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simplerpc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0c
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Wattam
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-05-28 00:00:00.000000000 Z
11
+ date: 2013-05-30 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A very simple and fast RPC library
14
14
  email: stephenwattam@gmail.com
@@ -37,9 +37,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
37
37
  version: '1.9'
38
38
  required_rubygems_version: !ruby/object:Gem::Requirement
39
39
  requirements:
40
- - - '>'
40
+ - - '>='
41
41
  - !ruby/object:Gem::Version
42
- version: 1.3.1
42
+ version: '0'
43
43
  requirements: []
44
44
  rubyforge_project:
45
45
  rubygems_version: 2.0.0