OSDN Git Service

Added test cases.
[shogi-server/shogi-server.git] / shogi-server
1 #! /usr/bin/env ruby
2 # $Id$
3 #
4 # Author:: NABEYA Kenichi, Daigo Moriwaki
5 # Homepage:: http://sourceforge.jp/projects/shogi-server/
6 #
7 #--
8 # Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
9 # Copyright (C) 2007-2008 Daigo Moriwaki (daigo at debian dot org)
10 #
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the GNU General Public License
22 # along with this program; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
24 #++
25 #
26 #
27
28 $topdir = nil
29 $league = nil
30 $logger = nil
31 $config = nil
32 $:.unshift File.dirname(__FILE__)
33 require 'shogi_server'
34 require 'shogi_server/config'
35 require 'shogi_server/util'
36 require 'shogi_server/league/floodgate_thread.rb'
37 require 'tempfile'
38
39 #################################################
40 # MAIN
41 #
42
43 ShogiServer.reload
44
45 # Return
46 #   - a received string
47 #   - :timeout
48 #   - :exception
49 #   - nil when a socket is closed
50 #
51 def gets_safe(socket, timeout=nil)
52   if r = select([socket], nil, nil, timeout)
53     return r[0].first.gets
54   else
55     return :timeout
56   end
57 rescue Exception => ex
58   log_error("gets_safe: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
59   return :exception
60 end
61
62 def usage
63     print <<EOM
64 NAME
65         shogi-server - server for CSA server protocol
66
67 SYNOPSIS
68         shogi-server [OPTIONS] event_name port_number
69
70 DESCRIPTION
71         server for CSA server protocol
72
73 OPTIONS
74         event_name
75                 a prefix of record files.
76         port_number
77                 a port number for the server to listen. 
78                 4081 is often used.
79         --pid-file file
80                 a file path in which a process ID will be written.
81                 Use with --daemon option.
82         --daemon dir
83                 run as a daemon. Log files will be put in dir.
84         --floodgate-games game_A[,...]
85                 enable Floodgate with various game names (separated by a comma)
86         --player-log-dir dir
87                 enable to log network messages for players. Log files
88                 will be put in the dir.
89
90 EXAMPLES
91
92         1. % ./shogi-server test 4081
93            Run the shogi-server. Then clients can connect to port#4081.
94            The server output logs to the stdout.
95
96         2. % ./shogi-server --daemon . --pid-file ./shogi-server.pid \
97                             --player-log-dir ./player-logs \
98                             test 4081
99            Run the shogi-server as a daemon. The server outputs regular logs
100            to shogi-server.log located in the current directory and network 
101            messages in ./player-logs directory.
102
103         3. % ./shogi-server --daemon . --pid-file ./shogi-server.pid \
104                             --player-log-dir ./player-logs \
105                             --floodgate-games floodgate-900-0,floodgate-3600-0 \
106                             test 4081
107            Run the shogi-server with two groups of Floodgate games.
108            Configuration files allow you to schedule starting times. Consult  
109            floodgate-0-240.conf.sample or shogi_server/league/floodgate.rb 
110            for format details.
111
112 FLOODGATE SCHEDULE CONFIGURATIONS
113
114             You need to set starting times of floodgate groups in
115             configuration files under the top directory. Each floodgate 
116             group requires a corresponding configuration file named
117             "<game_name>.conf". The file will be re-read once just after a
118             game starts. 
119             
120             For example, a floodgate-3600-30 game group requires
121             floodgate-3600-30.conf.  However, for floodgate-900-0 and
122             floodgate-3600-0, which were default enabled in previous
123             versions, configuration files are optional if you are happy with
124             default time settings.
125             File format is:
126               Line format: 
127                 # This is a comment line
128                 DoW Time
129                 ...
130               where
131                 DoW := "Sun" | "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" |
132                        "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" |
133                        "Friday" | "Saturday" 
134                 Time := HH:MM
135              
136               For example,
137                 Sat 13:00
138                 Sat 22:00
139                 Sun 13:00
140
141 LICENSE
142         GPL versoin 2 or later
143
144 SEE ALSO
145
146 RELEASE
147         #{ShogiServer::Release}
148
149 REVISION
150         #{ShogiServer::Revision}
151
152 EOM
153 end
154
155
156 def log_debug(str)
157   $logger.debug(str)
158 end
159
160 def log_message(str)
161   $logger.info(str)
162 end
163 def log_info(str)
164   log_message(str)
165 end
166
167 def log_warning(str)
168   $logger.warn(str)
169 end
170
171 def log_error(str)
172   $logger.error(str)
173 end
174
175
176 # Parse command line options. Return a hash containing the option strings
177 # where a key is the option name without the first two slashes. For example,
178 # {"pid-file" => "foo.pid"}.
179 #
180 def parse_command_line
181   options = Hash::new
182   parser = GetoptLong.new(
183     ["--daemon",            GetoptLong::REQUIRED_ARGUMENT],
184     ["--floodgate-games",   GetoptLong::REQUIRED_ARGUMENT],
185     ["--pid-file",          GetoptLong::REQUIRED_ARGUMENT],
186     ["--player-log-dir",    GetoptLong::REQUIRED_ARGUMENT])
187   parser.quiet = true
188   begin
189     parser.each_option do |name, arg|
190       name.sub!(/^--/, '')
191       options[name] = arg.dup
192     end
193   rescue
194     usage
195     raise parser.error_message
196   end
197   return options
198 end
199
200 # Check command line options.
201 # If any of them is invalid, exit the process.
202 #
203 def check_command_line
204   if (ARGV.length != 2)
205     usage
206     exit 2
207   end
208
209   if $options["daemon"]
210     $options["daemon"] = File.expand_path($options["daemon"], File.dirname(__FILE__))
211     unless is_writable_dir? $options["daemon"]
212       usage
213       $stderr.puts "Can not create a file in the daemon directory: %s" % [$options["daemon"]]
214       exit 5
215     end
216   end
217
218   $topdir = $options["daemon"] || File.expand_path(File.dirname(__FILE__))
219
220   if $options["player-log-dir"]
221     $options["player-log-dir"] = File.expand_path($options["player-log-dir"], $topdir)
222     unless is_writable_dir?($options["player-log-dir"])
223       usage
224       $stderr.puts "Can not write a file in the player log dir: %s" % [$options["player-log-dir"]]
225       exit 3
226     end 
227   end
228
229   if $options["pid-file"] 
230     $options["pid-file"] = File.expand_path($options["pid-file"], $topdir)
231     unless ShogiServer::is_writable_file? $options["pid-file"]
232       usage
233       $stderr.puts "Can not create the pid file: %s" % [$options["pid-file"]]
234       exit 4
235     end
236   end
237
238   if $options["floodgate-games"]
239     names = $options["floodgate-games"].split(",")
240     new_names = 
241       names.select do |name|
242         ShogiServer::League::Floodgate::game_name?(name)
243       end
244     if names.size != new_names.size
245       $stderr.puts "Found a wrong Floodgate game: %s" % [names.join(",")]
246       exit 6
247     end
248     $options["floodgate-games"] = new_names
249   end
250
251   if $options["floodgate-history"]
252     $stderr.puts "WARNING: --floodgate-history has been deprecated."
253     $options["floodgate-history"] = nil
254   end
255 end
256
257 # See if a file can be created in the directory.
258 # Return true if a file is writable in the directory, otherwise false.
259 #
260 def is_writable_dir?(dir)
261   unless File.directory? dir
262     return false
263   end
264
265   result = true
266
267   begin
268     temp_file = Tempfile.new("dummy-shogi-server", dir)
269     temp_file.close true
270   rescue
271     result = false
272   end
273
274   return result
275 end
276
277 def write_pid_file(file)
278   open(file, "w") do |fh|
279     fh.puts "#{$$}"
280   end
281 end
282
283 def mutex_watchdog(mutex, sec)
284   sec = 1 if sec < 1
285   queue = []
286   while true
287     if mutex.try_lock
288       queue.clear
289       mutex.unlock
290     else
291       queue.push(Object.new)
292       if queue.size > sec
293         # timeout
294         log_error("mutex watchdog timeout: %d sec" % [sec])
295         queue.clear
296       end
297     end
298     sleep(1)
299   end
300 end
301
302 def login_loop(client)
303   player = login = nil
304  
305   while r = select([client], nil, nil, ShogiServer::Login_Time) do
306     str = nil
307     begin
308       break unless str = r[0].first.gets
309     rescue Exception => ex
310       # It is posssible that the socket causes an error (ex. Errno::ECONNRESET)
311       log_error("login_loop: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
312       break
313     end
314     $mutex.lock # guards $league
315     begin
316       str =~ /([\r\n]*)$/
317       eol = $1
318       if (ShogiServer::Login::good_login?(str))
319         player = ShogiServer::Player::new(str, client, eol)
320         login  = ShogiServer::Login::factory(str, player)
321         if (current_player = $league.find(player.name))
322           if (current_player.password == player.password &&
323               current_player.status != "game")
324             log_message(sprintf("user %s login forcely", player.name))
325             current_player.kill
326           else
327             login.incorrect_duplicated_player(str)
328             player = nil
329             break
330           end
331         end
332         $league.add(player)
333         break
334       else
335         client.write("LOGIN:incorrect" + eol)
336         client.write("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
337       end
338     ensure
339       $mutex.unlock
340     end
341   end                       # login loop
342   return [player, login]
343 end
344
345 def setup_logger(log_file)
346   logger = ShogiServer::Logger.new(log_file, 'daily')
347   logger.formatter = ShogiServer::Formatter.new
348   logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO  
349   logger.datetime_format = "%Y-%m-%d %H:%M:%S"
350   return logger
351 end
352
353 def setup_watchdog_for_giant_lock
354   $mutex = Mutex::new
355   Thread::start do
356     Thread.pass
357     mutex_watchdog($mutex, 10)
358   end
359 end
360
361 def main
362   
363   $options = parse_command_line
364   check_command_line
365   $config = ShogiServer::Config.new $options
366
367   $league = ShogiServer::League.new($topdir)
368
369   $league.event = ARGV.shift
370   port = ARGV.shift
371
372   log_file = $options["daemon"] ? File.join($options["daemon"], "shogi-server.log") : STDOUT
373   $logger = setup_logger(log_file)
374
375   $league.dir = $topdir
376
377   config = {}
378   config[:Port]       = port
379   config[:ServerType] = WEBrick::Daemon if $options["daemon"]
380   config[:Logger]     = $logger
381
382   setup_floodgate = nil
383
384   config[:StartCallback] = Proc.new do
385     srand
386     if $options["pid-file"]
387       write_pid_file($options["pid-file"])
388     end
389     setup_watchdog_for_giant_lock
390     $league.setup_players_database
391     setup_floodgate = ShogiServer::SetupFloodgate.new($options["floodgate-games"])
392     setup_floodgate.start
393   end
394
395   config[:StopCallback] = Proc.new do
396     if $options["pid-file"]
397       FileUtils.rm($options["pid-file"], :force => true)
398     end
399   end
400
401   srand
402   server = WEBrick::GenericServer.new(config)
403   ["INT", "TERM"].each do |signal| 
404     trap(signal) do
405       server.shutdown
406       setup_floodgate.kill
407     end
408   end
409   unless (RUBY_PLATFORM.downcase =~ /mswin|mingw|cygwin|bccwin/)
410     trap("HUP") do
411       Dependencies.clear
412     end
413   end
414   $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] 
415   log_message("server started [Revision: #{ShogiServer::Revision}]")
416
417   server.start do |client|
418     begin
419       # client.sync = true # this is already set in WEBrick 
420       client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
421         # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
422       player, login = login_loop(client) # loop
423       next unless player
424
425       log_message(sprintf("user %s login", player.name))
426       login.process
427       player.setup_logger($options["player-log-dir"]) if $options["player-log-dir"]
428       player.run(login.csa_1st_str) # loop
429       $mutex.lock
430       begin
431         if (player.game)
432           player.game.kill(player)
433         end
434         player.finish # socket has been closed
435         $league.delete(player)
436         log_message(sprintf("user %s logout", player.name))
437       ensure
438         $mutex.unlock
439       end
440     rescue Exception => ex
441       log_error("server.start: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
442     end
443   end
444 end
445
446
447 if ($0 == __FILE__)
448   STDOUT.sync = true
449   STDERR.sync = true
450   TCPSocket.do_not_reverse_lookup = true
451   Thread.abort_on_exception = $DEBUG ? true : false
452
453   begin
454     main
455   rescue Exception => ex
456     if $logger
457       log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
458     else
459       $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
460     end
461   end
462 end