sockets-0.5.0.0: src/Socket/Stream/Uninterruptible/Bytes.hs
{-# language BangPatterns #-}
{-# language DataKinds #-}
{-# language LambdaCase #-}
{-# language MagicHash #-}
{-# language MultiWayIf #-}
{-# language TypeApplications #-}
-- | Communicate over a connection using immutable byte arrays.
-- Reception functions return 'ByteArray' instead of 'Bytes' since
-- this result always takes up the entirity of a 'ByteArray'. The
-- 'Bytes' would have redundant information since the offset would
-- be zero and the length would be the length of the 'ByteArray'
-- payload.
module Socket.Stream.Uninterruptible.Bytes
( send
, sendMany
, receiveExactly
, receiveOnce
, receiveBetween
) where
import Data.Bytes.Types (MutableBytes(..),Bytes(..))
import Data.Primitive (ByteArray)
import GHC.Exts (proxy#)
import Socket (Interruptibility(Uninterruptible))
import Socket.Stream (Connection,ReceiveException,SendException)
import qualified Data.Primitive as PM
import qualified Socket.Stream.Uninterruptible.Bytes.Send as Send
import qualified Socket.Stream.Uninterruptible.MutableBytes.Receive as Receive
-- Used for sendMany
import Control.Monad (when)
import Socket.Stream (SendException(..),Connection(..))
import Data.Primitive.Unlifted.Array (UnliftedArray)
import Socket.EventManager (Token)
import Control.Concurrent.STM (TVar)
import System.Posix.Types (Fd)
import Foreign.C.Types (CSize)
import Socket.Error (die)
import Foreign.C.Error (Errno(..), eAGAIN, eWOULDBLOCK, ePIPE, eCONNRESET)
import Socket.Debug (whenDebugging)
import qualified Foreign.C.Error.Describe as D
import qualified Socket.EventManager as EM
import qualified Data.Primitive.Unlifted.Array as PM
import qualified Posix.Socket as S
-- | Send a slice of a buffer. If needed, this calls POSIX @send@ repeatedly
-- until the entire contents of the buffer slice have been sent.
send ::
Connection -- ^ Connection
-> Bytes -- ^ Slice of a buffer
-> IO (Either (SendException 'Uninterruptible) ())
{-# inline send #-}
send = Send.send proxy#
-- | Receive a number of bytes exactly equal to the length of the
-- buffer slice. If needed, this may call @recv@ repeatedly until
-- the requested number of bytes have been received.
receiveExactly ::
Connection -- ^ Connection
-> Int -- ^ Exact number of bytes to receive
-> IO (Either (ReceiveException 'Uninterruptible) ByteArray)
{-# inline receiveExactly #-}
receiveExactly !conn !n = do
!marr <- PM.newByteArray n
Receive.receiveExactly proxy# conn (MutableBytes marr 0 n) >>= \case
Left err -> pure (Left err)
Right _ -> do
!arr <- PM.unsafeFreezeByteArray marr
pure $! Right $! arr
-- | Receive at most the specified number of bytes. This
-- only makes multiple calls to POSIX @recv@ if EAGAIN is returned. It makes at
-- most one @recv@ call that successfully fills the buffer.
receiveOnce ::
Connection -- ^ Connection
-> Int -- ^ Maximum number of bytes to receive
-> IO (Either (ReceiveException 'Uninterruptible) ByteArray)
{-# inline receiveOnce #-}
receiveOnce !conn !n = do
!marr0 <- PM.newByteArray n
Receive.receiveOnce proxy# conn (MutableBytes marr0 0 n) >>= \case
Left err -> pure (Left err)
Right sz -> do
marr1 <- PM.resizeMutableByteArray marr0 sz
!arr <- PM.unsafeFreezeByteArray marr1
pure $! Right $! arr
-- | Receive a number of bytes that is between the inclusive lower and
-- upper bounds. If needed, this may call @recv@ repeatedly until the
-- minimum requested number of bytes have been received.
receiveBetween ::
Connection -- ^ Connection
-> Int -- ^ Minimum number of bytes to receive
-> Int -- ^ Maximum number of bytes to receive
-> IO (Either (ReceiveException 'Uninterruptible) ByteArray)
{-# inline receiveBetween #-}
receiveBetween !conn !minLen !maxLen = do
!marr0 <- PM.newByteArray maxLen
Receive.receiveBetween proxy# conn (MutableBytes marr0 0 maxLen) minLen >>= \case
Left err -> pure (Left err)
Right sz -> do
marr1 <- PM.resizeMutableByteArray marr0 sz
!arr <- PM.unsafeFreezeByteArray marr1
pure $! Right $! arr
-- | Send many buffers with vectored I/O. This uses @sendmsg@ to send
-- multiple chunks at a time. It may call @sendmsg@ more than once
-- if there is not enough space in the operating system send buffer
-- to house all the chunks at the same time.
sendMany ::
Connection -- ^ Connection
-> UnliftedArray ByteArray -- ^ Byte arrays
-> IO (Either (SendException 'Uninterruptible) ())
{-# inline sendMany #-}
sendMany (Connection conn) bufs = do
-- TODO: Generalize sendMany so that an interruptible variant
-- can be provided.
let !mngr = EM.manager
tv <- EM.writer mngr conn
-- Do not forget to use the correct wait function when this
-- is generalized.
token0 <- EM.wait tv
sendManyLoop conn tv token0 bufs 0 (PM.sizeofUnliftedArray bufs) 0
sendManyLoop ::
Fd -> TVar Token -> Token
-> UnliftedArray ByteArray
-> Int -> Int -> Int
-> IO (Either (SendException 'Uninterruptible) ())
sendManyLoop !conn !tv !old !bufs !chunkIx !chunkCount !chunkOff = if chunkCount > 0
then S.uninterruptibleSendByteArrays conn bufs chunkIx chunkCount chunkOff S.noSignal >>= \case
Left e ->
if | e == eAGAIN || e == eWOULDBLOCK -> do
EM.unready old tv
-- Do not forget to use the correct wait function when this
-- is generalized.
new <- EM.wait tv
sendManyLoop conn tv new bufs chunkIx chunkCount chunkOff
| e == ePIPE -> pure (Left SendShutdown)
| e == eCONNRESET -> pure (Left SendReset)
| otherwise -> die ("Socket.Stream.sendMany: " ++ describeErrorCode e)
Right sz' -> do
let sz0 = fromIntegral @CSize @Int sz'
-- We enumerate the sent chunks, one by one,
-- to compute the new chunk index.
-- ix: chunk index
-- m: offset into the current chunk
-- n: remaining bytes to account for
let go !ix !m !n = if n > 0
then
let !fullSz = PM.sizeofByteArray (PM.indexUnliftedArray bufs ix)
!sz = fullSz - m
in if sz <= n
then go (ix + 1) 0 (n - sz)
else (ix, m + n)
else (ix,0)
(nextChunkIx,nextChunkOff) = go chunkIx chunkOff sz0
sendManyLoop conn tv old bufs nextChunkIx (chunkCount - (nextChunkIx - chunkIx)) nextChunkOff
-- chunkCount must be 0 in this case since it is never negative
else do
whenDebugging $ when (chunkCount < 0) $ die "sendManyLoop: negative chunks"
pure (Right ())
describeErrorCode :: Errno -> String
describeErrorCode err@(Errno e) = "error code " ++ D.string err ++ " (" ++ show e ++ ")"