OSDN Git Service

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