OSDN Git Service

Implemented CHALLENGE command
[shogi-server/shogi-server.git] / shogi-server
index 906fe10..f236f9c 100755 (executable)
@@ -24,6 +24,8 @@ require 'socket'
 require 'yaml'
 require 'yaml/store'
 require 'digest/md5'
+require 'webrick'
+require 'fileutils'
 
 
 class TCPSocket
@@ -46,8 +48,9 @@ class TCPSocket
         end
       rescue TimeoutError
         return :timeout
-      rescue
-        return nil
+      rescue Exception => ex
+        log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
+        return :exception
       end
     else
       begin
@@ -89,9 +92,14 @@ class League
     @games = Hash::new
     @players = Hash::new
     @event = nil
-    @db = YAML::Store.new( File.join(File.dirname(__FILE__), "players.yaml") )
+    @dir = File.dirname(__FILE__)
+  end
+  attr_accessor :players, :games, :event, :dir
+
+  # this should be called just after instanciating a League object.
+  def setup_players_database
+    @db = YAML::Store.new(File.join(@dir, "players.yaml"))
   end
-  attr_accessor :players, :games, :event
 
   def add(player)
     self.load(player) if player.id
@@ -128,6 +136,7 @@ class League
   def search(id)
     hash = nil
     @db.transaction do
+      break unless  @db["players"]
       @db["players"].each do |group, players|
         hash = players[id]
         break if hash
@@ -139,6 +148,7 @@ class League
   def rated_players
     players = []
     @db.transaction(true) do
+      break unless  @db["players"]
       @db["players"].each do |group, players_hash|
         players << players_hash.keys
       end
@@ -375,7 +385,7 @@ class Player < BasicPlayer
       # 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?)
+#        @socket.close if (! @socket.closed?)
       rescue
         log_message(sprintf("user %s finish failed", @name))    
       end
@@ -388,7 +398,13 @@ class Player < BasicPlayer
 
   def writer
     while (str = @write_queue.pop)
-      @socket.write_safe(str)
+      begin
+        @socket.write(str)
+      rescue Exception => ex
+        log_error("Failed to send a message to #{@name}.")
+        log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
+        return
+      end
     end
   end
 
@@ -409,13 +425,14 @@ class Player < BasicPlayer
     end
   end
 
-  def write_help
-    @socket.write_safe('##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"')
-  end
-
   def run(csa_1st_str=nil)
     while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout)))
       begin
+        if (@writer_thread == nil || @writer_thread.status == false)
+          # The writer_thread has been killed because of socket errors.
+          return
+        end
+
         $mutex.lock
         if (csa_1st_str)
           str = csa_1st_str
@@ -429,8 +446,13 @@ class Player < BasicPlayer
         if (@status == "finished")
           return
         end
-        str.chomp! if (str.class == String)
+        str.chomp! if (str.class == String) # may be strip! ?
         case str
+        when "" 
+          # Application-level protocol for Keep-Alive
+          # If the server gets LF, it sends back LF.
+          # 30 sec rule (client may not send LF again within 30 sec) is not implemented yet.
+          write_safe("\n")
         when /^[\+\-][^%]/
           if (@status == "game")
             array_str = str.split(",")
@@ -447,7 +469,18 @@ class Player < BasicPlayer
           if (@status == "game")
             s = @game.handle_one_move(str, self)
             return if (s && @protocol == LoginCSA::PROTOCOL)
+          # else
+          #   begin
+          #     @socket.write("##[KEEPALIVE] #{Time.now}\n")
+          #   rescue Exception => ex
+          #     log_error("Failed to send a keepalive to #{@name}.")
+          #     log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
+          #     return
+          #   end
           end
+        when :exception
+          log_error("Failed to receive a message from #{@name}.")
+          return
         when /^REJECT/
           if (@status == "agree_waiting")
             @game.reject(@name)
@@ -486,7 +519,8 @@ class Player < BasicPlayer
             LEAGUE.games[game_id].monitoroff(self)
           end
         when /^%%HELP/
