marilyn-rpc 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/LICENCE +18 -0
  2. data/README.md +170 -0
  3. data/doc/MarilynRPC/CallRequestMail.html +578 -0
  4. data/doc/MarilynRPC/CallResponseMail.html +418 -0
  5. data/doc/MarilynRPC/Envelope.html +705 -0
  6. data/doc/MarilynRPC/ExceptionMail.html +338 -0
  7. data/doc/MarilynRPC/Gentleman.html +658 -0
  8. data/doc/MarilynRPC/MailFactory.html +284 -0
  9. data/doc/MarilynRPC/MailHelper.html +489 -0
  10. data/doc/MarilynRPC/NativeClient.html +579 -0
  11. data/doc/MarilynRPC/NativeClientProxy.html +303 -0
  12. data/doc/MarilynRPC/Server.html +406 -0
  13. data/doc/MarilynRPC/Service.html +599 -0
  14. data/doc/MarilynRPC/ServiceCache.html +481 -0
  15. data/doc/MarilynRPC.html +108 -0
  16. data/doc/_index.html +219 -0
  17. data/doc/class_list.html +36 -0
  18. data/doc/css/common.css +1 -0
  19. data/doc/css/full_list.css +53 -0
  20. data/doc/css/style.css +318 -0
  21. data/doc/file.README.html +154 -0
  22. data/doc/file_list.html +38 -0
  23. data/doc/frames.html +13 -0
  24. data/doc/index.html +154 -0
  25. data/doc/js/app.js +203 -0
  26. data/doc/js/full_list.js +149 -0
  27. data/doc/js/jquery.js +16 -0
  28. data/doc/method_list.html +467 -0
  29. data/doc/top-level-namespace.html +88 -0
  30. data/examples/async/client.rb +40 -0
  31. data/examples/async/server.rb +26 -0
  32. data/examples/callbacks/client.rb +8 -0
  33. data/examples/callbacks/server.rb +26 -0
  34. data/examples/secure/client.rb +9 -0
  35. data/examples/secure/server.rb +22 -0
  36. data/kiss.png +0 -0
  37. data/lib/marilyn-rpc/client.rb +91 -26
  38. data/lib/marilyn-rpc/gentleman.rb +4 -1
  39. data/lib/marilyn-rpc/mails.rb +1 -1
  40. data/lib/marilyn-rpc/server.rb +27 -3
  41. data/lib/marilyn-rpc/service.rb +64 -0
  42. data/lib/marilyn-rpc/service_cache.rb +13 -2
  43. data/lib/marilyn-rpc/version.rb +1 -1
  44. metadata +39 -3
data/kiss.png ADDED
Binary file
@@ -1,35 +1,23 @@
1
1
  require 'socket'
2
+ require 'thread'
2
3
 
3
4
  module MarilynRPC
4
- class NativeClientProxy
5
+ class BlankSlate
6
+ instance_methods.each { |m| undef_method m unless m =~ /^__/ }
7
+ end
8
+
9
+ class NativeClientProxy < BlankSlate
5
10
  # Creates a new Native client proxy, were the calls get send to the remote
6
11
  # side.
7
12
  # @param [Object] path the path that is used to identify the service
8
- # @param [Socekt] socket the socket to use for communication
9
- def initialize(path, socket)
10
- @path, @socket = path, socket
13
+ # @param [NativeClient] client the client to use for communication
14
+ def initialize(path, client)
15
+ @path, @client = path, client
11
16
  end
12
17
 
13
18
  # Handler for calls to the remote system
14
19
  def method_missing(method, *args, &block)
15
- # since this client can't multiplex, we set the tag to nil
16
- @socket.write(MarilynRPC::MailFactory.build_call(nil, @path, method, args))
17
-
18
- # read the answer of the server back in
19
- answer = MarilynRPC::Envelope.new
20
- # read the header to have the size
21
- answer.parse!(@socket.read(4))
22
- # so now that we know the site, read the rest of the envelope
23
- answer.parse!(@socket.read(answer.size))
24
-
25
- # returns the result part of the mail or raise the exception if there is
26
- # one
27
- mail = MarilynRPC::MailFactory.unpack(answer)
28
- if mail.is_a? MarilynRPC::CallResponseMail
29
- mail.result
30
- else
31
- raise mail.exception
32
- end
20
+ @client.execute(@path, method, args)
33
21
  end
34
22
  end
35
23
 
@@ -47,6 +35,27 @@ module MarilynRPC
47
35
  # @param [Socket] socket the socket to manage
48
36
  def initialize(socket)
49
37
  @socket = socket
