OSDN Git Service

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