OSDN Git Service

807388933640df38d62f480ec735642d153c7ecc
[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.modifiled_at > ONE_DAY))
360             log_message("user %s login forcely (previously modified at %s)" % [player.name, player.modified_at])
361             current_player.kill
362           else
363             login.incorrect_duplicated_player(str)
364             player = nil
365             break
366           end
367         end
368         $league.add(player)
369         break
370       else
371         client.write("LOGIN:incorrect" + eol)
372         client.write("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
373       end
374     ensure
375       $mutex.unlock
376     end
377   end                       # login loop
378   return [player, login]
379 end
380
381 def setup_logger(log_file)
382   logger = ShogiServer::Logger.new(log_file, 'daily')
383   logger.formatter = ShogiServer::Formatter.new
384   logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO  
385   logger.datetime_format = "%Y-%m-%d %H:%M:%S"
386   return logger
387 end
388
389 def setup_watchdog_for_giant_lock
390   $mutex = Mutex::new
391   Thread::start do
392     Thread.pass
393     mutex_watchdog($mutex, 10)
394   end
395 end
396
397 def main
398   
399   $options = parse_command_line
400   check_command_line
401   $config = ShogiServer::Config.new $options
402
403   $league = ShogiServer::League.new($topdir)
404
405   $league.event = ARGV.shift
406   port = ARGV.shift
407
408   log_file = $options["daemon"] ? File.join($options["daemon"], "shogi-server.log") : STDOUT
409   $logger = setup_logger(log_file)
410
411   $league.dir = $topdir
412
413   config = {}
414   config[:BindAddress] = "0.0.0.0"
415   config[:Port]       = port
416   config[:ServerType] = WEBrick::Daemon if $options["daemon"]
417   config[:Logger]     = $logger
418
419   setup_floodgate = nil
420
421   config[:StartCallback] = Proc.new do
422     srand
423     if $options["pid-file"]
424       write_pid_file($options["pid-file"])
425     end
426     setup_watchdog_for_giant_lock
427     $league.setup_players_database
428     setup_floodgate = ShogiServer::SetupFloodgate.new($options["floodgate-games"])
429     setup_floodgate.start
430   end
431
432   config[:StopCallback] = Proc.new do
433     if $options["pid-file"]
434       FileUtils.rm($options["pid-file"], :force => true)
435     end
436   end
437
438   srand
439   server = WEBrick::GenericServer.new(config)
440   ["INT", "TERM"].each do |signal| 
441     trap(signal) do
442       server.shutdown
443       setup_floodgate.kill
444     end
445   end
446   unless (RUBY_PLATFORM.downcase =~ /mswin|mingw|cygwin|bccwin/)
447     trap("HUP") do
448       Dependencies.clear
449     end
450   end
451   $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] 
452   log_message("server started [Revision: #{ShogiServer::Revision}]")
453
454   server.start do |client|
455     begin
456       # client.sync = true # this is already set in WEBrick 
457       client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
458         # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
459       player, login = login_loop(client) # loop
460       unless player
461         log_error("Detected a timed out login attempt")
462         next
463       end
464
465       log_message(sprintf("user %s login", player.name))
466       login.process
467       player.setup_logger($options["player-log-dir"]) if $options["player-log-dir"]
468       player.run(login.csa_1st_str) # loop
469       $mutex.lock
470       begin
471         if (player.game)
472           player.game.kill(player)
473         end
474         player.finish
475         $league.delete(player)
476         log_message(sprintf("user %s logout", player.name))
477       ensure
478         $mutex.unlock
479       end
480       player.wait_write_thread_finish(1000) # milliseconds
481     rescue Exception => ex
482       log_error("server.start: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
483     end
484   end
485 end
486
487
488 if ($0 == __FILE__)
489   STDOUT.sync = true
490   STDERR.sync = true
491   TCPSocket.do_not_reverse_lookup = true
492   Thread.abort_on_exception = $DEBUG ? true : false
493
494   begin
495     main
496   rescue Exception => ex
497     if $logger
498       log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
499     else
500       $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
501     end
502   end
503 end