38
+ @semaphore = Mutex.new
39
+ @threads = {}
40
+ @responses = {}
41
+ @thread = Thread.new do
42
+ loop do
43
+ # read the answer of the server back in
44
+ answer = MarilynRPC::Envelope.new
45
+ # read the header to have the size
46
+ answer.parse!(@socket.read(4))
47
+ # so now that we know the site, read the rest of the envelope
48
+ answer.parse!(@socket.read(answer.size))
49
+
50
+ # returns the result part of the mail or raise the exception if there is
51
+ # one
52
+ mail = MarilynRPC::MailFactory.unpack(answer)
53
+ @semaphore.synchronize do
54
+ @responses[mail.tag] = mail # save the mail for the waiting thread
55
+ @threads.delete(mail.tag).wakeup # wake up the waiting thread
56
+ end
57
+ end
58
+ end
50
59
  end
51
60
 
52
61
  # Disconnect the client from the remote.
@@ -60,7 +69,7 @@ module MarilynRPC
60
69
  # @return [MarilynRPC::NativeClientProxy] the proxy obejct that will serve
61
70
  # all calls
62
71
  def for(path)
63
- NativeClientProxy.new(path, @socket)
72
+ NativeClientProxy.new(path, self)
64
73
  end
65
74
 
66
75
  # Connect to a unix domain socket.
@@ -73,9 +82,65 @@ module MarilynRPC
73
82
  # Connect to a tcp socket.
74
83
  # @param [String] host the host to cennect to (e.g. 'localhost')
75
84
  # @param [Integer] port the port to connect to (e.g. 8000)
76
- # @return [MarilynRPC::NativeClient] the cónnected client
77
- def self.connect_tcp(host, port)
78
- new(TCPSocket.open(host, port))
85
+ # @param [Hash] options the
86
+ # @option options [Boolean] :secure use tls/ssl for the connection
87
+ # `true` or `false`
88
+ # @option options [OpenSSL::SSL::SSLContext] :ssl_context can be used to
89
+ # change the ssl context of the newly created secure connection. Only
90
+ # takes effect if `:secure` option is enabled.
91
+ # @return [MarilynRPC::NativeClient] the connected client
92
+ def self.connect_tcp(host, port, options = {})
93
+ if options[:secure] == true
94
+ require 'openssl' # use openssl for secure connections
95
+ socket = TCPSocket.new(host, port)
96
+ if ssl_context = options[:ssl_context]
97
+ secure_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
98
+ else
99
+ secure_socket = OpenSSL::SSL::SSLSocket.new(socket)
100
+ end
101
+ secure_socket.connect
102
+ new(secure_socket)
103
+ else
104
+ new(TCPSocket.open(host, port))
105
+ end
106
+ end
107
+
108
+ # Executes a client call blocking. To issue an async call one needs to
109
+ # have start separate threads. THe Native client uses then multiplexing to
110
+ # avoid the other threads blocking.
111
+ # @api private
112
+ # @param [Object] path the path to identifiy the service
113
+ # @param [Symbol, String] method the method name to call on the service
114
+ # @param [Array<Object>] args the arguments that are passed to the remote
115
+ # side
116
+ # @return [Object] the result of the call
117
+ def execute(path, method, args)
118
+ thread = Thread.current
119
+ tag = "#{Time.now.to_f}:#{thread.object_id}"
120
+
121
+ @semaphore.synchronize do
122
+ # since this client can't multiplex, we set the tag to nil
123
+ @socket.write(MarilynRPC::MailFactory.build_call(tag, path, method, args))
124
+ end
125
+
126
+ # lets write our self to the list of waining threads
127
+ @semaphore.synchronize { @threads[tag] = thread }
128
+
129
+ # enable the listening for a response from the remote end
130
+ @thread.wakeup
131
+
132
+ # stop the current thread, the thread will be started after the response
133
+ # arrived
134
+ Thread.stop
135
+
136
+ # get mail from responses
137
+ mail = @semaphore.synchronize { @responses.delete(tag) }
138
+
139
+ if mail.is_a? MarilynRPC::CallResponseMail
140
+ mail.result
141
+ else
142
+ raise mail.exception
143
+ end
79
144
  end
80
145
  end
81
146
  end
@@ -51,12 +51,15 @@ class MarilynRPC::Gentleman
51
51
  # The handler that will send the response to the remote system
52
52
  # @param [Object] args the arguments that should be handled by the callback,
53
53
  # the reponse of the callback will be send as result
54
+ # @api private
54
55
  def handle(*args)
55
56
  mail = MarilynRPC::CallResponseMail.new(self.tag, self.callback.call(*args))
56
57
  connection.send_mail(mail)
57
58
  end
58
59
 
59
- # The helper that will be called by the deferable to call {handle} later
60
+ # The helper that will be called by the deferable to call
61
+ # {MarilynRPC::Gentleman#handle} later
62
+ # @api private
60
63
  def helper
61
64
  gentleman = self
