OSDN Git Service

* [shogi-server]
[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[:BindAddress] = "0.0.0.0"
376   config[:Port]       = port
377   config[:ServerType] = WEBrick::Daemon if $options["daemon"]
378   config[:Logger]     = $logger
379
380   setup_floodgate = nil
381
382   config[:StartCallback] = Proc.new do
383     srand
384     if $options["pid-file"]
385       write_pid_file($options["pid-file"])
386     end
387     setup_watchdog_for_giant_lock
388     $league.setup_players_database
389     setup_floodgate = ShogiServer::SetupFloodgate.new($options["floodgate-games"])
390     setup_floodgate.start
391   end
392
393   config[:StopCallback] = Proc.new do
394     if $options["pid-file"]
395       FileUtils.rm($options["pid-file"], :force => true)
396     end
397   end
398
399   srand
400   server = WEBrick::GenericServer.new(config)
401   ["INT", "TERM"].each do |signal| 
402     trap(signal) do
403       server.shutdown
404       setup_floodgate.kill
405     end
406   end
407   unless (RUBY_PLATFORM.downcase =~ /mswin|mingw|cygwin|bccwin/)
408     trap("HUP") do
409       Dependencies.clear
410     end
411   end
412   $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] 
413   log_message("server started [Revision: #{ShogiServer::Revision}]")
414
415   server.start do |client|
416     begin
417       # client.sync = true # this is already set in WEBrick 
418       client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
419         # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
420       player, login = login_loop(client) # loop
421       unless player
422         log_error("Detected a timed out login attempt")
423         next
424       end
425
426       log_message(sprintf("user %s login", player.name))
427       login.process
428       player.setup_logger($options["player-log-dir"]) if $options["player-log-dir"]
429       player.run(login.csa_1st_str) # loop
430       $mutex.lock
431       begin
432         if (player.game)
433           player.game.kill(player)
434         end
435         player.finish
436         $league.delete(player)
437         log_message(sprintf("user %s logout", player.name))
438       ensure
439         $mutex.unlock
440       end
441       player.wait_write_thread_finish(1000) # milliseconds
442     rescue Exception => ex
443       log_error("server.start: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
444     end
445   end
446 end
447
448
449 if ($0 == __FILE__)
450   STDOUT.sync = true
451   STDERR.sync = true
452   TCPSocket.do_not_reverse_lookup = true
453   Thread.abort_on_exception = $DEBUG ? true : false
454
455   begin
456     main
457   rescue Exception => ex
458     if $logger
459       log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
460     else
461       $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
462     end
463   end
464 end