OSDN Git Service

Merge branch 'master' of git.sourceforge.jp:/gitroot/shogi-server/shogi-server
[shogi-server/shogi-server.git] / shogi-server
1 #! /usr/bin/env 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-2008 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__)
33 require 'shogi_server'
34 require 'shogi_server/config'
35 require 'shogi_server/util'
36 require 'tempfile'
37
38 #################################################
39 # MAIN
40 #
41
42 ShogiServer.reload
43
44 # Return
45 #   - a received string
46 #   - :timeout
47 #   - :exception
48 #   - nil when a socket is closed
49 #
50 def gets_safe(socket, timeout=nil)
51   if r = select([socket], nil, nil, timeout)
52     return r[0].first.gets
53   else
54     return :timeout
55   end
56 rescue Exception => ex
57   log_error("gets_safe: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
58   return :exception
59 end
60
61 def usage
62     print <<EOM
63 NAME
64         shogi-server - server for CSA server protocol
65
66 SYNOPSIS
67         shogi-server [OPTIONS] event_name port_number
68
69 DESCRIPTION
70         server for CSA server protocol
71
72 OPTIONS
73         --pid-file file
74                 specify filename for logging process ID
75         --daemon dir
76                 run as a daemon. Log files will be put in dir.
77         --player-log-dir dir
78                 log network messages for each player. Log files
79                 will be put in the dir.
80
81 LICENSE
82         GPL versoin 2 or later
83
84 SEE ALSO
85
86 RELEASE
87         #{ShogiServer::Release}
88
89 REVISION
90         #{ShogiServer::Revision}
91
92 EOM
93 end
94
95
96 def log_debug(str)
97   $logger.debug(str)
98 end
99
100 def log_message(str)
101   $logger.info(str)
102 end
103 def log_info(str)
104   log_message(str)
105 end
106
107 def log_warning(str)
108   $logger.warn(str)
109 end
110
111 def log_error(str)
112   $logger.error(str)
113 end
114
115
116 # Parse command line options. Return a hash containing the option strings
117 # where a key is the option name without the first two slashes. For example,
118 # {"pid-file" => "foo.pid"}.
119 #
120 def parse_command_line
121   options = Hash::new
122   parser = GetoptLong.new(
123     ["--daemon",            GetoptLong::REQUIRED_ARGUMENT],
124     ["--pid-file",          GetoptLong::REQUIRED_ARGUMENT],
125     ["--player-log-dir",    GetoptLong::REQUIRED_ARGUMENT])
126   parser.quiet = true
127   begin
128     parser.each_option do |name, arg|
129       name.sub!(/^--/, '')
130       options[name] = arg.dup
131     end
132   rescue
133     usage
134     raise parser.error_message
135   end
136   return options
137 end
138
139 # Check command line options.
140 # If any of them is invalid, exit the process.
141 #
142 def check_command_line
143   if (ARGV.length != 2)
144     usage
145     exit 2
146   end
147
148   if $options["daemon"]
149     $options["daemon"] = File.expand_path($options["daemon"], File.dirname(__FILE__))
150     unless is_writable_dir? $options["daemon"]
151       usage
152       $stderr.puts "Can not create a file in the daemon directory: %s" % [$options["daemon"]]
153       exit 5
154     end
155   end
156
157   $topdir = $options["daemon"] || File.expand_path(File.dirname(__FILE__))
158
159   if $options["player-log-dir"]
160     $options["player-log-dir"] = File.expand_path($options["player-log-dir"], $topdir)
161     unless is_writable_dir?($options["player-log-dir"])
162       usage
163       $stderr.puts "Can not write a file in the player log dir: %s" % [$options["player-log-dir"]]
164       exit 3
165     end 
166   end
167
168   if $options["pid-file"] 
169     $options["pid-file"] = File.expand_path($options["pid-file"], $topdir)
170     unless ShogiServer::is_writable_file? $options["pid-file"]
171       usage
172       $stderr.puts "Can not create the pid file: %s" % [$options["pid-file"]]
173       exit 4
174     end
175   end
176
177   if $options["floodgate-history"]
178     $stderr.puts "WARNING: --floodgate-history has been deprecated."
179     $options["floodgate-history"] = nil
180   end
181 end
182
183 # See if a file can be created in the directory.
184 # Return true if a file is writable in the directory, otherwise false.
185 #
186 def is_writable_dir?(dir)
187   unless File.directory? dir
188     return false
189   end
190
191   result = true
192
193   begin
194     temp_file = Tempfile.new("dummy-shogi-server", dir)
195     temp_file.close true
196   rescue
197     result = false
198   end
199
200   return result
201 end
202
203 def write_pid_file(file)
204   open(file, "w") do |fh|
205     fh.puts "#{$$}"
206   end
207 end
208
209 def mutex_watchdog(mutex, sec)
210   sec = 1 if sec < 1
211   queue = []
212   while true
213     if mutex.try_lock
214       queue.clear
215       mutex.unlock
216     else
217       queue.push(Object.new)
218       if queue.size > sec
219         # timeout
220         log_error("mutex watchdog timeout: %d sec" % [sec])
221         queue.clear
222       end
223     end
224     sleep(1)
225   end
226 end
227
228 def login_loop(client)
229   player = login = nil
230  
231   while r = select([client], nil, nil, ShogiServer::Login_Time) do
232     str = nil
233     begin
234       break unless str = r[0].first.gets
235     rescue Exception => ex
236       # It is posssible that the socket causes an error (ex. Errno::ECONNRESET)
237       log_error("login_loop: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
238       break
239     end
240     $mutex.lock # guards $league
241     begin
242       str =~ /([\r\n]*)$/
243       eol = $1
244       if (ShogiServer::Login::good_login?(str))
245         player = ShogiServer::Player::new(str, client, eol)
246         login  = ShogiServer::Login::factory(str, player)
247         if (current_player = $league.find(player.name))
248           if (current_player.password == player.password &&
249               current_player.status != "game")
250             log_message(sprintf("user %s login forcely", player.name))
251             current_player.kill
252           else
253             login.incorrect_duplicated_player(str)
254             player = nil
255             break
256           end
257         end
258         $league.add(player)
259         break
260       else
261         client.write("LOGIN:incorrect" + eol)
262         client.write("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
263       end
264     ensure
265       $mutex.unlock
266     end
267   end                       # login loop
268   return [player, login]
269 end
270
271 def setup_logger(log_file)
272   logger = ShogiServer::Logger.new(log_file, 'daily')
273   logger.formatter = ShogiServer::Formatter.new
274   logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO  
275   logger.datetime_format = "%Y-%m-%d %H:%M:%S"
276   return logger
277 end
278
279 def setup_watchdog_for_giant_lock
280   $mutex = Mutex::new
281   Thread::start do
282     Thread.pass
283     mutex_watchdog($mutex, 10)
284   end
285 end
286
287 def floodgate_reload_log(leagues)
288   floodgate = leagues.min {|a,b| a.next_time <=> b.next_time}
289   diff = floodgate.next_time - Time.now
290   log_message("Floodgate reloaded. The next match will start at %s in %d seconds" % 
291               [floodgate.next_time, diff])
292 end
293
294 def setup_floodgate(game_names)
295   return Thread.start(game_names) do |game_names|
296     Thread.pass
297     leagues = game_names.collect do |game_name|
298       ShogiServer::League::Floodgate.new($league, 
299                                          {:game_name => game_name})
300     end
301     leagues.delete_if do |floodgate|
302       ret = false
303       unless floodgate.next_time 
304         log_error("Unsupported game name: %s" % floodgate.game_name)
305         ret = true
306       end
307       ret
308     end
309     if leagues.empty?
310       log_error("No valid Floodgate game names found")
311       return # exit from this thread
312     end
313     floodgate_reload_log(leagues)
314
315     while (true)
316       begin
317         floodgate = leagues.min {|a,b| a.next_time <=> b.next_time}
318         diff = floodgate.next_time - Time.now
319         if diff > 0
320           floodgate_reload_log(leagues) if $DEBUG
321           sleep(diff/2)
322           next
323         end
324         next_array = leagues.collect do |floodgate|
325           if (floodgate.next_time - Time.now) > 0
326             [floodgate.game_name, floodgate.next_time]
327           else
328             log_message("Starting Floodgate games...: %s" % [floodgate.game_name])
329             $league.reload
330             floodgate.match_game
331             floodgate.charge
332             [floodgate.game_name, floodgate.next_time] # next_time has been updated
333           end
334         end
335         $mutex.synchronize do
336           log_message("Reloading source...")
337           ShogiServer.reload
338         end
339         # Regenerate floodgate instances after ShogiServer.realod
340         leagues = next_array.collect do |game_name, next_time|
341           floodgate = ShogiServer::League::Floodgate.new($league, 
342                                                          {:game_name => game_name,
343                                                           :next_time => next_time})
344         end
345         floodgate_reload_log(leagues)
346       rescue Exception => ex 
347         # ignore errors
348         log_error("[in Floodgate's thread] #{ex} #{ex.backtrace}")
349       end
350     end
351   end
352 end
353
354 def main
355   
356   $options = parse_command_line
357   check_command_line
358   $config = ShogiServer::Config.new $options
359
360   $league = ShogiServer::League.new($topdir)
361
362   $league.event = ARGV.shift
363   port = ARGV.shift
364
365   log_file = $options["daemon"] ? File.join($options["daemon"], "shogi-server.log") : STDOUT
366   $logger = setup_logger(log_file)
367
368   $league.dir = $topdir
369
370   config = {}
371   config[:Port]       = port
372   config[:ServerType] = WEBrick::Daemon if $options["daemon"]
373   config[:Logger]     = $logger
374
375   fg_thread = nil
376
377   config[:StartCallback] = Proc.new do
378     srand
379     if $options["pid-file"]
380       write_pid_file($options["pid-file"])
381     end
382     setup_watchdog_for_giant_lock
383     $league.setup_players_database
384     fg_thread = setup_floodgate(["floodgate-900-0", "floodgate-3600-0"])
385   end
386
387   config[:StopCallback] = Proc.new do
388     if $options["pid-file"]
389       FileUtils.rm($options["pid-file"], :force => true)
390     end
391   end
392
393   srand
394   server = WEBrick::GenericServer.new(config)
395   ["INT", "TERM"].each do |signal| 
396     trap(signal) do
397       server.shutdown
398       fg_thread.kill if fg_thread
399     end
400   end
401   unless (RUBY_PLATFORM.downcase =~ /mswin|mingw|cygwin|bccwin/)
402     trap("HUP") do
403       Dependencies.clear
404     end
405   end
406   $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] 
407   log_message("server started [Revision: #{ShogiServer::Revision}]")
408
409   server.start do |client|
410       # client.sync = true # this is already set in WEBrick 
411       client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
412         # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
413       player, login = login_loop(client) # loop
414       next unless player
415
416       log_message(sprintf("user %s login", player.name))
417       login.process
418       player.setup_logger($options["player-log-dir"]) if $options["player-log-dir"]
419       player.run(login.csa_1st_str) # loop
420       $mutex.lock
421       begin
422         if (player.game)
423           player.game.kill(player)
424         end
425         player.finish # socket has been closed
426         $league.delete(player)
427         log_message(sprintf("user %s logout", player.name))
428       ensure
429         $mutex.unlock
430       end
431   end
432 end
433
434
435 if ($0 == __FILE__)
436   STDOUT.sync = true
437   STDERR.sync = true
438   TCPSocket.do_not_reverse_lookup = true
439   Thread.abort_on_exception = $DEBUG ? true : false
440
441   begin
442     main
443   rescue Exception => ex
444     if $logger
445       log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
446     else
447       $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
448     end
449   end
450 end