-          write_help
+          write_safe(
+            %!##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"\n!)
         when /^%%RATING/
           players = LEAGUE.rated_players
           players.sort {|a,b| b.rate <=> a.rate}.each do |p|
@@ -604,10 +638,16 @@ class Player < BasicPlayer
           @status = "connected"
           write_safe("LOGOUT:completed\n")
           return
+        when /^CHALLENGE/
+          # This command is only available for CSA's official testing server.
+          # So, this means nothing for this program.
+          write_safe("CHALLENGE ACCEPTED\n")
         when /^\s*$/
           ## ignore null string
         else
-          write_safe(sprintf("##[ERROR] unknown command %s\n", str))
+          msg = "##[ERROR] unknown command %s\n" % [str]
+          write_safe(msg)
+          log_error(msg)
         end
       ensure
         $mutex.unlock
@@ -1271,6 +1311,7 @@ class Board
     return true
   end
 
+  # sente is nil only if tests in test_board run
   def handle_one_move(str, sente=nil)
     if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
       sg = $1
@@ -1299,11 +1340,14 @@ class Board
       return :illegal
     end
     
+
     if (sg == "+")
-      sente = true
+      sente = true if sente == nil           # deprecated
+      return :illegal unless sente == true   # black player's move must be black
       hands = @sente_hands
     else
-      sente = false
+      sente = false if sente == nil          # deprecated
+      return :illegal unless sente == false  # white player's move must be white
       hands = @gote_hands
     end
     
@@ -1463,7 +1507,7 @@ class Game
     
     @id = sprintf("%s+%s+%s+%s+%s", 
                   LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
-    @logfile = @id + ".csa"
+    @logfile = File.join(LEAGUE.dir, @id + ".csa")
 
     LEAGUE.games[@id] = self
 
@@ -1914,7 +1958,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
@@ -1922,6 +1966,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.
 
 LICENSE
        this file is distributed under GPL version2 and might be compiled by Exerb
@@ -1929,33 +1975,31 @@ LICENSE
 SEE ALSO
 
 RELEASE
-       #{Release}
+       #{ShogiServer::Release}
 
 REVISION
-       #{Revision}
+       #{ShogiServer::Revision}
 EOM
 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]
+                         )
   parser.quiet = true
   begin
     parser.each_option do |name, arg|
@@ -2012,13 +2056,30 @@ def main
 
   write_pid_file($options["pid-file"]) if ($options["pid-file"])
 
-  server = TCPserver.open(port)
+  dir = $options["daemon"] || nil
+  if dir && ! File.exist?(dir)
+    FileUtils.mkdir(dir)
+  end
+  log_file = dir ? File.join(dir, "shogi-server.log") : STDOUT
+  $logger = WEBrick::Log.new(log_file)
+
+  LEAGUE.dir = dir || File.dirname(__FILE__)
+  LEAGUE.setup_players_database
+
+  config = {}
+  config[:Port]       = port
+  config[:ServerType] = WEBrick::Daemon if $options["daemon"]
+  config[:Logger]     = $logger
+
+  server = WEBrick::GenericServer.new(config)
+  ["INT", "TERM"].each {|signal| trap(signal){ server.shutdown } }
+  $stderr.puts("server started as a deamon") if $options["daemon"] 
   log_message("server started")
 
-  while true
-    Thread::start(server.accept) do |client|
-      Thread.pass
-      client.sync = true
+  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 = nil
       login  = nil
       while (str = client.gets_timeout(ShogiServer::Login_Time))
@@ -2037,8 +2098,11 @@ def main
                 LEAGUE.players[player.name].kill
               else
                 login.incorrect_duplicated_player(str)
-                Thread::exit
-                return
+                #Thread::exit
+                #return
+                # TODO
+                player = nil
+                break
               end
             end
             LEAGUE.add(player)
@@ -2052,9 +2116,10 @@ def main
         end
       end                       # login loop
       if (! player)
-        client.close
-        Thread::exit
-        return
+        #client.close
+        #Thread::exit
+        #return
+        next
       end
       log_message(sprintf("user %s login", player.name))
       login.process
@@ -2070,7 +2135,6 @@ def main
       ensure
         $mutex.unlock
       end
-    end
   end
 end
 
@@ -2080,7 +2144,7 @@ if ($0 == __FILE__)
   STDERR.sync = true
   TCPSocket.do_not_reverse_lookup = true
   Thread.abort_on_exception = true
-  
+
   LEAGUE = ShogiServer::League::new
   main
 end