{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TupleSections #-} {-| Module : Reflex.Backend.Socket.Accept Copyright : (c) 2018-2019, Commonwealth Scientific and Industrial Research Organisation License : BSD3 Maintainer : dave.laing.80@gmail.com, jack.kelly@data61.csiro.au Stability : experimental Portability : non-portable Use 'accept' to create listen sockets, and get an @'Event' t 'Socket'@ of new connections. -} module Reflex.Backend.Socket.Accept ( accept -- * Listen socket configuration , AcceptConfig(..) -- * Results of @accept@ , Accept(..) -- * Lenses -- ** 'AcceptConfig' , acHostname , acService , acListenQueue , acSocketOptions , acClose -- ** 'Accept' , aAcceptSocket , aClose , aError ) where import Control.Concurrent (forkIO) import qualified Control.Concurrent.STM as STM import Control.Exception (IOException, onException, try) import Control.Lens.TH (makeLenses) import Control.Monad.Except (ExceptT(..), runExceptT, withExceptT) import Control.Monad.STM (atomically) import Control.Monad.Trans (MonadIO(..)) import Data.Foldable (traverse_) import Data.Functor (($>), void) import Data.List.NonEmpty (NonEmpty, fromList) import Data.Semigroup.Foldable (asum1) import Network.Socket (AddrInfo(..), AddrInfoFlag(..), Socket) import qualified Network.Socket as NS import Reflex import Reflex.Backend.Socket.Error (SetupError(..)) -- | Configuration of a listen socket. data AcceptConfig t = AcceptConfig { _acHostname :: Maybe NS.HostName -- ^ The hostname to bind to. This will often be 'Nothing', to -- bind all interfaces. , _acService :: Maybe NS.ServiceName -- ^ The port number or service name to listen on. See the -- . , _acListenQueue :: Int -- ^ The length of the "pending connections" queue. See the -- . , _acSocketOptions :: [(NS.SocketOption, Int)] -- ^ List of socket options, passed one at a time to -- 'NS.setSocketOption'. Many people will want -- @[('NS.ReuseAddr', 1)]@ here. , _acClose :: Event t () -- ^ Close the listen socket when this event fires. If you plan to -- run forever, use 'never'. } $(makeLenses ''AcceptConfig) -- | Events produced by a running listen socket. data Accept t = Accept { _aAcceptSocket :: Event t (Socket, NS.SockAddr) -- ^ A new connection was accepted, including its remote address. , _aClose :: Event t () -- ^ The socket has closed. This will fire exactly once when the -- socket closes for any reason, including if your '_acClose' -- event fires or if the socket closes in response to a caught -- exception. , _aError :: Event t IOException -- ^ An exception occurred. Treat the socket as closed after you -- see this. You will see the '_aClose' event fire as well, but -- not necessarily in the same frame. } $(makeLenses ''Accept) -- | Create a listening socket. Sockets are accepted in a background -- thread. accept :: ( Reflex t , PerformEvent t m , PostBuild t m , TriggerEvent t m , MonadIO (Performable m) , MonadIO m ) => AcceptConfig t -> m (Event t (Either SetupError (Accept t))) -- ^ This event will fire exactly once. accept (AcceptConfig mHost mService listenQueue options eClose) = do (eAccept, onAccept) <- newTriggerEvent (eClosed, onClosed) <- newTriggerEvent (eError, onError) <- newTriggerEvent isOpen <- liftIO STM.newEmptyTMVarIO let start = liftIO $ makeListenSocket >>= \case Left exc -> pure $ Left exc Right sock -> do atomically $ STM.putTMVar isOpen sock void $ forkIO acceptLoop pure . Right $ Accept eAccept eClosed eError where makeListenSocket = let getAddrs :: ExceptT SetupError IO (NonEmpty AddrInfo) getAddrs = withExceptT GetAddrInfoError . ExceptT . try $ -- fromList is OK here, as getaddrinfo(3) is required to -- return a nonempty list of addrinfos. -- -- See: http://pubs.opengroup.org/onlinepubs/9699919799/functions/getaddrinfo.html -- And: https://github.com/haskell/network/issues/407 fromList <$> NS.getAddrInfo (Just passiveHints) mHost mService where passiveHints = NS.defaultHints { addrFlags = [AI_PASSIVE] } tryListen :: AddrInfo -> ExceptT (NonEmpty (AddrInfo, IOException)) IO Socket tryListen info = withExceptT (pure . (info,)) . ExceptT . try $ do sock <- NS.socket (addrFamily info) NS.Stream NS.defaultProtocol (`onException` NS.close sock) $ do traverse_ (uncurry $ NS.setSocketOption sock) options NS.bind sock (addrAddress info) NS.listen sock listenQueue pure sock in runExceptT $ do addrs <- getAddrs let attempts = tryListen <$> addrs withExceptT UseAddrInfoError $ asum1 attempts acceptLoop = let -- If we receive an exception when trying to accept, check -- the TMVar that's meant to hold our socket. If it's -- empty, then 'eClose' must have fired (and the socket -- closed under us) and we should go quietly. Otherwise, -- close the socket ourselves and signal 'eError'. exHandlerAccept e = atomically (STM.tryReadTMVar isOpen) >>= maybe (pure ()) (const $ close *> onError e) in atomically (STM.tryReadTMVar isOpen) >>= \case Nothing -> onClosed () Just sock -> do try (NS.accept sock) >>= either exHandlerAccept onAccept acceptLoop close = atomically (STM.tryTakeTMVar isOpen) >>= traverse_ NS.close performEvent_ $ eClose $> liftIO close ePostBuild <- getPostBuild performEvent $ ePostBuild $> start