gameServer/OfficialServer/checker.hs
branchhedgeroid
changeset 15515 7030706266df
parent 13677 8bd973ab9c9c
equal deleted inserted replaced
7861:bc7b6aa5d67a 15515:7030706266df
       
     1 {-
       
     2  * Hedgewars, a free turn based strategy game
       
     3  * Copyright (c) 2004-2015 Andrey Korotaev <unC0Rr@gmail.com>
       
     4  *
       
     5  * This program is free software; you can redistribute it and/or modify
       
     6  * it under the terms of the GNU General Public License as published by
       
     7  * the Free Software Foundation; version 2 of the License
       
     8  *
       
     9  * This program is distributed in the hope that it will be useful,
       
    10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
       
    11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
       
    12  * GNU General Public License for more details.
       
    13  *
       
    14  * You should have received a copy of the GNU General Public License
       
    15  * along with this program; if not, write to the Free Software
       
    16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
       
    17  \-}
       
    18 
       
    19 {-# LANGUAGE CPP, ScopedTypeVariables, OverloadedStrings #-}
       
    20 module Main where
       
    21 
       
    22 import qualified Control.Exception as Exception
       
    23 import System.IO
       
    24 import System.Log.Logger
       
    25 import qualified Data.ConfigFile as CF
       
    26 import Control.Monad.Error
       
    27 import System.Directory
       
    28 import Control.Monad.State
       
    29 import Control.Concurrent.Chan
       
    30 import Control.Concurrent
       
    31 import Network
       
    32 import Network.BSD
       
    33 import Network.Socket hiding (recv, sClose)
       
    34 import Network.Socket.ByteString
       
    35 import qualified Data.ByteString.Char8 as B
       
    36 import qualified Data.ByteString as BW
       
    37 import qualified Codec.Binary.Base64 as Base64
       
    38 import System.Process
       
    39 import Data.Maybe
       
    40 import Data.Either
       
    41 import qualified Data.List as L
       
    42 #if !defined(mingw32_HOST_OS)
       
    43 import System.Posix
       
    44 #endif
       
    45 
       
    46 readInt_ :: (Num a) => B.ByteString -> a
       
    47 readInt_ str =
       
    48   case B.readInt str of
       
    49        Just (i, t) | B.null t -> fromIntegral i
       
    50        _                      -> 0
       
    51 
       
    52 data Message = Packet [B.ByteString]
       
    53              | CheckFailed B.ByteString
       
    54              | CheckSuccess [B.ByteString]
       
    55     deriving Show
       
    56 
       
    57 serverAddress = "netserver.hedgewars.org"
       
    58 protocolNumber = "55"
       
    59 
       
    60 getLines :: Handle -> IO [B.ByteString]
       
    61 getLines h = g
       
    62     where
       
    63         g = do
       
    64             l <- liftM Just (B.hGetLine h) `Exception.catch` (\(_ :: Exception.IOException) -> return Nothing)
       
    65             if isNothing l then
       
    66                 return []
       
    67                 else
       
    68                 do
       
    69                 lst <- g
       
    70                 return $ fromJust l : lst
       
    71 
       
    72 
       
    73 engineListener :: Chan Message -> Handle -> String -> IO ()
       
    74 engineListener coreChan h fileName = do
       
    75     stats <- liftM (ps . L.dropWhile (not . start)) $ getLines h
       
    76     debugM "Engine" $ show stats
       
    77     if null stats then
       
    78         writeChan coreChan $ CheckFailed "No stats msg"
       
    79         else
       
    80         writeChan coreChan $ CheckSuccess stats
       
    81 
       
    82     removeFile fileName
       
    83     where
       
    84         start = flip L.elem ["WINNERS", "DRAW"]
       
    85         ps ("DRAW" : bs) = "DRAW" : ps bs
       
    86         ps ("WINNERS" : n : bs) = let c = readInt_ n in "WINNERS" : n : take c bs ++ (ps $ drop c bs)
       
    87         ps ("GHOST_POINTS" : n : bs) = let c = 2 * (readInt_ n) in "GHOST_POINTS" : n : take c bs ++ (ps $ drop c bs)
       
    88         ps ("ACHIEVEMENT" : typ : teamname : location : value : bs) =
       
    89             "ACHIEVEMENT" : typ : teamname : location : value : ps bs
       
    90         ps _ = []
       
    91 
       
    92 checkReplay :: String -> String -> String -> Chan Message -> [B.ByteString] -> IO ()
       
    93 checkReplay home exe prefix coreChan msgs = do
       
    94     tempDir <- getTemporaryDirectory
       
    95     (fileName, h) <- openBinaryTempFile tempDir "checker-demo"
       
    96     B.hPut h . B.concat . map (either (const B.empty) id . Base64.decode) $ msgs
       
    97     hFlush h
       
    98     hClose h
       
    99 
       
   100     (_, _, Just hOut, _) <- createProcess (proc exe
       
   101                 [fileName
       
   102                 , "--user-prefix", home
       
   103                 , "--prefix", prefix
       
   104                 , "--nomusic"
       
   105                 , "--nosound"
       
   106                 , "--stats-only"
       
   107                 ])
       
   108             {std_err = CreatePipe}
       
   109     hSetBuffering hOut LineBuffering
       
   110     void $ forkIO $ engineListener coreChan hOut fileName
       
   111 
       
   112 
       
   113 takePacks :: State B.ByteString [[B.ByteString]]
       
   114 takePacks = do
       
   115     modify (until (not . B.isPrefixOf pDelim) (B.drop 2))
       
   116     packet <- state $ B.breakSubstring pDelim
       
   117     buf <- get
       
   118     if B.null buf then put packet >> return [] else
       
   119         if B.null packet then return [] else do
       
   120             packets <- takePacks
       
   121             return (B.splitWith (== '\n') packet : packets)
       
   122     where
       
   123     pDelim = "\n\n"
       
   124 
       
   125 
       
   126 recvLoop :: Socket -> Chan Message -> IO ()
       
   127 recvLoop s chan =
       
   128         ((receiveWithBufferLoop B.empty >> return "Connection closed")
       
   129             `Exception.catch` (\(e :: Exception.SomeException) -> return . B.pack . show $ e)
       
   130         )
       
   131         >>= disconnected
       
   132     where
       
   133         disconnected msg = writeChan chan $ Packet ["BYE", msg]
       
   134         receiveWithBufferLoop recvBuf = do
       
   135             recvBS <- recv s 4096
       
   136             unless (B.null recvBS) $ do
       
   137                 let (packets, newrecvBuf) = runState takePacks $ B.append recvBuf recvBS
       
   138                 forM_ packets sendPacket
       
   139                 receiveWithBufferLoop $ B.copy newrecvBuf
       
   140 
       
   141         sendPacket packet = writeChan chan $ Packet packet
       
   142 
       
   143 
       
   144 session :: B.ByteString -> B.ByteString -> String -> String -> String -> Socket -> IO ()
       
   145 session l p home exe prefix s = do
       
   146     noticeM "Core" "Connected"
       
   147     coreChan <- newChan
       
   148     forkIO $ recvLoop s coreChan
       
   149     forever $ do
       
   150         p <- readChan coreChan
       
   151         case p of
       
   152             Packet p -> do
       
   153                 debugM "Network" $ "Recv: " ++ show p
       
   154                 onPacket coreChan p
       
   155             CheckFailed msg -> do
       
   156                 warningM "Check" "Check failed"
       
   157                 answer ["CHECKED", "FAIL", msg]
       
   158                 threadDelay 1500000
       
   159                 answer ["READY"]
       
   160             CheckSuccess msgs -> do
       
   161                 warningM "Check" "Check succeeded"
       
   162                 answer ("CHECKED" : "OK" : msgs)
       
   163                 threadDelay 1500000
       
   164                 answer ["READY"]
       
   165     where
       
   166     answer :: [B.ByteString] -> IO ()
       
   167     answer p = do
       
   168         debugM "Network" $ "Send: " ++ show p
       
   169         sendAll s $ B.unlines p `B.snoc` '\n'
       
   170     onPacket :: Chan Message -> [B.ByteString] -> IO ()
       
   171     onPacket _ ("CONNECTED":_) = do
       
   172         answer ["CHECKER", protocolNumber, l, p]
       
   173     onPacket _ ["PING"] = answer ["PONG"]
       
   174     onPacket _ ["LOGONPASSED"] = answer ["READY"]
       
   175     onPacket chan ("REPLAY":msgs) = do
       
   176         checkReplay home exe prefix chan msgs
       
   177         warningM "Check" "Started check"
       
   178     onPacket _ ("BYE" : xs) = error $ show xs
       
   179     onPacket _ _ = return ()
       
   180 
       
   181 
       
   182 main :: IO ()
       
   183 main = withSocketsDo . forever $ do
       
   184 #if !defined(mingw32_HOST_OS)
       
   185     installHandler sigPIPE Ignore Nothing
       
   186     installHandler sigCHLD Ignore Nothing
       
   187 #endif
       
   188 
       
   189     updateGlobalLogger "Core" (setLevel DEBUG)
       
   190     updateGlobalLogger "Network" (setLevel WARNING)
       
   191     updateGlobalLogger "Check" (setLevel DEBUG)
       
   192     updateGlobalLogger "Engine" (setLevel DEBUG)
       
   193 
       
   194     d <- getHomeDirectory
       
   195     Right (login, password) <- runErrorT $ do
       
   196         conf <- join . liftIO . CF.readfile CF.emptyCP $ d ++ "/.hedgewars/settings.ini"
       
   197         l <- CF.get conf "net" "nick"
       
   198         p <- CF.get conf "net" "passwordhash"
       
   199         return (B.pack l, B.pack p)
       
   200 
       
   201     Right (exeFullname, dataPrefix) <- runErrorT $ do
       
   202         conf <- join . liftIO . CF.readfile CF.emptyCP $ d ++ "/.hedgewars/checker.ini"
       
   203         l <- CF.get conf "engine" "exe"
       
   204         p <- CF.get conf "engine" "prefix"
       
   205         return (l, p)
       
   206 
       
   207 
       
   208     Exception.bracket
       
   209         setupConnection
       
   210         (\s -> noticeM "Core" "Shutting down" >> sClose s)
       
   211         (session login password (d ++ "/.hedgewars") exeFullname dataPrefix)
       
   212     where
       
   213         setupConnection = do
       
   214             noticeM "Core" "Connecting to the server..."
       
   215 
       
   216             proto <- getProtocolNumber "tcp"
       
   217             let hints = defaultHints { addrFlags = [AI_ADDRCONFIG, AI_CANONNAME] }
       
   218             (addr:_) <- getAddrInfo (Just hints) (Just serverAddress) Nothing
       
   219             let (SockAddrInet _ host) = addrAddress addr
       
   220             sock <- socket AF_INET Stream proto
       
   221             connect sock (SockAddrInet 46631 host)
       
   222             return sock