shogi-server source
Rev. | b4af8394df436d7b273c27d245465efe3f1e51ed |
---|---|
크기 | 15,990 bytes |
Time | 2020-12-06 18:37:55 |
Author | Daigo Moriwaki |
Log Message | [shogi-server] Bump up the revision to 20201206
|
#! /usr/bin/ruby
# $Id$
#
# Author:: NABEYA Kenichi, Daigo Moriwaki
# Homepage:: http://sourceforge.jp/projects/shogi-server/
#
#--
# Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
# Copyright (C) 2007-2012 Daigo Moriwaki (daigo at debian dot org)
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#++
#
#
$topdir = nil
$league = nil
$logger = nil
$config = nil
$:.unshift(File.dirname(File.expand_path(__FILE__)))
require 'shogi_server'
require 'shogi_server/config'
require 'shogi_server/util'
require 'shogi_server/league/floodgate_thread.rb'
require 'pathname'
require 'set'
require 'tempfile'
#################################################
# MAIN
#
ONE_DAY = 3600 * 24 # in seconds
ShogiServer.reload
# Return
# - a received string
# - :timeout
# - :exception
# - nil when a socket is closed
#
def gets_safe(socket, timeout=nil)
if r = select([socket], nil, nil, timeout)
return r[0].first.gets
else
return :timeout
end
rescue Exception => ex
log_error("gets_safe: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
return :exception
end
def usage
print <<EOM
NAME
shogi-server - server for CSA server protocol
SYNOPSIS
shogi-server [OPTIONS] event_name port_number
DESCRIPTION
server for CSA server protocol
OPTIONS
event_name
a prefix of record files.
port_number
a port number for the server to listen.
4081 is often used.
--least-time-per-move n
Least time in second per move: 0, 1 (default 1).
- 0: The new rule that CSA introduced in November 2014.
- 1: The old rule before it.
--max-identifier n
maximum length of an identifier
--max-moves n
when a game with the n-th move played does not end, make the game a draw.
Default 256. 0 disables this feature.
--pid-file file
a file path in which a process ID will be written.
Use with --daemon option.
--daemon dir
run as a daemon. Log files will be put in dir.
--floodgate-games game_A[,...]
enable Floodgate with various game names (separated by a comma)
--player-log-dir dir
enable to log network messages for players. Log files
will be put in the dir.
EXAMPLES
1. % ./shogi-server test 4081
Run the shogi-server. Then clients can connect to port#4081.
The server output logs to the stdout.
2. % ./shogi-server --max-moves 0 --least-time-per-move 1 test 4081
Run the shogi-server in compliance with CSA Protocol V1.1.2 or before.
3. % ./shogi-server --daemon . --pid-file ./shogi-server.pid \
--player-log-dir ./player-logs \
test 4081
Run the shogi-server as a daemon. The server outputs regular logs
to shogi-server.log located in the current directory and network
messages in ./player-logs directory.
4. % ./shogi-server --daemon . --pid-file ./shogi-server.pid \
--player-log-dir ./player-logs \
--floodgate-games floodgate-900-0,floodgate-3600-0 \
test 4081
Run the shogi-server with two groups of Floodgate games.
Configuration files allow you to schedule starting times. Consult
floodgate-0-240.conf.sample or shogi_server/league/floodgate.rb
for format details.
GRACEFUL SHUTDOWN
A file named "STOP" in the base directory prevents the server from
starting new games including Floodgate matches.
When you want to stop the server gracefully, first, create a STOP file
$ touch STOP
then wait for a while until all the running games complete.
Now you can stop the process with no game interruptted by the 'kill'
command.
Note that when a server gets started, a STOP file, if any, will be
deleted automatically.
FLOODGATE SCHEDULE CONFIGURATIONS
You need to set starting times of floodgate groups in
configuration files under the top directory. Each floodgate
group requires a corresponding configuration file named
"<game_name>.conf". The file will be re-read once just after a
game starts.
For example, a floodgate-3600-30 game group requires
floodgate-3600-30.conf. However, for floodgate-900-0 and
floodgate-3600-0, which were default enabled in previous
versions, configuration files are optional if you are happy with
default time settings.
File format is:
Line format:
# This is a comment line
DoW Time
...
where
DoW := "Sun" | "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" |
"Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" |
"Friday" | "Saturday"
Time := HH:MM
For example,
Sat 13:00
Sat 22:00
Sun 13:00
PAREMETER SETTING
In addition, this configuration file allows to set parameters
for the specific Floodaget group. A list of parameters is the
following:
* pairing_factory:
Specifies a factory function name generating a pairing
method which will be used in a specific Floodgate game.
ex. set pairing_factory floodgate_zyunisen
* sacrifice:
Specifies a sacrificed player.
ex. set sacrifice gps500+e293220e3f8a3e59f79f6b0efffaa931
LICENSE
GPL versoin 2 or later
SEE ALSO
REVISION
#{ShogiServer::Revision}
EOM
end
def log_debug(str)
$logger.debug(str)
end
def log_message(str)
$logger.info(str)
end
def log_info(str)
log_message(str)
end
def log_warning(str)
$logger.warn(str)
end
def log_error(str)
$logger.error(str)
end
# Parse command line options. Return a hash containing the option strings
# where a key is the option name without the first two slashes. For example,
# {"pid-file" => "foo.pid"}.
#
def parse_command_line
options = Hash::new
parser = GetoptLong.new(
["--daemon", GetoptLong::REQUIRED_ARGUMENT],
["--floodgate-games", GetoptLong::REQUIRED_ARGUMENT],
["--least-time-per-move", GetoptLong::REQUIRED_ARGUMENT],
["--max-identifier", GetoptLong::REQUIRED_ARGUMENT],
["--max-moves", GetoptLong::REQUIRED_ARGUMENT],
["--pid-file", GetoptLong::REQUIRED_ARGUMENT],
["--player-log-dir", GetoptLong::REQUIRED_ARGUMENT])
parser.quiet = true
begin
parser.each_option do |name, arg|
name.sub!(/^--/, '')
options[name] = arg.dup
end
rescue
usage
raise parser.error_message
end
return options
end
# Check command line options.
# If any of them is invalid, exit the process.
#
def check_command_line
if (ARGV.length != 2)
usage
exit 2
end
if $options["daemon"]
$options["daemon"] = File.expand_path($options["daemon"], File.dirname(__FILE__))
unless is_writable_dir? $options["daemon"]
usage
$stderr.puts "Can not create a file in the daemon directory: %s" % [$options["daemon"]]
exit 5
end
end
$topdir = $options["daemon"] || File.expand_path(File.dirname(__FILE__))
if $options["player-log-dir"]
$options["player-log-dir"] = File.expand_path($options["player-log-dir"], $topdir)
unless is_writable_dir?($options["player-log-dir"])
usage
$stderr.puts "Can not write a file in the player log dir: %s" % [$options["player-log-dir"]]
exit 3
end
end
if $options["pid-file"]
$options["pid-file"] = File.expand_path($options["pid-file"], $topdir)
path = Pathname.new($options["pid-file"])
path.dirname().mkpath()
unless ShogiServer::is_writable_file? $options["pid-file"]
usage
$stderr.puts "Can not create the pid file: %s" % [$options["pid-file"]]
exit 4
end
end
if $options["floodgate-games"]
names = $options["floodgate-games"].split(",")
new_names =
names.select do |name|
ShogiServer::League::Floodgate::game_name?(name)
end
if names.size != new_names.size
$stderr.puts "Found a wrong Floodgate game: %s" % [names.join(",")]
exit 6
end
$options["floodgate-games"] = new_names
end
if $options["floodgate-history"]
$stderr.puts "WARNING: --floodgate-history has been deprecated."
$options["floodgate-history"] = nil
end
$options["max-moves"] ||= ShogiServer::Default_Max_Moves
$options["max-moves"] = $options["max-moves"].to_i
$options["max-identifier"] ||= ShogiServer::Default_Max_Identifier_Length
$options["max-identifier"] = $options["max-identifier"].to_i
$options["least-time-per-move"] ||= ShogiServer::Default_Least_Time_Per_Move
$options["least-time-per-move"] = $options["least-time-per-move"].to_i
end
# See if a file can be created in the directory.
# Return true if a file is writable in the directory, otherwise false.
#
def is_writable_dir?(dir)
unless File.directory? dir
return false
end
result = true
begin
temp_file = Tempfile.new("dummy-shogi-server", dir)
temp_file.close true
rescue
result = false
end
return result
end
def write_pid_file(file)
open(file, "w") do |fh|
fh.puts "#{$$}"
end
end
def mutex_watchdog(mutex, sec)
sec = 1 if sec < 1
queue = []
while true
if mutex.try_lock
queue.clear
mutex.unlock
else
queue.push(Object.new)
if queue.size > sec
# timeout
log_error("mutex watchdog timeout: %d sec" % [sec])
queue.clear
end
end
sleep(1)
end
end
def login_loop(client)
player = login = nil
while r = select([client], nil, nil, ShogiServer::Login_Time) do
str = nil
begin
break unless str = r[0].first.gets
rescue Exception => ex
# It is posssible that the socket causes an error (ex. Errno::ECONNRESET)
log_error("login_loop: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
break
end
$mutex.lock # guards $league
begin
str =~ /([\r\n]*)$/
eol = $1
if (ShogiServer::Login::good_login?(str))
player = ShogiServer::Player::new(str, client, eol)
login = ShogiServer::Login::factory(str, player)
if (current_player = $league.find(player.name))
# Even if a player is in the 'game' state, when the status of the
# player has not been updated for more than a day, it is very
# likely that the player is stalling. In such a case, a new player
# can override the current player.
if (current_player.password == player.password &&
(current_player.status != "game" ||
Time.now - current_player.last_command_at > ONE_DAY))
log_message("player %s login forcibly, nudging the former player" % [player.name])
log_message(" the former player was in %s and received the last command at %s" % [current_player.status, current_player.last_command_at])
current_player.kill
else
login.incorrect_duplicated_player(str)
player = nil
break
end
end
$league.add(player)
break
else
client.write("LOGIN:incorrect" + eol)
client.write("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
end
ensure
$mutex.unlock
end
end # login loop
return [player, login]
end
def setup_logger(log_file)
logger = ShogiServer::Logger.new(log_file, 'daily')
logger.formatter = ShogiServer::Formatter.new
logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO
logger.datetime_format = "%Y-%m-%d %H:%M:%S"
return logger
end
def setup_watchdog_for_giant_lock
$mutex = Mutex::new
Thread::start do
Thread.pass
mutex_watchdog($mutex, 10)
end
end
def main
$options = parse_command_line
check_command_line
$config = ShogiServer::Config.new $options
$league = ShogiServer::League.new($topdir)
$league.event = ARGV.shift
port = ARGV.shift
log_file = $options["daemon"] ? File.join($options["daemon"], "shogi-server.log") : STDOUT
$logger = setup_logger(log_file)
$league.dir = $topdir
# Set of connected players
$players = Set.new
config = {}
config[:BindAddress] = nil # both IPv4 and IPv6
config[:Port] = port
config[:ServerType] = WEBrick::Daemon if $options["daemon"]
config[:Logger] = $logger
setup_floodgate = nil
config[:StartCallback] = Proc.new do
srand
if $options["pid-file"]
write_pid_file($options["pid-file"])
end
setup_watchdog_for_giant_lock
$league.setup_players_database
setup_floodgate = ShogiServer::SetupFloodgate.new($options["floodgate-games"])
setup_floodgate.start
end
config[:StopCallback] = Proc.new do
if $options["pid-file"]
FileUtils.rm($options["pid-file"], :force => true)
end
end
srand
server = WEBrick::GenericServer.new(config)
["INT", "TERM"].each do |signal|
trap(signal) do
$players.each {|p| p.kill}
server.shutdown
setup_floodgate.kill
end
end
unless (RUBY_PLATFORM.downcase =~ /mswin|mingw|cygwin|bccwin/)
trap("HUP") do
Dependencies.clear
end
end
$stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"]
log_message("server started [Revision: #{ShogiServer::Revision}]")
if ShogiServer::STOP_FILE.exist?
log_message("Deleted the STOP file")
ShogiServer::STOP_FILE.delete
end
server.start do |client|
begin
# 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, login = login_loop(client) # loop
unless player
log_error("Detected a timed out login attempt")
next
end
log_message(sprintf("user %s login", player.name))
login.process
player.setup_logger($options["player-log-dir"]) if $options["player-log-dir"]
$mutex.lock
begin
$players.add(player)
ensure
$mutex.unlock
end
player.run(login.csa_1st_str) # loop
$mutex.lock
begin
if (player.game)
player.game.kill(player)
end
player.finish
$league.delete(player)
log_message(sprintf("user %s logout", player.name))
$players.delete(player)
ensure
$mutex.unlock
end
player.wait_write_thread_finish(1000) # milliseconds
rescue Exception => ex
log_error("server.start: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
end
end
end
if ($0 == __FILE__)
STDOUT.sync = true
STDERR.sync = true
TCPSocket.do_not_reverse_lookup = true
Thread.abort_on_exception = $DEBUG ? true : false
begin
main
rescue Exception => ex
if $logger
log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
else
$stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
end
end
end