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 if FileTest.file?(file)
188 return FileTest.writable_real?(file)
195 open(file, "w") {|fh| }
204 # See if a file can be created in the directory.
205 # Return true if a file is writable in the directory, otherwise false.
207 def is_writable_dir?(dir)
208 unless File.directory? dir
215 temp_file = Tempfile.new("dummy-shogi-server", dir)
224 def write_pid_file(file)
225 open(file, "w") do |fh|
230 def mutex_watchdog(mutex, sec)
238 queue.push(Object.new)
241 log_error("mutex watchdog timeout: %d sec" % [sec])
249 def login_loop(client)
252 while r = select([client], nil, nil, ShogiServer::Login_Time) do
253 break unless str = r[0].first.gets
254 $mutex.lock # guards $league
258 if (ShogiServer::Login::good_login?(str))
259 player = ShogiServer::Player::new(str, client, eol)
260 login = ShogiServer::Login::factory(str, player)
261 if (current_player = $league.find(player.name))
262 if (current_player.password == player.password &&
263 current_player.status != "game")
264 log_message(sprintf("user %s login forcely", player.name))
267 login.incorrect_duplicated_player(str)
275 client.write("LOGIN:incorrect" + eol)
276 client.write("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
282 return [player, login]
285 def setup_logger(log_file)
286 logger = ShogiServer::Logger.new(log_file, 'daily')
287 logger.formatter = ShogiServer::Formatter.new
288 logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO
289 logger.datetime_format = "%Y-%m-%d %H:%M:%S"
293 def setup_watchdog_for_giant_lock
297 mutex_watchdog($mutex, 10)
302 return Thread.start do
304 floodgate = ShogiServer::League::Floodgate.new($league)
305 log_message("Flooddgate reloaded. The next match will start at %s." %
306 [floodgate.next_time])
310 diff = floodgate.next_time - Time.now
318 next_time = floodgate.next_time
319 $mutex.synchronize do
320 log_message("Reloading source...")
323 floodgate = ShogiServer::League::Floodgate.new($league, next_time)
324 log_message("Floodgate: The next match will start at %s." %
325 [floodgate.next_time])
326 rescue Exception => ex
328 log_error("[in Floodgate's thread] #{ex} #{ex.backtrace}")
336 $options = parse_command_line
339 $league = ShogiServer::League.new($topdir)
341 $league.event = ARGV.shift
344 log_file = $options["daemon"] ? File.join($options["daemon"], "shogi-server.log") : STDOUT
345 $logger = setup_logger(log_file)
347 $league.dir = $topdir
351 config[:ServerType] = WEBrick::Daemon if $options["daemon"]
352 config[:Logger] = $logger
356 config[:StartCallback] = Proc.new do
358 if $options["pid-file"]
359 write_pid_file($options["pid-file"])
361 setup_watchdog_for_giant_lock
362 $league.setup_players_database
363 fg_thread = setup_floodgate
366 config[:StopCallback] = Proc.new do
367 if $options["pid-file"]
368 FileUtils.rm($options["pid-file"], :force => true)
373 server = WEBrick::GenericServer.new(config)
374 ["INT", "TERM"].each do |signal|
377 fg_thread.kill if fg_thread
383 $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"]
384 log_message("server started [Revision: #{ShogiServer::Revision}]")
386 server.start do |client|
387 # client.sync = true # this is already set in WEBrick
388 client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
389 # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
390 player, login = login_loop(client) # loop
393 log_message(sprintf("user %s login", player.name))
395 player.setup_logger($options["player-log-dir"]) if $options["player-log-dir"]
396 player.run(login.csa_1st_str) # loop
400 player.game.kill(player)
402 player.finish # socket has been closed
403 $league.delete(player)
404 log_message(sprintf("user %s logout", player.name))
415 TCPSocket.do_not_reverse_lookup = true
416 Thread.abort_on_exception = $DEBUG ? true : false
420 rescue Exception => ex
422 log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
424 $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"