X-Git-Url: http://git.sourceforge.jp/view?p=shogi-server%2Fshogi-server.git;a=blobdiff_plain;f=shogi-server;h=5d33d6f37a6db7adce898d9dda9f55851378e314;hp=00a7bad77e5b6bcf8a5fd504a32228f1a7cfa092;hb=07e64e2aad7e94a3cc054a004af13d263497aa0f;hpb=10c7d01c87fe3b5f379094bcce3ebb87d41b67dc diff --git a/shogi-server b/shogi-server index 00a7bad..5d33d6f 100755 --- a/shogi-server +++ b/shogi-server @@ -18,6 +18,7 @@ ## along with this program; if not, write to the Free Software ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +require 'kconv' require 'getoptlong' require 'thread' require 'timeout' @@ -28,54 +29,19 @@ require 'digest/md5' require 'webrick' require 'fileutils' - -class TCPSocket - def gets_timeout(t = Default_Timeout) - begin - timeout(t) do - return self.gets - end - rescue TimeoutError - return nil - rescue - return nil - end - end - def gets_safe(t = nil) - if (t && t > 0) - begin - return :exception if closed? - timeout(t) do - return self.gets - end - rescue TimeoutError - return :timeout - rescue Exception => ex - log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}") - return :exception - end - else - begin - return nil if closed? - return self.gets - rescue - return nil - end - end - end - def write_safe(str) - begin - return self.write(str) - rescue - return nil - end +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("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}") + return :exception end - module ShogiServer # for a namespace -Max_Write_Queue_Size = 1000 Max_Identifier_Length = 32 Default_Timeout = 60 # for single socket operation @@ -149,6 +115,7 @@ class League Floodgate.game_name?(pl.game_name) && pl.sente == nil end + log_warning("DEBUG: %s" % [File.join(File.dirname(__FILE__), "pairing.rb")]) load File.join(File.dirname(__FILE__), "pairing.rb") Pairing.default_pairing.match(players) end @@ -207,6 +174,14 @@ class League return found.map {|a| a.last} end + def find(player_name) + found = nil + @mutex.synchronize do + found = @players[player_name] + end + return found + end + def get_player(status, game_name, sente, searcher) found = nil @mutex.synchronize do @@ -285,7 +260,8 @@ end class Login def Login.good_login?(str) tokens = str.split - if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) && + if (((tokens.length == 3) || + ((tokens.length == 4) && tokens[3] == "x1")) && (tokens[0] == "LOGIN") && (good_identifier?(tokens[1]))) return true @@ -312,7 +288,7 @@ class Login def Login.factory(str, player) (login, player.name, password, ext) = str.chomp.split - if (ext) + if ext return Loginx1.new(player, password) else return LoginCSA.new(player, password) @@ -474,28 +450,26 @@ end class Player < BasicPlayer - def initialize(str, socket) + def initialize(str, socket, eol=nil) super() @socket = socket - @status = "connected" # game_waiting -> agree_waiting -> start_waiting -> game -> finished + @status = "connected" # game_waiting -> agree_waiting -> start_waiting -> game -> finished @protocol = nil # CSA or x1 - @eol = "\m" # favorite eol code + @eol = eol || "\m" # favorite eol code @game = nil @game_name = "" @mytime = 0 # set in start method also @sente = nil - @write_queue = Queue::new + @socket_buffer = [] @main_thread = Thread::current - @writer_thread = Thread::start do - Thread.pass - writer() - end + @mutex_write_guard = Mutex.new end attr_accessor :socket, :status attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente - attr_accessor :main_thread, :writer_thread, :write_queue + attr_accessor :main_thread + attr_reader :socket_buffer def kill log_message(sprintf("user %s killed", @name)) @@ -510,8 +484,6 @@ class Player < BasicPlayer if (@status != "finished") @status = "finished" log_message(sprintf("user %s finish", @name)) - # 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?) rescue @@ -521,26 +493,25 @@ class Player < BasicPlayer end def write_safe(str) - @write_queue.push(str.gsub(/[\r\n]+/, @eol)) - end - - def writer - while (str = @write_queue.pop) + @mutex_write_guard.synchronize do begin - @socket.write(str) + if @socket.closed? + log_warning("%s's socket has been closed." % [@name]) + return + end + if r = select(nil, [@socket], nil, 20) + r[1].first.write(str) + else + log_error("Sending a message to #{@name} timed up.") + end rescue Exception => ex - log_error("Failed to send a message to #{@name}.") - log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}") - return + log_error("Failed to send a message to #{@name}. #{ex.class}: #{ex.message}\t#{ex.backtrace[0]}") end end end def to_s - if ((status == "game_waiting") || - (status == "start_waiting") || - (status == "agree_waiting") || - (status == "game")) + if ["game_waiting", "start_waiting", "agree_waiting", "game"].include?(status) if (@sente) return sprintf("%s %s %s %s +", @name, @protocol, @status, @game_name) elsif (@sente == false) @@ -554,29 +525,25 @@ class Player < BasicPlayer end def run(csa_1st_str=nil) - while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout))) + while ( csa_1st_str || + str = gets_safe(@socket, (@socket_buffer.empty? ? Default_Timeout : 1)) ) + $mutex.lock begin - $mutex.lock - - if (@writer_thread == nil || !@writer_thread.status) - # The writer_thread has been killed because of socket errors. - return + if (@game && @game.turn?(self)) + @socket_buffer << str + str = @socket_buffer.shift end + log_message("%s (%s)" % [str, @socket_buffer.map {|a| String === a ? a.strip : a }.join(",")]) if $DEBUG if (csa_1st_str) str = csa_1st_str csa_1st_str = nil end - if (@write_queue.size > Max_Write_Queue_Size) - log_warning(sprintf("write_queue of %s is %d", @name, @write_queue.size)) - return - end if (@status == "finished") return end str.chomp! if (str.class == String) # may be strip! ? - log_message(str) if $DEBUG case str when "" # Application-level protocol for Keep-Alive @@ -589,10 +556,10 @@ class Player < BasicPlayer move = array_str.shift additional = array_str.shift if /^'(.*)/ =~ additional - comment = array_str.unshift("'*#{$1}") + comment = array_str.unshift("'*#{$1.toeuc}") end s = @game.handle_one_move(move, self) - @game.fh.print("#{comment}\n") if (comment && !s) + @game.fh.print("#{Kconv.toeuc(comment.first)}\n") if (comment && comment.first && !s) return if (s && @protocol == LoginCSA::PROTOCOL) end when /^%[^%]/, :timeout @@ -685,7 +652,7 @@ class Player < BasicPlayer rival = nil if (League::Floodgate.game_name?(game_name)) if (my_sente_str != "*") - write_safe(sprintf("##[ERROR] You are not allowed to specify TEBAN %s for the game %s\n"), my_sente_str, game_name) + write_safe(sprintf("##[ERROR] You are not allowed to specify TEBAN %s for the game %s\n", my_sente_str, game_name)) next end @sente = nil @@ -789,7 +756,8 @@ class Player < BasicPlayer end # class class Piece - PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"} + PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", + "GI" => "NG", "KA" => "UM", "HI" => "RY"} def initialize(board, x, y, sente, promoted=false) @board = board @x = x @@ -1172,6 +1140,7 @@ class Board @gote_history = Hash::new(0) @array = [[], [], [], [], [], [], [], [], [], []] @move_count = 0 + @teban = nil # black => true, white => false end attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history attr_reader :move_count @@ -1206,6 +1175,7 @@ class Board (1..9).each do |i| PieceFU::new(self, i, 7, true) end + @teban = true end def have_piece?(hands, name) @@ -1244,6 +1214,7 @@ class Board @array[x0][y0].move_to(x1, y1) end @move_count += 1 + @teban = @teban ? false : true return true end @@ -1297,7 +1268,7 @@ class Board return false end else # gote - if ((rival_ou.y != 0) && + if ((rival_ou.y != 1) && (@array[rival_ou.x][rival_ou.y - 1]) && (@array[rival_ou.x][rival_ou.y - 1].name == "FU") && (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true @@ -1307,9 +1278,8 @@ class Board return false end end - + ## case: rival_ou is moving - escaped = false rival_ou.movable_grids.each do |(cand_x, cand_y)| tmp_board = Marshal.load(Marshal.dump(self)) s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente) @@ -1327,17 +1297,30 @@ class Board if (@array[x][y] && (@array[x][y].sente != sente) && @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable + + names = [] if (@array[x][y].promoted) - name = @array[x][y].promoted_name + names << @array[x][y].promoted_name else - name = @array[x][y].name + names << @array[x][y].name + if @array[x][y].promoted_name && + @array[x][y].move_to?(fu_x, fu_y, @array[x][y].promoted_name) + names << @array[x][y].promoted_name + end end - tmp_board = Marshal.load(Marshal.dump(self)) - s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente) - raise "internal error" if (s != true) - if (! tmp_board.checkmated?(! sente)) # good move - return false + names.map! do |name| + tmp_board = Marshal.load(Marshal.dump(self)) + s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente) + if s == :illegal + s # result + else + tmp_board.checkmated?(! sente) # result + end end + all_illegal = names.find {|a| a != :illegal} + raise "internal error: legal move not found" if all_illegal == nil + r = names.find {|a| a == false} # good move + return false if r == false # found good move end y = y + 1 end @@ -1480,7 +1463,6 @@ class Board return :illegal end - if (sg == "+") sente = true if sente == nil # deprecated return :illegal unless sente == true # black player's move must be black @@ -1523,7 +1505,6 @@ class Board end move_to(x0, y0, x1, y1, name, sente) - str = to_s update_sennichite(sente) return :normal @@ -1562,7 +1543,7 @@ class Board end a.push("\n") end - a.push("+\n") + a.push("%s\n" % [@teban ? "+" : "-"]) return a.join end end @@ -1620,26 +1601,25 @@ class Game end if (player0.sente) - @sente = player0 - @gote = player1 + @sente, @gote = player0, player1 else - @sente = player1 - @gote = player0 + @sente, @gote = player1, player0 end - @current_player = @sente - @next_player = @gote - + @sente.socket_buffer.clear + @gote.socket_buffer.clear + @current_player, @next_player = @sente, @gote @sente.game = self - @gote.game = self + @gote.game = self @last_move = "" @current_turn = 0 @sente.status = "agree_waiting" - @gote.status = "agree_waiting" - + @gote.status = "agree_waiting" + @id = sprintf("%s+%s+%s+%s+%s", - LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time) + LEAGUE.event, @game_name, + @sente.name, @gote.name, issue_current_time) @logfile = File.join(LEAGUE.dir, @id + ".csa") LEAGUE.games[@id] = self @@ -1662,6 +1642,10 @@ class Game @sente.rated? && @gote.rated? end + def turn?(player) + return player.status == "game" && @current_player == player + end + def monitoron(monitor) @monitors.delete(monitor) @monitors.push(monitor) @@ -1678,7 +1662,7 @@ class Game end def kill(killer) - if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting")) + if ["agree_waiting", "start_waiting"].include?(@sente.status) reject(killer.name) elsif (@current_player == killer) abnormal_lose() @@ -1710,81 +1694,86 @@ class Game LEAGUE.games.delete(@id) end + # class Game def handle_one_move(str, player) + unless turn?(player) + return false if str == :timeout + + @fh.puts("'Deferred %s" % [str]) + log_warning("Deferred a move [%s] scince it is not %s 's turn." % + [str, player.name]) + player.socket_buffer << str # always in the player's thread + return nil + end + finish_flag = true - if (@current_player == player) - @end_time = Time::new - t = (@end_time - @start_time).floor - t = Least_Time_Per_Move if (t < Least_Time_Per_Move) - - move_status = nil - if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0))) - status = :timeout - elsif (str == :timeout) - return false # time isn't expired. players aren't swapped. continue game - else - @current_player.mytime = @current_player.mytime - t - if (@current_player.mytime < 0) - @current_player.mytime = 0 - end + @end_time = Time::new + t = [(@end_time - @start_time).floor, Least_Time_Per_Move].max + + move_status = nil + if ((@current_player.mytime - t <= -@byoyomi) && + ((@total_time > 0) || (@byoyomi > 0))) + status = :timeout + elsif (str == :timeout) + return false # time isn't expired. players aren't swapped. continue game + else + @current_player.mytime -= t + if (@current_player.mytime < 0) + @current_player.mytime = 0 + end -# begin - move_status = @board.handle_one_move(str, @sente == @current_player) -# rescue -# log_error("handle_one_move raise exception for #{str}") -# move_status = :illegal -# end + move_status = @board.handle_one_move(str, @sente == @current_player) - if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore)) - @fh.printf("'ILLEGAL_MOVE(%s)\n", str) - else - if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite_sente_lose) || (move_status == :oute_sennichite_gote_lose)) - @sente.write_safe(sprintf("%s,T%d\n", str, t)) - @gote.write_safe(sprintf("%s,T%d\n", str, t)) - @fh.printf("%s\nT%d\n", str, t) - @last_move = sprintf("%s,T%d", str, t) - @current_turn = @current_turn + 1 - end + if [:illegal, :uchifuzme, :oute_kaihimore].include?(move_status) + @fh.printf("'ILLEGAL_MOVE(%s)\n", str) + else + if [:normal, :outori, :sennichite, :oute_sennichite_sente_lose, :oute_sennichite_gote_lose].include?(move_status) + # Thinking time includes network traffic + @sente.write_safe(sprintf("%s,T%d\n", str, t)) + @gote.write_safe(sprintf("%s,T%d\n", str, t)) + @fh.printf("%s\nT%d\n", str, t) + @last_move = sprintf("%s,T%d", str, t) + @current_turn += 1 + end - @monitors.each do |monitor| - monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] ")) - monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id)) - end + @monitors.each do |monitor| + monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] ")) + monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id)) end end + end - if (@next_player.status != "game") # rival is logout or disconnected - abnormal_win() - elsif (status == :timeout) - timeout_lose() - elsif (move_status == :illegal) - illegal_lose() - elsif (move_status == :kachi_win) - kachi_win() - elsif (move_status == :kachi_lose) - kachi_lose() - elsif (move_status == :toryo) - toryo_lose() - elsif (move_status == :outori) - outori_win() - elsif (move_status == :oute_sennichite_sente_lose) - oute_sennichite_win_lose(@gote, @sente) # Sente is checking - elsif (move_status == :oute_sennichite_gote_lose) - oute_sennichite_win_lose(@sente, @gote) # Gote is checking - elsif (move_status == :sennichite) - sennichite_draw() - elsif (move_status == :uchifuzume) - uchifuzume_lose() - elsif (move_status == :oute_kaihimore) - oute_kaihimore_lose() - else - finish_flag = false - end - finish() if finish_flag - (@current_player, @next_player) = [@next_player, @current_player] - @start_time = Time::new - return finish_flag + if (@next_player.status != "game") # rival is logout or disconnected + abnormal_win() + elsif (status == :timeout) + timeout_lose() + elsif (move_status == :illegal) + illegal_lose() + elsif (move_status == :kachi_win) + kachi_win() + elsif (move_status == :kachi_lose) + kachi_lose() + elsif (move_status == :toryo) + toryo_lose() + elsif (move_status == :outori) + outori_win() + elsif (move_status == :oute_sennichite_sente_lose) + oute_sennichite_win_lose(@gote, @sente) # Sente is checking + elsif (move_status == :oute_sennichite_gote_lose) + oute_sennichite_win_lose(@sente, @gote) # Gote is checking + elsif (move_status == :sennichite) + sennichite_draw() + elsif (move_status == :uchifuzume) + uchifuzume_lose() + elsif (move_status == :oute_kaihimore) + oute_kaihimore_lose() + else + finish_flag = false end + finish() if finish_flag + @current_player, @next_player = @next_player, @current_player + @start_time = Time::new + return finish_flag end def abnormal_win @@ -2104,8 +2093,8 @@ DESCRIPTION OPTIONS --pid-file file specify filename for logging process ID - --daemon dir - run as a daemon. Log files will be put in dir. + --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 @@ -2139,9 +2128,9 @@ end def parse_command_line options = Hash::new - parser = GetoptLong.new( ["--daemon", GetoptLong::REQUIRED_ARGUMENT], - ["--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| @@ -2179,6 +2168,42 @@ def mutex_watchdog(mutex, sec) end end +def login_loop(client) + player = login = nil + + while r = select([client], nil, nil, ShogiServer::Login_Time) do + break unless str = r[0].first.gets + $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)) + if (current_player.password == player.password && + current_player.status != "game") + log_message(sprintf("user %s login forcely", player.name)) + 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 main $mutex = Mutex::new @@ -2202,7 +2227,7 @@ def main FileUtils.mkdir(dir) end log_file = dir ? File.join(dir, "shogi-server.log") : STDOUT - $logger = WEBrick::Log.new(log_file) + $logger = WEBrick::Log.new(log_file) # thread safe LEAGUE.dir = dir || File.dirname(__FILE__) LEAGUE.setup_players_database @@ -2235,50 +2260,12 @@ def main # 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)) - begin - $mutex.lock - str =~ /([\r\n]*)$/ - eol = $1 - if (ShogiServer::Login::good_login?(str)) - player = ShogiServer::Player::new(str, client) - player.eol = eol - login = ShogiServer::Login::factory(str, player) - if (LEAGUE.players[player.name]) - if ((LEAGUE.players[player.name].password == player.password) && - (LEAGUE.players[player.name].status != "game")) - log_message(sprintf("user %s login forcely", player.name)) - LEAGUE.players[player.name].kill - else - login.incorrect_duplicated_player(str) - #Thread::exit - #return - # TODO - player = nil - break - end - end - LEAGUE.add(player) - break - else - client.write_safe("LOGIN:incorrect" + eol) - client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4) - end - ensure - $mutex.unlock - end - end # login loop - if (! player) - #client.close - #Thread::exit - #return - next - end + player, login = login_loop(client) # loop + next unless player + log_message(sprintf("user %s login", player.name)) login.process - player.run(login.csa_1st_str) + player.run(login.csa_1st_str) # loop begin $mutex.lock if (player.game) @@ -2298,7 +2285,7 @@ if ($0 == __FILE__) STDOUT.sync = true STDERR.sync = true TCPSocket.do_not_reverse_lookup = true - Thread.abort_on_exception = false + Thread.abort_on_exception = $DEBUG ? true : false LEAGUE = ShogiServer::League::new main