OSDN Git Service

c0f64f83cc4f466b04740c45435bccc53c0fa3e8
[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 LICENSE
142         GPL versoin 2 or later
143
144 SEE ALSO
145
146 REVISION
147         #{ShogiServer::Revision}
148
149 EOM
150 end
151
152
153 def log_debug(str)
154   $logger.debug(str)
155 end
156
157 def log_message(str)
158   $logger.info(str)
159 end
160 def log_info(str)
161   log_message(str)
162 end
163
164 def log_warning(str)
165   $logger.warn(str)
166 end
167
168 def log_error(str)
169   $logger.error(str)
170 end
171
172
173 # Parse command line options. Return a hash containing the option strings
174 # where a key is the option name without the first two slashes. For example,
175 # {"pid-file" => "foo.pid"}.
176 #
177 def parse_command_line
178   options = Hash::new
179   parser = GetoptLong.new(
180     ["--daemon",            GetoptLong::REQUIRED_ARGUMENT],
181     ["--floodgate-games",   GetoptLong::REQUIRED_ARGUMENT],
182     ["--pid-file",          GetoptLong::REQUIRED_ARGUMENT],
183     ["--player-log-dir",    GetoptLong::REQUIRED_ARGUMENT])
184   parser.quiet = true
185   begin
186     parser.each_option do |name, arg|
187       name.sub!(/^--/, '')
188       options[name] = arg.dup
189     end
190   rescue
191     usage
192     raise parser.error_message
193   end
194   return options
195 end
196
197 # Check command line options.
198 # If any of them is invalid, exit the process.
199 #
200 def check_command_line
201   if (ARGV.length != 2)
202     usage
203     exit 2
204   end
205
206   if $options["daemon"]
207     $options["daemon"] = File.expand_path($options["daemon"], File.dirname(__FILE__))
208     unless is_writable_dir? $options["daemon"]
209       usage
210       $stderr.puts "Can not create a file in the daemon directory: %s" % [$options["daemon"]]
211       exit 5
212     end
213   end
214
215   $topdir = $options["daemon"] || File.expand_path(File.dirname(__FILE__))
216
217   if $options["player-log-dir"]
218     $options["player-log-dir"] = File.expand_path($options["player-log-dir"], $topdir)
219     unless is_writable_dir?($options["player-log-dir"])
220       usage
221       $stderr.puts "Can not write a file in the player log dir: %s" % [$options["player-log-dir"]]
222       exit 3
223     end 
224   end
225
226   if $options["pid-file"] 
227     $options["pid-file"] = File.expand_path($options["pid-file"], $topdir)
228     unless ShogiServer::is_writable_file? $options["pid-file"]
229       usage
230       $stderr.puts "Can not create the pid file: %s" % [$options["pid-file"]]
231       exit 4
232     end
233   end
234
235   if $options["floodgate-games"]
236     names = $options["floodgate-games"].split(",")
237     new_names = 
238       names.select do |name|
239         ShogiServer::League::Floodgate::game_name?(name)
240       end
241     if names.size != new_names.size
242       $stderr.puts "Found a wrong Floodgate game: %s" % [names.join(",")]
243       exit 6
244     end
245     $options["floodgate-games"] = new_names
246   end
247
248   if $options["floodgate-history"]
249     $stderr.puts "WARNING: --floodgate-history has been deprecated."
250     $options["floodgate-history"] = nil
251   end
252 end
253
254 # See if a file can be created in the directory.
255 # Return true if a file is writable in the directory, otherwise false.
256 #
257 def is_writable_dir?(dir)
258   unless File.directory? dir
259     return false
260   end
261
262   result = true
263
264   begin
265     temp_file = Tempfile.new("dummy-shogi-server", dir)
266     temp_file.close true
267   rescue
268     result = false
269   end
270
271   return result
272 end
273
274 def write_pid_file(file)
275   open(file, "w") do |fh|
276     fh.puts "#{$$}"
277   end
278 end
279
280 def mutex_watchdog(mutex, sec)
281   sec = 1 if sec < 1
282   queue = []
283   while true
284     if mutex.try_lock
285       queue.clear
286       mutex.unlock
287     else
288       queue.push(Object.new)
289       if queue.size > sec
290         # timeout
291         log_error("mutex watchdog timeout: %d sec" % [sec])
292         queue.clear
293       end
294     end
295     sleep(1)
296   end
297 end
298
299 def login_loop(client)
300   player = login = nil
301  
302   while r = select([client], nil, nil, ShogiServer::Login_Time) do
303     str = nil
304     begin
305       break unless str = r[0].first.gets
306     rescue Exception => ex
307       # It is posssible that the socket causes an error (ex. Errno::ECONNRESET)
308       log_error("login_loop: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
309       break
310     end
311     $mutex.lock # guards $league
312     begin
313       str =~ /([\r\n]*)$/
314       eol = $1
315       if (ShogiServer::Login::good_login?(str))
316         player = ShogiServer::Player::new(str, client, eol)
317         login  = ShogiServer::Login::factory(str, player)
318         if (current_player = $league.find(player.name))
319           if (current_player.password == player.password &&
320               current_player.status != "game")
321             log_message(sprintf("user %s login forcely", player.name))
322             current_player.kill
323           else
324             login.incorrect_duplicated_player(str)
325             player = nil
326             break
327           end
328         end
329         $league.add(player)
330         break
331       else
332         client.write("LOGIN:incorrect" + eol)
333         client.write("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
334       end
335     ensure
336       $mutex.unlock
337     end
338   end                       # login loop
339   return [player, login]
340 end
341
342 def setup_logger(log_file)
343   logger = ShogiServer::Logger.new(log_file, 'daily')
344   logger.formatter = ShogiServer::Formatter.new
345   logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO  
346   logger.datetime_format = "%Y-%m-%d %H:%M:%S"
347   return logger
348 end
349
350 def setup_watchdog_for_giant_lock
351   $mutex = Mutex::new
352   Thread::start do
353     Thread.pass
354     mutex_watchdog($mutex, 10)
355   end
356 end
357
358 def main
359   
360   $options = parse_command_line
361   check_command_line
362   $config = ShogiServer::Config.new $options
363
364   $league = ShogiServer::League.new($topdir)
365
366   $league.event = ARGV.shift
367   port = ARGV.shift
368
369   log_file = $options["daemon"] ? File.join($options["daemon"], "shogi-server.log") : STDOUT
370   $logger = setup_logger(log_file)
371
372   $league.dir = $topdir
373
374   config = {}
375   config[:Port]       = port
376   config[:ServerType] = WEBrick::Daemon if $options["daemon"]
377   config[:Logger]     = $logger
378
379   setup_floodgate = nil
380
381   config[:StartCallback] = Proc.new do
382     srand
383     if $options["pid-file"]
384       write_pid_file($options["pid-file"])
385     end
386     setup_watchdog_for_giant_lock
387     $league.setup_players_database
388     setup_floodgate = ShogiServer::SetupFloodgate.new($options["floodgate-games"])
389     setup_floodgate.start
390   end
391
392   config[:StopCallback] = Proc.new do
393     if $options["pid-file"]
394       FileUtils.rm($options["pid-file"], :force => true)
395     end
396   end
397
398   srand
399   server = WEBrick::GenericServer.new(config)
400   ["INT", "TERM"].each do |signal| 
401     trap(signal) do
402       server.shutdown
403       setup_floodgate.kill
404     end
405   end
406   unless (RUBY_PLATFORM.downcase =~ /mswin|mingw|cygwin|bccwin/)
407     trap("HUP") do
408       Dependencies.clear
409     end
410   end
411   $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] 
412   log_message("server started [Revision: #{ShogiServer::Revision}]")
413
414   server.start do |client|
415     begin
416       # client.sync = true # this is already set in WEBrick 
417       client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
418         # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
419       player, login = login_loop(client) # loop
420       unless player
421         log_error("Detected a timed out login attempt")
422         next
423       end
424
425       log_message(sprintf("user %s login", player.name))
426       login.process
427       player.setup_logger($options["player-log-dir"]) if $options["player-log-dir"]
428       player.run(login.csa_1st_str) # loop
429       $mutex.lock
430       begin
431         if (player.game)
432           player.game.kill(player)
433         end
434         player.finish
435         $league.delete(player)
436         log_message(sprintf("user %s logout", player.name))
437       ensure
438         $mutex.unlock
439       end
440       player.wait_write_thread_finish(1000) # milliseconds
441     rescue Exception => ex
442       log_error("server.start: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
443     end
444   end
445 end
446
447
448 if ($0 == __FILE__)
449   STDOUT.sync = true
450   STDERR.sync = true
451   TCPSocket.do_not_reverse_lookup = true
452   Thread.abort_on_exception = $DEBUG ? true : false
453
454   begin
455     main
456   rescue Exception => ex
457     if $logger
458       log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
459     else
460       $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
461     end
462   end
463 end