• R/O
  • HTTP
  • SSH
  • HTTPS

shogi-server:

shogi-server source


File Info

Rev. 6e9f2816b581e6b6100b3df25700cd3b28ee9a7c
Size 15,975 bytes
Time 2020-10-04 11:33:03
Author Daigo Moriwaki
Log Message

Bump up revision

Content

#! /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 'pathname'
require 'set'
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
  else
    return :timeout
  end
rescue Exception => ex
  log_error("gets_safe: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
  return :exception
end

def usage
    print <<EOM
NAME
        shogi-server - server for CSA server protocol

SYNOPSIS
        shogi-server [OPTIONS] event_name port_number

DESCRIPTION
        server for CSA server protocol

OPTIONS
        event_name
                a prefix of record files.
        port_number
                a port number for the server to listen. 
                4081 is often used.
        --least-time-per-move n
                Least time in second per move: 0, 1 (default 1).
                  - 0: The new rule that CSA introduced in November 2014.
                  - 1: The old rule before it.
        --max-identifier n
	        maximum length of an identifier
        --max-moves n
                when a game with the n-th move played does not end, make the game a draw.
                Default 256. 0 disables this feature.
        --pid-file file
                a file path in which a process ID will be written.
                Use with --daemon option.
        --daemon dir
                run as a daemon. Log files will be put in dir.
        --floodgate-games game_A[,...]
                enable Floodgate with various game names (separated by a comma)
        --player-log-dir dir
                enable to log network messages for players. Log files
                will be put in the dir.

EXAMPLES

        1. % ./shogi-server test 4081
           Run the shogi-server. Then clients can connect to port#4081.
           The server output logs to the stdout.

        2. % ./shogi-server --max-moves 0 --least-time-per-move 1 test 4081
           Run the shogi-server in compliance with CSA Protocol V1.1.2 or before.

        3. % ./shogi-server --daemon . --pid-file ./shogi-server.pid \
                            --player-log-dir ./player-logs \
                            test 4081
           Run the shogi-server as a daemon. The server outputs regular logs
           to shogi-server.log located in the current directory and network 
           messages in ./player-logs directory.

        4. % ./shogi-server --daemon . --pid-file ./shogi-server.pid \
                            --player-log-dir ./player-logs \
                            --floodgate-games floodgate-900-0,floodgate-3600-0 \
                            test 4081
           Run the shogi-server with two groups of Floodgate games.
           Configuration files allow you to schedule starting times. Consult  
           floodgate-0-240.conf.sample or shogi_server/league/floodgate.rb 
           for format details.

GRACEFUL SHUTDOWN

	A file named "STOP" in the base directory prevents the server from
	starting new games including Floodgate matches.
	When you want to stop the server gracefully, first, create a STOP file

          $ touch STOP

	then wait for a while until all the running games complete.
	Now you can stop the process with no game interruptted by the 'kill'
	command.

	Note that when a server gets started, a STOP file, if any, will be
	deleted automatically.

FLOODGATE SCHEDULE CONFIGURATIONS

	    You need to set starting times of floodgate groups in
	    configuration files under the top directory. Each floodgate 
            group requires a corresponding configuration file named
	    "<game_name>.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

SEE ALSO

REVISION
        #{ShogiServer::Revision}

EOM
end


def log_debug(str)
  $logger.debug(str)
end

def log_message(str)
  $logger.info(str)
end
def log_info(str)
  log_message(str)
end

def log_warning(str)
  $logger.warn(str)
end

def log_error(str)
  $logger.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],
    ["--floodgate-games",     GetoptLong::REQUIRED_ARGUMENT],
    ["--least-time-per-move", GetoptLong::REQUIRED_ARGUMENT],
    ["--max-identifier",      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|
      name.sub!(/^--/, '')
      options[name] = arg.dup
    end
  rescue
    usage
    raise parser.error_message
  end
  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)
    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"]]
      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["max-identifier"] ||= ShogiServer::Default_Max_Identifier_Length
  $options["max-identifier"] = $options["max-identifier"].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 "#{$$}"
  end
end

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 login_loop(client)
  player = login = nil
 
  while r = select([client], nil, nil, ShogiServer::Login_Time) do
    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))
          # 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" ||
               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)
            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 setup_logger(log_file)
  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"
  return logger
end

def setup_watchdog_for_giant_lock
  $mutex = Mutex::new
  Thread::start do
    Thread.pass
    mutex_watchdog($mutex, 10)
  end
end

def main
  
  $options = parse_command_line
  check_command_line
  $config = ShogiServer::Config.new $options

  $league = ShogiServer::League.new($topdir)

  $league.event = ARGV.shift
  port = ARGV.shift

  log_file = $options["daemon"] ? File.join($options["daemon"], "shogi-server.log") : STDOUT
  $logger = setup_logger(log_file)

  $league.dir = $topdir

  # Set of connected players
  $players = Set.new

  config = {}
  config[:BindAddress] = "0.0.0.0"
  config[:Port]       = port
  config[:ServerType] = WEBrick::Daemon if $options["daemon"]
  config[:Logger]     = $logger

  setup_floodgate = 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
    setup_floodgate = ShogiServer::SetupFloodgate.new($options["floodgate-games"])
    setup_floodgate.start
  end

  config[:StopCallback] = Proc.new do
    if $options["pid-file"]
      FileUtils.rm($options["pid-file"], :force => true)
    end
  end

  srand
  server = WEBrick::GenericServer.new(config)
  ["INT", "TERM"].each do |signal|
    trap(signal) do
      $players.each {|p| p.kill}
      server.shutdown
      setup_floodgate.kill
    end
  end
  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}]")

  if ShogiServer::STOP_FILE.exist?
    log_message("Deleted the STOP file")
    ShogiServer::STOP_FILE.delete
  end

  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
      unless player
        log_error("Detected a timed out login attempt")
        next
      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
        if (player.game)
          player.game.kill(player)
        end
        player.finish
        $league.delete(player)
        log_message(sprintf("user %s logout", player.name))
	$players.delete(player)
      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


if ($0 == __FILE__)
  STDOUT.sync = true
  STDERR.sync = true
  TCPSocket.do_not_reverse_lookup = true
  Thread.abort_on_exception = $DEBUG ? true : false

  begin
    main
  rescue Exception => ex
    if $logger
      log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
    else
      $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
    end
  end
end
Show on old repository browser