$:.unshift File.dirname(__FILE__)
require 'shogi_server'
+require 'shogi_server/board' # not autoloaded
+require 'shogi_server/player'
+require 'shogi_server/game'
#################################################
# MAIN
return :timeout
end
rescue Exception => ex
- log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
+ log_error("gets_safe: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
return :exception
end
end
def mutex_watchdog(mutex, sec)
+ sec = 1 if sec < 1
+ queue = []
while true
- begin
- timeout(sec) do
- begin
- mutex.lock
- ensure
- mutex.unlock
- end
+ 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
- sleep(sec)
- rescue TimeoutError
- log_error("mutex watchdog timeout")
- exit(1)
end
+ sleep(1)
end
end
end
end
+def setup_floodgate
+ Thread.start do
+ Thread.pass
+ floodgate = ShogiServer::League::Floodgate.new(LEAGUE)
+ log_message("Flooddgate reloaded. The next match will start at %s." %
+ [floodgate.next_time])
+
+ while (true)
+ begin
+ sleep(10)
+ next if Time.now < floodgate.next_time
+ LEAGUE.reload
+ floodgate.match_game
+ floodgate.charge
+ next_time = floodgate.next_time
+ $mutex.synchronize do
+ Dependencies.clear
+ end
+ floodgate = ShogiServer::League::Floodgate.new(LEAGUE, next_time)
+ log_message("Flooddgate reloaded. The next match will start at %s." %
+ [floodgate.next_time])
+ rescue Exception => ex
+ # ignore errors
+ log_error("[in Floodgate's thread] #{ex} #{ex.backtrace}")
+ end
+ end
+ end
+end
+
def main
+
+ [ShogiServer::League::Floodgate, ShogiServer::Pairing].each do |klass|
+ Dependencies.unloadable klass
+ end
setup_watchdog_for_giant_lock
config[:Port] = port
config[:ServerType] = WEBrick::Daemon if $options["daemon"]
config[:Logger] = $logger
- if $options["pid-file"]
- pid_file = File.expand_path($options["pid-file"])
- config[:StartCallback] = Proc.new do
- write_pid_file(pid_file)
+
+ config[:StartCallback] = Proc.new do
+ if $options["pid-file"]
+ write_pid_file($options["pid-file"])
end
- config[:StopCallback] = Proc.new do
+ setup_floodgate
+ end
+
+ config[:StopCallback] = Proc.new do
+ if $options["pid-file"]
FileUtils.rm(pid_file, :force => true)
end
end
trap(signal) do
LEAGUE.shutdown
server.shutdown
+ # TODO Shutdown Floodgate's thread
end
end
trap("HUP") do
LEAGUE.shutdown
Dependencies.clear
- LEAGUE.restart
+ # TODO Restart Floodgate's thread
end
$stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"]
log_message("server started [Revision: #{ShogiServer::Revision}]")
TCPSocket.do_not_reverse_lookup = true
Thread.abort_on_exception = $DEBUG ? true : false
+ begin
LEAGUE = ShogiServer::League::new
- main
+ main
+ rescue Exception => ex
+ log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
+ end
end
module ShogiServer # for a namespace
class Board
- def initialize
+ def initialize(move_count=0)
@sente_hands = Array::new
@gote_hands = Array::new
@history = Hash::new(0)
@sente_history = Hash::new(0)
@gote_history = Hash::new(0)
@array = [[], [], [], [], [], [], [], [], [], []]
- @move_count = 0
+ @move_count = move_count
@teban = nil # black => true, white => false
end
attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history, :teban
attr_reader :move_count
+ def deep_copy
+ # return Marshal.load(Marshal.dump(self))
+ board = Board.new(self.move_count)
+ board.sente_hands = self.sente_hands.clone
+ board.gote_hands = self.gote_hands.clone
+ board.history = self.history.clone
+ board.history.default = 0
+ board.sente_history = self.sente_history.clone
+ board.sente_history.default = 0
+ board.gote_history = self.gote_history.clone
+ board.gote_history.default = 0
+ board.array = []
+ self.array.each {|a| board.array.push(a.clone)}
+ board.teban = self.teban
+ return board
+ end
+
def initial
PieceKY::new(self, 1, 1, false)
PieceKE::new(self, 2, 1, false)
if ((x0 == 0) || (y0 == 0))
piece = have_piece?(hands, name)
- return :illegal if (! piece.move_to?(x1, y1, name)) # TODO null check for the piece?
+ return :illegal if (piece == nil || ! piece.move_to?(x1, y1, name))
piece.move_to(x1, y1)
else
- return :illegal if (! @array[x0][y0].move_to?(x1, y1, name)) # TODO null check?
+ if (@array[x0][y0] == nil || !@array[x0][y0].move_to?(x1, y1, name))
+ return :illegal
+ end
if (@array[x0][y0].name != name) # promoted ?
@array[x0][y0].promoted = true
end
## case: rival_ou is moving
rival_ou.movable_grids.each do |(cand_x, cand_y)|
- tmp_board = Marshal.load(Marshal.dump(self))
+ tmp_board = deep_copy
s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente)
raise "internal error" if (s != true)
if (! tmp_board.checkmated?(! sente)) # good move
end
end
names.map! do |name|
- tmp_board = Marshal.load(Marshal.dump(self))
+ tmp_board = deep_copy
s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
if s == :illegal
s # result
return :illegal # can't put on existing piece
end
- tmp_board = Marshal.load(Marshal.dump(self))
+ tmp_board = deep_copy
return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
return :oute_kaihimore if (tmp_board.checkmated?(sente))
tmp_board.update_sennichite(sente)
end
move_status = @board.handle_one_move(str, @sente == @current_player)
+ # log_debug("move_status: %s for %s's %s" % [move_status, @sente == @current_player ? "BLACK" : "WHITE", str])
if [:illegal, :uchifuzume, :oute_kaihimore].include?(move_status)
@fh.printf("'ILLEGAL_MOVE(%s)\n", str)
## along with this program; if not, write to the Free Software
## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-#require 'shogi_server/league/floodgate'
-#require 'shogi_server/league/persistent'
-
module ShogiServer # for a namespace
######################################################
@players = Hash::new
@event = nil
@dir = File.dirname(__FILE__)
- @floodgate = Floodgate.new(self)
- #@floodgate.run
end
attr_accessor :players, :games, :event, :dir
@persistent.save(player)
end
end
- @floodgate.shutdown
- end
-
- def restart
- @floodgate = Floodgate.new(self)
- #@floodgate.run
end
# this should be called just after instanciating a League object.
end
end
- def initialize(league)
+ attr_reader :next_time, :league
+
+ def initialize(league, next_time=nil)
@league = league
- @next_time = nil
+ @next_time = next_time
charge
end
- def run
- @thread = Thread.new do
- Thread.pass
- while (true)
- begin
- sleep(10)
- next if Time.now < @next_time
- @league.reload
- match_game
- charge
- rescue Exception => ex
- # ignore errors
- log_error("[in Floodgate's thread] #{ex} #{ex.backtrace}")
- end
- end
- end
- end
-
- def shutdown
- @thread.kill if @thread
- end
-
- # private
-
def charge
now = Time.now
- # if now.min < 30
- # @next_time = Time.mktime(now.year, now.month, now.day, now.hour, 30)
- # else
- # @next_time = Time.mktime(now.year, now.month, now.day, now.hour) + 3600
- # end
- # for test
- if now.sec < 30
- @next_time = Time.mktime(now.year, now.month, now.day, now.hour, now.min, 30)
+ unless $DEBUG
+ # each 30 minutes
+ if now.min < 30
+ @next_time = Time.mktime(now.year, now.month, now.day, now.hour, 30)
+ else
+ @next_time = Time.mktime(now.year, now.month, now.day, now.hour) + 3600
+ end
else
- @next_time = Time.mktime(now.year, now.month, now.day, now.hour, now.min) + 60
+ # for test, each 30 seconds
+ if now.sec < 30
+ @next_time = Time.mktime(now.year, now.month, now.day, now.hour, now.min, 30)
+ else
+ @next_time = Time.mktime(now.year, now.month, now.day, now.hour, now.min) + 60
+ end
end
end
## along with this program; if not, write to the Free Software
## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+require 'monitor'
+
module ShogiServer # for a namespace
class BasicPlayer
@sente = nil
@socket_buffer = []
@main_thread = Thread::current
- @write_queue = Queue.new
+ @write_queue = []
+ @wq_mon = Monitor.new
+ @wq_cond = @wq_mon.new_cond
@player_logger = nil
start_write_thread
end
while !@socket.closed?
str = ""
begin
- begin
- timeout(5) do
- str = @write_queue.deq
+ timeout_flg = false
+ @wq_mon.synchronize do
+ if @write_queue.empty?
+ if @wq_cond.wait(15)
+ #timeout
+ timeout_flg = true
+ # log_debug("write_queue health check timeout")
+ end
end
- if str == nil
- break
+ if !timeout_flg && !@write_queue.empty?
+ str = @write_queue.shift
+ # log_debug("Dequeued %s from %s's write_queue." % [str, @name])
end
- rescue TimeoutError
- next
- end
+ end # synchronize
+ next if timeout_flg
+ break if str == nil # exit
+
if r = select(nil, [@socket], nil, 20)
r[1].first.write(str)
log(:info, :out, str)
# Note that sending a message is included in the giant lock.
#
def write_safe(str)
- @write_queue.enq str
+ @wq_mon.synchronize do
+ # log_debug("Enqueuing %s..." % [str])
+ @write_queue.push(str)
+ # log_debug("Enqueued %s into %s's write_queue." % [str, @name])
+ @wq_cond.broadcast
+ end
end
def to_s
--- /dev/null
+# queue = Queue.new
+# timeout(5) do
+# queue.deq
+# end
+#
+# is not good since not all of stdlib is safe with respect to
+# asynchronous exceptions.
+# This class is a safe implementation.
+# See: http://www.ruby-forum.com/topic/107864
+#
+
+require 'thread'
+require 'monitor'
+
+module ShogiServer
+
+class TimeoutQueue
+ def initialize
+ @lock = Mutex.new
+ @messages = []
+ @readers = []
+ end
+
+ def enq(msg)
+ @lock.synchronize do
+ unless @readers.empty?
+ @readers.pop << msg
+ else
+ @messages.push msg
+ end
+ end
+ end
+
+ #
+ # @param timeout
+ # @return nil if timeout
+ #
+ def deq(timeout=5)
+ timeout_thread = nil
+ mon = nil
+ empty_cond = nil
+
+ begin
+ reader = nil
+ @lock.synchronize do
+ unless @messages.empty?
+ # fast path
+ return @messages.shift
+ else
+ reader = Queue.new
+ @readers.push reader
+ if timeout
+ mon = Monitor.new
+ empty_cond = mon.new_cond
+
+ timeout_thread = Thread.new do
+ mon.synchronize do
+ if empty_cond.wait(timeout)
+ # timeout
+ @lock.synchronize do
+ @readers.delete reader
+ reader << nil
+ end
+ else
+ # timeout_thread was waked up before timeout
+ end
+ end
+ end # thread
+ end
+ end
+ end
+ # either timeout or writer will send to us
+ return reader.shift
+ ensure
+ # (try to) clean up timeout thread
+ if timeout_thread
+ mon.synchronize { empty_cond.signal }
+ Thread.pass
+ end
+ end
+ end
+end
+
+end