tserver 0.2.0

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.
@@ -0,0 +1,21 @@
1
+ = SVN
2
+
3
+ = 0.2.0
4
+
5
+ * Server.process do not have argument now
6
+ * TServer.connection, TServer.connection_addr and TServer.terminate_listener? is available.
7
+ * Reload method (terminate all exists listerners and spawn new without interupt service and established connection)
8
+ * TServer.connections work when a listener don't have active connection
9
+ * Testing server Logger output in './test.log'
10
+ * Plurialize methods: Tserver.listeners, TServer.waiting_listeners
11
+ * Add 'server' task to Rakefile: run 'test/exemple_server.rb'
12
+ * Add multiple callback to log event, can be overrited
13
+ * Use Logger for logging implementation
14
+
15
+ = 0.1.1
16
+
17
+ * Tests pass with 'win32' platform
18
+
19
+ = 0.1.0
20
+
21
+ * First release
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2007 Yann Lugrin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,51 @@
1
+ = TServer
2
+
3
+ Author:: Yann Lugrin (mailto:yann.lugrin@sans-savoir.net)
4
+ Copyright:: Copyright (c) 2007 Yann Lugrin
5
+ Licence:: MIT[link://files/LICENSE.html]
6
+ Version:: 0.2.0
7
+
8
+ This librarie implements a persistant multithread TCP server, it is alternative
9
+ to 'gserver'[http://ruby-doc.org/stdlib/libdoc/gserver/rdoc/index.html] standard
10
+ librarie. TServer is designed to be inherited by your custom server class. The
11
+ server can accepts multiple simultaneous connections from clients, can be
12
+ configured to have a maximum connection and a minimum permanent listener thread.
13
+ Can be imediatly stopped, gracefull shutdown (dont accept new connection but
14
+ wait established connection is closed before realy stop) or reloaded (terminate
15
+ listener after established connection is closed and respawn new).
16
+
17
+ == Example
18
+
19
+ This example can receive simple string from telnet connection
20
+
21
+ require 'tserver'
22
+
23
+ # A server can return
24
+ class ExampleServer < TServer
25
+ def process
26
+ connection.each do |line|
27
+ break if line =~ /(quit|exit|close)/
28
+
29
+ log '> ' + line.chomp
30
+ conn.puts Time.now.to_s + '> ' + line.chomp
31
+ end
32
+ end
33
+ end
34
+
35
+ # Create the server with logging enabled (server activity is displayed
36
+ # in console with received data)
37
+ server = ExampleServer.new
38
+ server.verbose = true
39
+
40
+ # Shutdown the server when script is interupted
41
+ Signal.trap('SIGINT') do
42
+ server.shutdown
43
+ end
44
+
45
+ # Start the server (joined is set to true and the line wait on server
46
+ # thread before continue, the default values of this parameter is set to
47
+ # false, you can also use 'server.join' after server.start)
48
+ server.start(true)
49
+
50
+ # Now you can open a telnet connection to 127.0.0.1:10001 (telnet 127.0.0.1 10001)
51
+ # and send text (use exit to close the connection)
@@ -0,0 +1,373 @@
1
+ #--
2
+ # The MIT License
3
+ #
4
+ # Copyright (c) 2007 Yann Lugrin
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ # THE SOFTWARE.
23
+ #++
24
+
25
+ require 'socket'
26
+ require 'thread'
27
+ require 'thwait'
28
+ require 'monitor'
29
+ require 'logger'
30
+
31
+ # Show README[link://files/README.html] for implementation example.
32
+ class TServer
33
+
34
+ # Server port (default: 10001).
35
+ attr_reader :port
36
+
37
+ # Server host (default: 127.0.0.1).
38
+ attr_reader :host
39
+
40
+ # Maximum simultaneous connection can be established with server (default: 4, minimum: 1).
41
+ attr_reader :max_connection
42
+
43
+ # Minimum listener permanently spawned (default: 1, minimum: 0).
44
+ attr_reader :min_listener
45
+
46
+ # Server logger instance (default level: Logger:WARN, default output: stderr).
47
+ attr_reader :logger
48
+
49
+ DEFAULT_OPTIONS = {
50
+ :port => 10001,
51
+ :host => '127.0.0.1',
52
+ :max_connection => 4,
53
+ :min_listener => 1,
54
+ :log_level => Logger::WARN,
55
+ :stdlog => $stderr }
56
+
57
+ # Initialize a new server (use start to run the server).
58
+ #
59
+ # Options are:
60
+ # * <tt>:port</tt> - Port which the server listen on (default: 10001).
61
+ # * <tt>:host</tt> - IP which the server listen on (default: 127.0.0.1).
62
+ # * <tt>:max_connection</tt> - Maximum number of simultaneous connection to server (default: 4, minimum: 1).
63
+ # * <tt>:min_listener</tt> - Minimum number of listener thread (default: 1, minimum: 0).
64
+ # * <tt>:log_level</tt> - Use Logger constants DEBUG, INFO, WARN, ERROR or FATAL to set log level (default: Logger:WARN).
65
+ # * <tt>:stdlog</tt> - IO or filepath for log output (default: $stderr).
66
+ def initialize(options = {})
67
+ options = DEFAULT_OPTIONS.merge(options)
68
+
69
+ @port = options[:port]
70
+ @host = options[:host]
71
+
72
+ @max_connection = options[:max_connection] < 1 ? 1 : options[:max_connection]
73
+ @min_listener = options[:min_listener] < 0 ? 0 : (options[:min_listener] > @max_connection ? @max_connection : options[:min_listener])
74
+
75
+ @logger = Logger.new(options[:stdlog])
76
+ @logger.level = options[:log_level]
77
+
78
+ @tcp_server = nil
79
+ @tcp_server_thread = nil
80
+ @connections = Queue.new
81
+
82
+ @listener_threads = []
83
+ @listener_threads.extend(MonitorMixin)
84
+ @listener_cond = @listener_threads.new_cond
85
+
86
+ @shutdown = false
87
+ end
88
+
89
+ # Start the server, if joined is set at true this method return only when
90
+ # the server is stopped (you can also use join method after start)
91
+ def start(joined = false)
92
+ @shutdown = false
93
+ @tcp_server = TCPServer.new(@host, @port)
94
+
95
+ @min_listener.times { spawn_listener }
96
+ Thread.pass while @connections.num_waiting < @min_listener
97
+
98
+ @tcp_server_thread = Thread.new do
99
+ begin
100
+ server_started
101
+
102
+ loop do
103
+ @listener_threads.synchronize do
104
+ if @connections.num_waiting == 0 && @listener_threads.size >= @max_connection
105
+ server_waiting_listener
106
+ @listener_cond.wait
107
+ end
108
+ end
109
+
110
+ server_waiting_connection
111
+ @connections << @tcp_server.accept rescue Thread.exit
112
+ spawn_listener if !@connections.empty? && @connections.num_waiting == 0
113
+ end
114
+ ensure
115
+ @tcp_server = nil
116
+ @tcp_server_thread = nil
117
+
118
+ server_stopped
119
+ end
120
+ end
121
+
122
+ join if joined
123
+ true
124
+ end
125
+
126
+ # Join the main thread of the server and return only when the server is stopped.
127
+ def join
128
+ @tcp_server_thread.join if @tcp_server_thread
129
+ end
130
+
131
+ # Stop imediatly the server (all established connection is interrupted).
132
+ def stop
133
+ @tcp_server.close rescue nil
134
+ @listener_threads.synchronize { @listener_threads.each {|l| l.exit} }
135
+ @tcp_server_thread.exit rescue nil
136
+
137
+ true
138
+ end
139
+
140
+ # Gracefull shutdown, the server can't accept new connection but wait current
141
+ # connection before exit.
142
+ def shutdown
143
+ return if stopped?
144
+ server_shutdown
145
+
146
+ @tcp_server.close rescue nil
147
+ Thread.pass until @connections.empty?
148
+
149
+ @listener_threads.synchronize do
150
+ @listener_threads.each do |listener|
151
+ listener[:terminate] = true
152
+ @connections << false
153
+ end
154
+ end
155
+
156
+ ThreadsWait.all_waits(*@listener_threads)
157
+ @tcp_server_thread.exit rescue nil
158
+ @tcp_server_thread = nil
159
+
160
+ true
161
+ end
162
+
163
+ # Reload the server
164
+ # * Spawn new listeners.
165
+ # * Terminate existing listeners when current connection is closed.
166
+ def reload
167
+ return if stopped?
168
+
169
+ listeners_to_exit = nil
170
+ @listener_threads.synchronize do
171
+ listeners_to_exit = @listener_threads.dup
172
+ @listener_threads.clear
173
+ end
174
+
175
+ listeners_to_exit.each do |listener|
176
+ listener[:connection].nil? ? listener.terminate : listener[:terminate] = true
177
+ end
178
+
179
+ @listener_threads.synchronize do
180
+ spawn_listener while @listener_threads.size < @min_listener
181
+ end
182
+
183
+ true
184
+ end
185
+
186
+ # Return the number of spawned listener.
187
+ def listeners
188
+ @listener_threads.synchronize { @listener_threads.size }
189
+ end
190
+
191
+ # Return the number of spawned listener waiting on new connection.
192
+ def waiting_listeners
193
+ @connections.num_waiting
194
+ end
195
+
196
+ # Returns an array of arrays, where each subarray contains:
197
+ # * address family: A string like "AF_INET" or "AF_INET6" if it is one of the commonly used families, the string "unknown:#" (where '#' is the address family number) if it is not one of the common ones. The strings map to the Socket::AF_* constants.
198
+ # * port: The port number.
199
+ # * name: Either the canonical name from looking the address up in the DNS, or the address in presentation format.
200
+ # * address: The address in presentation format (a dotted decimal string for IPv4, a hex string for IPv6).
201
+ def connections
202
+ @listener_threads.synchronize { @listener_threads.collect{|l| l[:connection].nil? ? nil : l[:connection].peeraddr } }.compact
203
+ end
204
+
205
+ # Return true if server running.
206
+ def started?
207
+ @listener_threads.synchronize { !@tcp_server_thread.nil? || @listener_threads.size > 0 }
208
+ end
209
+
210
+ # Return true if server dont running.
211
+ def stopped?
212
+ !started?
213
+ end
214
+
215
+ protected
216
+
217
+ # Override this method to implement a server, conn is a TCPSocket instance and
218
+ # is closed when this method return. Attribute 'connection' is available.
219
+ #
220
+ # Example (send 'Hello world!' string to client):
221
+ # def process
222
+ # connection.puts 'Hello world!'
223
+ # end
224
+ #
225
+ # For persistant connection, use loop and Timeout.timeout or Tserver.terminate_listener?
226
+ # to break (and terminate listener) when server shutdown or reload. If server stop,
227
+ # listener is killed but begin/ensure can be used to terminate current process.
228
+ def process
229
+ end
230
+
231
+ # Callback (call when server is started)
232
+ def server_started
233
+ @logger.info do
234
+ "server:#{Thread.current} [#{@host}:#{@port}] is started"
235
+ end
236
+ end
237
+
238
+ # Callback (call when server is stopped)
239
+ def server_stopped
240
+ @logger.info do
241
+ "server:#{Thread.current} [#{@host}:#{@port}] is stopped"
242
+ end
243
+ end
244
+
245
+ # Callback (call when server shutdown, before is stopped)
246
+ def server_shutdown
247
+ @logger.info do
248
+ "server:#{Thread.current} [#{@host}:#{@port}] shutdown"
249
+ end
250
+ end
251
+
252
+ # Callback (call when server wait new connection)
253
+ def server_waiting_connection
254
+ @logger.info do
255
+ "server:#{Thread.current} [#{@host}:#{@port}] wait on connection"
256
+ end
257
+ end
258
+
259
+ # Callback (call when server wait free listener, don't accept new connection)
260
+ def server_waiting_listener
261
+ @logger.info do
262
+ "server:#{Thread.current} [#{@host}:#{@port}] wait on listener"
263
+ end
264
+ end
265
+
266
+ # Callback (call when listener is spawned)
267
+ def listener_spawned
268
+ @logger.info do
269
+ "listener:#{Thread.current} is spawned by server:#{Thread.current} [#{@host}:#{@port}]"
270
+ end
271
+ end
272
+
273
+ # Callback (call when listener exit)
274
+ def listener_terminated
275
+ @logger.info do
276
+ "listener:#{Thread.current} is terminated"
277
+ end
278
+ end
279
+
280
+ # Callback (call when listener wait connection - free listener)
281
+ def listener_waiting_connection
282
+ @logger.info do
283
+ "listener:#{Thread.current} wait on connection from server:#{Thread.current} [#{@host}:#{@port}]"
284
+ end
285
+ end
286
+
287
+ # Callback (call when a connection is established with listener)
288
+ def connection_established
289
+ @logger.info do
290
+ "client:#{connection_addr[1]} #{connection_addr[2]}<#{connection_addr[3]}> is connected to listener:#{Thread.current}"
291
+ end
292
+ end
293
+
294
+ # Callback (call when the connection with listener close normally)
295
+ def connection_normally_closed
296
+ @logger.info do
297
+ "client:#{connection_addr[1]} #{connection_addr[2]}<#{connection_addr[3]}> is disconnected from listener:#{Thread.current}"
298
+ end
299
+ end
300
+
301
+ # Callback (call when the connection with listener do not close normally,
302
+ # reveive 'error' instance from rescue)
303
+ def connection_not_normally_closed(error)
304
+ @logger.warn do
305
+ "client:#{connection_addr[1]} #{connection_addr[2]}<#{connection_addr[3]}> make an error and is disconnected from listener:#{Thread.current}"
306
+ end
307
+
308
+ @logger.debug do
309
+ "#{error.class.to_s}: #{error.to_s}\n" +
310
+ error.backtrace.join("\n")
311
+ end
312
+ end
313
+
314
+ private
315
+
316
+ def spawn_listener #:nodoc:
317
+ listener_thread = Thread.new do
318
+ begin
319
+ listener_spawned
320
+ loop do
321
+ begin
322
+ listener_waiting_connection
323
+ self.connection = (@connections.empty? && (terminate_listener? || @connections.num_waiting >= @min_listener)) ? Thread.exit : @connections.pop
324
+
325
+ if connection.is_a?(TCPSocket)
326
+ connection_established
327
+ process
328
+ connection_normally_closed
329
+ else
330
+ Thread.exit
331
+ end
332
+ rescue => e
333
+ connection_not_normally_closed(e)
334
+ ensure
335
+ connection.close rescue nil
336
+ self.connection = nil
337
+ end
338
+
339
+ @listener_threads.synchronize { @listener_cond.signal }
340
+ end
341
+ ensure
342
+ @listener_threads.synchronize { @listener_threads.delete(Thread.current) }
343
+ listener_terminated
344
+ end
345
+ end
346
+
347
+ @listener_threads.synchronize { @listener_threads << listener_thread }
348
+ end
349
+
350
+ # Set connection for current Thread
351
+ def connection=(conn) #:nodoc:
352
+ Thread.current[:connection] = conn
353
+ end
354
+
355
+ # Return connection of current listener thread (or nil)
356
+ def connection
357
+ Thread.current[:connection]
358
+ end
359
+
360
+ # Return array with information for current thread connection:
361
+ # * address family: A string like "AF_INET" or "AF_INET6" if it is one of the commonly used families, the string "unknown:#" (where '#' is the address family number) if it is not one of the common ones. The strings map to the Socket::AF_* constants.
362
+ # * port: The port number.
363
+ # * name: Either the canonical name from looking the address up in the DNS, or the address in presentation format.
364
+ # * address: The address in presentation format (a dotted decimal string for IPv4, a hex string for IPv6).
365
+ def connection_addr
366
+ Thread.current[:connection_addr] ||= (Thread.current[:connection].nil? ? [nil] * 4 : Thread.current[:connection].peeraddr)
367
+ end
368
+
369
+ # Return true if server ask listener to exit (when shutdown or reload)
370
+ def terminate_listener?
371
+ Thread.current[:terminate] == true
372
+ end
373
+ end
@@ -0,0 +1,51 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../lib/tserver')
2
+
3
+ # Example server, lauch this script and open a telnet to communicate
4
+ # with the server. The server print logging information and received data
5
+ # in console. The client receive a copy of sending data.
6
+ #
7
+ # Syntax example :
8
+ # ruby example_server.rb
9
+ # ruby example_server.rb 127.0.0.1 10001
10
+ # ruby example_server.rb 127.0.0.1
11
+ # ruby example_server.rb 10001
12
+ # ruby example_server.rb 10001 127.0.0.1
13
+ #
14
+ # Default values :
15
+ host = '127.0.0.1'
16
+ port = 10001
17
+
18
+ ARGV.each do |argv|
19
+ if argv =~ /^\d+$/
20
+ port = argv.to_i
21
+ elsif argv =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
22
+ host = argv
23
+ end
24
+ end
25
+
26
+ # ExampleServer return string received from client.
27
+ # Send quit, exit or close to close connection or
28
+ # stop to kill server.
29
+ class ExampleServer < TServer
30
+ def process
31
+ connection.each do |line|
32
+ stop if line =~ /stop/
33
+ break if line =~ /(quit|exit|close)/
34
+
35
+ puts '> ' + line.chomp
36
+ connection.puts Time.now.to_s + ' > ' + line.chomp
37
+ end
38
+ end
39
+ end
40
+
41
+ # Start server
42
+ server = ExampleServer.new(:port => port, :host => host)
43
+ server.logger.level = Logger::DEBUG
44
+
45
+ Signal.trap('SIGINT') do
46
+ server.shutdown
47
+ end
48
+
49
+ server.start
50
+
51
+ sleep 0.5 while server.started?
@@ -0,0 +1,412 @@
1
+ require 'test/unit'
2
+ require 'timeout'
3
+ require 'thread'
4
+
5
+ require File.expand_path(File.dirname(__FILE__) + '/../lib/tserver')
6
+
7
+ TEST_LOG = 'test.log'
8
+ SERVER_READER = Queue.new
9
+
10
+ # The test server can send received data to an IO
11
+ class TestServer < TServer
12
+ def initialize(options= {})
13
+ super(options)
14
+ end
15
+
16
+ protected
17
+
18
+ # Send received data on IO and return the data to client
19
+ def process
20
+ loop do
21
+ SERVER_READER << string = connection.readline.chomp
22
+ connection.puts string
23
+ end
24
+ end
25
+ end
26
+
27
+ # The test client can send and receive data to a server
28
+ class TestClient
29
+ def initialize(host, port)
30
+ @host = host
31
+ @port = port
32
+
33
+ @socket = nil
34
+ end
35
+
36
+ def connect
37
+ @socket = TCPSocket.new(@host, @port)
38
+ Thread.pass
39
+ end
40
+
41
+ def close
42
+ @socket.close if @socket
43
+ Thread.pass
44
+ end
45
+
46
+ def send(string)
47
+ @socket.puts string
48
+ Thread.pass
49
+ end
50
+
51
+ def receive
52
+ @socket.readline.chomp
53
+ end
54
+ end
55
+
56
+ class TServerTest < Test::Unit::TestCase
57
+ def setup
58
+ SERVER_READER.clear
59
+
60
+ @server = TestServer.new(:log_level => Logger::DEBUG, :stdlog => TEST_LOG)
61
+ @client = TestClient.new(@server.host, @server.port)
62
+ end
63
+
64
+ def teardown
65
+ @client.close rescue nil
66
+
67
+ @server.stop rescue nil
68
+ @server.join rescue nil # join the server to ensure is stopped before start the next test
69
+ end
70
+
71
+ def test_should_can_create_with_default_values
72
+ server = TServer.new
73
+
74
+ assert_equal 10001, server.port
75
+ assert_equal '127.0.0.1', server.host
76
+
77
+ assert_equal 4, server.max_connection
78
+ assert_equal 1, server.min_listener
79
+
80
+ assert_kind_of Logger, server.logger
81
+ assert_equal Logger::WARN, server.logger.level
82
+ end
83
+
84
+ def test_should_can_create_with_custom_values
85
+ # Set all options
86
+ server = TServer.new(:port => 10002, :host => '192.168.1.1', :max_connection => 10, :min_listener => 2, :log_level => Logger::DEBUG)
87
+
88
+ assert_equal 10002, server.port
89
+ assert_equal '192.168.1.1', server.host
90
+
91
+ assert_equal 10, server.max_connection
92
+ assert_equal 2, server.min_listener
93
+
94
+ assert_kind_of Logger, server.logger
95
+ assert_equal Logger::DEBUG, server.logger.level
96
+ end
97
+
98
+ def test_should_dont_have_more_min_listener_that_of_max_connection
99
+ server = TServer.new(:max_connection => 5, :min_listener => 6)
100
+
101
+ assert_equal 5, server.max_connection
102
+ assert_equal 5, server.min_listener
103
+ end
104
+
105
+ def test_should_dont_have_minimum_max_connection_and_min_listener
106
+ server = TServer.new(:max_connection => 0, :min_listener => -1)
107
+
108
+ assert_equal 1, server.max_connection
109
+ assert_equal 0, server.min_listener
110
+ end
111
+
112
+ def test_should_be_started
113
+ # Start a server
114
+ assert_not_timeout('Server do not start') { @server.start }
115
+ assert @server.started?
116
+ assert !@server.stopped?
117
+
118
+ # Listener is spawned
119
+ assert_equal @server.min_listener, @server.listeners
120
+ assert_equal @server.min_listener, @server.waiting_listeners
121
+ end
122
+
123
+ def test_should_be_stopped
124
+ # Stop a non started server
125
+ assert_not_timeout('Server do not stop') { @server.stop }
126
+
127
+ # Start the server
128
+ assert_not_timeout('Server do not start') { @server.start }
129
+
130
+ # Stop the server
131
+ assert_not_timeout('Server do not stop'){ @server.stop }
132
+
133
+ # The server is stopped and dont accept connection
134
+ assert !@server.started?
135
+ assert @server.stopped?
136
+ assert_raise(RUBY_PLATFORM =~ /win32/ ? Errno::EBADF : Errno::ECONNREFUSED) do
137
+ @client.connect
138
+ end
139
+ end
140
+
141
+ def test_should_be_stopped_with_established_connection
142
+ # Start the server and a client
143
+ assert_not_timeout('Server do not start') { @server.start }
144
+ assert_not_timeout('Client do not connect') { @client.connect }
145
+
146
+ # Shutdown the server
147
+ assert_not_timeout('Server do not stop') { @server.stop }
148
+
149
+ # The server is stopped and dont accept connection
150
+ assert !@server.started?
151
+ assert @server.stopped?
152
+ assert_raise(RUBY_PLATFORM =~ /win32/ ? Errno::EBADF : Errno::ECONNREFUSED) do
153
+ @client.connect
154
+ end
155
+ end
156
+
157
+ def test_should_be_shutdown
158
+ # Shutdown a non started server
159
+ assert_not_timeout('Server do not shutdown') { @server.shutdown }
160
+
161
+ # Start the server
162
+ assert_not_timeout('Server do not start') { @server.start }
163
+
164
+ # Shutdown the server
165
+ assert_not_timeout('Server do not shutdown') { @server.shutdown }
166
+
167
+ # The server is stopped and dont accept connection
168
+ assert !@server.started?
169
+ assert @server.stopped?
170
+ assert_raise(RUBY_PLATFORM =~ /win32/ ? Errno::EBADF : Errno::ECONNREFUSED) do
171
+ @client.connect
172
+ end
173
+
174
+ # Shutdown a shutdowned server
175
+ assert_not_timeout('Server do not shutdown') { @server.shutdown }
176
+ end
177
+
178
+ def test_should_be_shutdown_with_established_connection
179
+ # Start server and client
180
+ assert_not_timeout('Server do not start') { @server.start }
181
+ assert_not_timeout('Client do not connect') { @client.connect }
182
+
183
+ # Shutdown the server
184
+ shutdown_thread = nil
185
+ shutdown_thread = Thread.new do
186
+ assert_not_timeout('Server do not shutdown') { @server.shutdown }
187
+ end
188
+
189
+ # The server isn't stopped because a connection is established
190
+ assert @server.started?
191
+ assert !@server.stopped?
192
+
193
+ # The client can communicate with server
194
+ assert_not_timeout 'Client do not communicate with server' do
195
+ @client.send 'test string'
196
+ assert_equal 'test string', SERVER_READER.pop.chomp
197
+ assert_equal 'test string', @client.receive
198
+ end
199
+
200
+ # Close client
201
+ assert_not_timeout('Client do not close connection') { @client.close }
202
+ wait_listeners
203
+
204
+ # Wait server shutdown
205
+ assert_not_timeout('Server do not shutdown') { shutdown_thread.join }
206
+
207
+ # The server is stopped and dont accept connection
208
+ assert !@server.started?
209
+ assert @server.stopped?
210
+ assert_raise(RUBY_PLATFORM =~ /win32/ ? Errno::EBADF : Errno::ECONNREFUSED) do
211
+ @client.connect
212
+ end
213
+ end
214
+
215
+ def test_should_be_reload
216
+ # Reload a non started server
217
+ assert_not_timeout('Server do not reload') { @server.reload }
218
+
219
+ # Do not spawn listeners!
220
+ assert_equal 0, @server.instance_variable_get(:@listener_threads).size
221
+
222
+ # Start the server
223
+ assert_not_timeout('Server do not start') { @server.start }
224
+
225
+ # The server is started and accept connection
226
+ assert_not_timeout('Client do not connect') { @client.connect }
227
+
228
+ # Copy list of current listeners
229
+ listeners_to_exit = @server.instance_variable_get(:@listener_threads).dup
230
+
231
+ # Reload the server
232
+ assert_not_timeout('Server do not reload') { @server.reload }
233
+
234
+ # Old listener is not terminated (connection with a client is established)
235
+ wait_listeners 2
236
+ assert_not_equal listeners_to_exit.first, @server.instance_variable_get(:@listener_threads).first
237
+ assert_not_equal, listeners_to_exit.first[:conn] = @server.instance_variable_get(:@listener_threads).first[:conn]
238
+
239
+ # The client can communicate with server
240
+ assert_not_timeout 'Client do not communicate with server' do
241
+ @client.send 'test string'
242
+ assert_equal 'test string', SERVER_READER.pop.chomp
243
+ assert_equal 'test string', @client.receive
244
+ end
245
+
246
+ # Close client
247
+ assert_not_timeout('Client do not close connection') { @client.close }
248
+
249
+ # Old listeners exit
250
+ ThreadsWait.all_waits(*listeners_to_exit)
251
+
252
+ # The server is started and accept connection
253
+ assert_not_timeout('Client do not connect') { @client.connect }
254
+
255
+ # The client can communicate with server
256
+ assert_not_timeout 'Client do not communicate with server' do
257
+ @client.send 'test string'
258
+ assert_equal 'test string', SERVER_READER.pop.chomp
259
+ assert_equal 'test string', @client.receive
260
+ end
261
+ end
262
+
263
+ def test_should_be_receive_connection
264
+ # Start server and client
265
+ assert_not_timeout('Server do not start') { @server.start }
266
+ assert_not_timeout('Client do not connect') { @client.connect }
267
+
268
+ # Client can communicate with the server
269
+ assert_not_timeout 'Client do not communicate with server' do
270
+ @client.send 'test string'
271
+ assert_equal 'test string', SERVER_READER.pop.chomp
272
+ assert_equal 'test string', @client.receive
273
+ end
274
+ end
275
+
276
+ def test_should_be_receive_multiple_connection
277
+ # Create multiple clients
278
+ @client_2 = TestClient.new(@server.host, @server.port)
279
+ @client_3 = TestClient.new(@server.host, @server.port)
280
+ @client_4 = TestClient.new(@server.host, @server.port)
281
+ @client_5 = TestClient.new(@server.host, @server.port)
282
+
283
+ # Start server and clients
284
+ assert_not_timeout('Server do not start') { @server.start }
285
+ assert_not_timeout('Client do not connect') { @client.connect }
286
+ assert_not_timeout('Client do not connect') { @client_2.connect }
287
+ assert_not_timeout('Client do not connect') { @client_3.connect }
288
+ assert_not_timeout('Client do not connect') { @client_4.connect }
289
+ assert_not_timeout('Client do not connect') { @client_5.connect }
290
+ wait_listeners(4)
291
+
292
+ # Only 4 listerner for 5 client
293
+ assert_equal 0, @server.waiting_listeners
294
+ assert_equal 4, @server.listeners
295
+
296
+ # All clients send data to server
297
+ assert_not_timeout('Client do not communicate with server') { @client.send 'test string 1' }
298
+ assert_not_timeout('Client do not communicate with server') { @client_2.send 'test string 2' }
299
+ assert_not_timeout('Client do not communicate with server') { @client_3.send 'test string 3' }
300
+ assert_not_timeout('Client do not communicate with server') { @client_4.send 'test string 4' }
301
+ assert_not_timeout('Client do not communicate with server') { @client_5.send 'test string 5' }
302
+
303
+ # Server receive data from 4 clients (but the last client waiting)
304
+ 1.upto(4) do |i|
305
+ assert_not_timeout 'Do not receive data from client' do
306
+ assert_match(/test string [1-4]/, SERVER_READER.pop)
307
+ end
308
+ end
309
+ assert SERVER_READER.empty?, 'Server receive data from client 5'
310
+
311
+ # Close client
312
+ assert_not_timeout('Client do not close connection') { @client.close }
313
+
314
+ # Server can recerive data from last client
315
+ assert_not_timeout 'Do not receive data from client' do
316
+ assert_equal 'test string 5', SERVER_READER.pop
317
+ end
318
+
319
+ # Close all clients [<client>, <number of listener after close>]
320
+ [@client_2, @client_3, @client_4, @client_5].each do |client|
321
+ assert_not_timeout('Client do not close connection') { client.close }
322
+ end
323
+ wait_listeners
324
+
325
+ # min_listener waiting connection
326
+ assert_equal @server.min_listener, @server.listeners
327
+ assert_equal @server.min_listener, @server.waiting_listeners
328
+ end
329
+
330
+ def test_should_be_receive_connection_with_log_level_to_debug
331
+ @server.logger.level = Logger::DEBUG
332
+
333
+ # Start server and client
334
+ assert_not_timeout('Server do not start') { @server.start }
335
+ assert_not_timeout('Client do not connect') { @client.connect }
336
+
337
+ # Client can communicate with the server
338
+ assert_not_timeout 'Client do not communicate with server' do
339
+ @client.send 'test string'
340
+ assert_equal 'test string', SERVER_READER.pop.chomp
341
+ assert_equal 'test string', @client.receive
342
+ end
343
+ end
344
+
345
+ def test_should_works_with_min_listener_at_0
346
+ @server = TestServer.new(:min_listener => 0, :stdlog => TEST_LOG)
347
+ assert_equal 0, @server.min_listener
348
+
349
+ # Start server and client
350
+ assert_not_timeout('Server do not start') { @server.start }
351
+ assert_not_timeout('Client do not connect') { @client.connect }
352
+
353
+ # Client can communicate with the server
354
+ assert_not_timeout 'Client do not communicate with server' do
355
+ @client.send 'test string'
356
+ assert_equal 'test string', SERVER_READER.pop.chomp
357
+ assert_equal 'test string', @client.receive
358
+ end
359
+
360
+ # Close client
361
+ assert_not_timeout('Client do not close connection') { @client.close }
362
+ wait_listeners
363
+
364
+ # min_listener
365
+ assert_equal @server.min_listener, @server.listeners
366
+ assert_equal @server.min_listener, @server.waiting_listeners
367
+ end
368
+
369
+ def test_should_have_connection_list
370
+ # Start server
371
+ assert_not_timeout('Server do not start') { @server.start }
372
+
373
+ # Zero connection
374
+ assert_equal @server.min_listener, @server.listeners
375
+ assert_equal @server.min_listener, @server.waiting_listeners
376
+ assert_equal [], @server.connections
377
+
378
+ # Start client
379
+ assert_not_timeout('Client do not connect') { @client.connect }
380
+ wait_connection
381
+
382
+ # Connection information
383
+ assert_equal 1, @server.connections.size
384
+ assert_equal 'AF_INET', @server.connections.first[0]
385
+ assert_match(/^\d+$/, @server.connections.first[1].to_s)
386
+ assert_match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, @server.connections.first[3])
387
+ end
388
+
389
+ protected
390
+
391
+ def assert_not_timeout(msg = 'Timeout')
392
+ assert_nothing_raised(msg) do
393
+ Timeout.timeout(5) do
394
+ yield
395
+ end
396
+ end
397
+ end
398
+
399
+ # Wait listener spawn
400
+ def wait_listeners(number = @server.min_listener)
401
+ assert_not_timeout 'Listener do not exit' do
402
+ sleep 0.1 until @server.listeners == number
403
+ end
404
+ end
405
+
406
+ # Wait connection established with listener
407
+ def wait_connection(number = @server.listeners)
408
+ assert_not_timeout 'Listener do not exit' do
409
+ sleep 0.1 until @server.connections.size == number
410
+ end
411
+ end
412
+ end
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.4
3
+ specification_version: 1
4
+ name: tserver
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.2.0
7
+ date: 2007-10-11 00:00:00 +02:00
8
+ summary: A persistant multithread TCP server
9
+ require_paths:
10
+ - lib
11
+ email: yann.lugrin@sans-savoir.net
12
+ homepage: http://dev.sans-savoir.net/trac/tserver
13
+ rubyforge_project:
14
+ description:
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Yann Lugrin
31
+ files:
32
+ - lib/tserver.rb
33
+ - test/example_server.rb
34
+ - test/tserver_test.rb
35
+ - README
36
+ - CHANGELOG
37
+ - LICENSE
38
+ test_files: []
39
+
40
+ rdoc_options: []
41
+
42
+ extra_rdoc_files:
43
+ - README
44
+ - CHANGELOG
45
+ - LICENSE
46
+ executables: []
47
+
48
+ extensions: []
49
+
50
+ requirements: []
51
+
52
+ dependencies: []
53
+