OSDN Git Service

Record files now include rating scores of players.
[shogi-server/shogi-server.git] / shogi-server
1 #! /usr/bin/ruby1.9.1
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 ONE_DAY = 3600 * 24   # in seconds
44
45 ShogiServer.reload
46
47 # Return
48 #   - a received string
49 #   - :timeout
50 #   - :exception
51 #   - nil when a socket is closed
52 #
53 def gets_safe(socket, timeout=nil)
54   if r = select([socket], nil, nil, timeout)
55     return r[0].first.gets
56   else
57     return :timeout
58   end
59 rescue Exception => ex
60   log_error("gets_safe: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
61   return :exception
62 end
63
64 def usage
65     print <<EOM
66 NAME
67         shogi-server - server for CSA server protocol
68
69 SYNOPSIS
70         shogi-server [OPTIONS] event_name port_number
71
72 DESCRIPTION
73         server for CSA server protocol
74
75 OPTIONS
76         event_name
77                 a prefix of record files.
78         port_number
79                 a port number for the server to listen. 
80                 4081 is often used.
81         --pid-file file
82                 a file path in which a process ID will be written.
83                 Use with --daemon option.
84         --daemon dir
85                 run as a daemon. Log files will be put in dir.
86         --floodgate-games game_A[,...]
87                 enable Floodgate with various game names (separated by a comma)
88         --player-log-dir dir
89                 enable to log network messages for players. Log files
90                 will be put in the dir.
91
92 EXAMPLES
93
94         1. % ./shogi-server test 4081
95            Run the shogi-server. Then clients can connect to port#4081.
96            The server output logs to the stdout.
97
98         2. % ./shogi-server --daemon . --pid-file ./shogi-server.pid \
99                             --player-log-dir ./player-logs \
100                             test 4081
101            Run the shogi-server as a daemon. The server outputs regular logs
102            to shogi-server.log located in the current directory and network 
103            messages in ./player-logs directory.
104
105         3. % ./shogi-server --daemon . --pid-file ./shogi-server.pid \
106                             --player-log-dir ./player-logs \
107                             --floodgate-games floodgate-900-0,floodgate-3600-0 \
108                             test 4081
109            Run the shogi-server with two groups of Floodgate games.
110            Configuration files allow you to schedule starting times. Consult  
111            floodgate-0-240.conf.sample or shogi_server/league/floodgate.rb 
112            for format details.
113
114 FLOODGATE SCHEDULE CONFIGURATIONS
115
116             You need to set starting times of floodgate groups in
117             configuration files under the top directory. Each floodgate 
118             group requires a corresponding configuration file named
119             "<game_name>.conf". The file will be re-read once just after a
120             game starts. 
121             
122             For example, a floodgate-3600-30 game group requires
123             floodgate-3600-30.conf.  However, for floodgate-900-0 and
124             floodgate-3600-0, which were default enabled in previous
125             versions, configuration files are optional if you are happy with
126             default time settings.
127             File format is:
128               Line format: 
129                 # This is a comment line
130                 DoW Time
131                 ...
132               where
133                 DoW := "Sun" | "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" |
134                        "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" |
135                        "Friday" | "Saturday" 
136                 Time := HH:MM
137              
138               For example,
139                 Sat 13:00
140                 Sat 22:00
141                 Sun 13:00
142
143             PAREMETER SETTING
144
145             In addition, this configuration file allows to set parameters
146             for the specific Floodaget group. A list of parameters is the
147             following:
148
149             * pairing_factory:
150               Specifies a factory function name generating a pairing
151               method which will be used in a specific Floodgate game.
152               ex. set pairing_factory floodgate_zyunisen
153             * sacrifice:
154               Specifies a sacrificed player.
155               ex. set sacrifice gps500+e293220e3f8a3e59f79f6b0efffaa931
156
157 LICENSE
158         GPL versoin 2 or later
159
160 SEE ALSO
161
162 REVISION
163         #{ShogiServer::Revision}
164
165 EOM
166 end
167
168
169 def log_debug(str)
170   $logger.debug(str)
171 end
172
173 def log_message(str)
174   $logger.info(str)
175 end
176 def log_info(str)
177   log_message(str)
178 end
179
180 def log_warning(str)
181   $logger.warn(str)
182 end
183
184 def log_error(str)
185   $logger.error(str)
186 end
187
188
189 # Parse command line options. Return a hash containing the option strings
190 # where a key is the option name without the first two slashes. For example,
191 # {"pid-file" => "foo.pid"}.
192 #
193 def parse_command_line
194   options = Hash::new
195   parser = GetoptLong.new(
196     ["--daemon",            GetoptLong::REQUIRED_ARGUMENT],
197     ["--floodgate-games",   GetoptLong::REQUIRED_ARGUMENT],
198     ["--pid-file",          GetoptLong::REQUIRED_ARGUMENT],
199     ["--player-log-dir",    GetoptLong::REQUIRED_ARGUMENT])
200   parser.quiet = true
201   begin
202     parser.each_option do |name, arg|
203       name.sub!(/^--/, '')
204       options[name] = arg.dup
205     end
206   rescue
207     usage
208     raise parser.error_message
209   end
210   return options
211 end
212
213 # Check command line options.
214 # If any of them is invalid, exit the process.
215 #
216 def check_command_line
217   if (ARGV.length != 2)
218     usage
219     exit 2
220   end
221
222   if $options["daemon"]
223     $options["daemon"] = File.expand_path($options["daemon"], File.dirname(__FILE__))
224     unless is_writable_dir? $options["daemon"]
225       usage
226       $stderr.puts "Can not create a file in the daemon directory: %s" % [$options["daemon"]]
227       exit 5
228     end
229   end
230
231   $topdir = $options["daemon"] || File.expand_path(File.dirname(__FILE__))
232
233   if $options["player-log-dir"]
234     $options["player-log-dir"] = File.expand_path($options["player-log-dir"], $topdir)
235     unless is_writable_dir?($options["player-log-dir"])
236       usage
237       $stderr.puts "Can not write a file in the player log dir: %s" % [$options["player-log-dir"]]
238       exit 3
239     end 
240   end
241
242   if $options["pid-file"] 
243     $options["pid-file"] = File.expand_path($options["pid-file"], $topdir)
244     unless ShogiServer::is_writable_file? $options["pid-file"]
245       usage
246       $stderr.puts "Can not create the pid file: %s" % [$options["pid-file"]]
247       exit 4
248     end
249   end
250
251   if $options["floodgate-games"]
252     names = $options["floodgate-games"].split(",")
253     new_names = 
254       names.select do |name|
255         ShogiServer::League::Floodgate::game_name?(name)
256       end
257     if names.size != new_names.size
258       $stderr.puts "Found a wrong Floodgate game: %s" % [names.join(",")]
259       exit 6
260     end
261     $options["floodgate-games"] = new_names
262   end
263
264   if $options["floodgate-history"]
265     $stderr.puts "WARNING: --floodgate-history has been deprecated."
266     $options["floodgate-history"] = nil
267   end
268 end
269
270 # See if a file can be created in the directory.
271 # Return true if a file is writable in the directory, otherwise false.
272 #
273 def is_writable_dir?(dir)
274   unless File.directory? dir
275     return false
276   end
277
278   result = true
279
280   begin
281     temp_file = Tempfile.new("dummy-shogi-server", dir)
282     temp_file.close true
283   rescue
284     result = false
285   end
286
287   return result
288 end
289
290 def write_pid_file(file)
291   open(file, "w") do |fh|
292     fh.puts "#{$$}"
293   end
294 end
295
296 def mutex_watchdog(mutex, sec)
297   sec = 1 if sec < 1
298   queue = []
299   while true
300     if mutex.try_lock
301       queue.clear
302       mutex.unlock
303     else
304       queue.push(Object.new)
305       if queue.size > sec
306         # timeout
307         log_error("mutex watchdog timeout: %d sec" % [sec])
308         queue.clear
309       end
310     end
311     sleep(1)
312   end
313 end
314
315 def login_loop(client)
316   player = login = nil
317  
318   while r = select([client], nil, nil, ShogiServer::Login_Time) do
319     str = nil
320     begin
321       break unless str = r[0].first.gets
322     rescue Exception => ex
323       # It is posssible that the socket causes an error (ex. Errno::ECONNRESET)
324       log_error("login_loop: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
325       break
326     end
327     $mutex.lock # guards $league
328     begin
329       str =~ /([\r\n]*)$/
330       eol = $1
331       if (ShogiServer::Login::good_login?(str))
332         player = ShogiServer::Player::new(str, client, eol)
333         login  = ShogiServer::Login::factory(str, player)
334         if (current_player = $league.find(player.name))
335           # Even if a player is in the 'game' state, when the status of the
336           # player has not been updated for more than a day, it is very
337           # likely that the player is stalling. In such a case, a new player
338           # can override the current player.
339           if (current_player.password == player.password &&
340               (current_player.status != "game" ||
341                Time.now - current_player.modifiled_at > ONE_DAY))
342             log_message("user %s login forcely (previously modified at %s)" % [player.name, player.modified_at])
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