sockets-0.6.1.0: src/Socket/SequencedPacket/Unix.hs
{-# language BangPatterns #-}
{-# language DataKinds #-}
{-# language DeriveAnyClass #-}
{-# language DerivingStrategies #-}
{-# language DuplicateRecordFields #-}
{-# language LambdaCase #-}
{-# language MagicHash #-}
{-# language MultiWayIf #-}
{-# language NamedFieldPuns #-}
{-# language UnboxedTuples #-}
{-# language ScopedTypeVariables #-}
{-# language TypeApplications #-}
-- | Unix-domain @seqpacket@ sockets.
module Socket.SequencedPacket.Unix
( Connection(..)
, Listener(..)
, BindException(..)
, openPair
, withPair
, listen
, unlisten
, unlisten_
, withListener
, connect
, tryConnect
, withConnection
, withAccepted
, accept
, disconnect
, disconnect_
) where
import Control.Concurrent.STM (TVar)
import Control.Exception (mask,onException)
import Data.Coerce (coerce)
import Data.Primitive (ByteArray)
import Foreign.C.Error (Errno(..),eNOENT,ePROTOTYPE,eAGAIN,eWOULDBLOCK)
import Foreign.C.Error (eNFILE,eMFILE,eCONNREFUSED,eNOTCONN)
import Foreign.C.Error (eADDRINUSE,eACCES,eCONNABORTED,ePERM)
import Socket (Connectedness(..),Family(..),SocketException(..),BindException(..))
import Socket.Datagram (Socket(..))
import Socket.Stream (ConnectException(..),CloseException(..),AcceptException(..))
import Socket.Error (die)
import Socket.IPv4 (Message(..))
import Socket.Datagram.Common (close)
import Socket (Interruptibility(Uninterruptible))
import Socket.Datagram.Unix.Connected (UnixAddress(..))
import System.Posix.Types (Fd)
import qualified Control.Concurrent.STM as STM
import qualified Data.Primitive as PM
import qualified Foreign.C.Error.Describe as D
import qualified Linux.Socket as L
import qualified Posix.Socket as S
import qualified Socket as SCK
import qualified Socket.EventManager as EM
import qualified Socket.Pair as Pair
newtype Connection = Connection Fd
newtype Listener = Listener Fd
-- | Unbracketed function for opening a connected socket pair. All warnings
-- that apply to 'open' apply to this function as well.
openPair :: IO (Either SocketException (Connection, Connection))
openPair = coerce
@(IO (Either SocketException (Fd,Fd)))
@(IO (Either SocketException (Connection,Connection)))
(Pair.open S.sequencedPacket)
withPair ::
(Connection -> Connection -> IO a)
-- ^ Callback providing the connected datagram sockets
-> IO (Either SocketException a)
withPair f = mask $ \restore -> openPair >>= \case
Left err -> pure (Left err)
Right (Connection fdA, Connection fdB) -> do
a <- onException
(restore (f (Connection fdA) (Connection fdB)))
(S.uninterruptibleErrorlessClose fdA *> S.uninterruptibleErrorlessClose fdB)
S.uninterruptibleClose fdA >>= \case
Left err -> die ("Socket.Datagram.Unix.Connected.close: " ++ describeErrorCode err)
Right _ -> S.uninterruptibleClose fdB >>= \case
Left err -> die ("Socket.Datagram.Unix.Connected.close: " ++ describeErrorCode err)
Right _ -> pure (Right a)
describeErrorCode :: Errno -> String
describeErrorCode err@(Errno e) = "error code " ++ D.string err ++ " (" ++ show e ++ ")"
-- | Open a socket that is used to listen for inbound connections.
withListener ::
UnixAddress -- ^ Address to bind to (path or abstract namespace name)
-> (Listener -> IO a) -- ^ Callback
-> IO (Either (BindException 'Unix) a)
withListener endpoint f = mask $ \restore -> do
listen endpoint >>= \case
Left err -> pure (Left err)
Right sck -> do
a <- onException (restore (f sck)) (unlisten_ sck)
unlisten sck
pure (Right a)
-- | Open a socket that can be used to listen for inbound connections.
-- Requirements:
--
-- * This function may only be called in contexts where exceptions
-- are masked.
-- * The caller /must/ be sure to call 'unlistener' on the resulting
-- 'Listener' exactly once to close underlying file descriptor.
-- * The 'Listener' cannot be used after being given as an argument
-- to 'unlistener'.
--
-- Noncompliant use of this function leads to undefined behavior. Prefer
-- 'withListener' unless you are writing an integration with a
-- resource-management library.
listen :: UnixAddress -> IO (Either (BindException 'Unix) Listener)
listen (UnixAddress remote) = do
e1 <- S.uninterruptibleSocket S.Unix
(L.applySocketFlags (L.closeOnExec <> L.nonblocking) S.sequencedPacket)
S.defaultProtocol
case e1 of
Left err -> handleSocketListenException err
Right fd -> do
let sockAddr = id
$ S.encodeSocketAddressUnix
$ S.SocketAddressUnix
$ remote
S.uninterruptibleBind fd sockAddr >>= \case
Left err -> do
S.uninterruptibleErrorlessClose fd
handleBindListenException err
Right _ -> S.uninterruptibleListen fd 16 >>= \case
-- We hardcode the listen backlog to 16. The author is unfamiliar
-- with use cases where gains are realized from tuning this parameter.
-- Open an issue if this causes problems for anyone.
Left err -> do
_ <- S.uninterruptibleClose fd
handleBindListenException err
Right _ -> do
-- The getsockname is copied from code in Socket.Datagram.IPv4.Undestined.
-- Consider factoring this out.
let !mngr = EM.manager
EM.register mngr fd
pure (Right (Listener fd))
-- | Open a socket and connect to a peer. Requirements:
--
-- * This function may only be called in contexts where exceptions
-- are masked.
-- * The caller /must/ be sure to call 'disconnect' or 'disconnect_'
-- on the resulting 'Connection' exactly once to close underlying
-- file descriptor.
-- * The 'Connection' cannot be used after being given as an argument
-- to 'disconnect' or 'disconnect_'.
--
-- Noncompliant use of this function leads to undefined behavior. Prefer
-- 'withConnection' unless you are writing an integration with a
-- resource-management library.
connect ::
UnixAddress
-- ^ Peer address
-> IO (Either (ConnectException 'Unix 'Uninterruptible) Connection)
connect (UnixAddress !remote) = do
e1 <- S.uninterruptibleSocket S.Unix
(L.applySocketFlags (L.closeOnExec <> L.nonblocking) S.sequencedPacket)
S.defaultProtocol
case e1 of
Left err -> handleConnectException err
Right fd -> do
let sockAddr = id
$ S.encodeSocketAddressUnix
$ S.SocketAddressUnix
$ remote
let !mngr = EM.manager
-- TODO: I believe it is sound to make both the write and
-- read channels start off as not ready. After all, the
-- socket is brand new and is not connected to a peer.
-- Consequently, there's no way we could miss events.
EM.register mngr fd
-- This is currently wrong. Redo this later.
S.uninterruptibleConnect fd sockAddr >>= \case
Left err2 -> do
S.uninterruptibleErrorlessClose fd
handleConnectException err2
Right _ -> pure (Right (Connection fd))
-- | Variant of 'connect' that does not block. Returns 'Nothing' if
-- the connection cannot be established immidiately.
tryConnect ::
UnixAddress
-- ^ Peer address
-> IO (Either (ConnectException 'Unix 'Uninterruptible) (Maybe Connection))
tryConnect (UnixAddress !remote) = do
e1 <- S.uninterruptibleSocket S.Unix
(L.applySocketFlags (L.closeOnExec <> L.nonblocking) S.sequencedPacket)
S.defaultProtocol
case e1 of
Left err -> handleConnectException err
Right fd -> do
let sockAddr = id
$ S.encodeSocketAddressUnix
$ S.SocketAddressUnix
$ remote
let !mngr = EM.manager
-- TODO: I believe it is sound to make both the write and
-- read channels start off as not ready. After all, the
-- socket is brand new and is not connected to a peer.
-- Consequently, there's no way we could miss events.
EM.register mngr fd
-- This is currently wrong. Redo this later.
S.uninterruptibleConnect fd sockAddr >>= \case
Left err2 -> do
S.uninterruptibleErrorlessClose fd
if | err2 == eAGAIN -> pure (Right Nothing)
| err2 == eWOULDBLOCK -> pure (Right Nothing)
| otherwise -> handleConnectException err2
Right _ -> pure (Right (Just (Connection fd)))
-- These are the exceptions that can happen as a result
-- of calling @socket@ with the intent of using the socket
-- to listen for inbound connections.
handleSocketListenException :: Errno -> IO (Either (BindException 'Unix) a)
handleSocketListenException e
| e == eMFILE = pure (Left BindFileDescriptorLimit)
| e == eNFILE = pure (Left BindFileDescriptorLimit)
| otherwise = die ("Socket.SequencedPacket.Unix.socket: " ++ describeErrorCode e)
-- These are the exceptions that can happen as a result
-- of calling @bind@ with the intent of using the socket
-- to listen for inbound connections. This is also used
-- to clean up the error codes of @listen@. The two can
-- report some of the same error codes, and those happen
-- to be the error codes we are interested in.
--
-- NB: EACCES only happens on @bind@, not on @listen@.
handleBindListenException :: Errno -> IO (Either (BindException 'Unix) a)
handleBindListenException !e
| e == eACCES = pure (Left BindPermissionDenied)
| e == eADDRINUSE = pure (Left BindAddressInUse)
| otherwise = die ("Socket.SequencedPacket.Unix.bindListen: " ++ describeErrorCode e)
handleConnectException :: Errno -> IO (Either (ConnectException 'Unix i) a)
handleConnectException e
| e == eNOENT = pure (Left ConnectNoEntry)
| e == ePROTOTYPE = pure (Left ConnectProtocolType)
| e == eCONNREFUSED = pure (Left ConnectRefused)
| otherwise = die
("Socket.Datagram.Unix.Connected.connect: " ++ describeErrorCode e)
-- | Close a listener. This does not check to see whether or not
-- the operating system successfully closed the socket. It never
-- throws exceptions of any kind. This should only be preferred
-- to 'unlistener' in exception-cleanup contexts where there is
-- already an exception that will be rethrown. See the implementation
-- of 'withListener' for an example of appropriate use of both
-- 'unlistener' and 'unlistener_'.
unlisten_ :: Listener -> IO ()
unlisten_ (Listener fd) = S.uninterruptibleErrorlessClose fd
-- | Close a listener. This throws an unrecoverable exception if
-- the socket cannot be closed.
unlisten :: Listener -> IO ()
unlisten (Listener fd) = S.uninterruptibleClose fd >>= \case
Left err -> die ("Socket.SequencedPacket.Unix.unlisten: " ++ describeErrorCode err)
Right _ -> pure ()
-- | Establish a connection to a server.
withConnection ::
UnixAddress
-- ^ Peer address
-> (Either CloseException () -> a -> IO b)
-- ^ Callback to handle an ungraceful close.
-> (Connection -> IO a)
-- ^ Callback to consume connection. Must not return the connection.
-> IO (Either (ConnectException 'Unix 'Uninterruptible) b)
withConnection !remote g f = mask $ \restore -> do
connect remote >>= \case
Left err -> pure (Left err)
Right conn -> do
a <- onException (restore (f conn)) (disconnect_ conn)
m <- disconnect conn
b <- g m a
pure (Right b)
-- | Close a connection gracefully, reporting a 'CloseException' when
-- the connection has to be terminated by sending a TCP reset. This
-- uses a combination of @shutdown@, @recv@, @close@ to detect when
-- resets need to be sent.
disconnect :: Connection -> IO (Either CloseException ())
disconnect (Connection fd) = gracefulCloseA fd
gracefulCloseA :: Fd -> IO (Either CloseException ())
gracefulCloseA fd = do
let !mngr = EM.manager
!tv <- EM.reader mngr fd
token0 <- STM.readTVarIO tv
S.uninterruptibleShutdown fd S.write >>= \case
-- On Linux (not sure about others), calling shutdown
-- on the write channel fails with with ENOTCONN if the
-- write channel is already closed. It is common for this to
-- happen (e.g. if the peer calls @close@ before the local
-- process runs gracefulClose, the local operating system
-- will have already closed the write channel). However,
-- it does not pose a problem. We just proceed as we would
-- have since either way we become certain that the write channel
-- is closed.
Left err -> if err == eNOTCONN
then gracefulCloseB tv token0 fd
else do
S.uninterruptibleErrorlessClose fd
die ("Socket.SequencedPacket.Unix.gracefulCloseA[shutdown]: " ++ describeErrorCode err)
Right _ -> gracefulCloseB tv token0 fd
-- The second part of the shutdown function must call itself recursively
-- since we may receive false read-ready notifications at any time.
gracefulCloseB :: TVar EM.Token -> EM.Token -> Fd -> IO (Either CloseException ())
gracefulCloseB !tv !token0 !fd = do
!buf <- PM.newByteArray 1
S.uninterruptibleReceiveMutableByteArray fd buf 0 1 mempty >>= \case
Left err1 -> if err1 == eWOULDBLOCK || err1 == eAGAIN
then do
token1 <- EM.persistentUnreadyAndWait token0 tv
gracefulCloseB tv token1 fd
else do
_ <- S.uninterruptibleClose fd
-- We treat all @recv@ errors except for the nonblocking
-- notices as unrecoverable.
die ("Socket.SequencedPacket.Unix.gracefulCloseB[recv]: " ++ describeErrorCode err1)
Right sz -> if sz == 0
then S.uninterruptibleClose fd >>= \case
Left err ->
die ("Socket.SequencedPacket.Unix.gracefulCloseB[close]: " ++ describeErrorCode err)
Right _ -> pure (Right ())
else do
_ <- S.uninterruptibleClose fd
pure (Left ClosePeerContinuedSending)
-- | Close a connection. This does not check to see whether or not
-- the connection was brought down gracefully. It just calls @close@.
-- It never throws exceptions of any kind (even if @close@ fails).
-- This should only be preferred to 'disconnect' in exception-cleanup
-- contexts where there is already an exception that will be rethrown.
-- See the implementation of 'withConnection' for an example of
-- appropriate use of both 'disconnect' and 'disconnect_'.
disconnect_ :: Connection -> IO ()
disconnect_ (Connection fd) = S.uninterruptibleErrorlessClose fd
-- | Accept a connection on the listener and run the supplied callback
-- on it. This closes the connection when the callback finishes or if
-- an exception is thrown. Since this function blocks the thread until
-- the callback finishes, it is only suitable for stream socket clients
-- that handle one connection at a time. The variant 'forkAcceptedUnmasked'
-- is preferrable for servers that need to handle connections concurrently
-- (most use cases).
withAccepted ::
Listener
-> (Either CloseException () -> a -> IO b)
-- ^ Callback to handle an ungraceful close.
-> (Connection -> IO a)
-- ^ Callback to consume connection. Must not return the connection.
-> IO (Either (AcceptException 'Uninterruptible) b)
withAccepted !lstn consumeException cb = do
r <- mask $ \restore -> do
accept lstn >>= \case
Left e -> pure (Left e)
Right conn -> do
a <- onException (restore (cb conn)) (disconnect_ conn)
e <- disconnect conn
pure (Right (e,a))
-- Notice that consumeException gets run in an unmasked context.
case r of
Left e -> pure (Left e)
Right (e,a) -> fmap Right (consumeException e a)
-- | Listen for an inbound connection.
accept :: Listener -> IO (Either (AcceptException 'Uninterruptible) Connection)
accept (Listener !fd) = do
-- Although this function must be called in a context where
-- exceptions are masked, recall that EM.wait uses an STM
-- action that might retry, meaning that this first part is
-- still interruptible. This is a good thing in the case of
-- this function.
let !mngr = EM.manager
-- The listener should already be registered, so we can just
-- ask for the reader directly.
!tv <- EM.reader mngr fd
let go !oldToken = do
waitlessAccept fd >>= \case
Left merr -> case merr of
Nothing -> EM.unreadyAndWait oldToken tv >>= go
Just err -> pure (Left err)
Right r@(Connection conn) -> do
EM.register mngr conn
pure (Right r)
go =<< STM.readTVarIO tv
waitlessAccept :: Fd -> IO (Either (Maybe (AcceptException i)) Connection)
waitlessAccept !lstn = do
-- TODO: add a variant of accept4 in posix-api that discards the peer address.
L.uninterruptibleAccept4 lstn 128 (L.closeOnExec <> L.nonblocking) >>= \case
Left err -> handleAcceptException err
Right (_,_,acpt) -> pure (Right (Connection acpt))
-- These are the exceptions that can happen as a result
-- of calling @accept@.
-- TODO: There is no way a UNIX-domain connection could be firewalled.
-- Is EPERM even possible in this context?
handleAcceptException :: Errno -> IO (Either (Maybe (AcceptException i)) a)
handleAcceptException e
| e == eAGAIN = pure (Left Nothing)
| e == eWOULDBLOCK = pure (Left Nothing)
| e == eCONNABORTED = pure (Left (Just AcceptConnectionAborted))
| e == eMFILE = pure (Left (Just AcceptFileDescriptorLimit))
| e == eNFILE = pure (Left (Just AcceptFileDescriptorLimit))
| e == ePERM = pure (Left (Just AcceptFirewalled))
| otherwise = die ("Socket.Stream.IPv4.accept: " ++ describeErrorCode e)