OSDN Git Service

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