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