fix-engine 0.0.24
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 +7 -0
- data/LICENSE +17 -0
- data/README.md +4 -0
- data/bin/fix-engine +9 -0
- data/lib/fix/engine.rb +38 -0
- data/lib/fix/engine/client.rb +56 -0
- data/lib/fix/engine/connection.rb +292 -0
- data/lib/fix/engine/logger.rb +33 -0
- data/lib/fix/engine/message_buffer.rb +83 -0
- data/lib/fix/engine/server.rb +51 -0
- data/lib/fix/engine/version.rb +10 -0
- metadata +166 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c91eff8188ecd2d2a756b520272008fe02b84eda
|
4
|
+
data.tar.gz: f8a6ca41d7b37da9f38811a869d03013a1f925f1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a773023d43105871d71e3300ce97e9b8156e7412535f0a2867b2ec43bc826a7bd139c9e5cdb0a99c65f786ea5efced658bd1bd6b1cf590ccf03f6e8bbe72e0bd
|
7
|
+
data.tar.gz: bca29ee94ce994971a868db41c1fdb110b913923cd79d4e04b0a42633476d3c9d1b008921f669fee078d8e1edeb09b15e0adfd650313bbb22ebc5128e2ae73f1
|
data/LICENSE
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
2
|
+
of this software and associated documentation files (the "Software"), to deal
|
3
|
+
in the Software without restriction, including without limitation the rights
|
4
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
5
|
+
copies of the Software, and to permit persons to whom the Software is
|
6
|
+
furnished to do so, subject to the following conditions:
|
7
|
+
|
8
|
+
The above copyright notice and this permission notice shall be included in
|
9
|
+
all copies or substantial portions of the Software.
|
10
|
+
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
12
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
13
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
14
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
15
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
16
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
17
|
+
THE SOFTWARE.
|
data/README.md
ADDED
data/bin/fix-engine
ADDED
data/lib/fix/engine.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'fix/engine/logger'
|
2
|
+
require 'fix/engine/server'
|
3
|
+
|
4
|
+
#
|
5
|
+
# Main FIX namespace
|
6
|
+
#
|
7
|
+
module Fix
|
8
|
+
|
9
|
+
#
|
10
|
+
# Main Fix::Engine namespace
|
11
|
+
#
|
12
|
+
module Engine
|
13
|
+
|
14
|
+
# The default IP on which the server will listen
|
15
|
+
DEFAULT_IP = '0.0.0.0'
|
16
|
+
|
17
|
+
# The default port on which the server will listen
|
18
|
+
DEFAULT_PORT = 8359
|
19
|
+
|
20
|
+
#
|
21
|
+
# Runs a FIX server engine
|
22
|
+
#
|
23
|
+
def self.run!(ip = DEFAULT_IP, port = DEFAULT_PORT, handler = FE::Connection)
|
24
|
+
Server.new(ip, port, handler).run!
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# Alias the +Fix::Engine+ namespace to +FE+ if possible, because lazy is not necessarily dirty
|
29
|
+
#
|
30
|
+
def self.alias_namespace!
|
31
|
+
Object.const_set(:FE, Engine) unless Object.const_defined?(:FE)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
Fix::Engine.alias_namespace!
|
38
|
+
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'fix/engine/logger'
|
2
|
+
|
3
|
+
module Fix
|
4
|
+
module Engine
|
5
|
+
|
6
|
+
#
|
7
|
+
# Represents a connected client
|
8
|
+
#
|
9
|
+
class Client
|
10
|
+
|
11
|
+
@@clients = {}
|
12
|
+
|
13
|
+
attr_accessor :ip, :port, :connection, :client_id
|
14
|
+
|
15
|
+
include Logger
|
16
|
+
|
17
|
+
def initialize(ip, port, connection)
|
18
|
+
@ip = ip
|
19
|
+
@port = port
|
20
|
+
@connection = connection
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Returns a client instance from its connection IP
|
25
|
+
#
|
26
|
+
# @param ip [String] The connection IP
|
27
|
+
# @return [Fix::Engine::Client] The client connected for this IP
|
28
|
+
#
|
29
|
+
def self.get(ip, port, connection)
|
30
|
+
@@clients[key(ip, port)] ||= Client.new(ip, port, connection)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.count
|
34
|
+
@@clients.count
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.delete(ip, port)
|
38
|
+
@@clients.delete(key(ip, port))
|
39
|
+
end
|
40
|
+
|
41
|
+
def has_session?
|
42
|
+
!!client_id
|
43
|
+
end
|
44
|
+
|
45
|
+
def key
|
46
|
+
self.class.key(ip, port)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.key(ip, port)
|
50
|
+
"#{ip}:#{port}"
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
@@ -0,0 +1,292 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
require 'fix/engine/message_buffer'
|
4
|
+
require 'fix/engine/client'
|
5
|
+
|
6
|
+
module Fix
|
7
|
+
module Engine
|
8
|
+
|
9
|
+
#
|
10
|
+
# The client connection handling logic and method overrides
|
11
|
+
#
|
12
|
+
class Connection < EM::Connection
|
13
|
+
|
14
|
+
include Logger
|
15
|
+
|
16
|
+
#
|
17
|
+
# Timespan during which a client must send a logon message after connecting
|
18
|
+
#
|
19
|
+
LOGON_TIMEOUT = 10
|
20
|
+
|
21
|
+
#
|
22
|
+
# Timespan during which we will wait for a heartbeat response from the client
|
23
|
+
#
|
24
|
+
HRTBT_TIMEOUT = 10
|
25
|
+
|
26
|
+
#
|
27
|
+
# Grace time before we disconnect a client that doesn't reply to a test request
|
28
|
+
#
|
29
|
+
TEST_REQ_GRACE_TIME = 15
|
30
|
+
|
31
|
+
attr_accessor :ip, :port, :client, :msg_buf, :hrtbt_int, :last_request_at
|
32
|
+
|
33
|
+
#
|
34
|
+
# Our own company ID
|
35
|
+
#
|
36
|
+
DEFAULT_COMP_ID = 'PYMBTC'
|
37
|
+
|
38
|
+
#
|
39
|
+
# Run after a client has connected
|
40
|
+
#
|
41
|
+
def post_init
|
42
|
+
@port, @ip = Socket.unpack_sockaddr_in(get_peername)
|
43
|
+
@client = Client.get(ip, port, self)
|
44
|
+
@expected_clt_seq_num = 1
|
45
|
+
|
46
|
+
log("Client connected from <#{@client.key}>, expecting logon message in the next #{LOGON_TIMEOUT}s")
|
47
|
+
|
48
|
+
@comp_id ||= DEFAULT_COMP_ID
|
49
|
+
|
50
|
+
# The sent messages
|
51
|
+
@messages = []
|
52
|
+
|
53
|
+
# TODO : How do we test this
|
54
|
+
# TODO : Do we cancel the periodic timeout when leaving ?
|
55
|
+
EM.add_timer(LOGON_TIMEOUT) { logon_timeout }
|
56
|
+
end
|
57
|
+
|
58
|
+
def logon_timeout
|
59
|
+
unless client.has_session?
|
60
|
+
log("Client <#{client.key}> failed to authenticate before timeout, closing connection")
|
61
|
+
close_connection_after_writing
|
62
|
+
Client.delete(ip, port)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def manage_hrtbts
|
67
|
+
@last_send_at ||= 0
|
68
|
+
@last_request_at ||= 0
|
69
|
+
@hrtbt_int ||= 0
|
70
|
+
|
71
|
+
# Send a regular heartbeat when we don't send anything down the line for a while
|
72
|
+
if @hrtbt_int > 0 && (@last_send_at < (Time.now.to_i - @hrtbt_int))
|
73
|
+
send_heartbeat
|
74
|
+
end
|
75
|
+
|
76
|
+
# Trigger a test req message when we haven't received anything for a while
|
77
|
+
if !@pending_test_req_id && (last_request_at < (Time.now.to_i - @hrtbt_int))
|
78
|
+
tr = FP::Messages::TestRequest.new
|
79
|
+
tr.test_req_id = SecureRandom.hex(6)
|
80
|
+
send_msg(tr)
|
81
|
+
@pending_test_req_id = tr.test_req_id
|
82
|
+
|
83
|
+
EM.add_timer(TEST_REQ_GRACE_TIME) do
|
84
|
+
@pending_test_req_id && kill!
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def send_msg(msg)
|
90
|
+
@send_seq_num ||= 1
|
91
|
+
|
92
|
+
msg.msg_seq_num = @send_seq_num
|
93
|
+
msg.sender_comp_id = @comp_id
|
94
|
+
msg.target_comp_id ||= @client_comp_id
|
95
|
+
|
96
|
+
log("Sending <#{msg.class}> to <#{ip}:#{port}> with sequence number <#{msg.msg_seq_num}>")
|
97
|
+
|
98
|
+
if msg.valid?
|
99
|
+
@messages[msg.msg_seq_num] = msg
|
100
|
+
send_data(msg.dump)
|
101
|
+
@send_seq_num += 1
|
102
|
+
@last_send_at = Time.now.to_i
|
103
|
+
else
|
104
|
+
log(msg.errors.join(', '))
|
105
|
+
raise "Tried to send invalid message!"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def set_heartbeat_interval(interval)
|
110
|
+
@hrtbt_int && raise("Can't set heartbeat interval twice")
|
111
|
+
@hrtbt_int = interval
|
112
|
+
|
113
|
+
log("Heartbeat interval for <#{ip}:#{port}> : <#{hrtbt_int}s>")
|
114
|
+
@hrtbt_monitor = EM.add_periodic_timer(1) { manage_hrtbts }
|
115
|
+
end
|
116
|
+
|
117
|
+
def kill!
|
118
|
+
if @client_comp_id
|
119
|
+
log("Logging out client <#{ip}:#{port}>")
|
120
|
+
logout = FP::Messages::Logout.new
|
121
|
+
logout.text = 'Bye!'
|
122
|
+
send_msg(logout)
|
123
|
+
end
|
124
|
+
|
125
|
+
close_connection_after_writing
|
126
|
+
end
|
127
|
+
|
128
|
+
def unbind
|
129
|
+
log("Terminating client <#{ip}:#{port}>")
|
130
|
+
Client.delete(ip, port)
|
131
|
+
@hrtbt_monitor && @hrtbt_monitor.cancel
|
132
|
+
end
|
133
|
+
|
134
|
+
def client_error(error_msg, msg_seq_num, opts = {})
|
135
|
+
log("Client error: \"#{error_msg}\"")
|
136
|
+
rjct = FP::Messages::Reject.new
|
137
|
+
rjct.text = error_msg
|
138
|
+
rjct.ref_seq_num = msg_seq_num
|
139
|
+
rjct.target_comp_id = opts[:target_comp_id] if opts[:target_comp_id]
|
140
|
+
send_msg(rjct)
|
141
|
+
kill!
|
142
|
+
end
|
143
|
+
|
144
|
+
def handle_msg(msg)
|
145
|
+
@recv_seq_num = msg.msg_seq_num
|
146
|
+
|
147
|
+
log("Received a <#{msg.class}> from <#{ip}:#{port}> with sequence number <#{msg.msg_seq_num}>")
|
148
|
+
|
149
|
+
# If sequence number == expected, then process it normally
|
150
|
+
if (@expected_clt_seq_num == @recv_seq_num)
|
151
|
+
|
152
|
+
if @comp_id && msg.target_comp_id != @comp_id
|
153
|
+
@client_comp_id = msg.sender_comp_id
|
154
|
+
|
155
|
+
if (msg.target_comp_id != @comp_id)
|
156
|
+
client_error("Incorrect TARGET_COMP_ID in message, expected <#{@comp_id}>, got <#{msg.target_comp_id}>", msg.header.msg_seq_num)
|
157
|
+
end
|
158
|
+
|
159
|
+
else
|
160
|
+
if !@client_comp_id && msg.is_a?(FP::Messages::Logon)
|
161
|
+
log("Client authenticated as <#{msg.username}> with heartbeat interval of <#{msg.heart_bt_int}s> and message sequence number start <#{msg.msg_seq_num}>")
|
162
|
+
client.client_id = msg.username
|
163
|
+
@client_comp_id = msg.sender_comp_id
|
164
|
+
set_heartbeat_interval(msg.heart_bt_int)
|
165
|
+
|
166
|
+
logon = FP::Messages::Logon.new
|
167
|
+
logon.username = msg.username
|
168
|
+
logon.target_comp_id = msg.sender_comp_id
|
169
|
+
logon.sender_comp_id = msg.target_comp_id
|
170
|
+
logon.reset_seq_num_flag = true
|
171
|
+
send_msg(logon)
|
172
|
+
|
173
|
+
elsif @client_comp_id && msg.is_a?(FP::Messages::Logon)
|
174
|
+
log("Received second logon message, reset_seq_num_flag <#{msg.reset_seq_num_flag}>")
|
175
|
+
if msg.reset_seq_num_flag = 'Y'
|
176
|
+
@send_seq_num = 1
|
177
|
+
@messages = []
|
178
|
+
end
|
179
|
+
|
180
|
+
elsif !@client_comp_id
|
181
|
+
client_error("The session must be started with a logon message", msg.msg_seq_num, target_comp_id: msg.sender_comp_id)
|
182
|
+
|
183
|
+
elsif msg.is_a?(FP::Messages::Heartbeat)
|
184
|
+
# If we were expecting an answer to a test request we can sign it off and
|
185
|
+
# cancel the scheduled connection termination
|
186
|
+
if @pending_test_req_id && msg.test_req_id && (@pending_test_req_id == msg.test_req_id)
|
187
|
+
@pending_test_req_id = nil
|
188
|
+
end
|
189
|
+
|
190
|
+
elsif msg.is_a?(FP::Messages::TestRequest)
|
191
|
+
# Answer test requests with a matching heartbeat
|
192
|
+
hb = FP::Messages::Heartbeat.new
|
193
|
+
hb.test_req_id = msg.test_req_id
|
194
|
+
send_msg(hb)
|
195
|
+
|
196
|
+
elsif msg.is_a?(FP::Messages::ResendRequest)
|
197
|
+
# Re-send requested message range
|
198
|
+
@messages[msg.begin_seq_no, msg.end_seq_no.zero? ? @messages.length : msg.end_seq_no].each do |m|
|
199
|
+
log("Re-sending <#{m.class}> to <#{ip}:#{port}> with sequence number <#{m.msg_seq_num}>")
|
200
|
+
send_data(m.dump)
|
201
|
+
@last_send_at = Time.now.to_i
|
202
|
+
end
|
203
|
+
|
204
|
+
elsif msg.is_a?(FP::Message)
|
205
|
+
on_message(msg)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
@expected_clt_seq_num += 1
|
210
|
+
|
211
|
+
elsif msg.is_a?(FP::Messages::Logon)
|
212
|
+
client_error("Expected logon message to have msg_seq_num = <1>, received <#{msg.msg_seq_num}>", msg.msg_seq_num, target_comp_id: msg.sender_comp_id)
|
213
|
+
|
214
|
+
elsif (@expected_clt_seq_num > @recv_seq_num)
|
215
|
+
log("Ignoring message <#{msg}> with stale sequence number <#{msg.msg_seq_num}>, expecting <#{@expected_clt_seq_num}>")
|
216
|
+
|
217
|
+
elsif (@expected_clt_seq_num < @recv_seq_num) && @client_comp_id
|
218
|
+
# Request missing range when detect a gap
|
219
|
+
rr = FP::Messages::ResendRequest.new
|
220
|
+
rr.begin_seq_no = @expected_clt_seq_num
|
221
|
+
send_msg(rr)
|
222
|
+
end
|
223
|
+
|
224
|
+
self.last_request_at = Time.now.to_i
|
225
|
+
end
|
226
|
+
|
227
|
+
def on_message(msg)
|
228
|
+
end
|
229
|
+
|
230
|
+
#
|
231
|
+
# Run when a client has sent a chunk of data, it gets appended to a buffer
|
232
|
+
# and a parsing attempt is made at the buffered data
|
233
|
+
#
|
234
|
+
# @param data [String] The received data chunk
|
235
|
+
#
|
236
|
+
def receive_data(data)
|
237
|
+
data_chunk = data.chomp
|
238
|
+
msg_buf << data_chunk
|
239
|
+
|
240
|
+
begin
|
241
|
+
parse_messages_from_buffer
|
242
|
+
rescue
|
243
|
+
log("Client <#{@client.key}> raised exception when parsing data <#{data.gsub(/\x01/, '|')}>, terminating.")
|
244
|
+
log($!.message + $!.backtrace.join("\n"))
|
245
|
+
kill!
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
#
|
250
|
+
# Attempts to parse fields from the message buffer, if the fields that get parsed
|
251
|
+
# complete the temporary message, it is handled
|
252
|
+
#
|
253
|
+
def parse_messages_from_buffer
|
254
|
+
while idx = msg_buf.index("\x01")
|
255
|
+
field = msg_buf.slice!(0, idx + 1).gsub(/\x01\Z/, '')
|
256
|
+
msg.append(field)
|
257
|
+
|
258
|
+
if msg.complete?
|
259
|
+
parsed = msg.parse!
|
260
|
+
if parsed.is_a?(FP::Message)
|
261
|
+
handle_msg(parsed)
|
262
|
+
elsif parsed.is_a?(FP::ParseFailure)
|
263
|
+
client_error(parsed.errors.join(", "), @expected_clt_seq_num, target_comp_id: (@client_comp_id || 'UNKNOWN'))
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
#
|
270
|
+
# The data buffer string
|
271
|
+
#
|
272
|
+
def msg_buf
|
273
|
+
@msg_buf ||= ''
|
274
|
+
end
|
275
|
+
|
276
|
+
#
|
277
|
+
# The temporary message to which fields get appended
|
278
|
+
#
|
279
|
+
def msg
|
280
|
+
@msg ||= MessageBuffer.new(@client)
|
281
|
+
end
|
282
|
+
|
283
|
+
def send_heartbeat(test_req_id = nil)
|
284
|
+
msg = FP::Messages::Heartbeat.new
|
285
|
+
test_req_id && msg.test_req_id = test_req_id
|
286
|
+
send_msg(msg)
|
287
|
+
end
|
288
|
+
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Fix
|
4
|
+
module Engine
|
5
|
+
|
6
|
+
#
|
7
|
+
# Naive logger implementation used in development
|
8
|
+
#
|
9
|
+
module Logger
|
10
|
+
|
11
|
+
@@logger = nil
|
12
|
+
|
13
|
+
#
|
14
|
+
# Logs a message to the standard output
|
15
|
+
#
|
16
|
+
# @param msg [String] The message to log
|
17
|
+
#
|
18
|
+
def log(msg)
|
19
|
+
FE::Logger.log(msg)
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# Class-methods are easier to stub to disable logging while
|
24
|
+
# running specs
|
25
|
+
#
|
26
|
+
def self.log(msg)
|
27
|
+
@logger ||= ::Logger.new(STDOUT)
|
28
|
+
@logger.debug(msg)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'fix/protocol'
|
2
|
+
|
3
|
+
module Fix
|
4
|
+
module Engine
|
5
|
+
|
6
|
+
#
|
7
|
+
# A FIX message to which fields get appended, once it is completed by a
|
8
|
+
# proper terminator it is handled
|
9
|
+
#
|
10
|
+
class MessageBuffer
|
11
|
+
|
12
|
+
include Logger
|
13
|
+
|
14
|
+
attr_accessor :fields, :client
|
15
|
+
|
16
|
+
def initialize(client)
|
17
|
+
@fields = []
|
18
|
+
@client = client
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Append a single FIX field to the message
|
23
|
+
#
|
24
|
+
# @param fld [String] A FIX formatted field, such as "35=0\x01"
|
25
|
+
#
|
26
|
+
def append(fld)
|
27
|
+
raise "Cannot append to complete message" if complete?
|
28
|
+
field = fld.split('=')
|
29
|
+
field[0] = field[0].to_i
|
30
|
+
field[1] = field[1].gsub(/\x01\Z/, '')
|
31
|
+
@fields << field
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# Returns true if the last field of the collection is a FIX checksum
|
36
|
+
#
|
37
|
+
# @return [Boolean] Whether the message is complete
|
38
|
+
#
|
39
|
+
def complete?
|
40
|
+
(@fields.count > 0) && (@fields.last[0] == 10)
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Parses the message into a FP::Message instance
|
45
|
+
#
|
46
|
+
def parse
|
47
|
+
msg = FP.parse(to_s)
|
48
|
+
if (msg.class == FP::ParseFailure) || !msg.errors.count.zero?
|
49
|
+
log("Failed to parse message <#{debug}>")
|
50
|
+
log_errors(msg)
|
51
|
+
end
|
52
|
+
|
53
|
+
msg
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# Parses the message and empties the fields array so a new message
|
58
|
+
# can start to get buffered right away
|
59
|
+
#
|
60
|
+
def parse!
|
61
|
+
parsed = parse
|
62
|
+
@fields = []
|
63
|
+
parsed
|
64
|
+
end
|
65
|
+
|
66
|
+
def debug
|
67
|
+
to_s('|')
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_s(sep = "\x01")
|
71
|
+
fields.map { |f| f.join('=') }.join(sep) + sep
|
72
|
+
end
|
73
|
+
|
74
|
+
def log_errors(msg)
|
75
|
+
log("Invalid message received <#{debug}>")
|
76
|
+
msg.errors.each { |e| log(" >>> #{e}") }
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
require 'fix/protocol'
|
4
|
+
require 'fix/engine/version'
|
5
|
+
require 'fix/engine/connection'
|
6
|
+
|
7
|
+
module Fix
|
8
|
+
module Engine
|
9
|
+
|
10
|
+
#
|
11
|
+
# Main FIX engine server class
|
12
|
+
#
|
13
|
+
class Server
|
14
|
+
|
15
|
+
include Logger
|
16
|
+
|
17
|
+
REPORT_INTERVAL = 10
|
18
|
+
|
19
|
+
attr_accessor :ip, :port
|
20
|
+
|
21
|
+
def initialize(ip, port, handler)
|
22
|
+
@ip = ip
|
23
|
+
@port = port
|
24
|
+
@handler = handler
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# Starts running the server engine
|
29
|
+
#
|
30
|
+
def run!
|
31
|
+
trap('INT') { EM.stop }
|
32
|
+
log("Starting FIX engine v#{FE::VERSION}, listening on <#{ip}:#{port}>, exit with <Ctrl-C>")
|
33
|
+
EM.run { start_server }
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# Starts a listener inside a running reactor
|
38
|
+
#
|
39
|
+
def start_server
|
40
|
+
raise "EventMachine must be running to start a server" unless EM.reactor_running?
|
41
|
+
EM.start_server(ip, port, @handler)
|
42
|
+
REPORT_INTERVAL && EM.add_periodic_timer(REPORT_INTERVAL) { report_status }
|
43
|
+
end
|
44
|
+
|
45
|
+
def report_status
|
46
|
+
log("#{Client.count} client(s) currently connected")
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
metadata
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fix-engine
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.24
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David François
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-01-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: yard
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pry
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: redcarpet
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: simplecov
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: fix-protocol
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: eventmachine
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: FIX engine handling connections, sessions, and message callbacks
|
126
|
+
email:
|
127
|
+
- david.francois@paymium.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- LICENSE
|
133
|
+
- README.md
|
134
|
+
- bin/fix-engine
|
135
|
+
- lib/fix/engine.rb
|
136
|
+
- lib/fix/engine/client.rb
|
137
|
+
- lib/fix/engine/connection.rb
|
138
|
+
- lib/fix/engine/logger.rb
|
139
|
+
- lib/fix/engine/message_buffer.rb
|
140
|
+
- lib/fix/engine/server.rb
|
141
|
+
- lib/fix/engine/version.rb
|
142
|
+
homepage: https://github.com/paymium/fix-engine
|
143
|
+
licenses: []
|
144
|
+
metadata: {}
|
145
|
+
post_install_message:
|
146
|
+
rdoc_options: []
|
147
|
+
require_paths:
|
148
|
+
- lib
|
149
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
150
|
+
requirements:
|
151
|
+
- - '>='
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
154
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - '>='
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: 1.3.6
|
159
|
+
requirements: []
|
160
|
+
rubyforge_project:
|
161
|
+
rubygems_version: 2.4.2
|
162
|
+
signing_key:
|
163
|
+
specification_version: 4
|
164
|
+
summary: FIX engine handling connections, sessions, and message callbacks
|
165
|
+
test_files: []
|
166
|
+
has_rdoc:
|