myxi 1.2.0 → 1.4.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cdd11c1300de89acea4cb8a73d3992613d58f7f2
4
- data.tar.gz: c2ea7abf4a441def1601963eb2ffe7f4e50dedeb
3
+ metadata.gz: 938c04e3a273895a36a7658893822de7f0409322
4
+ data.tar.gz: 13b6a2a52b393e9139b94a9902f817b0d43445d1
5
5
  SHA512:
6
- metadata.gz: 2efc388890bf6c0650b658af3179dcba083a970cc9802b39b43892b5d006a87b188e3d3cb05cc19ababdcdbf03fb03fe315572bef505363233b3ab9129cad33f
7
- data.tar.gz: a102bd2d3fb000bfa6aa700133a2c2ab439e222d69897db61a76b1752fbd8994c8438b85dde6e7efd803e0e5391be4832448a1854ee43c0dd38794b372aeeee1
6
+ metadata.gz: 83411ecfe5283f033c8c74252acebeb27915141a7426510f8b15d79029744c3ff661e97459bf14c89de3b75058b323470a5bf19d44ab1e1e870be2a627257a23
7
+ data.tar.gz: 780e26b6594c422d500544c1c4d55adde585e2cdea5c0385c89ac661f0fb7102ac7354e9feb88a6051a6aae5f9affe9757506c0782eaeddcc8db5ead335282f1
@@ -20,6 +20,8 @@ module Myxi
20
20
  rescue Environment::Error => e
21
21
  session.send('Error', :error => e.class.to_s.split('::').last)
22
22
  rescue => e
23
+ Myxi.logger.debug "[#{session.id}] \e[41;37mERROR\e[0m \e[31m#{e.class.to_s} #{e.message}\e[0m"
24
+ e.backtrace { |br| Myxi.logger.debug "[#{session.id}] \e[41;37mERROR\e[0m #{br}" }
23
25
  session.send('InternalError', :error => e.class.to_s, :message => e.message)
24
26
  end
25
27
 
@@ -0,0 +1,42 @@
1
+ module Myxi
2
+ class EventableSocket
3
+ def initialize(event_loop, socket)
4
+ @event_loop = event_loop
5
+ @socket = socket
6
+ @monitor = @event_loop.selector.register(@socket, :r)
7
+ @monitor.value = self
8
+ @read_buffer = String.new.force_encoding('BINARY')
9
+ @write_buffer = String.new.force_encoding('BINARY')
10
+ end
11
+
12
+ def handle_w
13
+ bytes_sent = @socket.write_nonblock(@write_buffer)
14
+ # Send as much data as possible
15
+ if bytes_sent >= @write_buffer.bytesize
16
+ @write_buffer = String.new.force_encoding('BINARY')
17
+ @monitor.interests = :r
18
+ close if @close_after_write
19
+ else
20
+ @write_buffer.slice!(0, bytes_sent)
21
+ end
22
+ rescue Errno::ECONNRESET, IOError
23
+ close
24
+ end
25
+
26
+ def write(data)
27
+ @event_loop.wakeup
28
+ @write_buffer << data.force_encoding('BINARY')
29
+ @monitor.interests = :rw
30
+ end
31
+
32
+ def close_after_write
33
+ @close_after_write = true
34
+ @monitor.interests = :w
35
+ end
36
+
37
+ def close
38
+ @socket.close
39
+ @event_loop.selector.deregister(@socket)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,34 @@
1
+ require 'socket'
2
+ require 'myxi/session'
3
+ module Myxi
4
+ class Listener
5
+
6
+ def initialize(event_loop, options)
7
+ @event_loop = event_loop
8
+ port = (options[:port] || ENV['MYXI_PORT'] || ENV['PORT'] || 5005).to_i
9
+ Myxi.logger.info "Running Myxi Web Socket Server on 0.0.0.0:#{port}"
10
+ if ENV['SERVER_FD']
11
+ @socket = TCPServer.for_fd(ENV['SERVER_FD'].to_i)
12
+ Process.kill('TERM', Process.ppid)
13
+ else
14
+ @socket = TCPServer.open(options[:bind_address] || ENV['MYXI_BIND_ADDRESS'], port)
15
+ ENV['SERVER_FD'] = @socket.to_i.to_s
16
+ end
17
+ @socket.close_on_exec = false
18
+ monitor = event_loop.selector.register(@socket, :r)
19
+ monitor.value = self
20
+ end
21
+
22
+ def handle_r
23
+ # Incoming client connection
24
+ client_socket = @socket.accept
25
+ Session.new(@event_loop, client_socket)
26
+ end
27
+
28
+ def close
29
+ @socket.close
30
+ @event_loop.selector.deregister(@socket)
31
+ end
32
+
33
+ end
34
+ end
@@ -1,87 +1,98 @@
1
- require 'json'
2
- require 'em-websocket'
3
- require 'myxi'
4
- require 'myxi/session'
5
- require 'myxi/action'
1
+ require 'nio'
2
+ require 'timers'
3
+ require 'myxi/listener'
6
4
 
