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
31 $:.unshift File.dirname(__FILE__)
32 require 'shogi_server'
35 #################################################
41 def gets_safe(socket, timeout=nil)
42 if r = select([socket], nil, nil, timeout)
43 return r[0].first.gets
47 rescue Exception => ex
48 log_error("gets_safe: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
55 shogi-server - server for CSA server protocol
58 shogi-server [OPTIONS] event_name port_number
61 server for CSA server protocol
65 specify filename for logging process ID
67 run as a daemon. Log files will be put in dir.
69 log network messages for each player. Log files
70 will be put in the dir.
72 file name to record Floodgate game history
73 default: './floodgate_history.yaml'
76 GPL versoin 2 or later
81 #{ShogiServer::Release}
84 #{ShogiServer::Revision}
110 # Parse command line options. Return a hash containing the option strings
111 # where a key is the option name without the first two slashes. For example,
112 # {"pid-file" => "foo.pid"}.
114 def parse_command_line
116 parser = GetoptLong.new(
117 ["--daemon", GetoptLong::REQUIRED_ARGUMENT],
118 ["--pid-file", GetoptLong::REQUIRED_ARGUMENT],
119 ["--player-log-dir", GetoptLong::REQUIRED_ARGUMENT],
120 ["--floodgate-history", GetoptLong::REQUIRED_ARGUMENT])
123 parser.each_option do |name, arg|
125 options[name] = arg.dup
129 raise parser.error_message
134 # Check command line options.
135 # If any of them is invalid, exit the process.
137 def check_command_line
138 if (ARGV.length != 2)
143 if $options["daemon"]
144 $options["daemon"] = File.expand_path($options["daemon"], File.dirname(__FILE__))
145 unless is_writable_dir? $options["daemon"]
147 $stderr.puts "Can not create a file in the daemon directory: %s" % [$options["daemon"]]
152 $topdir = $options["daemon"] || File.expand_path(File.dirname(__FILE__))
154 if $options["player-log-dir"]
155 $options["player-log-dir"] = File.expand_path($options["player-log-dir"], $topdir)
156 unless is_writable_dir?($options["player-log-dir"])
158 $stderr.puts "Can not write a file in the player log dir: %s" % [$options["player-log-dir"]]
163 if $options["pid-file"]
164 $options["pid-file"] = File.expand_path($options["pid-file"], $topdir)
165 unless is_writable_file? $options["pid-file"]
167 $stderr.puts "Can not create the pid file: %s" % [$options["pid-file"]]
172 $options["floodgate-history"] ||= File.join($topdir, "floodgate_history.yaml")
173 $options["floodgate-history"] = File.expand_path($options["floodgate-history"], $topdir)
174 unless is_writable_file? $options["floodgate-history"]
176 $stderr.puts "Can not create the floodgate history file: %s" % [$options["floodgate-history"]]
181 # See if the file is writable. The file will be created if it does not exist
183 # Return true if the file is writable, otherwise false.
185 def is_writable_file?(file)
187 open(file, "w") {|fh| }
191 unless FileTest.file? file
198 # See if a file can be created in the directory.
199 # Return true if a file is writable in the directory, otherwise false.
201 def is_writable_dir?(dir)
202 unless File.directory? dir
209 temp_file = Tempfile.new("dummy-shogi-server", dir)
218 def write_pid_file(file)
219 open(file, "w") do |fh|
224 def mutex_watchdog(mutex, sec)
232 queue.push(Object.new)
235 log_error("mutex watchdog timeout: %d sec" % [sec])
243 def login_loop(client)
246 while r = select([client], nil, nil, ShogiServer::Login_Time) do
247 break unless str = r[0].first.gets
248 $mutex.lock # guards $league
252 if (ShogiServer::Login::good_login?(str))
253 player = ShogiServer::Player::new(str, client, eol)
254 login = ShogiServer::Login::factory(str, player)
255 if (current_player = $league.find(player.name))
256 if (current_player.password == player.password &&
257 current_player.status != "game")
258 log_message(sprintf("user %s login forcely", player.name))
261 login.incorrect_duplicated_player(str)
269 client.write("LOGIN:incorrect" + eol)
270 client.write("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
276 return [player, login]
279 def setup_logger(log_file)
280 logger = ShogiServer::Logger.new(log_file, 'daily')
281 logger.formatter = ShogiServer::Formatter.new
282 logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO
283 logger.datetime_format = "%Y-%m-%d %H:%M:%S"
287 def setup_watchdog_for_giant_lock
291 mutex_watchdog($mutex, 10)
296 return Thread.start do
298 floodgate = ShogiServer::League::Floodgate.new($league)
299 log_message("Flooddgate reloaded. The next match will start at %s." %
300 [floodgate.next_time])
304 diff = floodgate.next_time - Time.now
312 next_time = floodgate.next_time
313 $mutex.synchronize do
314 log_message("Reloading source...")
317 floodgate = ShogiServer::League::Floodgate.new($league, next_time)
318 log_message("Floodgate: The next match will start at %s." %
319 [floodgate.next_time])
320 rescue Exception => ex
322 log_error("[in Floodgate's thread] #{ex} #{ex.backtrace}")
330 $options = parse_command_line
333 $league = ShogiServer::League.new($topdir)
335 $league.event = ARGV.shift
338 log_file = $options["daemon"] ? File.join($options["daemon"], "shogi-server.log") : STDOUT
339 $logger = setup_logger(log_file)
341 $league.dir = $topdir
345 config[:ServerType] = WEBrick::Daemon if $options["daemon"]
346 config[:Logger] = $logger
350 config[:StartCallback] = Proc.new do
352 if $options["pid-file"]
353 write_pid_file($options["pid-file"])
355 setup_watchdog_for_giant_lock
356 $league.setup_players_database
357 fg_thread = setup_floodgate
360 config[:StopCallback] = Proc.new do
361 if $options["pid-file"]
362 FileUtils.rm($options["pid-file"], :force => true)
367 server = WEBrick::GenericServer.new(config)
368 ["INT", "TERM"].each do |signal|
371 fg_thread.kill if fg_thread
377 $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"]
378 log_message("server started [Revision: #{ShogiServer::Revision}]")
380 server.start do |client|
381 # client.sync = true # this is already set in WEBrick
382 client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
383 # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
384 player, login = login_loop(client) # loop
387 log_message(sprintf("user %s login", player.name))
389 player.setup_logger($options["player-log-dir"]) if $options["player-log-dir"]
390 player.run(login.csa_1st_str) # loop
394 player.game.kill(player)
396 player.finish # socket has been closed
397 $league.delete(player)
398 log_message(sprintf("user %s logout", player.name))
409 TCPSocket.do_not_reverse_lookup = true
410 Thread.abort_on_exception = $DEBUG ? true : false
414 rescue Exception => ex
416 log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
418 $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"