X-Git-Url: http://git.sourceforge.jp/view?p=shogi-server%2Fshogi-server.git;a=blobdiff_plain;f=shogi-server;h=07e42a63adbac4653eff2ffc8dd48c614984ff08;hp=56b0960ca87ccd8b3fbfa0c70088b487de42c353;hb=6150d6fd590bf23c1a2e46be19820de0d3ed5857;hpb=5657dd1f6ab0b0e27570fb0dcb8d25ab91de641d diff --git a/shogi-server b/shogi-server index 56b0960..07e42a6 100755 --- a/shogi-server +++ b/shogi-server @@ -1,7 +1,8 @@ #! /usr/bin/env ruby -## -*-Ruby-*- $RCSfile$ $Revision$ $Name$ +## $Id$ -## Copyright (C) 2004 nanami@2ch +## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch) +## Copyright (C) 2007-2008 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 @@ -17,834 +18,25 @@ ## along with this program; if not, write to the Free Software ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -Max_Write_Queue_Size = 1000 -Max_Identifier_Length = 32 -Default_Timeout = 60 # for single socket operation +TOP_DIR = File.expand_path(File.dirname(__FILE__)) +$:.unshift File.dirname(__FILE__) +require 'shogi_server' -Default_Game_Name = "default:1500:0" +################################################# +# MAIN +# -One_Time = 10 -Least_Time_Per_Move = 1 -Watchdog_Time = 30 # time for ping -Login_Time = 300 # time for LOGIN +ShogiServer.reload -Release = "$Name$".split[1].sub(/\A[^\d]*/, '').gsub(/_/, '.') -Release.concat("-") if (Release == "") -Revision = "$Revision$".gsub(/[^\.\d]/, '') - -STDOUT.sync = true -STDERR.sync = true - -require 'getoptlong' -require 'thread' -require 'timeout' -require 'socket' -require 'ping' - -TCPSocket.do_not_reverse_lookup = true - -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 - timeout(t) do - return self.gets - end - rescue TimeoutError - return :timeout - rescue - return nil - end - else - begin - return self.gets - rescue - return nil - end - end - end - def write_safe(str) - begin - return self.write(str) - rescue - return nil - end - end -end - - -class League - def initialize - @games = Hash::new - @players = Hash::new - @event = nil - end - attr_accessor :players, :games, :event - - def add(player) - @players[player.name] = player - end - def delete(player) - @players.delete(player.name) - end - def get_player(status, game_name, sente, searcher=nil) - @players.each do |name, player| - if ((player.status == status) && - (player.game_name == game_name) && - ((player.sente == nil) || (player.sente == sente)) && - ((searcher == nil) || (player != searcher))) - return player - end - end - return nil - end -end - -class Player - def initialize(str, socket) - @name = nil - @password = nil - @socket = socket - @status = "connected" # game_waiting -> agree_waiting -> start_waiting -> game -> finished - - @protocol = nil # CSA or x1 - @eol = "\m" # favorite eol code - @game = nil - @game_name = "" - @mytime = 0 # set in start method also - @sente = nil - @watchdog_thread = nil - @writer_thread = nil - @main_thread = nil - @write_queue = Queue::new - login(str) - end - - attr_accessor :name, :password, :socket, :status - attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente - attr_accessor :main_thread, :watchdog_thread, :writer_thread, :write_queue - def kill - finish - Thread::kill(@main_thread) if @main_thread - end - - def finish - if (@status != "finished") - @status = "finished" - log_message(sprintf("user %s finish", @name)) - Thread::kill(@watchdog_thread) if @watchdog_thread - Thread::kill(@writer_thread) if @writer_thread - begin - @socket.close if (! @socket.closed?) - rescue - log_message(sprintf("user %s finish failed", @name)) - end - end - end - - def write_safe(str) - @write_queue.push(str.gsub(/[\r\n]+/, @eol)) - end - - def writer - while (str = @write_queue.pop) - @socket.write_safe(str) - end - end - - def watchdog(time) - while true - begin - Ping.pingecho(@socket.addr[3]) - rescue - end - sleep(time) - end - end - - def to_s - if ((status == "game_waiting") || - (status == "agree_waiting") || - (status == "game")) - if (@sente) - return sprintf("%s %s %s %s +", @name, @protocol, @status, @game_name) - elsif (@sente == false) - return sprintf("%s %s %s %s -", @name, @protocol, @status, @game_name) - elsif (@sente == nil) - return sprintf("%s %s %s %s +-", @name, @protocol, @status, @game_name) - end - else - return sprintf("%s %s %s", @name, @protocol, @status) - end - end - - def write_help - @socket.write_safe('##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"') - end - - def login(str) - str =~ /([\r\n]*)$/ - @eol = $1 - str.chomp! - (login, @name, @password, ext) = str.split - if (ext) - @protocol = "x1" - else - @protocol = "CSA" - end - @main_thread = Thread::current - @watchdog_thread = Thread::start do - watchdog(Watchdog_Time) - end - @writer_thread = Thread::start do - writer() - end - end - - def run - write_safe(sprintf("LOGIN:%s OK\n", @name)) - if (@protocol != "CSA") - log_message(sprintf("user %s run in %s mode", @name, @protocol)) - write_safe(sprintf("##[LOGIN] +OK %s\n", @protocol)) - else - log_message(sprintf("user %s run in CSA mode", @name)) - csa_1st_str = "%%GAME #{Default_Game_Name} +-" - end - - while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout))) - begin - $mutex.lock - 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) - case str - when /^[\+\-%][^%]/, :timeout - if (@status == "game") - s = @game.handle_one_move(str, self) - return if (s && @protocol == "CSA") - end - when /^REJECT/ - if (@status == "agree_waiting") - @game.reject(@name) - return if (@protocol == "CSA") - else - write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status)) - end - when /^AGREE/ - if (@status == "agree_waiting") - @status = "start_waiting" - if ((@game.sente.status == "start_waiting") && - (@game.gote.status == "start_waiting")) - @game.start - @game.sente.status = "game" - @game.gote.status = "game" - end - else - write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status)) - end - when /^%%MONITORON\s+(\S+)/ - game_name = $1 - if (LEAGUE.games[game_name]) - LEAGUE.games[game_name].monitoron(self) - end - when /^%%MONITOROFF\s+(\S+)/ - game_name = $1 - if (LEAGUE.games[game_name]) - LEAGUE.games[game_name].monitoroff(self) - end - when /^%%HELP/ - write_help - when /^%%GAME\s+(\S+)\s+([\+\-]+)$/ - game_name = $1 - sente_str = $2 - if (! good_game_name?(game_name)) - write_safe(sprintf("##[ERROR] bad game name\n")) - elsif ((@status == "connected") || (@status == "game_waiting")) - @status = "game_waiting" - else - write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status)) - end - @status = "game_waiting" - @game_name = $1 - if (sente_str == "+") - @sente = true - rival_sente = false - elsif (sente_str == "-") - @sente = false - rival_sente = true - else - @sente = nil - rival_sente = nil - end - rival = LEAGUE.get_player("game_waiting", @game_name, rival_sente, self) - rival = LEAGUE.get_player("game_waiting", @game_name, nil, self) if (! rival) - if (rival) - if (@sente == nil) - if (rand(2) == 0) - @sente = true - rival_sente = false - else - @sente = false - rival_sente = true - end - elsif (rival_sente == nil) - if (@sente) - rival_sente = false - else - rival_sente = true - end - end - rival.sente = rival_sente - Game::new(@game_name, self, rival) - self.status = "agree_waiting" - rival.status = "agree_waiting" - end - when /^%%CHAT\s+(.+)/ - message = $1 - LEAGUE.players.each do |name, player| - if (player.protocol != "CSA") - s = player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message)) - player.status = "zombie" if (! s) - end - end - when /^%%LIST/ - buf = Array::new - LEAGUE.games.each do |id, game| - buf.push(sprintf("##[LIST] %s\n", id)) - end - buf.push("##[LIST] +OK\n") - write_safe(buf.join) - when /^%%SHOW\s+(\S+)/ - id = $1 - if (LEAGUE.games[id]) - write_safe(LEAGUE.games[id].board.to_s.gsub(/^/, '##[SHOW] ')) - end - write_safe("##[SHOW] +OK\n") - when /^%%WHO/ - buf = Array::new - LEAGUE.players.each do |name, player| - buf.push(sprintf("##[WHO] %s\n", player.to_s)) - end - buf.push("##[WHO] +OK\n") - write_safe(buf.join) - when /^LOGOUT/ - @status = "connected" - write_safe("LOGOUT:completed\n") - return - else - write_safe(sprintf("##[ERROR] unknown command %s\n", str)) - end - ensure - $mutex.unlock - end - end # enf of while - end -end - -class Piece - PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"} - def initialize(name, sente) - @name = name - @sente = sente - @promoted = false - end - attr_accessor :name, :promoted, :sente - - def promoted_name - PROMOTE[name] - end - - def to_s - if (@sente) - sg = "+" - else - sg = "-" - end - if (@promoted) - n = PROMOTE[@name] - else - n = @name - end - return sg + n - end -end - - - -class Board - def initialize - @sente_hands = Array::new - @gote_hands = Array::new - @array = [[], [], [], [], [], [], [], [], [], []] - end - attr_accessor :array, :sente_hands, :gote_hands - - def initial - @array[1][1] = Piece::new("KY", false) - @array[2][1] = Piece::new("KE", false) - @array[3][1] = Piece::new("GI", false) - @array[4][1] = Piece::new("KI", false) - @array[5][1] = Piece::new("OU", false) - @array[6][1] = Piece::new("KI", false) - @array[7][1] = Piece::new("GI", false) - @array[8][1] = Piece::new("KE", false) - @array[9][1] = Piece::new("KY", false) - @array[2][2] = Piece::new("KA", false) - @array[8][2] = Piece::new("HI", false) - @array[1][3] = Piece::new("FU", false) - @array[2][3] = Piece::new("FU", false) - @array[3][3] = Piece::new("FU", false) - @array[4][3] = Piece::new("FU", false) - @array[5][3] = Piece::new("FU", false) - @array[6][3] = Piece::new("FU", false) - @array[7][3] = Piece::new("FU", false) - @array[8][3] = Piece::new("FU", false) - @array[9][3] = Piece::new("FU", false) - - @array[1][9] = Piece::new("KY", true) - @array[2][9] = Piece::new("KE", true) - @array[3][9] = Piece::new("GI", true) - @array[4][9] = Piece::new("KI", true) - @array[5][9] = Piece::new("OU", true) - @array[6][9] = Piece::new("KI", true) - @array[7][9] = Piece::new("GI", true) - @array[8][9] = Piece::new("KE", true) - @array[9][9] = Piece::new("KY", true) - @array[2][8] = Piece::new("HI", true) - @array[8][8] = Piece::new("KA", true) - @array[1][7] = Piece::new("FU", true) - @array[2][7] = Piece::new("FU", true) - @array[3][7] = Piece::new("FU", true) - @array[4][7] = Piece::new("FU", true) - @array[5][7] = Piece::new("FU", true) - @array[6][7] = Piece::new("FU", true) - @array[7][7] = Piece::new("FU", true) - @array[8][7] = Piece::new("FU", true) - @array[9][7] = Piece::new("FU", true) - end - - def get_piece_from_hands(hands, name) - p = hands.find { |i| - i.name == name - } - if (p) - hands.delete(p) - end - return p - end - - def handle_one_move(str) - begin - if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/) - p = $1 - x0 = $2.to_i - y0 = $3.to_i - x1 = $4.to_i - y1 = $5.to_i - name = $6 - - if ((x1 == 0) || (y1 == 0)) - return "illegal" - end - elsif (str =~ /^%KACHI/) - return "kachi" - elsif (str =~ /^%TORYO/) - return "toryo" - else - return "illegal" - end - - if (p == "+") - sente = true - hands = @sente_hands - else - sente = false - hands = @gote_hands - end - if (@array[x1][y1]) - if (@array[x1][y1] == sente) # this is mine - return "illegal" - elsif (@array[x1][y1].name == "OU") - return "outori" - end - hands.push(@array[x1][y1]) - @array[x1][y1] = nil - end - if ((x0 == 0) && (y0 == 0)) - p = get_piece_from_hands(hands, name) - return "illegal" if (! p) # i don't have this one - @array[x1][y1] = p - p.sente = sente - p.promoted = false - else - if (@array[x0][y0] == nil) - return "illegal"; - end - if (@array[x0][y0].name != name) # promoted ? - return "illegal" if (@array[x0][y0].promoted_name != name) # can't promote - @array[x0][y0].promoted = true - end - @array[x1][y1] = @array[x0][y0] - @array[x0][y0] = nil - end - return "normal" # legal move - rescue - return "illegal" - end - end - - def to_s - a = Array::new - y = 1 - while (y <= 9) - a.push(sprintf("P%d", y)) - x = 9 - while (x >= 1) - piece = @array[x][y] - if (piece) - s = piece.to_s - else - s = " * " - end - a.push(s) - x = x - 1 - end - a.push(sprintf("\n")) - y = y + 1 - end - if (! sente_hands.empty?) - a.push("P+") - sente_hands.each do |p| - a.push("00" + p.name) - end - a.push("\n") - end - if (! gote_hands.empty?) - a.push("P-") - gote_hands.each do |p| - a.push("00" + p.name) - end - a.push("\n") - end - return a.join - end -end - -class Game - def initialize(game_name, player0, player1) - @monitors = Array::new - @game_name = game_name - if (@game_name =~ /:(\d+):(\d+)/) - @total_time = $1.to_i - @byoyomi = $2.to_i - end - - if (player0.sente) - @sente = player0 - @gote = player1 - else - @sente = player1 - @gote = player0 - end - @current_player = @sente - @next_player = @gote - - @sente.game = self - @gote.game = self - - @sente.status = "agree_waiting" - @gote.status = "agree_waiting" - @id = sprintf("%s+%s+%s+%s+%s", - LEAGUE.event, @game_name, @sente.name, @gote.name, - Time::new.strftime("%Y%m%d%H%M%S")) - - LEAGUE.games[@id] = self - - - log_message(sprintf("game created %s %s %s", game_name, sente.name, gote.name)) - - @logfile = @id + ".csa" - @board = Board::new - @board.initial - @start_time = nil - @fh = nil - - propose - end - attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors - - def monitoron(monitor) - @monitors.delete(monitor) - @monitors.push(monitor) - monitor.write_safe(@board.to_s.gsub(/^/, "##[MONITOR][#{@id}] ")) - monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id)) - end - - def monitoroff(monitor) - @monitors.delete(monitor) - end - - def reject(rejector) - @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector)) - @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector)) - finish - end - - def kill(killer) - if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting")) - reject(killer.name) - elsif (@current_player == killer) - abnormal_lose() - end - end - - def finish - log_message(sprintf("game finished %s %s %s", game_name, sente.name, gote.name)) - @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S")) - @fh.close - - @sente.game = nil - @gote.game = nil - @sente.status = "connected" - @gote.status = "connected" - - if (@current_player.protocol == "CSA") - @current_player.finish - end - if (@next_player.protocol == "CSA") - @next_player.finish - end - @monitors = Array::new - @sente = nil - @gote = nil - @current_player = nil - @next_player = nil - LEAGUE.games.delete(@id) - end - - def handle_one_move(str, player) - finish_flag = true - if (@current_player == player) - @end_time = Time::new - t = @end_time - @start_time - t = Least_Time_Per_Move if (t < Least_Time_Per_Move) - - move_status = nil - if ((@current_player.mytime - t <= 0) && (@total_time > 0)) - status = :timeout - elsif (str == :timeout) - return false # time isn't expired. players aren't swapped. continue game - else - move_status = @board.handle_one_move(str) - if (move_status == "normal") - @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) - - @monitors.each do |monitor| - monitor.write_safe(sprintf("##[MONITOR][%s] %s\n", @id, str)) - monitor.write_safe(@board.to_s.gsub(/^/, "##[MONITOR][#{@id}] ")) - monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id)) - end - elsif (move_status == "illegal") - @fh.printf("'ILLEGAL_MOVE(%s)\n", str) - end - end - - if (@current_player.mytime - t < @byoyomi) - @current_player.mytime = @byoyomi - else - @current_player.mytime = @current_player.mytime - t - 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") - kachi_win() - elsif (move_status == "toryo") - toryo_lose() - elsif (move_status == "outori") - outori_win() - 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 - end - - def abnormal_win - @current_player.status = "connected" - @next_player.status = "connected" - @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n") - @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n") - @monitors.each do |monitor| - monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id)) - end - end - - def abnormal_lose - @current_player.status = "connected" - @next_player.status = "connected" - @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n") - @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n") - @monitors.each do |monitor| - monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id)) - end - end - - def illegal_lose - @current_player.status = "connected" - @next_player.status = "connected" - @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n") - @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n") - @monitors.each do |monitor| - monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id)) - end - end - - def timeout_lose - @current_player.status = "connected" - @next_player.status = "connected" - @current_player.write_safe("#TIME_UP\n#LOSE\n") - @next_player.write_safe("#TIME_UP\n#WIN\n") - @monitors.each do |monitor| - monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id)) - end - end - - def kachi_win - @current_player.status = "connected" - @next_player.status = "connected" - @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n") - @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n") - @monitors.each do |monitor| - monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id)) - end - end - - def toryo_lose - @current_player.status = "connected" - @next_player.status = "connected" - @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n") - @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n") - @monitors.each do |monitor| - monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id)) - end - end - - def outori_win - @current_player.status = "connected" - @next_player.status = "connected" - @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n") - @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n") - @monitors.each do |monitor| - monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id)) - end - end - - def start - log_message(sprintf("game started %s %s %s", game_name, sente.name, gote.name)) - @sente.write_safe(sprintf("START:%s\n", @id)) - @gote.write_safe(sprintf("START:%s\n", @id)) - @sente.mytime = @total_time - @gote.mytime = @total_time - @start_time = Time::new - end - - def propose - begin - @fh = open(@logfile, "w") - @fh.sync = true - - @fh.printf("V2\n") - @fh.printf("N+%s\n", @sente.name) - @fh.printf("N-%s\n", @gote.name) - @fh.printf("$EVENT:%s\n", @id) - - @sente.write_safe(propose_message("+")) - @gote.write_safe(propose_message("-")) - - @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S")) - @fh.print < ex + log_error("gets_safe: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}") + return :exception end def usage @@ -853,7 +45,7 @@ NAME 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 @@ -861,40 +53,52 @@ DESCRIPTION OPTIONS --pid-file file specify filename for logging process ID + --daemon dir + run as a daemon. Log files will be put in dir. + --player-log-dir dir + log network messages for each player. Log files + will be put in the dir. + --floodgate_history + file name to record Floodgate game history + default: './floodgate_history.yaml' LICENSE - this file is distributed under GPL version2 and might be compiled by Exerb + GPL versoin 2 or later SEE ALSO RELEASE - #{Release} + #{ShogiServer::Release} REVISION - #{Revision} + #{ShogiServer::Revision} EOM end + +def log_debug(str) + $logger.debug(str) +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], + ["--player-log-dir", GetoptLong::REQUIRED_ARGUMENT]) parser.quiet = true begin parser.each_option do |name, arg| @@ -908,135 +112,223 @@ def parse_command_line return options end -LEAGUE = League::new - -def good_game_name?(str) - if ((str =~ /^(.+):\d+:\d+$/) && - (good_identifier?($1))) - return true - else - return false +def write_pid_file(file) + open(file, "w") do |fh| + fh.puts "#{$$}" end end -def good_identifier?(str) - if ((str =~ /\A[\w\d_@\-\.]+\z/) && - (str.length < Max_Identifier_Length)) - return true - else - return false +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 good_login?(str) - tokens = str.split - if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) && - (tokens[0] == "LOGIN") && - (good_identifier?(tokens[1]))) - return true - else - return false - 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 write_pid_file(file) - open(file, "w") do |fh| - fh.print Process::pid, "\n" +def setup_logger(log_file) + logger = 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 mutex_watchdog(mutex, sec) - while true - begin - timeout(sec) do - mutex.lock - mutex.unlock +def setup_floodgate + return 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 + diff = floodgate.next_time - Time.now + if diff > 0 + sleep(diff/2) + next + end + LEAGUE.reload + floodgate.match_game + floodgate.charge + next_time = floodgate.next_time + $mutex.synchronize do + log_message("Reloading source...") + ShogiServer.reload + end + floodgate = ShogiServer::League::Floodgate.new(LEAGUE, next_time) + log_message("Floodgate will start the next match at %s." % + [floodgate.next_time]) + rescue Exception => ex + # ignore errors + log_error("[in Floodgate's thread] #{ex} #{ex.backtrace}") end - sleep sec - rescue TimeoutError - log_error("mutex watchdog timeout") - exit(1) end end end def main - $mutex = Mutex::new - Thread::start do - mutex_watchdog($mutex, 10) - end - + $options = parse_command_line if (ARGV.length != 2) usage exit 2 end + if $options["player-log-dir"] + $options["player-log-dir"] = File.expand_path($options["player-log-dir"]) + end + if $options["player-log-dir"] && + !File.directory?($options["player-log-dir"]) + usage + exit 3 + end + if $options["pid-file"] + $options["pid-file"] = File.expand_path($options["pid-file"]) + end + $options["floodgate-history"] ||= File.join(File.dirname(__FILE__), "floodgate_history.yaml") + $options["floodgate-history"] = File.expand_path($options["floodgate-history"]) LEAGUE.event = ARGV.shift port = ARGV.shift - write_pid_file($options["pid-file"]) if ($options["pid-file"]) + dir = $options["daemon"] + dir = File.expand_path(dir) if dir + if dir && ! File.exist?(dir) + FileUtils.mkdir(dir) + end + log_file = dir ? File.join(dir, "shogi-server.log") : STDOUT + $logger = setup_logger(log_file) - Thread.abort_on_exception = true + LEAGUE.dir = dir || TOP_DIR - server = TCPserver.open(port) - log_message("server started") + config = {} + config[:Port] = port + config[:ServerType] = WEBrick::Daemon if $options["daemon"] + config[:Logger] = $logger + + fg_thread = 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 + fg_thread = setup_floodgate + end + + config[:StopCallback] = Proc.new do + if $options["pid-file"] + FileUtils.rm($options["pid-file"], :force => true) + end + end + + server = WEBrick::GenericServer.new(config) + ["INT", "TERM"].each do |signal| + trap(signal) do + server.shutdown + fg_thread.kill if fg_thread + end + end + trap("HUP") do + Dependencies.clear + end + $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] + log_message("server started [Revision: #{ShogiServer::Revision}]") + + 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, login = login_loop(client) # loop + next unless player - while true - Thread::start(server.accept) do |client| - client.sync = true - player = nil - while (str = client.gets_timeout(Login_Time)) - begin - $mutex.lock - str =~ /([\r\n]*)$/ - eol = $1 - if (good_login?(str)) - player = Player::new(str, client) - 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 - client.write_safe("LOGIN:incorrect" + eol) - client.write_safe(sprintf("username %s is already connected%s", player.name, eol)) if (str.split.length >= 4) - client.close - Thread::exit - 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 - end log_message(sprintf("user %s login", player.name)) - player.run + login.process + player.setup_logger($options["player-log-dir"]) if $options["player-log-dir"] + player.run(login.csa_1st_str) # loop + $mutex.lock begin - $mutex.lock if (player.game) player.game.kill(player) end - player.finish + player.finish # socket has been closed LEAGUE.delete(player) log_message(sprintf("user %s logout", player.name)) ensure $mutex.unlock end - end end end + if ($0 == __FILE__) - main + STDOUT.sync = true + STDERR.sync = true + TCPSocket.do_not_reverse_lookup = true + Thread.abort_on_exception = $DEBUG ? true : false + + begin + LEAGUE = ShogiServer::League.new(TOP_DIR) + main + rescue Exception => ex + log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}") + end end