62
65
  lambda { |*args| gentleman.handle(*args) }
@@ -56,7 +56,7 @@ module MarilynRPC
56
56
  end
57
57
  end
58
58
 
59
- class ExceptionMail < Struct.new(:exception)
59
+ class ExceptionMail < Struct.new(:tag, :exception)
60
60
  include MarilynRPC::MailHelper
61
61
  TYPE = MarilynRPC::MailHelper.type(3)
62
62
 
@@ -1,9 +1,30 @@
1
+ # The server will be used to make incomming connections possible. The server
2
+ # handles the low level networking functions, so that the services don't have
3
+ # to deal with them. Because of the way eventmachine works you can have as many
4
+ # servers as you want.
5
+ #
6
+ # @example a server which is available througth 3 connections:
7
+ # EM.run {
8
+ # EM.start_server "localhost", 8000, MarilynRPC::Server
9
+ # EM.start_server "localhost", 8008, MarilynRPC::Server, :secure => true
10
+ # EM.start_unix_domain_server("tmp.socket", MarilynRPC::Server)
11
+ # }
12
+ #
1
13
  module MarilynRPC::Server
14
+ # initalize the server with connection options
15
+ # @param [Hash] options the options passed to the server
16
+ # @option options [Boolean] :secure enable secure transfer for the server
17
+ # possible values `true` or `false`
18
+ def initialize(options = {})
19
+ @secure = options[:secure]
20
+ end
21
+
2
22
  # Initialize the first recieving envelope for the connection and create the
3
23
  # service cache since each connection gets it's own service instance.
4
24
  def post_init
5
25
  @envelope = MarilynRPC::Envelope.new
6
26
  @cache = MarilynRPC::ServiceCache.new
27
+ start_tls if @secure
7
28
  end
8
29
 
9
30
  # Handler for the incoming data. EventMachine compatible.
@@ -15,14 +36,15 @@ module MarilynRPC::Server
15
36
  if @envelope.finished?
16
37
  begin
17
38
  # grep the request
18
- answer = @cache.call(MarilynRPC::MailFactory.unpack(@envelope))
39
+ mail = MarilynRPC::MailFactory.unpack(@envelope)
40
+ answer = @cache.call(mail)
19
41
  if answer.is_a? MarilynRPC::CallResponseMail
20
42
  send_mail(answer)
21
43
  else
22
44
  answer.connection = self # pass connection for async responses
23
45
  end
24
46
  rescue => exception
25
- send_mail(MarilynRPC::ExceptionMail.new(exception))
47
+ send_mail(MarilynRPC::ExceptionMail.new(mail.tag, exception))
26
48
  end
27
49
 
28
50
  # initialize the next envelope
@@ -39,5 +61,7 @@ module MarilynRPC::Server
39
61
  end
40
62
 
41
63
  # Handler for client disconnect
42
- def unbind; end
64
+ def unbind
65
+ @cache.disconnect!
66
+ end
43
67
  end
@@ -1,3 +1,24 @@
1
+ # This class represents the base for all events, it is used for registering
2
+ # services and defining callbacks for certain service events.
3
+ # @example a service that makes use of the available helpers
4
+ # class EventsService < MarilynRPC::Service
5
+ # register :events
6
+ # after_connect :connected
7
+ # after_disconnect :disconnected
8
+ #
9
+ # def connected
10
+ # puts "client connected"
11
+ # end
12
+ #
13
+ # def notify(msg)
14
+ # puts msg
15
+ # end
16
+ #
17
+ # def disconnected
18
+ # puts "client disconnected"
19
+ # end
20
+ # end
21
+ #
1
22
  class MarilynRPC::Service
2
23
  # registers the class where is was called as a service
3
24
  # @param [String] path the path of the service
@@ -7,9 +28,52 @@ class MarilynRPC::Service
7
28
  end
8
29
 
9
30
  # returns all services, that where registered
31
+ # @api private
10
32
  # @return [Hash<String, Object>] all registered services with path as key and
11
33
  # the registered service as object
12
34
  def self.registry
13
35
  @@registry || {}
14
36
  end