7
5
  module Myxi
8
6
  class Server
9
-
10
- attr_reader :options
7
+ attr_reader :selector, :timers, :options, :sessions
11
8
 
12
9
  def initialize(options = {})
13
10
  @options = options
11
+ @selector = NIO::Selector.new
12
+ @timers = Timers::Group.new
13
+ @sessions = []
14
14
  end
15
15
 
16
- def sessions
17
- @sessions ||= []
18
- end
19
-
20
- def monitor_sessions
21
- unless options[:touch_interval] == 0
22
- Thread.new do
23
- loop do
24
- sessions.each(&:touch)
25
- sleep options[:touch_interval] || 60
26
- end
27
- end
28
- end
16
+ def wakeup
17
+ @selector.wakeup
29
18
  end
30
19
 
31
20
  def run
32
21
  Myxi::Exchange.declare_all
33
- port = (options[:port] || ENV['MYXI_PORT'] || ENV['PORT'] || 5005).to_i
34
- Myxi.logger.info "Running Myxi Web Socket Server on 0.0.0.0:#{port}"
35
- monitor_sessions
36
- EM.run do
37
- EM::WebSocket.run(:host => options[:bind_address] || ENV['MYXI_BIND_ADDRESS'] || '0.0.0.0', :port => port) do |ws|
22
+ @listener = Listener.new(self, options)
38
23
 
39
- sessions << session = Session.new(self, ws)
24
+ unless options[:touch_interval] == 0
25
+ @timers.every(options[:touch_interval] || 60) do
26
+ @sessions.each(&:touch)
27
+ end
28
+ end
40
29
 
41
- ws.onopen do |handshake|
42
- case handshake.path
43
- when /\A\/pushwss/
44
- Myxi.logger.debug "[#{session.id}] Connection opened"
45
- ws.send({:event => 'Welcome', :payload => {:id => session.id}}.to_json)
30
+ Signal.trap("TERM") do
31
+ if @options[:shutdown_time]
32
+ @timers.after(0) do
33
+ Myxi.logger.info("Received TERM signal, beginning #{@options[:shutdown_time]} second shutdown.")
34
+ @listener.close
35
+ end
46
36
 
47
- session.queue = Myxi.channel.queue("", :exclusive => true)
48
- session.queue.subscribe do |delivery_info, properties, body|
49
- if hash = JSON.parse(body) rescue nil
50
- hash['mq'] = {'e' => delivery_info.exchange, 'rk' => delivery_info.routing_key}
51
- ws.send(hash.to_json.force_encoding('UTF-8'))
52
- end
37
+ @timers.every(1) do
38
+ @shutdown_timer ||= 0
39
+ @sessions.each do |session|
40
+ if session.hash % @options[:shutdown_time] == @shutdown_timer % @options[:shutdown_time]
41
+ session.close
53
42
  end
54
- else
55
- Myxi.logger.debug "[#{session.id}] Invalid path"
56
- ws.send({:event => 'Error', :payload => {:error => 'PathNotFound'}}.to_json)
57
- ws.close
58
43
  end
