packages feed

Nomyx-0.0.1: src/Server.hs

-- | This server handles asynchronous text connections.
-- It has 4 threads:
-- mainLoop: dispatch messages between every threads (this is the main program thread, not forked)
-- acceptLoop: handles new client connections
-- multiLoop: handles game commands. It contains the whole game state.
-- clientLoop: one per client, handles clients communications.

-- And 5 channels:
-- acceptChan: used by acceptLoop to publish new clients connections to mainLoop.
-- gameChan: used for the communication between mainLoop and multiLoop.
-- clientChan: one per client, use by the clients to communicate with mainLoop.
-- handleFree: one per client, used for upstream comunication with clientLoops, to control usage of the Handle.
-- debugChan: used to pass debug commands

   
module Server where

{-
-- Module Network is the simple networking library, presenting a Handle-based interface.
import Network (listenOn, accept, sClose, Socket,
                withSocketsDo, PortID(..))

import Control.Exception (finally)
import Control.Concurrent
import Control.Concurrent.STM
import Multi
import Control.Monad.State
import System.IO
import Data.Char
import Control.Applicative
import Data.List
import Network.BSD
import Language.Haskell.Interpreter.Server --TODO: hide in Interpret
import Control.Concurrent.Process hiding (Handle)
import Commands
import Data.Function (on)
import Language.Nomic.Expression

type ClientNumber = Int

-- | associate a player number with a handle
data PlayerClient = PlayerClient { cPlayerNumber :: PlayerNumber,
                                   cHandle :: Handle}
                                   deriving (Eq, Show)
   
-- | A structure to hold the active games and players
data Server = Server { playerClients :: [PlayerClient],
                       interpreterHandle :: ServerHandle}
                       --deriving (Eq)

-- | A State to pass around active games and players.
-- Furthermore, the output are to be made with Comm to output to the right console.
type ServerState = StateT Server IO ()
                       
-- the channels to pass commands
type CommandChan = TChan String

-- type of the channel to pass commands upstream to the client thread
--data ClientCommands = Ready | Quit
type ClientChan = TChan String
                                   
-- | communication between clients and server
data ClientComm = ClientComm {inChan :: TChan String,
                              outChan :: TChan String,
                              handle :: Handle}

-- | a channel where to publish new clients connection
type AcceptChan = TChan ClientComm
      
defaultServer :: ServerHandle -> Server
defaultServer = Server []


type DebugServer = (ServerState, TChan Server)

defaultDebugServer = do
   debugChan <- atomically newTChan
   return (debugViewState, debugChan)


-- | An helper function that makes it clear how to use the state transformer Server.
runWithServer :: Server -> ServerState -> IO ()
runWithServer = flip evalStateT


-- | a loop that will handle game commands
runMulti :: AcceptChan -> DebugServer -> ServerHandle -> IO ()
runMulti acceptChan debug sh = runWithServer (defaultServer sh) (mainLoop acceptChan [] debug)

   
-- | Start Nomic in server mode
serverStart :: PortNumber -> ServerHandle -> IO ()
serverStart port sh = withSocketsDo $ do
    servSock <- listenOn $ PortNumber port
    host <- getHostName
    putStrLn $ "Starting telnet server...\nTo connect, try \"telnet " ++ host ++ " " ++ (show port) ++ "\" in a shell window"
    startAll servSock sh `catch` (\_ -> putStrLn "serverStart: Closing") `finally` sClose servSock


-- | starts every threads
startAll :: Socket -> ServerHandle -> IO ()
startAll servSock sh = do
    -- Fork the loop that will handle new client connections along with its channel
    acceptChan <- atomically newTChan

    --start the loop that will  handle client's connections
    --forkIO $ acceptLoop servSock acceptChan
    spawn $ makeProcess id (acceptLoop servSock acceptChan)-- `catch` (\_ -> do putStrLn "acceptLoop: Closing"; return ())

    -- the multi loop will centralize and dispatch communications
    def <- defaultDebugServer
    runMulti acceptChan def sh

    


-- | the loop will handle new client connections and fork a subsequent thread for each client
acceptLoop :: Socket -> AcceptChan -> Process () ()
acceptLoop servSock acceptChan = do -- acceptChan
   (cHandle, _, _) <- lift $ accept servSock
   --hSetEcho cHandle True
   liftIO $ hSetBuffering cHandle NoBuffering
	