37
+
38
+ # register one or more connect callbacks, a callback is simply a method
39
+ # defined in the class
40
+ # @param [Array<Symbol>, Array<String>] callbacks the method names
41
+ def self.after_connect(*callbacks)
42
+ register_callbacks :after_connect, callbacks
43
+ end
44
+
45
+ # register one or more disconnect callbacks, a callback is simply a method
46
+ # defined in the class
47
+ # @param [Array<Symbol>, Array<String>] callbacks the method names
48
+ def self.after_disconnect(*callbacks)
49
+ register_callbacks :after_disconnect, callbacks
50
+ end
51
+
52
+ # registers a callbacks for the service class
53
+ # @param [Symbol] name the name under which the callbacks should be saved
54
+ # @param [Array<Symbol>, Array<String>] callbacks the method names
55
+ # @api private
56
+ def self.register_callbacks(name, callbacks)
57
+ @_callbacks ||= {} # initialize callbacks
58
+ @_callbacks[name] ||= [] # initialize specific set
59
+ @_callbacks[name] += callbacks
60
+ end
61
+
62
+ # returns the registered callbacks for name
63
+ # @param [Symbol] name the name to lookup callbacks for
64
+ # @return [Array<String>, Array<Symbol>] an array of callback names, or an
65
+ # empty array
66
+ # @api private
67
+ def self.registered_callbacks(name)
68
+ (@_callbacks || {})[name] || []
69
+ end
70
+
71
+ # calls the defined connect callbacks
72
+ # @param [Symbol] the name of the callbacks to run
73
+ # @api private
74
+ def run_callbacks!(name)
75
+ self.class.registered_callbacks(name).each do |callback|
76
+ self.send(callback)
77
+ end
78
+ end
15
79
  end
@@ -39,9 +39,20 @@ class MarilynRPC::ServiceCache
39
39
  return service
40
40
  # it's not in the cache, so try lookup in the service registry
41
41
  elsif service = MarilynRPC::Service.registry[path]
42
- return (@services[path] = service.new)
42
+ @services[path] = service.new
43
+ @services[path].run_callbacks! :after_connect
44
+ return @services[path]
43
45
  else
44
- raise ArgumentError.new("Service #{mail.path} unknown!")
46
+ raise ArgumentError.new("Service #{path} unknown!")
47
+ end
48
+ end
49
+
50
+ # issue the disconnect callbacks for all living services of this connection
51
+ # @node this should only be called once by the server
52
+ # @api private
53
+ def disconnect!
54
+ @services.each do |path, service|
55
+ service.run_callbacks! :after_disconnect
45
56
  end
46
57
  end
47
58
  end
@@ -1,3 +1,3 @@
1
1
  module MarilynRPC
2
- VERSION = '0.0.1'
2
+ VERSION = '0.0.2'
3
3
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: marilyn-rpc
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
4
+ hash: 27
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 1
10
- version: 0.0.1
9
+ - 2
10
+ version: 0.0.2
11
11
  platform: ruby
12
12
  authors:
13
13
  - Vincent Landgraf
@@ -62,6 +62,40 @@ extra_rdoc_files: []
62
62
  files:
63
63
  - benchmark/client.rb
64
64
  - benchmark/server.rb
65
+ - doc/_index.html
66
+ - doc/class_list.html
67
+ - doc/css/common.css
68
+ - doc/css/full_list.css
69
+ - doc/css/style.css
70
+ - doc/file.README.html
71
+ - doc/file_list.html
72
+ - doc/frames.html
73
+ - doc/index.html
74
+ - doc/js/app.js
75
+ - doc/js/full_list.js
76
+ - doc/js/jquery.js
77
+ - doc/MarilynRPC/CallRequestMail.html
78
+ - doc/MarilynRPC/CallResponseMail.html
79
+ - doc/MarilynRPC/Envelope.html
80
+ - doc/MarilynRPC/ExceptionMail.html
81
+ - doc/MarilynRPC/Gentleman.html
82
+ - doc/MarilynRPC/MailFactory.html
83
+ - doc/MarilynRPC/MailHelper.html
84
+ - doc/MarilynRPC/NativeClient.html
85
+ - doc/MarilynRPC/NativeClientProxy.html
86
+ - doc/MarilynRPC/Server.html
87
+ - doc/MarilynRPC/Service.html
88
+ - doc/MarilynRPC/ServiceCache.html
89
+ - doc/MarilynRPC.html
90
+ - doc/method_list.html
91
+ - doc/top-level-namespace.html
92
+ - examples/async/client.rb
93
+ - examples/async/server.rb
94
+ - examples/callbacks/client.rb
95
+ - examples/callbacks/server.rb
96
+ - examples/secure/client.rb
97
+ - examples/secure/server.rb
98
+ - kiss.png
65
99
  - lib/marilyn-rpc/client.rb
66
100
  - lib/marilyn-rpc/envelope.rb
67
101
  - lib/marilyn-rpc/gentleman.rb
@@ -71,7 +105,9 @@ files:
71
105
  - lib/marilyn-rpc/service_cache.rb
72
106
  - lib/marilyn-rpc/version.rb
73
107
  - lib/marilyn-rpc.rb
108
+ - LICENCE
74
109
  - marilyn-rpc.gemspec
110
+ - README.md
75
111
  - spec/envelope_spec.rb
76
112
  - spec/gentleman_spec.rb
77
113
  - spec/mails_spec.rb