59
- end
44
+ @shutdown_timer += 1
60
45
 
61
- ws.onclose do
62
- session.close
63
- sessions.delete(session)
46
+ if @sessions.size == 0
47
+ Myxi.logger.info("All clients disconnected. Shutdown complete.")
48
+ Process.exit(0)
49
+ end
50
+ end
51
+ wakeup
52
+ else
53
+ @timers.after(0) do
54
+ Myxi.logger.info("Received TERM signal, shutting down immediately")
55
+ Process.exit(0)
64
56
  end
57
+ end
58
+ end
65
59
 
66
- ws.onmessage do |msg|
67
- if ws.state == :connected
68
- json = JSON.parse(msg) rescue nil
69
- if json.is_a?(Hash)
70
- session.tag = json['tag'] || nil
71
- payload = json['payload'] || {}
72
- if action = Myxi::Action::ACTIONS[json['action'].to_s.to_sym]
73
- action.execute(session, payload)
74
- else
75
- ws.send({:event => 'Error', :tag => session.tag, :payload => {:error => 'InvalidAction'}}.to_json)
76
- end
60
+ loop do
61
+ wi = @timers.wait_interval
62
+ if wi > 0
63
+ wait_interval = wi
64
+ else
65
+ wait_interval = 0
66
+ end
67
+
68
+ selector.select(wait_interval) do |monitor|
69
+ begin
70
+ monitor.value.handle_r if monitor.readable?
71
+ monitor.value.handle_w if monitor.writeable?
72
+ rescue => e
73
+ # Try to recover wherever possible
74
+ if monitor && monitor.value
75
+ if monitor.value == @listener
76
+ raise
77
77
  else
78
- ws.send({:event => 'Error', :payload => {:error => 'InvalidJSON'}}.to_json)
78
+ monitor.value.close rescue nil
79
79
  end
80
+ else
81
+ raise
82
+ end
83
+ begin
84
+ Myxi.logger.info(e.class.to_s + ' ' + e.message.to_s)
85
+ e.backtrace.each do |line|
86
+ Myxi.logger.info(' ' + line)
87
+ end
88
+ rescue
80
89
  end
81
90
  end
82
91
  end
92
+ @timers.fire
83
93
  end
84
94
 
85
95
  end
96
+
86
97
  end
87
98
  end
@@ -1,24 +1,85 @@
1
+ require 'websocket'
1
2
  require 'json'
2
3
  require 'myxi/exchange'
4
+ require 'myxi/eventable_socket'
3
5
 
4
6
  module Myxi
5
- class Session
7
+ class Session < EventableSocket
6
8
 
7
- def initialize(server, ws)
8
- @server = server
9
- @ws = ws
9
+ def initialize(event_loop, client_socket)
10
10
  @id = SecureRandom.hex(8)
11
11
  @closure_callbacks = []
12
12
  @data = {}
13
+
14
+ @handshake = WebSocket::Handshake::Server.new
15
+ @state = :handshake
16
+ super
17
+ @event_loop.sessions << self
18
+
13
19
  end
14
20
 
15
21
  attr_reader :id
16
- attr_reader :server
17
- attr_reader :ws
18
- attr_accessor :queue
22
+ #attr_accessor :queue
19
23
  attr_accessor :auth_object
20
24
  attr_accessor :tag
21
25
 
