X-Git-Url: http://git.sourceforge.jp/view?p=shogi-server%2Fshogi-server.git;a=blobdiff_plain;f=shogi-server;h=b5e1ecfe4a36d440ab127e801d34329b3263d713;hp=049c45faa9c3e9c2c07530d2538f71630848d628;hb=61b4add1cd544134a04466551bcbc9428fd1a777;hpb=3bb39d8bb22382c0c4a79f251268d6a0c11f0ac3 diff --git a/shogi-server b/shogi-server index 049c45f..b5e1ecf 100755 --- a/shogi-server +++ b/shogi-server @@ -1,33 +1,55 @@ -#! /usr/bin/env ruby -## $Id$ - -## 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 -## 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 - -TOP_DIR = File.expand_path(File.dirname(__FILE__)) -$:.unshift File.dirname(__FILE__) +#! /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 '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 @@ -42,36 +64,114 @@ end def usage print <.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 + GPL versoin 2 or later SEE ALSO -RELEASE - #{ShogiServer::Release} - REVISION - #{ShogiServer::Revision} + #{ShogiServer::Revision} + EOM end @@ -96,13 +196,19 @@ def log_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], - ["--pid-file", GetoptLong::REQUIRED_ARGUMENT], - ["--player-log-dir", GetoptLong::REQUIRED_ARGUMENT], - ["--floodgate-history", GetoptLong::REQUIRED_ARGUMENT]) + ["--daemon", GetoptLong::REQUIRED_ARGUMENT], + ["--floodgate-games", GetoptLong::REQUIRED_ARGUMENT], + ["--least-time-per-move", 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| @@ -116,6 +222,89 @@ def parse_command_line 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) + 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["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 "#{$$}" @@ -145,18 +334,31 @@ 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 + 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)) + 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") - log_message(sprintf("user %s login forcely", player.name)) + (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) @@ -164,7 +366,7 @@ def login_loop(client) break end end - LEAGUE.add(player) + $league.add(player) break else client.write("LOGIN:incorrect" + eol) @@ -178,7 +380,7 @@ def login_loop(client) end def setup_logger(log_file) - logger = Logger.new(log_file, 'daily') + 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" @@ -193,80 +395,29 @@ def setup_watchdog_for_giant_lock end end -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: 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 $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"]) + check_command_line + $config = ShogiServer::Config.new $options - LEAGUE.event = ARGV.shift - port = ARGV.shift + $league = ShogiServer::League.new($topdir) - dir = $options["daemon"] - dir = File.expand_path(dir) if dir - if dir && ! File.exist?(dir) - FileUtils.mkdir(dir) - end + $league.event = ARGV.shift + port = ARGV.shift - log_file = dir ? File.join(dir, "shogi-server.log") : STDOUT + log_file = $options["daemon"] ? File.join($options["daemon"], "shogi-server.log") : STDOUT $logger = setup_logger(log_file) - LEAGUE.dir = dir || TOP_DIR + $league.dir = $topdir config = {} + config[:BindAddress] = "0.0.0.0" config[:Port] = port config[:ServerType] = WEBrick::Daemon if $options["daemon"] config[:Logger] = $logger - fg_thread = nil + setup_floodgate = nil config[:StartCallback] = Proc.new do srand @@ -274,8 +425,9 @@ def main write_pid_file($options["pid-file"]) end setup_watchdog_for_giant_lock - LEAGUE.setup_players_database - fg_thread = setup_floodgate + $league.setup_players_database + setup_floodgate = ShogiServer::SetupFloodgate.new($options["floodgate-games"]) + setup_floodgate.start end config[:StopCallback] = Proc.new do @@ -289,21 +441,27 @@ def main ["INT", "TERM"].each do |signal| trap(signal) do server.shutdown - fg_thread.kill if fg_thread + setup_floodgate.kill end end - trap("HUP") do - Dependencies.clear + 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}]") 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 - next unless player + unless player + log_error("Detected a timed out login attempt") + next + end log_message(sprintf("user %s login", player.name)) login.process @@ -314,12 +472,16 @@ def main if (player.game) player.game.kill(player) end - player.finish # socket has been closed - LEAGUE.delete(player) + player.finish + $league.delete(player) log_message(sprintf("user %s logout", player.name)) 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 @@ -331,7 +493,6 @@ if ($0 == __FILE__) Thread.abort_on_exception = $DEBUG ? true : false begin - LEAGUE = ShogiServer::League.new(TOP_DIR) main rescue Exception => ex if $logger