require 'yaml'
require 'yaml/store'
require 'digest/md5'
+require 'webrick'
+require 'fileutils'
class TCPSocket
end
rescue TimeoutError
return :timeout
- rescue
- return nil
+ rescue Exception => ex
+ log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
+ return :exception
end
else
begin
@games = Hash::new
@players = Hash::new
@event = nil
- @db = YAML::Store.new( File.join(File.dirname(__FILE__), "players.yaml") )
+ @dir = File.dirname(__FILE__)
+ end
+ attr_accessor :players, :games, :event, :dir
+
+ # this should be called just after instanciating a League object.
+ def setup_players_database
+ @db = YAML::Store.new(File.join(@dir, "players.yaml"))
end
- attr_accessor :players, :games, :event
def add(player)
self.load(player) if player.id
def search(id)
hash = nil
@db.transaction do
+ break unless @db["players"]
@db["players"].each do |group, players|
hash = players[id]
break if hash
def rated_players
players = []
@db.transaction(true) do
+ break unless @db["players"]
@db["players"].each do |group, players_hash|
players << players_hash.keys
end
# TODO you should confirm that there is no message in the queue.
Thread::kill(@writer_thread) if @writer_thread
begin
- @socket.close if (! @socket.closed?)
+# @socket.close if (! @socket.closed?)
rescue
log_message(sprintf("user %s finish failed", @name))
end
def writer
while (str = @write_queue.pop)
- @socket.write_safe(str)
+ begin
+ @socket.write(str)
+ rescue Exception => ex
+ log_error("Failed to send a message to #{@name}.")
+ log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
+ return
+ end
end
end
end
end
- def write_help
- @socket.write_safe('##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"')
- end
-
def run(csa_1st_str=nil)
while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout)))
begin
+ if (@writer_thread == nil || @writer_thread.status == false)
+ # The writer_thread has been killed because of socket errors.
+ return
+ end
+
$mutex.lock
if (csa_1st_str)
str = csa_1st_str
if (@status == "finished")
return
end
- str.chomp! if (str.class == String)
+ str.chomp! if (str.class == String) # may be strip! ?
case str
+ when ""
+ # Application-level protocol for Keep-Alive
+ # If the server gets LF, it sends back LF.
+ # 30 sec rule (client may not send LF again within 30 sec) is not implemented yet.
+ write_safe("\n")
when /^[\+\-][^%]/
if (@status == "game")
array_str = str.split(",")
if (@status == "game")
s = @game.handle_one_move(str, self)
return if (s && @protocol == LoginCSA::PROTOCOL)
+ # else
+ # begin
+ # @socket.write("##[KEEPALIVE] #{Time.now}\n")
+ # rescue Exception => ex
+ # log_error("Failed to send a keepalive to #{@name}.")
+ # log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
+ # return
+ # end
end
+ when :exception
+ log_error("Failed to receive a message from #{@name}.")
+ return
when /^REJECT/
if (@status == "agree_waiting")
@game.reject(@name)
LEAGUE.games[game_id].monitoroff(self)
end
when /^%%HELP/
- write_help
+ write_safe(
+ %!##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"\n!)
when /^%%RATING/
players = LEAGUE.rated_players
players.sort {|a,b| b.rate <=> a.rate}.each do |p|
@status = "connected"
write_safe("LOGOUT:completed\n")
return
+ when /^CHALLENGE/
+ # This command is only available for CSA's official testing server.
+ # So, this means nothing for this program.
+ write_safe("CHALLENGE ACCEPTED\n")
when /^\s*$/
## ignore null string
else
- write_safe(sprintf("##[ERROR] unknown command %s\n", str))
+ msg = "##[ERROR] unknown command %s\n" % [str]
+ write_safe(msg)
+ log_error(msg)
end
ensure
$mutex.unlock
return true
end
+ # sente is nil only if tests in test_board run
def handle_one_move(str, sente=nil)
if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
sg = $1
return :illegal
end
+
if (sg == "+")
- sente = true
+ sente = true if sente == nil # deprecated
+ return :illegal unless sente == true # black player's move must be black
hands = @sente_hands
else
- sente = false
+ sente = false if sente == nil # deprecated
+ return :illegal unless sente == false # white player's move must be white
hands = @gote_hands
end
@id = sprintf("%s+%s+%s+%s+%s",
LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
- @logfile = @id + ".csa"
+ @logfile = File.join(LEAGUE.dir, @id + ".csa")
LEAGUE.games[@id] = self
shogi-server - server for CSA server protocol
SYNOPSIS
- shogi-server event_name port_number
+ shogi-server [OPTIONS] event_name port_number
DESCRIPTION
server for CSA server protocol
OPTIONS
--pid-file file
specify filename for logging process ID
+ --daemon dir
+ run as a daemon. Log files will be put in dir.
LICENSE
this file is distributed under GPL version2 and might be compiled by Exerb
SEE ALSO
RELEASE
- #{Release}
+ #{ShogiServer::Release}
REVISION
- #{Revision}
+ #{ShogiServer::Revision}
EOM
end
def log_message(str)
- printf("%s message: %s\n", Time::new.to_s, str)
+ $logger.info(str)
end
def log_warning(str)
- printf("%s warning: %s\n", Time::new.to_s, str)
+ $logger.warn(str)
end
def log_error(str)
- printf("%s error: %s\n", Time::new.to_s, str)
+ $logger.error(str)
end
def parse_command_line
options = Hash::new
- parser = GetoptLong.new
- parser.ordering = GetoptLong::REQUIRE_ORDER
- parser.set_options(
- ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
-
+ parser = GetoptLong.new( ["--daemon", GetoptLong::REQUIRED_ARGUMENT],
+ ["--pid-file", GetoptLong::REQUIRED_ARGUMENT]
+ )
parser.quiet = true
begin
parser.each_option do |name, arg|
write_pid_file($options["pid-file"]) if ($options["pid-file"])
- server = TCPserver.open(port)
+ dir = $options["daemon"] || nil
+ if dir && ! File.exist?(dir)
+ FileUtils.mkdir(dir)
+ end
+ log_file = dir ? File.join(dir, "shogi-server.log") : STDOUT
+ $logger = WEBrick::Log.new(log_file)
+
+ LEAGUE.dir = dir || File.dirname(__FILE__)
+ LEAGUE.setup_players_database
+
+ config = {}
+ config[:Port] = port
+ config[:ServerType] = WEBrick::Daemon if $options["daemon"]
+ config[:Logger] = $logger
+
+ server = WEBrick::GenericServer.new(config)
+ ["INT", "TERM"].each {|signal| trap(signal){ server.shutdown } }
+ $stderr.puts("server started as a deamon") if $options["daemon"]
log_message("server started")
- while true
- Thread::start(server.accept) do |client|
- Thread.pass
- client.sync = true
+ server.start do |client|
+ # client.sync = true # this is already set in WEBrick
+ client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
+ # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
player = nil
login = nil
while (str = client.gets_timeout(ShogiServer::Login_Time))
LEAGUE.players[player.name].kill
else
login.incorrect_duplicated_player(str)
- Thread::exit
- return
+ #Thread::exit
+ #return
+ # TODO
+ player = nil
+ break
end
end
LEAGUE.add(player)
end
end # login loop
if (! player)
- client.close
- Thread::exit
- return
+ #client.close
+ #Thread::exit
+ #return
+ next
end
log_message(sprintf("user %s login", player.name))
login.process
ensure
$mutex.unlock
end
- end
end
end
STDERR.sync = true
TCPSocket.do_not_reverse_lookup = true
Thread.abort_on_exception = true
-
+
LEAGUE = ShogiServer::League::new
main
end