26
+ def on_connect
27
+ Myxi.logger.debug "[#{id}] Connection opened"
28
+ send_text_data({:event => 'Welcome', :payload => {:id => id}}.to_json)
29
+ begin
30
+ @queue = Myxi.channel.queue("", :exclusive => true)
31
+ rescue NoMethodError
32
+ # This exception may be raised when something goes very wrong with the RabbitMQ connection
33
+ # Unfortunately the only practical solution is to restart the client
34
+ Process.exit(1)
35
+ end
36
+ @queue.subscribe do |delivery_info, properties, body|
37
+ if hash = JSON.parse(body) rescue nil
38
+ hash['mq'] = {'e' => delivery_info.exchange, 'rk' => delivery_info.routing_key}
39
+ payload = hash.to_json.force_encoding('UTF-8')
40
+ Myxi.logger.debug "[#{id}] \e[45;37mEVENT\e[0m \e[35m#{payload}\e[0m (to #{delivery_info.exchange}/#{delivery_info.routing_key})"
41
+ send_text_data(payload)
42
+ end
43
+ end
44
+ end
45
+
46
+ def handle_r
47
+ case @state
48
+ when :handshake
49
+ @handshake << @socket.readpartial(1048576)
50
+ if @handshake.finished?
51
+ write(@handshake.to_s)
52
+ if @handshake.valid?
53
+ on_connect
54
+ @state = :established
55
+ @frame_handler = WebSocket::Frame::Incoming::Server.new(version: @handshake.version)
56
+ else
57
+ close_after_write
58
+ end
59
+ end
60
+ when :established
61
+ @frame_handler << @socket.readpartial(1048576)
62
+ while frame = @frame_handler.next
63
+ msg = frame.data
64
+ json = JSON.parse(msg) rescue nil
65
+ if json.is_a?(Hash)
66
+ tag = json['tag'] || nil
67
+ payload = json['payload'] || {}
68
+ Myxi.logger.debug "[#{id}] \e[43;37mACTION\e[0m \e[33m#{json}\e[0m"
69
+ if action = Myxi::Action::ACTIONS[json['action'].to_s.to_sym]
70
+ action.execute(self, payload)
71
+ else
72
+ send_text_data({:event => 'Error', :tag => tag, :payload => {:error => 'InvalidAction'}}.to_json)
73
+ end
74
+ else
75
+ send_text_data({:event => 'Error', :payload => {:error => 'InvalidJSON'}}.to_json)
76
+ end
77
+ end
78
+ end
79
+ rescue EOFError, Errno::ECONNRESET, IOError
80
+ close
81
+ end
82
+
22
83
  def [](name)
23
84
  @data[name.to_sym]
24
85
  end
@@ -39,7 +100,9 @@ module Myxi
39
100
  # Send an event back to the client on this session
40
101
  #
41
102
  def send(name, payload = {})
42
- ws.send({:event => name, :tag => tag, :payload => payload}.to_json.force_encoding('UTF-8'))
103
+ payload = {:event => name, :tag => tag, :payload => payload}.to_json.force_encoding('UTF-8')
104
+ send_text_data(payload)
105
+ Myxi.logger.debug "[#{id}] \e[46;37mMESSAGE\e[0m \e[36m#{payload}\e[0m"
43
106
  end
44
107
 
45
108
  #
@@ -52,9 +115,9 @@ module Myxi
52
115
  if subscriptions[exchange_name.to_s].include?(routing_key.to_s)
53
116
  send('Error', :error => 'AlreadySubscribed', :exchange => exchange_name, :routing_key => routing_key)
54
117
  else
55
- queue.bind(exchange.exchange_name.to_s, :routing_key => routing_key.to_s)
118
+ @queue.bind(exchange.exchange_name.to_s, :routing_key => routing_key.to_s)
56
119
  subscriptions[exchange_name.to_s] << routing_key.to_s
57
- Myxi.logger.debug "[#{id}] Subscribed to #{exchange_name} / #{routing_key}"
120
+ Myxi.logger.debug "[#{id}] \e[42;37mSUBSCRIBED\e[0m \e[32m#{exchange_name} / #{routing_key}\e[0m"
58
121
  send('Subscribed', :exchange => exchange_name, :routing_key => routing_key)
59
122
  end
60
123
  else
@@ -69,11 +132,11 @@ module Myxi
69
132
  # Unsubscribe this session from the given exchange name and routing key
70
133
  #
71
134
  def unsubscribe(exchange_name, routing_key, auto = false)
72
- queue.unbind(exchange_name.to_s, :routing_key => routing_key.to_s)
135
+ @queue.unbind(exchange_name.to_s, :routing_key => routing_key.to_s)
73
136
  if subscriptions[exchange_name.to_s]
