+2020-12-06 Daigo Moriwaki <daigo at debian dot org>
+
+ * [shogi-server] Improve timed-up detection (continued).
+ The server now checks timed up when it receives a-single-space keep
+ alive messages as well.
+ Thanks to mizar for reports and patches.
+ (Closes #40821)
+ * [shogi-server] Support listening on IPv6 addresses
+ Thanks to mizar for a patch.
+ (Closes #40822)
+ * [shogi-server] Make invalid comments illegal
+ Some client sent moves with comments in an invalid format like
+ "+7776FU '* 30 -3334FU +2726FU". Such messages are now deemed
+ illegal.
+ Thanks to mizar for a report.
+ * [shogi-server] Bump up the revision to 20201206
+
+2020-10-04 Daigo Moriwaki <daigo at debian dot org>
+
+ * [shogi-server] Improve timed-up detection.
+ Previously, the server checked if a game got timed up when a player
+ in turn sent no message for a certain amount of time mainly defined
+ by Default_Timeout. If the player sent keep alive frequently, the
+ timed-up detection could be quite delayed.
+ This issue has been addressed. The server now checks timed up with
+ keep alive received as well. Players are notified with TIME_UP not
+ long before games gets timed up.
+
2018-08-25 Daigo Moriwaki <daigo at debian dot org>
* [shogi-server] Support a graceful shutdown.
A file named "STOP" in the base directory prevents the server from
starting new games including Floodgate matches.
(Closes #38544)
+ * [shogi-server] Create a directory for a PID file.
+ To put a PID file such as /var/run/shogi-server/shogi-server.pid, if
+ directories do not exist, they will be created recursively.
+ (Closes #38546)
2018-04-07 Daigo Moriwaki <daigo at debian dot org>
require 'shogi_server/config'
require 'shogi_server/util'
require 'shogi_server/league/floodgate_thread.rb'
+require 'pathname'
+require 'set'
require 'tempfile'
#################################################
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"]]
$league.dir = $topdir
+ # Set of connected players
+ $players = Set.new
+
config = {}
- config[:BindAddress] = "0.0.0.0"
+ config[:BindAddress] = nil # both IPv4 and IPv6
config[:Port] = port
config[:ServerType] = WEBrick::Daemon if $options["daemon"]
config[:Logger] = $logger
srand
server = WEBrick::GenericServer.new(config)
- ["INT", "TERM"].each do |signal|
+ ["INT", "TERM"].each do |signal|
trap(signal) do
+ $players.each {|p| p.kill}
server.shutdown
setup_floodgate.kill
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
player.finish
$league.delete(player)
log_message(sprintf("user %s logout", player.name))
+ $players.delete(player)
ensure
$mutex.unlock
end
Default_Least_Time_Per_Move = 0
One_Time = 10
Login_Time = 300 # time for LOGIN
-Revision = "20180825"
+Revision = "20201206"
RELOAD_FILES = ["shogi_server/league/floodgate.rb",
"shogi_server/league/persistent.rb",
# - :max_moves
#
def handle_one_move(str, sente=nil)
- if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
+ if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})$/)
sg = $1
x0 = $2.to_i
y0 = $3.to_i
#
def Command.factory(str, player, time=Time.now)
cmd = nil
- case str
- when ""
- cmd = KeepAliveCommand.new(str, player)
+ case str
+ when "", " ", /^%[^%]/, :timeout
+ cmd = SpecialCommand.new(str, player)
when /^[\+\-][^%]/
cmd = MoveCommand.new(str, player)
- when /^%[^%]/, :timeout
- cmd = SpecialCommand.new(str, player)
when :exception
cmd = ExceptionCommand.new(str, player)
when /^REJECT/
end
end
- # Application-level protocol for Keep-Alive.
- # If the server receives an LF, it sends back an LF. Note that the 30 sec
- # rule (client may not send LF again within 30 sec) is not implemented
- # yet.
- #
- class KeepAliveCommand < Command
- def initialize(str, player)
- super
- end
-
- def call
- @player.write_safe("\n")
- return :continue
- end
- end
-
# Command of moving a piece.
#
class MoveCommand < Command
end
end
- # Command like "%TORYO" or :timeout
+ # Command like "%TORYO", :timeout, or keep alive
+ #
+ # Keep Alive is an application-level protocol here. There are two representations:
+ # 1) LF (empty string)
+ # The server sends back an LF (empty string).
+ # Note that the 30 sec rule (client may not send LF again within 30 sec)
+ # is not implemented yet.
+ # This is compliant with CSA's protocol in certain situations.
+ # 2) Space + LF (a single space)
+ # The sever replies nothing.
+ # This is an enhancement to CSA's protocol.
#
class SpecialCommand < Command
def initialize(str, player)
def call
rc = :continue
+
+ if @str == "" || @str == " " # keep alive
+ log_debug("received keep alive from #{@player.name}")
+ @player.write_safe("\n") if @str == ""
+ # Fall back to :timeout to check the game gets timed up
+ @str = :timeout
+ end
+
if (@player.status == "game")
rc = in_game_status()
elsif ["agree_waiting", "start_waiting"].include?(@player.status)
@socket_buffer << str
str = @socket_buffer.shift
end
- log_debug("%s (%s)" % [str, @socket_buffer.map {|a| String === a ? a.strip : a }.join(",")])
+ log_debug("dump socket buffer: '%s' (%s)" % [String === str ? str.strip : str,
+ @socket_buffer.map {|a| String === a ? a.strip : a }.join(",")])
if (csa_1st_str)
str = csa_1st_str
end
end
end
+
+
+class Test_illegal < Test::Unit::TestCase
+ def test_invaild_comment
+ b = ShogiServer::Board.new
+ b.set_from_str(<<EOM)
+P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
+P2 * -HI * * * * * -KA *
+P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
+P4 * * * * * * * * *
+P5 * * * * * * * * *
+P6 * * * * * * * * *
+P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
+P8 * +KA * * * * * +HI *
+P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
+EOM
+
+ assert_equal(:illegal, b.handle_one_move("+7776FU '* 30 -3334FU +2726FU"))
+ end
+end
def test_keep_alive_command
cmd = ShogiServer::Command.factory("", @p)
- assert_instance_of(ShogiServer::KeepAliveCommand, cmd)
+ assert_instance_of(ShogiServer::SpecialCommand, cmd)
+ end
+
+ def test_keep_alive_command_space
+ cmd = ShogiServer::Command.factory(" ", @p)
+ assert_instance_of(ShogiServer::SpecialCommand, cmd)
end
def test_move_command
end
def test_space_command
- cmd = ShogiServer::Command.factory(" ", @p)
+ cmd = ShogiServer::Command.factory(" ", @p)
assert_instance_of(ShogiServer::SpaceCommand, cmd)
end
#
#
-class TestKeepAliveCommand < Test::Unit::TestCase
- def setup
- @p = MockPlayer.new
- end
-
- def test_call
- cmd = ShogiServer::KeepAliveCommand.new("", @p)
- rc = cmd.call
- assert_equal(:continue, rc)
- end
-end
-
-#
-#
class TestMoveCommand < Test::Unit::TestCase
def setup
@p = MockPlayer.new
assert_equal("'*comment", @game.log.first)
end
+ def test_comment_illegal
+ cmd = ShogiServer::MoveCommand.new("+7776FU 'comment", @p)
+ rc = cmd.call
+ assert_equal(:continue, rc)
+ assert_nil(@game.log.first)
+ end
+
def test_x1_return
@game.finish_flag = true
@p.protocol = ShogiServer::LoginCSA::PROTOCOL
rc = cmd.call
assert_equal(:continue, rc)
end
+
+ def test_keep_alive
+ cmd = ShogiServer::SpecialCommand.new("", @p)
+ rc = cmd.call
+ assert_equal(:continue, rc)
+ end
+
+ def test_keep_alive_space
+ cmd = ShogiServer::SpecialCommand.new(" ", @p)
+ rc = cmd.call
+ assert_equal(:continue, rc)
+ end
end
#
end
def test_call
- cmd = ShogiServer::SpaceCommand.new("", @p)
+ cmd = ShogiServer::SpaceCommand.new(" ", @p)
rc = cmd.call
assert_equal(:continue, rc)
$logger.info(msg)
end
+def log_debug(msg)
+ $logger.info(msg)
+end
+