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 +4 -4
- data/lib/simplerpc/client.rb +329 -104
- data/lib/simplerpc/encryption.rb +9 -9
- data/lib/simplerpc/server.rb +146 -124
- data/lib/simplerpc/socket_protocol.rb +26 -19
- data/lib/simplerpc.rb +1 -2
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fe0eb8578d0b1f0f41f18696580bf8b8bb26439d
|
4
|
+
data.tar.gz: ab97b64f7519ea0b18e9f04c7b7fc52a6206917d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0152af8e2cff5b277fdb40f02fc18732b14c6a249e33840587a682fc9a7ae9a5867301394ec049eacbed133f321f3d1a1877d982dddf7f9071839c91c5ceed6d
|
7
|
+
data.tar.gz: 6d5ba75c705eb414ae87482f144f638001623b7d6250aa9dec2ee3addc156e862f58ea200ec1d376d818dc41da9374578991d03330016cffd546a5617b591508
|
data/lib/simplerpc/client.rb
CHANGED
@@ -1,12 +1,26 @@
|
|
1
1
|
require 'socket' # Sockets are in standard library
|
2
2
|
require 'simplerpc/socket_protocol'
|
3
3
|
|
4
|
-
|
4
|
+
# rubocop:disable LineLength
|
5
5
|
|
6
|
-
|
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
|
-
#
|
19
|
-
#
|
20
|
-
# c.
|
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
|
-
#
|
33
|
-
#
|
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
|
-
#
|
39
|
-
#
|
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
|
-
#
|
42
|
-
# client is not connected
|
43
|
-
#
|
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
|
79
|
-
attr_accessor
|
80
|
-
attr_writer
|
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
|
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]
|
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
|
-
@
|
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
|
-
#
|
205
|
+
# All options are the same as #new
|
123
206
|
#
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
-
#
|
132
|
-
|
133
|
-
|
134
|
-
|
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
|
-
#
|
139
|
-
|
282
|
+
# Close all persistent connections to the server.
|
283
|
+
def disconnect
|
284
|
+
persist(0)
|
285
|
+
end
|
140
286
|
|
141
|
-
# Is
|
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
|
-
|
144
|
-
|
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
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
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
|
-
|
328
|
+
SocketProtocol::Stream.send(s, [m, args, block_given?, persist], @serialiser, @timeout)
|
173
329
|
|
174
|
-
#
|
175
|
-
success, result =
|
330
|
+
# Call with args
|
331
|
+
success, result = SocketProtocol::Stream.recv(s, @serialiser, @timeout)
|
176
332
|
|
177
|
-
#
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
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
|
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
|
-
|
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
|
-
|
201
|
-
|
361
|
+
# Accept the originating client
|
362
|
+
def initialize(client)
|
363
|
+
@client = client
|
364
|
+
end
|
202
365
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
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
|
-
|
209
|
-
|
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
|
-
|
221
|
-
|
387
|
+
s = Socket.tcp(@hostname, @port, nil, nil, connect_timeout: @timeout)
|
388
|
+
|
222
389
|
# Disable Nagle's algorithm
|
223
|
-
|
224
|
-
|
390
|
+
s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
391
|
+
|
225
392
|
# if auth is required
|
226
|
-
if @password
|
227
|
-
salt = SocketProtocol::Simple.recv(
|
228
|
-
challenge = Encryption.encrypt(
|
229
|
-
SocketProtocol::Simple.send(
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
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
|
408
|
+
return s
|
240
409
|
end
|
241
410
|
|
242
|
-
#
|
243
|
-
|
244
|
-
|
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
|
-
|
248
|
-
@s = nil
|
474
|
+
s.close if s && !s.closed?
|
249
475
|
end
|
250
476
|
|
251
|
-
#
|
252
|
-
def _connected?
|
253
|
-
|
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
|
-
|
data/lib/simplerpc/encryption.rb
CHANGED
@@ -15,30 +15,30 @@ module SimpleRPC
|
|
15
15
|
CIPHER_STRENGTH = 256
|
16
16
|
|
17
17
|
# Encrypt data
|
18
|
-
def self.encrypt(
|
18
|
+
def self.encrypt(password, secret, salt)
|
19
19
|
# Encrypt with salted key
|
20
|
-
cipher = OpenSSL::Cipher::AES.new(
|
20
|
+
cipher = OpenSSL::Cipher::AES.new(CIPHER_STRENGTH, :CBC)
|
21
21
|
cipher.encrypt
|
22
|
-
cipher.key = salt_key(
|
22
|
+
cipher.key = salt_key(salt, secret)
|
23
23
|
return cipher.update(password) + cipher.final
|
24
|
-
rescue StandardError
|
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(
|
29
|
+
def self.decrypt(raw, secret, salt)
|
30
30
|
# Decrypt raw input
|
31
|
-
decipher = OpenSSL::Cipher::AES.new(
|
31
|
+
decipher = OpenSSL::Cipher::AES.new(CIPHER_STRENGTH, :CBC)
|
32
32
|
decipher.decrypt
|
33
|
-
decipher.key = salt_key(
|
33
|
+
decipher.key = salt_key(salt, secret)
|
34
34
|
return decipher.update(raw) + decipher.final
|
35
|
-
rescue StandardError
|
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(
|
41
|
+
def self.salt_key(salt, key)
|
42
42
|
return salt + key
|
43
43
|
end
|
44
44
|
|
data/lib/simplerpc/server.rb
CHANGED
@@ -1,20 +1,41 @@
|
|
1
1
|
|
2
|
-
|
3
2
|
require 'socket' # Get sockets from stdlib
|
4
3
|
require 'simplerpc/socket_protocol'
|
5
4
|
|
6
|
-
|
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
|
-
#
|
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
|
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]
|
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] ==
|
114
|
-
if
|
134
|
+
@threaded = !(opts[:threaded] == false)
|
135
|
+
if @threaded
|
115
136
|
@clients = {}
|
116
|
-
@
|
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
|
-
|
154
|
+
s = create_server_socket
|
127
155
|
|
128
156
|
# Handle clients
|
129
|
-
loop
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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
|
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
|
-
|
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
|
177
|
-
|
178
|
-
|
179
|
-
|
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
|
-
|
192
|
-
|
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([
|
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
|
210
|
-
if
|
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
|
-
|
240
|
+
@close = true
|
241
|
+
s.close # close server socket
|
242
|
+
return nil
|
215
243
|
end
|
216
|
-
return
|
217
|
-
rescue IOError
|
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
|
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(
|
233
|
-
SocketProtocol::Simple.send(
|
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(
|
264
|
+
raw = SocketProtocol::Simple.recv(c, @timeout)
|
237
265
|
|
238
266
|
# D/c if failed
|
239
|
-
|
240
|
-
SocketProtocol::Simple.send(
|
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(
|
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
|
276
|
+
while !@close && persist do
|
250
277
|
|
251
|
-
|
252
|
-
|
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
|
282
|
+
if m && args
|
255
283
|
|
256
284
|
# Record success status
|
257
285
|
result = nil
|
258
|
-
success =
|
286
|
+
success = SocketProtocol::REQUEST_SUCCESS
|
259
287
|
|
260
288
|
# Try to make the call, catching exceptions
|
261
289
|
begin
|
262
|
-
|
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 =
|
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
|
282
|
-
|
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
|
-
|
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
|
336
|
+
# Creates a new socket
|
337
|
+
# and returns it
|
307
338
|
def create_server_socket
|
308
|
-
if @hostname
|
309
|
-
|
339
|
+
if @hostname
|
340
|
+
s = TCPServer.open(@hostname, @port)
|
310
341
|
else
|
311
|
-
|
342
|
+
s = TCPServer.open(@port)
|
312
343
|
end
|
313
|
-
@port =
|
314
|
-
|
315
|
-
@s.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
|
316
|
-
end
|
344
|
+
@port = s.addr[1]
|
317
345
|
|
318
|
-
|
319
|
-
def close_server_sockets
|
320
|
-
return if not @s
|
346
|
+
s.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
|
321
347
|
|
322
|
-
|
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(
|
34
|
-
raise Errno::ETIMEDOUT
|
35
|
-
return serialiser.dump(
|
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(
|
40
|
-
raise Errno::ETIMEDOUT
|
41
|
-
return serialiser.load(
|
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(
|
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
|
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
|
66
|
-
sent += s.write(
|
72
|
+
while sent < buflen && (x = IO.select([], [s], [], timeout)) do
|
73
|
+
sent += s.write(buf[sent..-1])
|
67
74
|
end
|
68
|
-
raise Errno::ETIMEDOUT
|
75
|
+
raise Errno::ETIMEDOUT unless x
|
69
76
|
|
70
77
|
end
|
71
78
|
|
72
79
|
# Receive a buffer
|
73
|
-
def self.recv(
|
74
|
-
raise Errno::ETIMEDOUT
|
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
|
82
|
-
str = s.read(
|
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
|
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 =
|
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.
|
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-
|
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:
|
42
|
+
version: '0'
|
43
43
|
requirements: []
|
44
44
|
rubyforge_project:
|
45
45
|
rubygems_version: 2.0.0
|