network-transport-tcp-0.3.1: src/Network/Transport/TCP/Mock/Socket.hs
{-# LANGUAGE EmptyDataDecls #-}
module Network.Transport.TCP.Mock.Socket
( -- * Types
HostName
, ServiceName
, Socket
, SocketType(..)
, SocketOption(..)
, AddrInfo(..)
, Family
, SockAddr
, ProtocolNumber
, ShutdownCmd(..)
-- * Functions
, getAddrInfo
, socket
, bindSocket
, listen
, setSocketOption
, accept
, sClose
, connect
, shutdown
-- * Constants
, defaultHints
, defaultProtocol
, sOMAXCONN
-- * Debugging API
, scheduleReadAction
-- * Internal API
, writeSocket
, readSocket
, Message(..)
) where
import Data.Word (Word8)
import Data.Map (Map)
import qualified Data.Map as Map
import Control.Exception (throwIO)
import Control.Category ((>>>))
import Control.Concurrent.MVar
import Control.Concurrent.Chan
import System.IO.Unsafe (unsafePerformIO)
import Data.Accessor (Accessor, accessor, (^=), (^.), (^:))
import qualified Data.Accessor.Container as DAC (mapMaybe)
import System.Timeout (timeout)
--------------------------------------------------------------------------------
-- Mock state --
--------------------------------------------------------------------------------
data MockState = MockState {
_boundSockets :: !(Map SockAddr Socket)
, _nextSocketId :: !Int
, _validHostnames :: [HostName]
}
initialMockState :: MockState
initialMockState = MockState {
_boundSockets = Map.empty
, _nextSocketId = 0
, _validHostnames = ["localhost", "127.0.0.1"]
}
mockState :: MVar MockState
{-# NOINLINE mockState #-}
mockState = unsafePerformIO $ newMVar initialMockState
get :: Accessor MockState a -> IO a
get acc = timeoutThrow mvarThreshold $ withMVar mockState $ return . (^. acc)
set :: Accessor MockState a -> a -> IO ()
set acc val = timeoutThrow mvarThreshold $ modifyMVar_ mockState $ return . (acc ^= val)
boundSockets :: Accessor MockState (Map SockAddr Socket)
boundSockets = accessor _boundSockets (\bs st -> st { _boundSockets = bs })
boundSocketAt :: SockAddr -> Accessor MockState (Maybe Socket)
boundSocketAt addr = boundSockets >>> DAC.mapMaybe addr
nextSocketId :: Accessor MockState Int
nextSocketId = accessor _nextSocketId (\sid st -> st { _nextSocketId = sid })
validHostnames :: Accessor MockState [HostName]
validHostnames = accessor _validHostnames (\ns st -> st { _validHostnames = ns })
--------------------------------------------------------------------------------
-- The public API (mirroring Network.Socket) --
--------------------------------------------------------------------------------
type HostName = String
type ServiceName = String
type PortNumber = String
type HostAddress = String
data SocketType = Stream
data SocketOption = ReuseAddr
data ShutdownCmd = ShutdownSend
data Family
data ProtocolNumber
data Socket = Socket {
socketState :: MVar SocketState
, socketDescription :: String
}
data SocketState =
Uninit
| BoundSocket {
socketBacklog :: Chan (Socket, SockAddr, MVar Socket)
}
| Connected {
socketBuff :: Chan Message
, _socketPeer :: Maybe Socket
, _scheduledReadActions :: [(Int, IO ())]
}
| Closed
data Message =
Payload Word8
| CloseSocket
data AddrInfo = AddrInfo {
addrFamily :: Family
, addrAddress :: SockAddr
}
data SockAddr = SockAddrInet PortNumber HostAddress
deriving (Eq, Ord, Show)
instance Show AddrInfo where
show = show . addrAddress
instance Show Socket where
show sock = "<<socket " ++ socketDescription sock ++ ">>"
socketPeer :: Accessor SocketState (Maybe Socket)
socketPeer = accessor _socketPeer (\peer st -> st { _socketPeer = peer })
scheduledReadActions :: Accessor SocketState [(Int, IO ())]
scheduledReadActions = accessor _scheduledReadActions (\acts st -> st { _scheduledReadActions = acts })
getAddrInfo :: Maybe AddrInfo -> Maybe HostName -> Maybe ServiceName -> IO [AddrInfo]
getAddrInfo _ (Just host) (Just port) = do
validHosts <- get validHostnames
if host `elem` validHosts
then return . return $ AddrInfo {
addrFamily = error "Family unused"
, addrAddress = SockAddrInet port host
}
else throwSocketError $ "getAddrInfo: invalid hostname '" ++ host ++ "'"
getAddrInfo _ _ _ = error "getAddrInfo: unsupported arguments"
defaultHints :: AddrInfo
defaultHints = error "defaultHints not implemented"
socket :: Family -> SocketType -> ProtocolNumber -> IO Socket
socket _ Stream _ = do
state <- newMVar Uninit
sid <- get nextSocketId
set nextSocketId (sid + 1)
return Socket {
socketState = state
, socketDescription = show sid
}
bindSocket :: Socket -> SockAddr -> IO ()
bindSocket sock addr = do
timeoutThrow mvarThreshold $ modifyMVar_ (socketState sock) $ \st -> case st of
Uninit -> do
backlog <- newChan
return BoundSocket {
socketBacklog = backlog
}
_ ->
throwSocketError "bind: socket already initialized"
set (boundSocketAt addr) (Just sock)
listen :: Socket -> Int -> IO ()
listen _ _ = return ()
defaultProtocol :: ProtocolNumber
defaultProtocol = error "defaultProtocol not implemented"
setSocketOption :: Socket -> SocketOption -> Int -> IO ()
setSocketOption _ ReuseAddr 1 = return ()
setSocketOption _ _ _ = error "setSocketOption: unsupported arguments"
accept :: Socket -> IO (Socket, SockAddr)
accept serverSock = do
backlog <- timeoutThrow mvarThreshold $ withMVar (socketState serverSock) $ \st -> case st of
BoundSocket {} ->
return (socketBacklog st)
_ ->
throwSocketError "accept: socket not bound"
(theirSocket, theirAddress, reply) <- readChan backlog
ourBuff <- newChan
ourState <- newMVar Connected {
socketBuff = ourBuff
, _socketPeer = Just theirSocket
, _scheduledReadActions = []
}
let ourSocket = Socket {
socketState = ourState
, socketDescription = ""
}
timeoutThrow mvarThreshold $ putMVar reply ourSocket
return (ourSocket, theirAddress)
sClose :: Socket -> IO ()
sClose sock = do
-- Close the peer socket
writeSocket sock CloseSocket
-- Close our socket
timeoutThrow mvarThreshold $ modifyMVar_ (socketState sock) $ \st ->
case st of
Connected {} -> do
-- In case there is a parallel read stuck on a readChan
writeChan (socketBuff st) CloseSocket
return Closed
_ ->
return Closed
connect :: Socket -> SockAddr -> IO ()
connect us serverAddr = do
mServer <- get (boundSocketAt serverAddr)
case mServer of
Just server -> do
serverBacklog <- timeoutThrow mvarThreshold $ withMVar (socketState server) $ \st -> case st of
BoundSocket {} ->
return (socketBacklog st)
_ ->
throwSocketError "connect: server socket not bound"
reply <- newEmptyMVar
writeChan serverBacklog (us, SockAddrInet "" "", reply)
them <- timeoutThrow mvarThreshold $ readMVar reply
timeoutThrow mvarThreshold $ modifyMVar_ (socketState us) $ \st -> case st of
Uninit -> do
buff <- newChan
return Connected {
socketBuff = buff
, _socketPeer = Just them
, _scheduledReadActions = []
}
_ ->
throwSocketError "connect: already connected"
Nothing -> throwSocketError "connect: unknown address"
sOMAXCONN :: Int
sOMAXCONN = error "sOMAXCONN not implemented"
shutdown :: Socket -> ShutdownCmd -> IO ()
shutdown sock ShutdownSend = do
writeSocket sock CloseSocket
timeoutThrow mvarThreshold $ modifyMVar_ (socketState sock) $ \st -> case st of
Connected {} ->
return (socketPeer ^= Nothing $ st)
_ ->
return st
--------------------------------------------------------------------------------
-- Functions with no direct public counterpart --
--------------------------------------------------------------------------------
peerBuffer :: Socket -> IO (Either String (Chan Message))
peerBuffer sock = do
mPeer <- timeoutThrow mvarThreshold $ withMVar (socketState sock) $ \st -> case st of
Connected {} ->
return (st ^. socketPeer)
_ ->
return Nothing
case mPeer of
Just peer -> timeoutThrow mvarThreshold $ withMVar (socketState peer) $ \st -> case st of
Connected {} ->
return (Right (socketBuff st))
_ ->
return (Left "Peer socket closed")
Nothing ->
return (Left "Socket closed")
throwSocketError :: String -> IO a
throwSocketError = throwIO . userError
writeSocket :: Socket -> Message -> IO ()
writeSocket sock msg = do
theirBuff <- peerBuffer sock
case theirBuff of
Right buff -> writeChan buff msg
Left err -> case msg of Payload _ -> throwSocketError $ "writeSocket: " ++ err
CloseSocket -> return ()
readSocket :: Socket -> IO (Maybe Word8)
readSocket sock = do
mBuff <- timeoutThrow mvarThreshold $ modifyMVar (socketState sock) $ \st -> case st of
Connected {} -> do
let (later, now) = tick $ st ^. scheduledReadActions
return ( scheduledReadActions ^= later $ st
, Just (socketBuff st, now)
)
_ ->
return (st, Nothing)
case mBuff of
Just (buff, actions) -> do
sequence actions
msg <- timeoutThrow readSocketThreshold $ readChan buff
case msg of
Payload w -> return (Just w)
CloseSocket -> timeoutThrow mvarThreshold $ modifyMVar (socketState sock) $ \st -> case st of
Connected {} ->
return (Closed, Nothing)
_ ->
throwSocketError "readSocket: socket in unexpected state"
Nothing ->
return Nothing
-- | Given a list of scheduled actions, reduce all delays by 1, and return the
-- actions that should be executed now.
tick :: [(Int, IO ())] -> ([(Int, IO ())], [IO ()])
tick = go [] []
where
go later now [] = (reverse later, reverse now)
go later now ((n, action) : actions)
| n == 0 = go later (action : now) actions
| otherwise = go ((n - 1, action) : later) now actions
--------------------------------------------------------------------------------
-- Debugging API --
--------------------------------------------------------------------------------
-- | Schedule an action to be executed after /n/ reads on this socket
--
-- If /n/ is zero we execute the action immediately.
scheduleReadAction :: Socket -> Int -> IO () -> IO ()
scheduleReadAction _ 0 action = action
scheduleReadAction sock n action =
modifyMVar_ (socketState sock) $ \st -> case st of
Connected {} ->
return (scheduledReadActions ^: ((n, action) :) $ st)
_ ->
throwSocketError "scheduleReadAction: socket not connected"
--------------------------------------------------------------------------------
-- Util --
--------------------------------------------------------------------------------
mvarThreshold :: Int
mvarThreshold = 1000000
readSocketThreshold :: Int
readSocketThreshold = 10000000
timeoutThrow :: Int -> IO a -> IO a
timeoutThrow n p = do
ma <- timeout n p
case ma of
Just a -> return a
Nothing -> throwIO (userError "timeout")