OSDN Git Service

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