packages feed

EtaMOO-0.1.0.0: src/MOO/Network.hs

{-# LANGUAGE OverloadedStrings #-}

module MOO.Network ( Listener
                   , Connection
                   , PortNumber
                   , connectionEstablishedTime
                   , connectionActivityTime
                   , formatListener
                   , connectionOption
                   , setConnectionOption
                   , bootPlayer
                   , notify
                   , getConnectionName
                   , listen
                   , unlisten
                   , openNetworkConnection
                   ) where

import Control.Concurrent
import Control.Concurrent.STM
import Control.Exception
import Control.Monad
import Network
import Data.Set (Set)
import Data.Text (Text)
import Data.Time
import System.IO
import System.IO.Error hiding (try)

import MOO.Types
import MOO.Task
import {-# SOURCE #-} MOO.Database (systemObject, getServerOption')

import qualified Data.ByteString as BS
import qualified Data.Map as M
import qualified Data.Set as S
import qualified Data.Text as T
import qualified Data.Vector as V

data Listener = Listener {
    listenerSocket        :: Socket
  , listenerThread        :: ThreadId

  , listenerPort          :: PortID
  , listenerObject        :: ObjId
  , listenerPrintMessages :: Bool
  }

initListener = Listener {
    listenerSocket        = undefined
  , listenerThread        = undefined

  , listenerPort          = PortNumber 0
  , listenerObject        = systemObject
  , listenerPrintMessages = True
  }

formatListener :: Listener -> Value
formatListener listener =
  fromList [ Obj $ listenerObject listener
           , formatPoint $ listenerPort listener
           , truthValue $ listenerPrintMessages listener
           ]

  where formatPoint (PortNumber port) = Int $ fromIntegral port
        formatPoint (UnixSocket path) = Str $ T.pack path

data Connection = Connection {
    connectionHandle                  :: Handle
  , connectionName                    :: Text

  , connectionEstablishedTime         :: Maybe UTCTime
  , connectionActivityTime            :: UTCTime

  , connectionOutputPrefix            :: Text
  , connectionOutputSuffix            :: Text

  , connectionOptionBinary            :: Bool
  , connectionOptionHoldInput         :: Bool
  , connectionOptionDisableOob        :: Bool
  , connectionOptionClientEcho        :: Bool
  , connectionOptionFlushCommand      :: Text
  , connectionOptionIntrinsicCommands :: Set IntrinsicCommand
  }

initConnection = Connection {
    connectionHandle                  = undefined
  , connectionName                    = T.empty

  , connectionEstablishedTime         = Nothing
  , connectionActivityTime            = undefined

  , connectionOutputPrefix            = T.empty
  , connectionOutputSuffix            = T.empty

  , connectionOptionBinary            = False
  , connectionOptionHoldInput         = False
  , connectionOptionDisableOob        = False
  , connectionOptionClientEcho        = True
  , connectionOptionFlushCommand      = ".flush"
  , connectionOptionIntrinsicCommands = allIntrinsicCommands
  }

data IntrinsicCommand = IntrinsicProgram
                      | IntrinsicPrefix
                      | IntrinsicSuffix
                      | IntrinsicOutputPrefix
                      | IntrinsicOutputSuffix
                      deriving (Enum, Bounded, Eq, Ord, Show)

ic2text :: IntrinsicCommand -> Text
ic2text IntrinsicProgram      = ".program"
ic2text IntrinsicPrefix       = "PREFIX"
ic2text IntrinsicSuffix       = "SUFFIX"
ic2text IntrinsicOutputPrefix = "OUTPUTPREFIX"
ic2text IntrinsicOutputSuffix = "OUTPUTSUFFIX"

text2ic :: Text -> Maybe IntrinsicCommand
text2ic str | T.toCaseFold str == ".program" = Just IntrinsicProgram
text2ic "PREFIX"       = Just IntrinsicPrefix
text2ic "SUFFIX"       = Just IntrinsicSuffix
text2ic "OUTPUTPREFIX" = Just IntrinsicOutputPrefix
text2ic "OUTPUTSUFFIX" = Just IntrinsicOutputSuffix
text2ic _              = Nothing

allIntrinsicCommands :: Set IntrinsicCommand
allIntrinsicCommands = S.fromList [minBound ..]

allConnectionOptions :: [Text]
allConnectionOptions = ["binary", "hold-input", "disable-oob", "client-echo",
                        "flush-command", "intrinsic-commands"]

connectionOption :: StrT -> Maybe (Connection -> Value)
connectionOption "binary"             =
  Just (truthValue . connectionOptionBinary)
connectionOption "hold-input"         =
  Just (truthValue . connectionOptionHoldInput)
connectionOption "disable-oob"        =
  Just (truthValue . connectionOptionDisableOob)
connectionOption "client-echo"        =
  Just (truthValue . connectionOptionClientEcho)
connectionOption "flush-command"      =
  Just (Str . connectionOptionFlushCommand)
connectionOption "intrinsic-commands" =
  Just (fromListBy (Str . ic2text) .
        S.toList . connectionOptionIntrinsicCommands)
connectionOption _                    = Nothing

setConnectionOption :: StrT -> Value -> Connection -> MOO Connection
setConnectionOption "binary" value conn =
  return conn { connectionOptionBinary = truthOf value }
setConnectionOption "hold-input" value conn =
  return conn { connectionOptionHoldInput = truthOf value }
setConnectionOption "disable-oob" value conn =
  return conn { connectionOptionDisableOob = truthOf value }
setConnectionOption "client-echo" value conn = do
  let clientEcho = truthOf value
      telnetCmd  = BS.pack [ telnetIAC
                           , if clientEcho then telnetWON'T else telnetWILL
                           , telnetECHO
                           ]
      telnetIAC   = 255
      telnetWILL  = 251
      telnetWON'T = 252
      telnetECHO  =   1
  delayIO $ BS.hPut (connectionHandle conn) telnetCmd
  return conn { connectionOptionClientEcho = clientEcho }
setConnectionOption "flush-command" value conn =
  return conn { connectionOptionFlushCommand = flushCommand }
  where flushCommand = case value of
          Str t -> t
          _     -> T.empty
setConnectionOption "intrinsic-commands" value conn = do
  intrinsicCommands <- case value of
    Lst v -> foldM command S.empty (V.toList v)
      where command set (Str cmd) = maybe (raise E_INVARG)
                                    (return . flip S.insert set) $ text2ic cmd
            command _   _         = raise E_INVARG
    Int 0 -> return S.empty
    Int _ -> return allIntrinsicCommands
    _     -> raise E_INVARG
  return conn { connectionOptionIntrinsicCommands = intrinsicCommands }
setConnectionOption _ _ _ = raise E_INVARG

bootPlayer :: ObjId -> MOO ()
bootPlayer oid = notyet "bootPlayer"

notify :: ObjId -> Text -> MOO ()
notify who what = delayIO (putStrLn $ T.unpack what)

getConnectionName :: ObjId -> MOO Text
getConnectionName player = do
  world <- getWorld
  case M.lookup player (connections world) of
    Just conn -> return $ connectionName conn
    Nothing   -> raise E_INVARG

listen :: PortNumber -> ObjId -> Bool -> MOO PortNumber
listen port object printMessages = do
  world <- getWorld
  when (M.member port $ listeners world) $ raise E_INVARG

  world' <- getWorld'
  result <- requestIO $ do
    result <- try $ listenOn (PortNumber port)
    case result of
      Left err
        | isPermissionError err -> return (Left E_PERM)
        | otherwise             -> return (Left E_QUOTA)
      Right socket -> do
        port <- socketPort socket
        let listener = initListener {
                listenerSocket        = socket
              , listenerPort          = port
              , listenerObject        = object
              , listenerPrintMessages = printMessages
              }
        thread <- forkIO $ listenerIO world' listener
        return $ Right listener { listenerThread = thread }

  case result of
    Left  err      -> raise err
    Right listener -> do
      let (PortNumber canon) = listenerPort listener
      putWorld world { listeners = M.insert canon listener (listeners world) }
      return canon

unlisten :: PortNumber -> MOO ()
unlisten port = do
  world <- getWorld
  case M.lookup port $ listeners world of
    Just Listener { listenerThread = thread, listenerSocket = socket } -> do
      putWorld world { listeners = M.delete port (listeners world) }
      delayIO $ killThread thread >> sClose socket
    Nothing -> raise E_INVARG

listenerIO :: TVar World -> Listener -> IO ()
listenerIO world' Listener {
    listenerSocket        = socket
  , listenerPort          = port
  , listenerObject        = object
  , listenerPrintMessages = printMessages
  } = mask $ \restore -> do

  let lport = case port of
        PortNumber port -> T.pack (show port)
        UnixSocket path -> T.pack (show path)

  forever $ do
    (handle, host, port) <- accept socket

    now <- getCurrentTime
    let connection = initConnection {
            connectionHandle       = handle
          , connectionName         = T.concat [ "port ", lport
                                              , " from ", T.pack host
                                              , ", port ", T.pack (show port)
                                              ]
          , connectionActivityTime = now
          }

    connId <- addConnection world' connection

    forkIO $
      restore (connectionIO world' object printMessages (connId, connection))
      `finally` (removeConnection world' connId >> hClose handle)

addConnection :: TVar World -> Connection -> IO ObjId
addConnection world' connection = atomically $ do
  world <- readTVar world'
  let connId = nextConnectionId world
  writeTVar world' world {
      connections = M.insert connId connection (connections world)
    , nextConnectionId = connId - 1
    }
  return connId

removeConnection :: TVar World -> ObjId -> IO ()
removeConnection world' connId =
  atomically $ modifyTVar world' $ \world ->
    world { connections = M.delete connId (connections world) }

openNetworkConnection :: HostName -> PortNumber -> ObjId -> MOO ObjId
openNetworkConnection host port object = do
  world' <- getWorld'
  result <- requestIO $ do
    result <- try $ connectTo host (PortNumber port)
    case result of
      Left err
        | isDoesNotExistError err -> return (Left E_INVARG)
        | otherwise               -> return (Left E_QUOTA)
      Right handle -> do
        now <- getCurrentTime
        let lport = "0"  -- XXX
            connection = initConnection {
                connectionHandle       = handle
              , connectionName         =
                T.concat [ "port ", lport
                         , " to ", T.pack host
                         , ", port ", T.pack (show port)
                         ]
              , connectionActivityTime = now
              }

        connId <- addConnection world' connection
        return $ Right (connId, connection)

  case result of
    Left  err -> raise err
    Right (connId, connection) -> do
      delayIO $ void $ forkIO $
        connectionIO world' object False (connId, connection)
        `finally` do
          removeConnection world' connId
          hClose $ connectionHandle connection

      return connId

connectionIO :: TVar World -> ObjId -> Bool -> (ObjId, Connection) -> IO ()
connectionIO world' object printMessages (connId, connection) = do
  let handle = connectionHandle connection
  hSetBuffering handle LineBuffering
  return ()

serverMessage :: ObjId -> Id -> [Text] -> MOO [Text]
serverMessage object name defaultMessage = do
  maybeValue <- getServerOption' object name
  undefined