myxi 1.2.0 → 1.4.2

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 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