4 # Author:: NABEYA Kenichi, Daigo Moriwaki
5 # Homepage:: http://sourceforge.jp/projects/shogi-server/
8 # Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
9 # Copyright (C) 2007-2008 Daigo Moriwaki (daigo at debian dot org)
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.
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.
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
32 $:.unshift File.dirname(__FILE__)
33 require 'shogi_server'
34 require 'shogi_server/config'
37 #################################################
43 def gets_safe(socket, timeout=nil)
44 if r = select([socket], nil, nil, timeout)
45 return r[0].first.gets
49 rescue Exception => ex
50 log_error("gets_safe: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
57 shogi-server - server for CSA server protocol
60 shogi-server [OPTIONS] event_name port_number
63 server for CSA server protocol
67 specify filename for logging process ID
69 run as a daemon. Log files will be put in dir.
71 log network messages for each player. Log files
72 will be put in the dir.
74 file name to record Floodgate game history
75 default: './floodgate_history.yaml'
78 GPL versoin 2 or later
83 #{ShogiServer::Release}
86 #{ShogiServer::Revision}
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"}.
116 def parse_command_line
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])
125 parser.each_option do |name, arg|
127 options[name] = arg.dup
131 raise parser.error_message
136 # Check command line options.
137 # If any of them is invalid, exit the process.
139 def check_command_line
140 if (ARGV.length != 2)
145 if $options["daemon"]
146 $options["daemon"] = File.expand_path($options["daemon"], File.dirname(__FILE__))
147 unless is_writable_dir? $options["daemon"]
149 $stderr.puts "Can not create a file in the daemon directory: %s" % [$options["daemon"]]
154 $topdir = $options["daemon"] || File.expand_path(File.dirname(__FILE__))
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"])
160 $stderr.puts "Can not write a file in the player log dir: %s" % [$options["player-log-dir"]]
165 if $options["pid-file"]
166 $options["pid-file"] = File.expand_path($options["pid-file"], $topdir)
167 unless is_writable_file? $options["pid-file"]
169 $stderr.puts "Can not create the pid file: %s" % [$options["pid-file"]]
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"]
178 $stderr.puts "Can not create the floodgate history file: %s" % [$options["floodgate-history"]]
183 # See if the file is writable. The file will be created if it does not exist
185 # Return true if the file is writable, otherwise false.
187 def is_writable_file?(file)
189 if FileTest.file?(file)
190 return FileTest.writable_real?(file)
197 open(file, "w") {|fh| }
206 # See if a file can be created in the directory.
207 # Return true if a file is writable in the directory, otherwise false.
209 def is_writable_dir?(dir)
210 unless File.directory? dir
217 temp_file = Tempfile.new("dummy-shogi-server", dir)
226 def write_pid_file(file)
227 open(file, "w") do |fh|
232 def mutex_watchdog(mutex, sec)
240 queue.push(Object.new)
243 log_error("mutex watchdog timeout: %d sec" % [sec])
251 def login_loop(client)
254 while r = select([client], nil, nil, ShogiServer::Login_Time) do
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]}")
263 $mutex.lock # guards $league
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))
276 login.incorrect_duplicated_player(str)
284 client.write("LOGIN:incorrect" + eol)
285 client.write("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
291 return [player, login]
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"
302 def setup_watchdog_for_giant_lock
306 mutex_watchdog($mutex, 10)
311 return Thread.start do
313 floodgate = ShogiServer::League::Floodgate.new($league)
314 log_message("Flooddgate reloaded. The next match will start at %s." %
315 [floodgate.next_time])
319 diff = floodgate.next_time - Time.now
327 next_time = floodgate.next_time
328 $mutex.synchronize do
329 log_message("Reloading source...")
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
337 log_error("[in Floodgate's thread] #{ex} #{ex.backtrace}")
345 $options = parse_command_line
347 $config = ShogiServer::Config.new $options
349 $league = ShogiServer::League.new($topdir)
351 $league.event = ARGV.shift
354 log_file = $options["daemon"] ? File.join($options["daemon"], "shogi-server.log") : STDOUT
355 $logger = setup_logger(log_file)
357 $league.dir = $topdir
361 config[:ServerType] = WEBrick::Daemon if $options["daemon"]
362 config[:Logger] = $logger
366 config[:StartCallback] = Proc.new do
368 if $options["pid-file"]
369 write_pid_file($options["pid-file"])
371 setup_watchdog_for_giant_lock
372 $league.setup_players_database
373 fg_thread = setup_floodgate
376 config[:StopCallback] = Proc.new do
377 if $options["pid-file"]
378 FileUtils.rm($options["pid-file"], :force => true)
383 server = WEBrick::GenericServer.new(config)
384 ["INT", "TERM"].each do |signal|
387 fg_thread.kill if fg_thread
390 unless (RUBY_PLATFORM.downcase =~ /mswin|mingw|cygwin|bccwin/)
395 $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"]
396 log_message("server started [Revision: #{ShogiServer::Revision}]")
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
405 log_message(sprintf("user %s login", player.name))
407 player.setup_logger($options["player-log-dir"]) if $options["player-log-dir"]
408 player.run(login.csa_1st_str) # loop
412 player.game.kill(player)
414 player.finish # socket has been closed
415 $league.delete(player)
416 log_message(sprintf("user %s logout", player.name))
427 TCPSocket.do_not_reverse_lookup = true
428 Thread.abort_on_exception = $DEBUG ? true : false
432 rescue Exception => ex
434 log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
436 $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"