74
137
  subscriptions[exchange_name.to_s].delete(routing_key.to_s)
75
138
  end
76
- Myxi.logger.debug "[#{id}] Unsubscribed from #{exchange_name}/#{routing_key}"
139
+ Myxi.logger.debug "[#{id}] \e[42;37mUNSUBSCRIBED\e[0m \e[32m#{exchange_name} / #{routing_key}\e[0m"
77
140
  send('Unsubscribed', :exchange_name => exchange_name, :routing_key => routing_key, :auto => auto)
78
141
  end
79
142
 
@@ -119,10 +182,12 @@ module Myxi
119
182
  #
120
183
  def close
121
184
  Myxi.logger.debug "[#{id}] Session closed"
122
- self.queue.delete if self.queue
185
+ @event_loop.sessions.delete(self)
186
+ @queue.delete if @queue
123
187
  while callback = @closure_callbacks.shift
124
188
  callback.call
125
189
  end
190
+ super
126
191
  end
127
192
 
128
193
  #
@@ -132,5 +197,10 @@ module Myxi
132
197
  @closure_callbacks << block
133
198
  end
134
199
 
200
+ def send_text_data(data)
201
+ sender = WebSocket::Frame::Outgoing::Server.new(version: @handshake.version, data: data, type: :text)
202
+ write(sender.to_s)
203
+ end
204
+
135
205
  end
136
206
  end
@@ -1,3 +1,3 @@
1
1
  module Myxi
2
- VERSION = '1.2.0'
2
+ VERSION = '1.4.2'
3
3
  end
@@ -43,6 +43,7 @@ class Myxi.Connection
43
43
  @websocket.onclose = (event)=>
44
44
  if @connected
45
45
  @_runCallbacks('SocketDisconnected')
46
+ @_runCallbacks('SocketClosed')
46
47
  @connected = false
47
48
  @authenticated = false
48
49
  @_markAllSubscriptionsAsUnsubscribed()
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: myxi
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Cooke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-02-04 00:00:00.000000000 Z
11
+ date: 2018-01-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -16,7 +16,7 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 2.2.0
19
+ version: 2.5.1
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
22
  version: '3'
@@ -26,30 +26,64 @@ dependencies:
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: 2.2.0
29
+ version: 2.5.1
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '3'
33
33
  - !ruby/object:Gem::Dependency
34
- name: em-websocket
34
+ name: websocket
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: 0.5.1
39
+ version: 1.2.4
40
40
  - - "<"
41
41
  - !ruby/object:Gem::Version
42
- version: '1'
42
+ version: '2'
43
43
  type: :runtime
44
44
  prerelease: false
45
45
  version_requirements: !ruby/object:Gem::Requirement
46
46
  requirements:
47
47
  - - ">="
48
48
  - !ruby/object:Gem::Version
49
- version: 0.5.1
49
+ version: 1.2.4
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
- version: '1'
52
+ version: '2'
53
+ - !ruby/object:Gem::Dependency
54
+ name: nio4r
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '1.2'
60
+ type: :runtime
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '1.2'
67
+ - !ruby/object:Gem::Dependency
68
+ name: timers
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 4.1.2
74
+ - - "<"
75
+ - !ruby/object:Gem::Version
76
+ version: '5'
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 4.1.2
84
+ - - "<"
85
+ - !ruby/object:Gem::Version
86
+ version: '5'
53
87
  description: A RabbitMQ-based web socket server & framework
54
88
  email:
55
89
  - me@adamcooke.io
@@ -61,7 +95,9 @@ files:
61
95
  - lib/myxi/action.rb
62
96
  - lib/myxi/default_actions.rb
63
97
  - lib/myxi/environment.rb
98
+ - lib/myxi/eventable_socket.rb
64
99
  - lib/myxi/exchange.rb
100
+ - lib/myxi/listener.rb
65
101
  - lib/myxi/railtie.rb
66
102
  - lib/myxi/server.rb
67
103
  - lib/myxi/session.rb