tserver 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+