OSDN Git Service

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