EtaMOO-0.2.0.0: src/MOO/Connection.hs
{-# LANGUAGE OverloadedStrings #-}
module MOO.Connection (
Connection
, ConnectionHandler
, connectionHandler
, firstConnectionId
, withConnections
, withConnection
, withMaybeConnection
, connectionName
, connectionConnectedTime
, connectionActivityTime
, connectionOutputDelimiters
, sendToConnection
, closeConnection
, notify
, notify'
, bufferedOutputLength
, forceInput
, flushInput
, bootPlayer
, bootPlayer'
, setConnectionOption
, getConnectionOptions
) where
import Control.Concurrent (forkIO)
import Control.Concurrent.STM (STM, TVar, TMVar, atomically, newTVar,
newEmptyTMVar, tryPutTMVar, takeTMVar,
readTVar, writeTVar, modifyTVar, swapTVar,
readTVarIO)
import Control.Concurrent.STM.TBMQueue (TBMQueue, newTBMQueue, closeTBMQueue,
readTBMQueue, writeTBMQueue,
tryReadTBMQueue, tryWriteTBMQueue,
unGetTBMQueue, isEmptyTBMQueue,
freeSlotsTBMQueue)
import Control.Exception (SomeException, try, bracket)
import Control.Monad ((<=<), join, when, unless, foldM, forever, void, liftM)
import Control.Monad.Trans (lift)
import Data.ByteString (ByteString)
import Data.Map (Map)
import Data.Maybe (fromMaybe, isNothing, fromJust)
import Data.Monoid ((<>))
import Data.Text (Text)
import Data.Text.Encoding (Decoding(..), encodeUtf8, streamDecodeUtf8With)
import Data.Text.Encoding.Error (lenientDecode)
import Data.Time (UTCTime, getCurrentTime)
import Pipes (Producer, Consumer, Pipe, await, yield, runEffect,
for, cat, (>->))
import Pipes.Concurrent (Buffer(..), Output(..), spawn, spawn',
fromInput, toOutput)
import qualified Data.ByteString as BS
import qualified Data.Map as M
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Vector as V
import MOO.Command
import MOO.Database
import MOO.Object
import MOO.Task
import MOO.Types
import qualified MOO.String as Str
data Connection = Connection {
connectionObject :: ObjId
, connectionPlayer :: TVar ObjId
, connectionInput :: TBMQueue ConnectionMessage
, connectionOutput :: TBMQueue ConnectionMessage
, connectionName :: ConnectionName
, connectionConnectedTime :: TVar (Maybe UTCTime)
, connectionActivityTime :: TVar UTCTime
, connectionOutputDelimiters :: TVar (Text, Text)
, connectionOptions :: TVar ConnectionOptions
, connectionReader :: TMVar Wake
, connectionDisconnect :: TMVar Disconnect
}
data ConnectionMessage = Line Text | Binary ByteString
data ConnectionOptions = ConnectionOptions {
optionBinaryMode :: Bool
, optionHoldInput :: Bool
, optionDisableOOB :: Bool
, optionClientEcho :: Bool
, optionFlushCommand :: Text
, optionIntrinsicCommands :: Map Text IntrinsicCommand
}
initConnectionOptions = ConnectionOptions {
optionBinaryMode = False
, optionHoldInput = False
, optionDisableOOB = False
, optionClientEcho = True
, optionFlushCommand = ".flush"
, optionIntrinsicCommands = allIntrinsicCommands
}
data ConnectionOption = Option {
optionName :: Id
, optionGet :: ConnectionOptions -> Value
, optionSet :: Connection -> Value ->
ConnectionOptions -> MOO ConnectionOptions
}
coBinary = Option "binary" get set
where get = truthValue . optionBinaryMode
set _ v options = return options { optionBinaryMode = truthOf v }
coHoldInput = Option "hold-input" get set
where get = truthValue . optionHoldInput
set _ v options = return options { optionHoldInput = truthOf v }
coDisableOOB = Option "disable-oob" get set
where get = truthValue . optionDisableOOB
set _ v options = return options { optionDisableOOB = truthOf v }
coClientEcho = Option "client-echo" get set
where get = truthValue . optionClientEcho
set conn v options = do
let clientEcho = truthOf v
telnetCommand = BS.pack [telnetIAC, telnetOption, telnetECHO]
telnetOption = if clientEcho then telnetWON'T else telnetWILL
liftSTM $ enqueueOutput False conn (Binary telnetCommand)
return options { optionClientEcho = clientEcho }
where telnetIAC = 255
telnetWILL = 251
telnetWON'T = 252
telnetECHO = 1
coFlushCommand = Option "flush-command" get set
where get = Str . Str.fromText . optionFlushCommand
set _ v options = do
let flushCommand = case v of
Str flush -> Str.toText flush
_ -> T.empty
return options { optionFlushCommand = flushCommand }
coIntrinsicCommands = Option "intrinsic-commands" get set
where get = fromListBy (Str . Str.fromText) . M.keys . optionIntrinsicCommands
set _ v options = do
commands <- case v of
Lst vs -> foldM addCommand M.empty (V.toList vs)
Int 0 -> return M.empty
Int _ -> return allIntrinsicCommands
_ -> raise E_INVARG
return options { optionIntrinsicCommands = commands }
addCommand cmds value = case value of
Str cmd -> do
ic <- maybe (raise E_INVARG) return $
M.lookup (Str.toText cmd) allIntrinsicCommands
return $ M.insert (intrinsicCommand ic) ic cmds
_ -> raise E_INVARG
allConnectionOptions :: Map Id ConnectionOption
allConnectionOptions = M.fromList $ map assoc connectionOptions
where assoc co = (optionName co, co)
connectionOptions = [
coBinary
, coHoldInput
, coDisableOOB
, coClientEcho
, coFlushCommand
, coIntrinsicCommands
]
data IntrinsicCommand = Intrinsic {
intrinsicCommand :: Text
, intrinsicFunction :: Connection -> Text -> IO ()
}
modifyOutputDelimiters :: Connection -> ((Text, Text) -> (Text, Text)) -> IO ()
modifyOutputDelimiters conn =
atomically . modifyTVar (connectionOutputDelimiters conn)
icPREFIX = Intrinsic "PREFIX" $ \conn prefix ->
modifyOutputDelimiters conn $ \(_, suffix) -> (prefix, suffix)
icSUFFIX = Intrinsic "SUFFIX" $ \conn suffix ->
modifyOutputDelimiters conn $ \(prefix, _) -> (prefix, suffix)
icOUTPUTPREFIX = icPREFIX { intrinsicCommand = "OUTPUTPREFIX" }
icOUTPUTSUFFIX = icSUFFIX { intrinsicCommand = "OUTPUTSUFFIX" }
icProgram = Intrinsic ".program" $ \conn argstr ->
-- XXX verify programmer
atomically $ sendToConnection conn ".program: Not yet implemented" -- XXX
allIntrinsicCommands :: Map Text IntrinsicCommand
allIntrinsicCommands = M.fromList $ map assoc intrinsicCommands
where assoc ic = (intrinsicCommand ic, ic)
intrinsicCommands = [
icPREFIX
, icSUFFIX
, icOUTPUTPREFIX
, icOUTPUTSUFFIX
, icProgram
]
outOfBandPrefix :: Text
outOfBandPrefix = "#$#"
outOfBandQuotePrefix :: Text
outOfBandQuotePrefix = "#$\""
maxQueueLength :: Int
maxQueueLength = 512
type ConnectionName = STM String
type ConnectionHandler = ConnectionName -> (Producer ByteString IO (),
Consumer ByteString IO ()) -> IO ()
data Disconnect = Disconnected | ClientDisconnected
connectionHandler :: TVar World -> ObjId -> Bool -> ConnectionHandler
connectionHandler world' object printMessages connectionName (input, output) =
bracket newConnection deleteConnection $ \conn -> do
forkIO $ runConnection world' printMessages conn
forkIO $ do
try (runEffect $ input >-> connectionRead conn) >>=
either (\err -> let _ = err :: SomeException in return ()) return
atomically $ do
tryPutTMVar (connectionDisconnect conn) ClientDisconnected
closeTBMQueue (connectionOutput conn)
runEffect $ connectionWrite conn >-> output
atomically $ do
tryPutTMVar (connectionDisconnect conn) Disconnected
closeTBMQueue (connectionInput conn)
where newConnection :: IO Connection
newConnection = do
now <- getCurrentTime
atomically $ do
world <- readTVar world'
let connectionId = nextConnectionId world
playerVar <- newTVar connectionId
inputQueue <- newTBMQueue maxQueueLength
outputQueue <- newTBMQueue maxQueueLength
cTimeVar <- newTVar Nothing
aTimeVar <- newTVar now
delimsVar <- newTVar (T.empty, T.empty)
optionsVar <- newTVar initConnectionOptions {
optionFlushCommand = defaultFlushCommand $
serverOptions (database world) }
readerVar <- newEmptyTMVar
disconnectVar <- newEmptyTMVar
let connection = Connection {
connectionObject = object
, connectionPlayer = playerVar
, connectionInput = inputQueue
, connectionOutput = outputQueue
, connectionName = connectionName
, connectionConnectedTime = cTimeVar
, connectionActivityTime = aTimeVar
, connectionOutputDelimiters = delimsVar
, connectionOptions = optionsVar
, connectionReader = readerVar
, connectionDisconnect = disconnectVar
}
nextId = succConnectionId
(`M.member` connections world) connectionId
writeTVar world' world {
connections = M.insert connectionId connection
(connections world)
, nextConnectionId = nextId
}
writeTBMQueue inputQueue (Line T.empty)
return connection
deleteConnection :: Connection -> IO ()
deleteConnection conn = do
atomically $ do
connectionId <- readTVar (connectionPlayer conn)
modifyTVar world' $ \world ->
world { connections = M.delete connectionId (connections world) }
how <- atomically $ takeTMVar (connectionDisconnect conn)
player <- readTVarIO (connectionPlayer conn)
let systemVerb = case how of
Disconnected -> "user_disconnected"
ClientDisconnected -> "user_client_disconnected"
comp = fromMaybe zero `liftM`
callSystemVerb' object systemVerb [Obj player] Str.empty
void $ runTask =<< newTask world' player comp
runConnection :: TVar World -> Bool -> Connection -> IO ()
runConnection world' printMessages conn = loop
where loop = do
input <- atomically $ readTBMQueue (connectionInput conn)
options <- readTVarIO (connectionOptions conn)
let processLine'
| optionDisableOOB options = processLine options
| otherwise = processLineWithOOB options
case input of
Just (Line line) -> processLine' line >> loop
Just (Binary bytes) -> processBinary bytes >> loop
Nothing -> return ()
processLineWithOOB :: ConnectionOptions -> Text -> IO ()
processLineWithOOB options line
| outOfBandQuotePrefix `T.isPrefixOf` line = processLine options line'
| outOfBandPrefix `T.isPrefixOf` line = processOOB line
| otherwise = processLine options line
where line' = T.drop (T.length outOfBandQuotePrefix) line
processOOB :: Text -> IO ()
processOOB line =
void $ runServerVerb' "do_out_of_band_command" (cmdWords line) line
processLine :: ConnectionOptions -> Text -> IO ()
processLine options line = do
player <- readTVarIO (connectionPlayer conn)
if player < 0 then processUnLoggedIn line
else do
let cmd = parseCommand line
case M.lookup (Str.toText $ commandVerb cmd)
(optionIntrinsicCommands options) of
Just Intrinsic { intrinsicFunction = runIntrinsic } ->
runIntrinsic conn (Str.toText $ commandArgStr cmd)
Nothing -> do
(prefix, suffix) <- readTVarIO (connectionOutputDelimiters conn)
let maybeSend delim = unless (T.null delim) $
sendToConnection conn delim
void $ runServerTask $ do
liftSTM $ maybeSend prefix
delayIO $ atomically $ maybeSend suffix
-- XXX it would be better to send the suffix as part of the
-- atomic command, but we don't currently have a way of
-- ensuring anything is run after an uncaught exception
result <- callSystemVerb "do_command" (cmdWords line) line
case result of
Just value | truthOf value -> return zero
_ -> runCommand cmd
processBinary :: ByteString -> IO ()
processBinary bytes = return ()
processUnLoggedIn :: Text -> IO ()
processUnLoggedIn line = do
result <- runServerTask $ do
maxObject <- maxObject `liftM` getDatabase
player <- fromMaybe zero `liftM`
callSystemVerb "do_login_command" (cmdWords line) line
return $ fromList [Obj maxObject, player]
case result of
Just (Lst v) -> case V.toList v of
[Obj maxObject, Obj player] -> connectPlayer player maxObject
_ -> return ()
_ -> return ()
connectPlayer :: ObjId -> ObjId -> IO ()
connectPlayer player maxObject = do
now <- getCurrentTime
maybeOldConn <- atomically $ do
writeTVar (connectionConnectedTime conn) (Just now)
oldPlayer <- swapTVar (connectionPlayer conn) player
world <- readTVar world'
let maybeOldConn = M.lookup player (connections world)
case maybeOldConn of
Just oldConn -> do
writeTVar (connectionPlayer oldConn) oldPlayer
let oldObject = connectionObject oldConn
newObject = connectionObject conn
when (oldObject /= newObject) $ void $
tryPutTMVar (connectionDisconnect oldConn) ClientDisconnected
-- print' oldConn redirectFromMsg
closeConnection oldConn
Nothing -> return ()
writeTVar world' world {
connections = M.insert player conn $
M.delete oldPlayer (connections world) }
return maybeOldConn
let (systemVerb, msg)
| player > maxObject = ("user_created", createMsg)
| isNothing maybeOldConn ||
connectionObject (fromJust maybeOldConn) /=
connectionObject conn = ("user_connected", connectMsg)
| otherwise = ("user_reconnected", redirectToMsg)
print msg
runServerVerb systemVerb [Obj player]
callSystemVerb :: StrT -> [Value] -> Text -> MOO (Maybe Value)
callSystemVerb vname args argstr =
callSystemVerb' object vname args (Str.fromText argstr)
where object = connectionObject conn
runServerVerb :: StrT -> [Value] -> IO ()
runServerVerb vname args = void $ runServerVerb' vname args T.empty
runServerVerb' :: StrT -> [Value] -> Text -> IO (Maybe Value)
runServerVerb' vname args argstr = runServerTask $
fromMaybe zero `liftM`
callSystemVerb' object vname args (Str.fromText argstr)
where object = connectionObject conn
runServerTask :: MOO Value -> IO (Maybe Value)
runServerTask comp = do
player <- readTVarIO (connectionPlayer conn)
runTask =<< newTask world' player comp
print' :: Connection -> (ObjId -> MOO [Text]) -> IO ()
print' conn msg
| printMessages = void $ runServerTask $
printMessage conn msg >> return zero
| otherwise = return ()
print :: (ObjId -> MOO [Text]) -> IO ()
print = print' conn
cmdWords :: Text -> [Value]
cmdWords = map Str . parseWords
-- | Connection reading thread: deliver messages from the network to our input
-- 'TBMQueue' until EOF, handling binary mode and the flush command, if any.
connectionRead :: Connection -> Consumer ByteString IO ()
connectionRead conn = do
(outputLines, inputLines) <- lift $ spawn Unbounded
(outputMessages, inputMessages) <- lift $ spawn Unbounded
lift $ forkIO $ runEffect $
fromInput inputLines >->
readUtf8 >-> readLines >-> sanitize >-> lineMessage >->
toOutput outputMessages
lift $ forkIO $ runEffect $ fromInput inputMessages >-> deliverMessages
forever $ do
bytes <- await
lift $ getCurrentTime >>=
atomically . writeTVar (connectionActivityTime conn)
options <- lift $ readTVarIO (connectionOptions conn)
lift $ atomically $
if optionBinaryMode options
then send outputMessages (Binary bytes)
else send outputLines bytes
where readUtf8 :: Monad m => Pipe ByteString Text m ()
readUtf8 = await >>= readUtf8' . streamDecodeUtf8With lenientDecode
where readUtf8' (Some text _ continue) = do
unless (T.null text) $ yield text
await >>= readUtf8' . continue
readLines :: Monad m => Pipe Text Text m ()
readLines = readLines' TL.empty
where readLines' prev = await >>= yieldLines prev >>= readLines'
yieldLines :: Monad m => TL.Text -> Text ->
Pipe Text Text m TL.Text
yieldLines prev text =
let concatenate = TL.append prev . TL.fromStrict
(end, rest) = T.break (== '\n') text
line = TL.toStrict (concatenate end)
line' = if not (T.null line) && T.last line == '\r'
then T.init line else line
in if T.null rest
then return $ concatenate text
else do
yield line'
yieldLines TL.empty (T.tail rest)
sanitize :: Monad m => Pipe Text Text m ()
sanitize = for cat (yield . T.filter Str.validChar)
lineMessage :: Monad m => Pipe Text ConnectionMessage m ()
lineMessage = for cat (yield . Line)
deliverMessages :: Consumer ConnectionMessage IO ()
deliverMessages = forever $ do
message <- await
case message of
Line line -> do
options <- lift $ readTVarIO (connectionOptions conn)
let flushCmd = optionFlushCommand options
lift $ if not (T.null flushCmd) && line == flushCmd
then flushQueue else enqueue message
Binary _ -> lift $ enqueue message
enqueue :: ConnectionMessage -> IO ()
enqueue = atomically . writeTBMQueue (connectionInput conn)
flushQueue :: IO ()
flushQueue = atomically $ flushInput True conn
-- | Connection writing thread: deliver messages from our output 'TBMQueue' to
-- the network until the queue is closed, which is our signal to close the
-- connection.
connectionWrite :: Connection -> Producer ByteString IO ()
connectionWrite conn = do
(outputBinary, inputBinary, sealBinary) <- lift $ spawn' Unbounded
(outputLines, inputLines, sealLines) <- lift $ spawn' Unbounded
lift $ forkIO $ do
runEffect $ fromInput inputLines >-> writeLines >-> writeUtf8 >->
toOutput outputBinary
atomically sealBinary
lift $ forkIO $ processQueue outputLines outputBinary sealLines
fromInput inputBinary
where writeLines :: Monad m => Pipe Text Text m ()
writeLines = forever $ await >>= yield >> yield "\r\n"
writeUtf8 :: Monad m => Pipe Text ByteString m ()
writeUtf8 = for cat (yield . encodeUtf8)
processQueue :: Output Text -> Output ByteString -> STM () -> IO ()
processQueue outputLines outputBinary sealLines = loop
where loop = do
message <- atomically $ readTBMQueue (connectionOutput conn)
case message of
Just (Line text) ->
atomically (send outputLines text) >> loop
Just (Binary bytes) ->
atomically (send outputBinary bytes) >> loop
Nothing -> atomically sealLines
-- | The first un-logged-in connection ID. Avoid conflicts with #-1
-- ($nothing), #-2 ($ambiguous_match), and #-3 ($failed_match).
firstConnectionId :: ObjId
firstConnectionId = -4
-- | Determine the next valid un-logged-in connection ID, making sure it
-- remains negative and doesn't pass the predicate.
succConnectionId :: (ObjId -> Bool) -> ObjId -> ObjId
succConnectionId invalid = findNext
where findNext connId
| nextId >= 0 = findNext firstConnectionId
| invalid nextId = findNext nextId
| otherwise = nextId
where nextId = connId - 1
-- | Do something with the 'Map' of all active connections.
withConnections :: (Map ObjId Connection -> MOO a) -> MOO a
withConnections f = f . connections =<< getWorld
-- | Do something with the connection for given object, if any.
withMaybeConnection :: ObjId -> (Maybe Connection -> MOO a) -> MOO a
withMaybeConnection oid f = withConnections $ f . M.lookup oid
-- | Do something with the connection for the given object, raising 'E_INVARG'
-- if no such connection exists.
withConnection :: ObjId -> (Connection -> MOO a) -> MOO a
withConnection oid = withMaybeConnection oid . maybe (raise E_INVARG)
enqueueOutput :: Bool -> Connection -> ConnectionMessage -> STM Bool
enqueueOutput noFlush conn message = do
let queue = connectionOutput conn
result <- tryWriteTBMQueue queue message
case result of
Just False -> if noFlush then return False
else do readTBMQueue queue -- XXX count flushed lines
enqueueOutput noFlush conn message
_ -> return True
sendToConnection :: Connection -> Text -> STM ()
sendToConnection conn line = void $ enqueueOutput False conn (Line line)
printMessage :: Connection -> (ObjId -> MOO [Text]) -> MOO ()
printMessage conn msg = serverMessage conn msg >>= liftSTM
serverMessage :: Connection -> (ObjId -> MOO [Text]) -> MOO (STM ())
serverMessage conn msg =
mapM_ (sendToConnection conn) `liftM` msg (connectionObject conn)
-- | Send data to a connection, flushing if necessary.
notify :: ObjId -> StrT -> MOO Bool
notify = notify' False
-- | Send data to a connection, optionally without flushing.
notify' :: Bool -> ObjId -> StrT -> MOO Bool
notify' noFlush who what =
withMaybeConnection who . maybe (return True) $ \conn -> do
options <- liftSTM $ readTVar (connectionOptions conn)
message <- if optionBinaryMode options
then Binary `fmap` binaryString what
else return (Line $ Str.toText what)
liftSTM $ enqueueOutput noFlush conn message
-- | Return the number of items currently buffered for output to a connection,
-- or the maximum number of items that will be buffered up for output on any
-- connection.
bufferedOutputLength :: Maybe Connection -> STM Int
bufferedOutputLength Nothing = return maxQueueLength
bufferedOutputLength (Just conn) =
(maxQueueLength -) `fmap` freeSlotsTBMQueue (connectionOutput conn)
-- | Force a line of input for a connection.
forceInput :: Bool -> ObjId -> StrT -> MOO ()
forceInput atFront oid line =
withConnection oid $ \conn -> do
let queue = connectionInput conn
message = Line (Str.toText line)
success <- liftSTM $
if atFront then unGetTBMQueue queue message >> return True
else fromMaybe True `fmap` tryWriteTBMQueue queue message
unless success $ raise E_QUOTA
-- | Flush a connection's input queue, optionally showing what was flushed.
flushInput :: Bool -> Connection -> STM ()
flushInput showMessages conn = do
let queue = connectionInput conn
notify = when showMessages . void . enqueueOutput False conn . Line
empty <- isEmptyTBMQueue queue
if empty then notify ">> No pending input to flush..."
else do notify ">> Flushing the following pending input:"
flushQueue queue notify
notify ">> (Done flushing)"
where flushQueue queue notify = loop
where loop = do
item <- join `fmap` tryReadTBMQueue queue
case item of
Just (Line line) -> do
notify $ ">> " <> line
loop
Just (Binary _) -> do
notify ">> * [binary data]"
loop
Nothing -> return ()
-- | Initiate the closing of a connection by closing its output queue.
closeConnection :: Connection -> STM ()
closeConnection = closeTBMQueue . connectionOutput
-- | Close the connection associated with an object.
bootPlayer :: ObjId -> MOO ()
bootPlayer = bootPlayer' False
-- | Close the connection associated with an object, with message varying on
-- whether the object is being recycled.
bootPlayer' :: Bool -> ObjId -> MOO ()
bootPlayer' recycled oid =
withMaybeConnection oid . maybe (return ()) $ \conn -> do
printMessage conn $ if recycled then recycleMsg else bootMsg
liftSTM $ closeConnection conn
modifyWorld $ \world -> world { connections =
M.delete oid (connections world) }
return ()
withConnectionOptions :: ObjId -> (ConnectionOptions -> MOO a) -> MOO a
withConnectionOptions oid f =
withConnection oid $ f <=< liftSTM . readTVar . connectionOptions
modifyConnectionOptions :: ObjId -> (Connection -> ConnectionOptions ->
MOO ConnectionOptions) -> MOO ()
modifyConnectionOptions oid f =
withConnection oid $ \conn -> do
let optionsVar = connectionOptions conn
options <- liftSTM (readTVar optionsVar)
liftSTM . writeTVar optionsVar =<< f conn options
-- | Set a connection option for an active connection.
setConnectionOption :: ObjId -> Id -> Value -> MOO ()
setConnectionOption oid option value =
modifyConnectionOptions oid $ \conn options -> do
option <- maybe (raise E_INVARG) return $
M.lookup option allConnectionOptions
optionSet option conn value options
-- | Return a 'Map' of all currently set connection options for a connection.
getConnectionOptions :: ObjId -> MOO (Map Id Value)
getConnectionOptions oid =
withConnectionOptions oid $ \options ->
let optionValue option = optionGet option options
in return $ M.map optionValue allConnectionOptions
msgFor :: Id -> [Text] -> ObjId -> MOO [Text]
msgFor msg def oid
| oid == systemObject = systemMessage
| otherwise = getServerMessage oid msg systemMessage
where systemMessage = getServerMessage systemObject msg (return def)
bootMsg = msgFor "boot_msg" ["*** Disconnected ***"]
connectMsg = msgFor "connect_msg" ["*** Connected ***"]
createMsg = msgFor "create_msg" ["*** Created ***"]
recycleMsg = msgFor "recycle_msg" ["*** Recycled ***"]
redirectFromMsg = msgFor "redirect_from_msg"
["*** Redirecting connection to new port ***"]
redirectToMsg = msgFor "redirect_to_msg"
["*** Redirecting old connection to this port ***"]
serverFullMsg = msgFor "server_full_msg"
[ "*** Sorry, but the server cannot accept any more connections right now."
, "*** Please try again later." ]