OSDN Git Service

New feature: max moves
[shogi-server/shogi-server.git] / shogi-server
1 #! /usr/bin/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-2012 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.expand_path(__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         --max-moves n
80                 when a game with the n-th move played does not end, make the game a draw.
81                 Default 256. 0 disables this feature.
82         --pid-file file
83                 a file path in which a process ID will be written.
84                 Use with --daemon option.
85         --daemon dir
86                 run as a daemon. Log files will be put in dir.
87         --floodgate-games game_A[,...]
88                 enable Floodgate with various game names (separated by a comma)
89         --player-log-dir dir
90                 enable to log network messages for players. Log files
91                 will be put in the dir.
92
93 EXAMPLES
94
95         1. % ./shogi-server test 4081
96            Run the shogi-server. Then clients can connect to port#4081.
97            The server output logs to the stdout.
98
99         2. % ./shogi-server --daemon . --pid-file ./shogi-server.pid \
100                             --player-log-dir ./player-logs \
101                             test 4081
102            Run the shogi-server as a daemon. The server outputs regular logs
103            to shogi-server.log located in the current directory and network 
104            messages in ./player-logs directory.
105
106         3. % ./shogi-server --daemon . --pid-file ./shogi-server.pid \
107                             --player-log-dir ./player-logs \
108                             --floodgate-games floodgate-900-0,floodgate-3600-0 \
109                             test 4081
110            Run the shogi-server with two groups of Floodgate games.
111            Configuration files allow you to schedule starting times. Consult  
112            floodgate-0-240.conf.sample or shogi_server/league/floodgate.rb 
113            for format details.
114
115 FLOODGATE SCHEDULE CONFIGURATIONS
116
117             You need to set starting times of floodgate groups in
118             configuration files under the top directory. Each floodgate 
119             group requires a corresponding configuration file named
120             "<game_name>.conf". The file will be re-read once just after a
121             game starts. 
122             
123             For example, a floodgate-3600-30 game group requires
124             floodgate-3600-30.conf.  However, for floodgate-900-0 and
125             floodgate-3600-0, which were default enabled in previous
126             versions, configuration files are optional if you are happy with
127             default time settings.
128             File format is:
129               Line format: 
130                 # This is a comment line
131                 DoW Time
132                 ...
133               where
134                 DoW := "Sun" | "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" |
135                        "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" |
136                        "Friday" | "Saturday" 
137                 Time := HH:MM
138              
139               For example,
140                 Sat 13:00
141                 Sat 22:00
142                 Sun 13:00
143
144             PAREMETER SETTING
145
146             In addition, this configuration file allows to set parameters
147             for the specific Floodaget group. A list of parameters is the
148             following:
149
150             * pairing_factory:
151               Specifies a factory function name generating a pairing
152               method which will be used in a specific Floodgate game.
153               ex. set pairing_factory floodgate_zyunisen
154             * sacrifice:
155               Specifies a sacrificed player.
156               ex. set sacrifice gps500+e293220e3f8a3e59f79f6b0efffaa931
157
158 LICENSE
159         GPL versoin 2 or later
160
161 SEE ALSO
162
163 REVISION
164         #{ShogiServer::Revision}
165
166 EOM
167 end
168
169
170 def log_debug(str)
171   $logger.debug(str)
172 end
173
174 def log_message(str)
175   $logger.info(str)
176 end
177 def log_info(str)
178   log_message(str)
179 end
180
181 def log_warning(str)
182   $logger.warn(str)
183 end
184
185 def log_error(str)
186   $logger.error(str)
187 end
188
189
190 # Parse command line options. Return a hash containing the option strings
191 # where a key is the option name without the first two slashes. For example,
192 # {"pid-file" => "foo.pid"}.
193 #
194 def parse_command_line
195   options = Hash::new
196   parser = GetoptLong.new(
197     ["--daemon",            GetoptLong::REQUIRED_ARGUMENT],
198     ["--floodgate-games",   GetoptLong::REQUIRED_ARGUMENT],
199     ["--max-moves",         GetoptLong::REQUIRED_ARGUMENT],
200     ["--pid-file",          GetoptLong::REQUIRED_ARGUMENT],
201     ["--player-log-dir",    GetoptLong::REQUIRED_ARGUMENT])
202   parser.quiet = true
203   begin
204     parser.each_option do |name, arg|
205       name.sub!(/^--/, '')
206       options[name] = arg.dup
207     end
208   rescue
209     usage
210     raise parser.error_message
211   end
212   return options
213 end
214
215 # Check command line options.
216 # If any of them is invalid, exit the process.
217 #
218 def check_command_line
219   if (ARGV.length != 2)
220     usage
221     exit 2
222   end
223
224   if $options["daemon"]
225     $options["daemon"] = File.expand_path($options["daemon"], File.dirname(__FILE__))
226     unless is_writable_dir? $options["daemon"]
227       usage
228       $stderr.puts "Can not create a file in the daemon directory: %s" % [$options["daemon"]]
229       exit 5
230     end
231   end
232
233   $topdir = $options["daemon"] || File.expand_path(File.dirname(__FILE__))
234
235   if $options["player-log-dir"]
236     $options["player-log-dir"] = File.expand_path($options["player-log-dir"], $topdir)
237     unless is_writable_dir?($options["player-log-dir"])
238       usage
239       $stderr.puts "Can not write a file in the player log dir: %s" % [$options["player-log-dir"]]
240       exit 3
241     end 
242   end
243
244   if $options["pid-file"] 
245     $options["pid-file"] = File.expand_path($options["pid-file"], $topdir)
246     unless ShogiServer::is_writable_file? $options["pid-file"]
247       usage
248       $stderr.puts "Can not create the pid file: %s" % [$options["pid-file"]]
249       exit 4
250     end
251   end
252
253   if $options["floodgate-games"]
254     names = $options["floodgate-games"].split(",")
255     new_names = 
256       names.select do |name|
257         ShogiServer::League::Floodgate::game_name?(name)
258       end
259     if names.size != new_names.size
260       $stderr.puts "Found a wrong Floodgate game: %s" % [names.join(",")]
261       exit 6
262     end
263     $options["floodgate-games"] = new_names
264   end
265
266   if $options["floodgate-history"]
267     $stderr.puts "WARNING: --floodgate-history has been deprecated."
268     $options["floodgate-history"] = nil
269   end
270
271   $options["max-moves"] ||= 256
272   $options["max-moves"] = $options["max-moves"].to_i
273 end
274
275 # See if a file can be created in the directory.
276 # Return true if a file is writable in the directory, otherwise false.
277 #
278 def is_writable_dir?(dir)
279   unless File.directory? dir
280     return false
281   end
282
283   result = true
284
285   begin
286     temp_file = Tempfile.new("dummy-shogi-server", dir)
287     temp_file.close true
288   rescue
289     result = false
290   end
291
292   return result
293 end
294
295 def write_pid_file(file)
296   open(file, "w") do |fh|
297     fh.puts "#{$$}"
298   end
299 end
300
301 def mutex_watchdog(mutex, sec)
302   sec = 1 if sec < 1
303   queue = []
304   while true
305     if mutex.try_lock
306       queue.clear
307       mutex.unlock
308     else
309       queue.push(Object.new)
310       if queue.size > sec
311         # timeout
312         log_error("mutex watchdog timeout: %d sec" % [sec])
313         queue.clear
314       end
315     end
316     sleep(1)
317   end
318 end
319
320 def login_loop(client)
321   player = login = nil
322  
323   while r = select([client], nil, nil, ShogiServer::Login_Time) do
324     str = nil
325     begin
326       break unless str = r[0].first.gets
327     rescue Exception => ex
328       # It is posssible that the socket causes an error (ex. Errno::ECONNRESET)
329       log_error("login_loop: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
330       break
331     end
332     $mutex.lock # guards $league
333     begin
334       str =~ /([\r\n]*)$/
335       eol = $1
336       if (ShogiServer::Login::good_login?(str))
337         player = ShogiServer::Player::new(str, client, eol)
338         login  = ShogiServer::Login::factory(str, player)
339         if (current_player = $league.find(player.name))
340           if (current_player.password == player.password &&
341               current_player.status != "game")
342             log_message(sprintf("user %s login forcely", player.name))
343             current_player.kill
344           else
345             login.incorrect_duplicated_player(str)
346             player = nil
347             break
348           end
349         end
350         $league.add(player)
351         break
352       else
353         client.write("LOGIN:incorrect" + eol)
354         client.write("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
355       end
356     ensure
357       $mutex.unlock
358     end
359   end                       # login loop
360   return [player, login]
361 end
362
363 def setup_logger(log_file)
364   logger = ShogiServer::Logger.new(log_file, 'daily')
365   logger.formatter = ShogiServer::Formatter.new
366   logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO  
367   logger.datetime_format = "%Y-%m-%d %H:%M:%S"
368   return logger
369 end
370
371 def setup_watchdog_for_giant_lock
372   $mutex = Mutex::new
373   Thread::start do
374     Thread.pass
375     mutex_watchdog($mutex, 10)
376   end
377 end
378
379 def main
380   
381   $options = parse_command_line
382   check_command_line
383   $config = ShogiServer::Config.new $options
384
385   $league = ShogiServer::League.new($topdir)
386
387   $league.event = ARGV.shift
388   port = ARGV.shift
389
390   log_file = $options["daemon"] ? File.join($options["daemon"], "shogi-server.log") : STDOUT
391   $logger = setup_logger(log_file)
392
393   $league.dir = $topdir
394
395   config = {}
396   config[:BindAddress] = "0.0.0.0"
397   config[:Port]       = port
398   config[:ServerType] = WEBrick::Daemon if $options["daemon"]
399   config[:Logger]     = $logger
400
401   setup_floodgate = nil
402
403   config[:StartCallback] = Proc.new do
404     srand
405     if $options["pid-file"]
406       write_pid_file($options["pid-file"])
407     end
408     setup_watchdog_for_giant_lock
409     $league.setup_players_database
410     setup_floodgate = ShogiServer::SetupFloodgate.new($options["floodgate-games"])
411     setup_floodgate.start
412   end
413
414   config[:StopCallback] = Proc.new do
415     if $options["pid-file"]
416       FileUtils.rm($options["pid-file"], :force => true)
417     end
418   end
419
420   srand
421   server = WEBrick::GenericServer.new(config)
422   ["INT", "TERM"].each do |signal| 
423     trap(signal) do
424       server.shutdown
425       setup_floodgate.kill
426     end
427   end
428   unless (RUBY_PLATFORM.downcase =~ /mswin|mingw|cygwin|bccwin/)
429     trap("HUP") do
430       Dependencies.clear
431     end
432   end
433   $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] 
434   log_message("server started [Revision: #{ShogiServer::Revision}]")
435
436   server.start do |client|
437     begin
438       # client.sync = true # this is already set in WEBrick 
439       client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
440         # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
441       player, login = login_loop(client) # loop
442       unless player
443         log_error("Detected a timed out login attempt")
444         next
445       end
446
447       log_message(sprintf("user %s login", player.name))
448       login.process
449       player.setup_logger($options["player-log-dir"]) if $options["player-log-dir"]
450       player.run(login.csa_1st_str) # loop
451       $mutex.lock
452       begin
453         if (player.game)
454           player.game.kill(player)
455         end
456         player.finish
457         $league.delete(player)
458         log_message(sprintf("user %s logout", player.name))
459       ensure
460         $mutex.unlock
461       end
462       player.wait_write_thread_finish(1000) # milliseconds
463     rescue Exception => ex
464       log_error("server.start: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
465     end
466   end
467 end
468
469
470 if ($0 == __FILE__)
471   STDOUT.sync = true
472   STDERR.sync = true
473   TCPSocket.do_not_reverse_lookup = true
474   Thread.abort_on_exception = $DEBUG ? true : false
475
476   begin
477     main
478   rescue Exception => ex
479     if $logger
480       log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
481     else
482       $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
483     end
484   end
485 end