arpie 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README +7 -4
- data/Rakefile +1 -1
- data/lib/arpie.rb +1 -0
- data/lib/arpie/client.rb +6 -5
- data/lib/arpie/protocol.rb +300 -207
- data/lib/arpie/proxy.rb +29 -18
- data/lib/arpie/server.rb +4 -4
- data/lib/arpie/xmlrpc.rb +108 -0
- data/spec/client_server_spec.rb +107 -0
- data/spec/protocol_merge_and_split_spec.rb +47 -0
- data/spec/protocol_spec.rb +152 -0
- data/spec/spec_helper.rb +58 -0
- data/tools/benchmark.rb +3 -2
- data/tools/protocol_benchmark.rb +11 -14
- metadata +10 -5
data/lib/arpie/proxy.rb
CHANGED
@@ -45,24 +45,34 @@ module Arpie
|
|
45
45
|
def _handle endpoint, message
|
46
46
|
if !@handler.respond_to?(message.meth.to_sym) ||
|
47
47
|
(@interface && !@interface.index(message.meth.to_sym))
|
48
|
-
|
48
|
+
endpoint.write_message NoMethodError.new("No such method: #{message.meth.inspect}")
|
49
|
+
return
|
49
50
|
end
|
50
51
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
@uuids.reject! {|uuid, (time, value)|
|
56
|
-
time < latest_timestamp
|
57
|
-
} if latest_timestamp
|
52
|
+
begin
|
53
|
+
# Prune old serials. This can probably be optimized, but works well enough for now.
|
54
|
+
if @uuid_tracking && message.uuid && message.uuid.is_a?(Numeric)
|
55
|
+
message.uuid &= 0xffffffffffffffff
|
58
56
|
|
59
|
-
|
60
|
-
|
57
|
+
timestamps = @uuids.values.map {|v| v[0] }.sort
|
58
|
+
latest_timestamp = timestamps[-@max_uuids]
|
59
|
+
@uuids.reject! {|uuid, (time, value)|
|
60
|
+
time < latest_timestamp
|
61
|
+
} if latest_timestamp
|
61
62
|
|
62
|
-
|
63
|
-
|
64
|
-
end
|
63
|
+
endpoint.write_message((@uuids[message.uuid] ||=
|
64
|
+
[Time.now, @handler.send(message.meth, *message.argv)])[1])
|
65
65
|
|
66
|
+
else
|
67
|
+
endpoint.write_message @handler.send(message.meth, *message.argv)
|
68
|
+
end
|
69
|
+
rescue IOError
|
70
|
+
raise
|
71
|
+
|
72
|
+
rescue Exception => e
|
73
|
+
endpoint.write_message RuntimeError.new("Internal Error")
|
74
|
+
raise
|
75
|
+
end
|
66
76
|
end
|
67
77
|
end
|
68
78
|
|
@@ -70,12 +80,13 @@ module Arpie
|
|
70
80
|
# method calls to the remote ProxyServer.
|
71
81
|
# Note that the methods of Client cannot be proxied.
|
72
82
|
class ProxyClient < RPCClient
|
83
|
+
attr_accessor :namespace
|
73
84
|
|
74
|
-
def initialize
|
75
|
-
super
|
76
|
-
@protocol, @namespace = protocol,
|
85
|
+
def initialize *protocols
|
86
|
+
super
|
87
|
+
@protocol, @namespace = protocol, ""
|
77
88
|
@uuid_generator = lambda {|client, method, argv|
|
78
|
-
UUID.
|
89
|
+
UUID.random_create.to_i
|
79
90
|
}
|
80
91
|
end
|
81
92
|
|
@@ -95,7 +106,7 @@ module Arpie
|
|
95
106
|
uuid = @uuid_generator ?
|
96
107
|
@uuid_generator.call(self, meth, argv) : nil
|
97
108
|
|
98
|
-
call =
|
109
|
+
call = Arpie::RPCall.new(@namespace, meth, argv, uuid)
|
99
110
|
ret = self.request(call)
|
100
111
|
case ret
|
101
112
|
when Exception
|
data/lib/arpie/server.rb
CHANGED
@@ -36,11 +36,11 @@ module Arpie
|
|
36
36
|
|
37
37
|
attr_reader :endpoints
|
38
38
|
|
39
|
-
# Create a new Server with the given
|
39
|
+
# Create a new Server with the given protocols.
|
40
40
|
# You will need to define a handler, and an acceptor
|
41
41
|
# before it becomes operational.
|
42
|
-
def initialize
|
43
|
-
@protocol =
|
42
|
+
def initialize *protocols
|
43
|
+
@protocol = Arpie::ProtocolChain.new(*protocols)
|
44
44
|
@endpoints = []
|
45
45
|
|
46
46
|
@on_connect = lambda {|server, endpoint| }
|
@@ -120,7 +120,7 @@ module Arpie
|
|
120
120
|
def _acceptor_thread
|
121
121
|
loop do
|
122
122
|
client = @acceptor.call(self)
|
123
|
-
c = @protocol.
|
123
|
+
c = @protocol.endpoint_class.new(@protocol.clone, client)
|
124
124
|
Thread.new { _read_thread(c) }
|
125
125
|
end
|
126
126
|
end
|
data/lib/arpie/xmlrpc.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'xmlrpc/create'
|
2
|
+
require 'xmlrpc/parser'
|
3
|
+
require 'xmlrpc/config'
|
4
|
+
|
5
|
+
module Arpie
|
6
|
+
|
7
|
+
class XMLRPCProtocol < Protocol
|
8
|
+
private_class_method :new
|
9
|
+
|
10
|
+
attr_accessor :writer
|
11
|
+
attr_accessor :parser
|
12
|
+
|
13
|
+
def setup
|
14
|
+
@writer ||= XMLRPC::Create.new
|
15
|
+
@parser ||= XMLRPC::XMLParser::REXMLStreamParser.new
|
16
|
+
end
|
17
|
+
private :setup
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
# A XMLRPC Protocol based on rubys xmlrpc stdlib.
|
22
|
+
# This does not encode HTTP headers; usage together with
|
23
|
+
# a real webserver is advised.
|
24
|
+
class XMLRPCClientProtocol < XMLRPCProtocol
|
25
|
+
public_class_method :new
|
26
|
+
|
27
|
+
def to object
|
28
|
+
setup
|
29
|
+
raise ArgumentError, "Can only encode Arpie::RPCall" unless
|
30
|
+
object.is_a?(Arpie::RPCall)
|
31
|
+
|
32
|
+
@writer.methodCall((object.ns.nil? ? '' : object.ns + '.') + object.meth, *object.argv)
|
33
|
+
end
|
34
|
+
|
35
|
+
def from binary
|
36
|
+
setup
|
37
|
+
yield @parser.parseMethodResponse(binary)[1]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class XMLRPCServerProtocol < XMLRPCProtocol
|
42
|
+
public_class_method :new
|
43
|
+
|
44
|
+
def to object
|
45
|
+
setup
|
46
|
+
case object
|
47
|
+
when Exception
|
48
|
+
# TODO: wrap XMLFault
|
49
|
+
raise NotImplementedError, "Cannot encode exceptions"
|
50
|
+
|
51
|
+
else
|
52
|
+
@writer.methodResponse(true, object)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def from binary
|
57
|
+
setup
|
58
|
+
vv = @parser.parseMethodCall(binary)
|
59
|
+
ns, meth = vv[0].split('.')
|
60
|
+
meth.nil? and begin meth, ns = ns, nil end
|
61
|
+
yield RPCall.new(ns, meth, vv[1])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# This simulates a very basic HTTP XMLRPC client/server.
|
66
|
+
# It is not recommended to use this with production code.
|
67
|
+
class HTTPTestProtocol < Protocol
|
68
|
+
CAN_SEPARATE_MESSAGES = true
|
69
|
+
|
70
|
+
private_class_method :new
|
71
|
+
|
72
|
+
def from binary
|
73
|
+
# Simply strip all HTTP headers.
|
74
|
+
binary && binary.size > 0 or incomplete!
|
75
|
+
header, body_and_rest = binary.split(/\r\n\r\n/, 2)
|
76
|
+
header && body_and_rest or incomplete!
|
77
|
+
|
78
|
+
header =~ /^\s*content-length:\s+(\d+)$\s*/xi or stream_error! "No content-length was provided."
|
79
|
+
content_length = $1.to_i
|
80
|
+
|
81
|
+
content_length <= 0
|
82
|
+
body_and_rest.size >= content_length or incomplete!
|
83
|
+
|
84
|
+
body = body_and_rest[0, content_length]
|
85
|
+
|
86
|
+
yield body
|
87
|
+
|
88
|
+
header.size + 4 + content_length
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class HTTPClientTestProtocol < HTTPTestProtocol
|
93
|
+
public_class_method :new
|
94
|
+
|
95
|
+
def to object
|
96
|
+
"GET / HTTP/1.[01]\r\nContent-Length: #{object.size}\r\n\r\n" + object
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
class HTTPServerTestProtocol < HTTPTestProtocol
|
102
|
+
public_class_method :new
|
103
|
+
|
104
|
+
def to object
|
105
|
+
"HTTP/1.0 200 OK\r\nContent-Length: #{object.size}\r\n\r\n" + object
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
require 'timeout'
|
5
|
+
|
6
|
+
describe "ClientServer", :shared => true do
|
7
|
+
before(:all) do
|
8
|
+
@tcp_server = TCPServer.new('127.0.0.1', 56000)
|
9
|
+
end
|
10
|
+
|
11
|
+
before do
|
12
|
+
@handler_errors = 0
|
13
|
+
$evaluate_calls = 0
|
14
|
+
@client = Arpie::Client.new(Arpie::MarshalProtocol.new, Arpie::SizedProtocol.new)
|
15
|
+
@server = Arpie::Server.new(Arpie::MarshalProtocol.new, Arpie::SizedProtocol.new)
|
16
|
+
@client.connect do TCPSocket.new('127.0.0.1', 56000) end
|
17
|
+
@server.accept do @tcp_server.accept end
|
18
|
+
@server.on_handler_error do |s, e, p, x|
|
19
|
+
@handler_errors += 1
|
20
|
+
end
|
21
|
+
|
22
|
+
@server.handle do |s, e, m|
|
23
|
+
e.write_message m
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "ProxyClientServer", :shared => true do
|
29
|
+
before(:all) do
|
30
|
+
@tcp_server = TCPServer.new('127.0.0.1', 56001)
|
31
|
+
end
|
32
|
+
|
33
|
+
before do
|
34
|
+
@handler_errors = 0
|
35
|
+
$evaluate_calls = 0
|
36
|
+
@client = Arpie::ProxyClient.new(Arpie::MarshalProtocol.new, Arpie::SizedProtocol.new)
|
37
|
+
@server = Arpie::ProxyServer.new(Arpie::MarshalProtocol.new, Arpie::SizedProtocol.new)
|
38
|
+
@client.connect do TCPSocket.new('127.0.0.1', 56001) end
|
39
|
+
@server.accept do @tcp_server.accept end
|
40
|
+
@server.on_handler_error do |s, e, p, x|
|
41
|
+
@handler_errors += 1
|
42
|
+
end
|
43
|
+
|
44
|
+
@server.handle((Class.new do
|
45
|
+
def evaluate_calls
|
46
|
+
$evaluate_calls += 1
|
47
|
+
end
|
48
|
+
def raise_something
|
49
|
+
raise "test"
|
50
|
+
end
|
51
|
+
end).new)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe Arpie::Client do
|
56
|
+
it_should_behave_like "ClientServer"
|
57
|
+
end
|
58
|
+
|
59
|
+
describe Arpie::EventedClient do
|
60
|
+
it_should_behave_like "ClientServer"
|
61
|
+
|
62
|
+
before do
|
63
|
+
@client = Arpie::EventedClient.new(Arpie::MarshalProtocol.new, Arpie::SizedProtocol.new)
|
64
|
+
@handler_calls = 0
|
65
|
+
@queue = Queue.new
|
66
|
+
@client.handle do
|
67
|
+
@queue.push true
|
68
|
+
@handler_calls += 1
|
69
|
+
end
|
70
|
+
@client.connect do TCPSocket.new('127.0.0.1', 56000) end
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should not allow reading messages" do
|
74
|
+
@client.should_not respond_to :read_message
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should call the handler for incoming messages" do
|
78
|
+
@client.write_message "test"
|
79
|
+
lambda {Timeout.timeout(1.0) { @queue.pop } }.should_not raise_error TimeoutError
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe Arpie::Server do
|
84
|
+
it_should_behave_like "ClientServer"
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "ProxyServer" do
|
88
|
+
it_should_behave_like "ProxyClientServer"
|
89
|
+
it "should raise handler errors to the client" do
|
90
|
+
lambda { @client.raise_something }.should(raise_error RuntimeError, "Internal Error")
|
91
|
+
@handler_errors.should == 1
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should not re-evaluate for already-seen uuids" do
|
95
|
+
@client.evaluate_calls
|
96
|
+
@client.evaluate_calls
|
97
|
+
@client.uuid_generator do 100 end
|
98
|
+
@client.evaluate_calls
|
99
|
+
@client.evaluate_calls
|
100
|
+
$evaluate_calls.should == 3
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should not call handler errors for missing methods" do
|
104
|
+
lambda { @client.missing }.should raise_error NoMethodError
|
105
|
+
$evaluate_calls.should == 0
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
class Splitter < Arpie::Protocol
|
4
|
+
def from binary
|
5
|
+
yield binary.size
|
6
|
+
binary.each_char do |c|
|
7
|
+
yield c
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Merger < Arpie::Protocol
|
13
|
+
def from binary
|
14
|
+
unless @expect
|
15
|
+
@expect = binary or incomplete!
|
16
|
+
gulp!
|
17
|
+
end
|
18
|
+
|
19
|
+
assemble! binary, :d, :size => @expect
|
20
|
+
end
|
21
|
+
|
22
|
+
def assemble binaries, token, meta
|
23
|
+
binaries.size >= meta[:size] or incomplete!
|
24
|
+
yield binaries.join('')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "Merger::Splitter::Sized" do subject { [Splitter, Arpie::SizedProtocol] }
|
29
|
+
it_should_behave_like "ProtocolChainSetup"
|
30
|
+
|
31
|
+
it "should split messages correctly" do
|
32
|
+
chain_write(t = 'test')
|
33
|
+
chain_read.should == t.size
|
34
|
+
for i in 1..t.size do
|
35
|
+
chain_read.should == t[i-1, 1]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "Merger::Splitter::Sized" do subject { [Merger, Splitter, Arpie::SizedProtocol] }
|
41
|
+
it_should_behave_like "ProtocolChainSetup"
|
42
|
+
|
43
|
+
it "should assemble split messages correctly" do
|
44
|
+
chain_write(t = 'test')
|
45
|
+
chain_read.should == t
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
describe "ProtocolChain", :shared => true do
|
4
|
+
it_should_behave_like "ProtocolChainSetup"
|
5
|
+
|
6
|
+
it "should convert without io correctly" do
|
7
|
+
v = @chain.to @testdata_a
|
8
|
+
@chain.from(v).should == [@testdata_a]
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should read written binary data correctly" do
|
12
|
+
chain_write(@testdata_a)
|
13
|
+
chain_read.should == @testdata_a
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should read multiple written messages correctly" do
|
17
|
+
write = []
|
18
|
+
for i in 0...4 do
|
19
|
+
write << (i % 2 == 0 ? @testdata_a : @testdata_b)
|
20
|
+
end
|
21
|
+
chain_write(*write)
|
22
|
+
|
23
|
+
for i in 0...4 do
|
24
|
+
chain_read.should == (i % 2 == 0 ? @testdata_a : @testdata_b)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should not clobber the buffer before the first read" do
|
29
|
+
chain_write(@testdata_a)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should parse a io-written buffer correctly" do
|
33
|
+
write = []
|
34
|
+
for i in 0...4 do
|
35
|
+
write << (i % 2 == 0 ? @testdata_a : @testdata_b)
|
36
|
+
end
|
37
|
+
chain_write(*write)
|
38
|
+
|
39
|
+
@chain.from(@r.readpartial(4096)).should == [@testdata_a, @testdata_b, @testdata_a, @testdata_b]
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should read messages greater than MTU correctly" do
|
43
|
+
chain_write(message = (@testdata_a * (Arpie::MTU + 10)))
|
44
|
+
chain_read.should == message
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should not fail on interleaved io streams" do
|
48
|
+
r2, w2 = IO.pipe
|
49
|
+
chain_write(@testdata_a)
|
50
|
+
@chain.write_message(w2, @testdata_b)
|
51
|
+
w2.close
|
52
|
+
chain_read.should == @testdata_a
|
53
|
+
@chain.read_message(r2).should == @testdata_b
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "ObjectProtocolChain", :shared => true do
|
58
|
+
it_should_behave_like "ProtocolChain"
|
59
|
+
# Now, lets try some variations.
|
60
|
+
|
61
|
+
it "should read written objects correctly" do
|
62
|
+
chain_write(
|
63
|
+
a = [1, 2.4, false, true, nil, "string"],
|
64
|
+
b = {1 => "hi", 2 => "test", 3 => "bloh"}
|
65
|
+
)
|
66
|
+
|
67
|
+
chain_read.should == a
|
68
|
+
chain_read.should == b
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "RPCProtocolChain", :shared => true do
|
73
|
+
it_should_behave_like "RPCProtocolChainSetup"
|
74
|
+
|
75
|
+
it "should send namespace-less RPC calls correctly encoded" do
|
76
|
+
call = Arpie::RPCall.new(nil, 'meth', [1, 2, 3])
|
77
|
+
@client.write_message(@w, call)
|
78
|
+
@w.close
|
79
|
+
@server.read_message(@r).should == call
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should send namespaced RPC calls correctly encoded" do
|
83
|
+
call = Arpie::RPCall.new('ns', 'meth', [1, 2, 3])
|
84
|
+
@client.write_message(@w, call)
|
85
|
+
@w.close
|
86
|
+
@server.read_message(@r).should == call
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should encode result values correctly" do
|
90
|
+
for r in inp = [1, 2.4, false, true, "string", {"1"=>"hash"}, [1,2,3]]
|
91
|
+
@server.write_message(@w, r)
|
92
|
+
end
|
93
|
+
@w.close
|
94
|
+
|
95
|
+
for r in inp
|
96
|
+
@client.read_message(@r).should == r
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Now, lets try some variations.
|
102
|
+
|
103
|
+
describe "Sized" do subject { [Arpie::SizedProtocol] }
|
104
|
+
it_should_behave_like "ProtocolChain"
|
105
|
+
end
|
106
|
+
|
107
|
+
describe "Sized::Sized" do subject { [Arpie::SizedProtocol, Arpie::SizedProtocol] }
|
108
|
+
it_should_behave_like "ProtocolChain"
|
109
|
+
end
|
110
|
+
|
111
|
+
describe "Sized::Marshal::Sized" do subject { [Arpie::SizedProtocol, Arpie::MarshalProtocol, Arpie::SizedProtocol] }
|
112
|
+
it_should_behave_like "ProtocolChain"
|
113
|
+
end
|
114
|
+
|
115
|
+
# Shellwords is a bit of a special case, because it only encodes arrays.
|
116
|
+
describe "Shellwords::Separator" do subject { [Arpie::ShellwordsProtocol, Arpie::SeparatorProtocol] }
|
117
|
+
it_should_behave_like "ProtocolChain"
|
118
|
+
before do
|
119
|
+
@testdata_a, @testdata_b = ['I am test', '1'], ['I am test', '2']
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
describe "HTTPTest" do subject { [Arpie::HTTPClientTestProtocol] }
|
124
|
+
it_should_behave_like "ProtocolChain"
|
125
|
+
end
|
126
|
+
|
127
|
+
describe "HTTPTest" do subject { [Arpie::HTTPServerTestProtocol] }
|
128
|
+
it_should_behave_like "ProtocolChain"
|
129
|
+
end
|
130
|
+
|
131
|
+
describe "YAML" do subject { [Arpie::YAMLProtocol] }
|
132
|
+
it_should_behave_like "ObjectProtocolChain"
|
133
|
+
end
|
134
|
+
|
135
|
+
describe "XMLRPC::Sized" do subject {
|
136
|
+
[
|
137
|
+
[Arpie::XMLRPCClientProtocol, Arpie::SizedProtocol],
|
138
|
+
[Arpie::XMLRPCServerProtocol, Arpie::SizedProtocol]
|
139
|
+
]
|
140
|
+
}
|
141
|
+
it_should_behave_like "RPCProtocolChain"
|
142
|
+
end
|
143
|
+
|
144
|
+
describe "XMLRPC::HTTPTest" do subject {
|
145
|
+
[
|
146
|
+
[Arpie::XMLRPCClientProtocol, Arpie::HTTPClientTestProtocol],
|
147
|
+
[Arpie::XMLRPCServerProtocol, Arpie::HTTPServerTestProtocol]
|
148
|
+
]
|
149
|
+
}
|
150
|
+
it_should_behave_like "RPCProtocolChain"
|
151
|
+
end
|
152
|
+
|