	-- Fork loops that will handle client communication
   cc <- liftIO $ newClientComm cHandle
   liftIO $ forkIO $ clientIn  cc `catch` (\_ -> putStrLn "acceptLoop: clientIn exception")
   liftIO $ forkIO $ clientOut cc `catch` (\_ -> putStrLn "acceptLoop: clientOut exception")
   
	-- publish new client connection with its chan and handle on acceptChan
   lift $ atomically $ writeTChan acceptChan cc
   acceptLoop servSock acceptChan


newClientComm :: Handle -> IO ClientComm
newClientComm h = do
   inChan <- liftIO $ atomically newTChan
   outChan <- liftIO $ atomically newTChan
   return ClientComm {inChan = inChan,
                      outChan = outChan,
                      handle = h}


-- | a loop that will handle client communication
clientIn :: ClientComm -> IO ()
clientIn cc = do
   s <- hGetLine $ handle cc
   atomically $ writeTChan (inChan cc) s
   clientIn cc

-- | a loop that will handle client communication
clientOut :: ClientComm -> IO ()
clientOut cc = do
   s <- atomically $ readTChan (outChan cc)
   hPutStr (handle cc) s
   clientOut cc



selectIn :: [ClientComm] -> STM (String, ClientComm)
selectIn l = do
   let f cc@ClientComm {inChan = ic} = do
        s <- readTChan ic
        return (s, cc)
   foldl orElse retry (map f l)

selectOut :: [ClientComm] -> STM (String, ClientComm)
selectOut l = do
   let f cc@ClientComm {outChan = oc} = do
        s <- readTChan oc
        return (s, cc)
   foldl orElse retry (map f l)

data ClientData = ClientAccept ClientComm
                | ClientInput (String, ClientComm)

-- | the server loop will dispatch messages between threads
mainLoop :: AcceptChan -> [ClientComm] -> DebugServer -> ServerState
mainLoop acceptChan clients d@(debugState, debugChan) = do

   --read on both acceptChan and all client's chans
   r <- lift $ atomically $ (ClientAccept `fmap` readTChan acceptChan)
                              `orElse`
                            (ClientInput `fmap` selectIn clients)

   case r of
      -- new data on the accept chan (CommandChan, ClientChan, Handle)
      ClientAccept ca -> do
         --register new client
         newClient ca
         --loop
         mainLoop acceptChan (ca:clients) d
               
      -- new data on the clients chan (String, ClientChan, Handle)
      ClientInput (l,cc) -> do  
         -- do some filtering
         let myLine = filter isPrint l
         --putStrLn $ "data: " ++ myLine ++ " from handle: " ++ show h
         case myLine of
            "debug read" -> do
               s <- get
               --write state into the channel (for external reading).
               lift $ atomically $ writeTChan debugChan s
            "debug write" -> do
               --execute the debug monad.
               debugState
            "quit" -> playerQuit (handle cc)
            ""     -> return ()
            -- every other command is passed through
            _      -> issuePlayerCommand myLine cc



   --loop
   mainLoop acceptChan clients d


newClient :: ClientComm -> ServerState
newClient cc = do
   (Server pcs sh) <- get
   let comm = (Communication (inChan cc) (outChan cc) sh)
   pn <- liftIO $ evalStateT newPlayer comm
   modify (\ser -> ser { playerClients = PlayerClient { cPlayerNumber = pn,
                                                        cHandle = (handle cc)} : pcs})



-- | issue the player's command with the right Comm and Multi environnement
issuePlayerCommand :: String -> ClientComm -> ServerState
issuePlayerCommand l cc = do
   (Server pcs sh) <- get
   let comm = (Communication (inChan cc) (outChan cc) sh)
   --issue the player's command 
   case getPlayerNumber (handle cc) pcs of
      Nothing -> error "issuePlayerCommand: player's handle not found"
      Just pn -> liftIO $ evalStateT (runLine l pn) comm


playerQuit :: Handle -> ServerState
playerQuit h = do
   pcs <- gets playerClients
   -- erase player client
   modify (\ser -> ser { playerClients = filter (\PlayerClient {cHandle = myh} -> myh /= h) pcs})

-- | gives the player's number associated to that handle
getPlayerNumber :: Handle -> [PlayerClient] -> Maybe PlayerNumber
getPlayerNumber h ps = cPlayerNumber <$> find (\PlayerClient {cHandle = myh} -> myh==h) ps

debugViewState :: ServerState
debugViewState = lift . print =<< get  

instance Ord PlayerClient where
   (<=) = (<=) `on`cPlayerNumber

instance Show Server where
   show Server{playerClients =pcs} = "\n" ++ (show $ sort pcs)
-}