#! /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
  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-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.

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-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)
    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 "#{$$}"
  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

  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
      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}]")

  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"]
      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))
      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
