OSDN Git Service

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