OSDN Git Service

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