OSDN Git Service

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