arpie 0.0.3 → 0.0.4

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.
@@ -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
- raise NoMethodError, "No such method: #{message.meth.inspect}"
48
+ endpoint.write_message NoMethodError.new("No such method: #{message.meth.inspect}")
49
+ return
49
50
  end
50
51
 
51
- # Prune old serials. This can probably be optimized, but works well enough for now.
52
- if @uuid_tracking && message.uuid
53
- timestamps = @uuids.values.map {|v| v[0] }.sort
54
- latest_timestamp = timestamps[-@max_uuids]
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
- endpoint.write_message((@uuids[message.uuid] ||=
60
- [Time.now, @handler.send(message.meth, *message.argv)])[1])
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
- else
63
- endpoint.write_message @handler.send(message.meth, *message.argv)
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 protocol, namespace = ""
75
- super(protocol)
76
- @protocol, @namespace = protocol, namespace
85
+ def initialize *protocols
86
+ super
87
+ @protocol, @namespace = protocol, ""
77
88
  @uuid_generator = lambda {|client, method, argv|
78
- UUID.timestamp_create.to_i.to_s
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 = RPCProtocol::Call.new(@namespace, meth, argv, uuid)
109
+ call = Arpie::RPCall.new(@namespace, meth, argv, uuid)
99
110
  ret = self.request(call)
100
111
  case ret
101
112
  when Exception
@@ -36,11 +36,11 @@ module Arpie
36
36
 
37
37
  attr_reader :endpoints
38
38
 
39
- # Create a new Server with the given +Protocol+.
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 protocol
43
- @protocol = 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.endpoint_klass.new(@protocol.clone, client)
123
+ c = @protocol.endpoint_class.new(@protocol.clone, client)
124
124
  Thread.new { _read_thread(c) }
125
125
  end
126
126
  end
@@ -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
+