OSDN Git Service

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