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 +4 -4
- data/lib/myxi/action.rb +2 -0
- data/lib/myxi/eventable_socket.rb +42 -0
- data/lib/myxi/listener.rb +34 -0
- data/lib/myxi/server.rb +68 -57
- data/lib/myxi/session.rb +83 -13
- data/lib/myxi/version.rb +1 -1
- data/vendor/assets/javascripts/myxi/connection.coffee +1 -0
- metadata +45 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 938c04e3a273895a36a7658893822de7f0409322
|
4
|
+
data.tar.gz: 13b6a2a52b393e9139b94a9902f817b0d43445d1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 83411ecfe5283f033c8c74252acebeb27915141a7426510f8b15d79029744c3ff661e97459bf14c89de3b75058b323470a5bf19d44ab1e1e870be2a627257a23
|
7
|
+
data.tar.gz: 780e26b6594c422d500544c1c4d55adde585e2cdea5c0385c89ac661f0fb7102ac7354e9feb88a6051a6aae5f9affe9757506c0782eaeddcc8db5ead335282f1
|
data/lib/myxi/action.rb
CHANGED
@@ -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
|
data/lib/myxi/server.rb
CHANGED
@@ -1,87 +1,98 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
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
|
17
|
-
@
|
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
|
-
|
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
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
44
|
+
@shutdown_timer += 1
|
60
45
|
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
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
|
data/lib/myxi/session.rb
CHANGED
@@ -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(
|
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
|
-
|
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
|
-
|
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}]
|
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}]
|
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
|
-
|
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
|
data/lib/myxi/version.rb
CHANGED
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
|
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:
|
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.
|
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.
|
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:
|
34
|
+
name: websocket
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
36
36
|
requirements:
|
37
37
|
- - ">="
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version:
|
39
|
+
version: 1.2.4
|
40
40
|
- - "<"
|
41
41
|
- !ruby/object:Gem::Version
|
42
|
-
version: '
|
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:
|
49
|
+
version: 1.2.4
|
50
50
|
- - "<"
|
51
51
|
- !ruby/object:Gem::Version
|
52
|
-
version: '
|
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
|