sockets 0.5.0.0 → 0.6.1.0
raw patch · 40 files changed
+2600/−303 lines, 40 filesdep ~byteslicedep ~bytestringdep ~posix-api
Dependency ranges changed: byteslice, bytestring, posix-api, primitive
Files
- CHANGELOG.md +12/−0
- sockets.cabal +40/−21
- src-datagram-send/Datagram/Send/Indefinite.hs +23/−2
- src-internal/Socket.hs +65/−0
- src-internal/Socket/Connected/UnmanagedBytes.hs +1/−1
- src-internal/Socket/Datagram.hs +11/−1
- src-internal/Socket/Destined/IPv4/UnmanagedBytes.hs +1/−5
- src-internal/Socket/Discard.hs +90/−30
- src-internal/Socket/EventManager.hs +26/−26
- src-internal/Socket/IPv4.hs +65/−30
- src-internal/Socket/Interruptible.hs +29/−2
- src-internal/Socket/SequencedPacket.hs +63/−0
- src-internal/Socket/Slab.hs +51/−0
- src-internal/Socket/Stream.hs +4/−1
- src-internal/Socket/Uninterruptible.hs +11/−0
- src-interrupt/Socket/Interrupt.hsig +7/−0
- src-no-mmsg/Socket/Datagram/Interruptible/MutableBytes/Many.hs +2/−2
- src-no-mmsg/Socket/Datagram/Uninterruptible/MutableBytes/Many.hs +2/−3
- src-stream-receive/SequencedPacket/Receive/Indefinite.hs +85/−0
- src-stream-send/SequencedPacket/Send/Indefinite.hs +82/−0
- src/Socket/Datagram/Common.hs +25/−0
- src/Socket/Datagram/IPv4/Connected.hs +30/−14
- src/Socket/Datagram/IPv4/Unconnected.hs +93/−51
- src/Socket/Datagram/Interruptible/ByteString.hs +6/−2
- src/Socket/Datagram/Interruptible/Bytes.hs +44/−8
- src/Socket/Datagram/Uninterruptible/ByteString.hs +29/−0
- src/Socket/Datagram/Uninterruptible/Bytes.hs +82/−7
- src/Socket/Datagram/Uninterruptible/MutableBytes.hs +0/−1
- src/Socket/Datagram/Unix/Connected.hs +162/−0
- src/Socket/Datagram/Unix/Unconnected.hs +107/−0
- src/Socket/Pair.hs +42/−0
- src/Socket/SequencedPacket/Uninterruptible/Bytes.hs +81/−0
- src/Socket/SequencedPacket/Unix.hs +417/−0
- src/Socket/Stream/IPv4.hs +272/−83
- src/Socket/Stream/Interruptible/Bytes.hs +24/−0
- src/Socket/Stream/Uninterruptible/Bytes.hs +3/−1
- src/Socket/Stream/Uninterruptible/MutableBytes.hs +23/−1
- src/Socket/Stream/Unix.hs +361/−0
- src/Socket/Systemd.hs +54/−0
- test/Main.hs +75/−11
CHANGELOG.md view
@@ -1,5 +1,17 @@ # Revision history for stream-sockets +## 0.6.1.0++* Correct implementation of withListenerReuse+* Add interruptibleForkAcceptedUnmasked for UDS. Add `SO_REUSEADDR` inet functions.++## 0.6.0.0++* Give slab types a phantom type argument that indicates whether+ or not they operate on pinned byte arrays. This makes it possible+ to provide a `bytestring` interface to `recvmmsg`.+* Upgrade interruptible datagram interface to work with new slabs.+ ## 0.5.0.0 * Add support for getting back `ECONNREFUSED` when send or receiving
sockets.cabal view
@@ -1,6 +1,6 @@-cabal-version: 2.2+cabal-version: 3.0 name: sockets-version: 0.5.0.0+version: 0.6.1.0 synopsis: High-level network sockets description: This library provides a high-level abstraction for network sockets. It uses@@ -53,7 +53,7 @@ flag mmsg manual: True- description: Use sendmmsg and recvmmsg + description: Use sendmmsg and recvmmsg default: False flag verbose-errors@@ -63,12 +63,14 @@ flag debug manual: True- description: Print debug output + description: Print debug output default: False flag checked manual: True- description: Add bounds-checking to primitive array operations+ description:+ Add bounds-checking to primitive array operations and to+ certain operations that expect pinned byte arrays. default: False flag example@@ -79,15 +81,15 @@ library sockets-internal build-depends: , base >= 4.11.1.0 && <5- , bytestring >=0.10 && <0.11+ , bytestring >=0.10 && <0.12 , ip >= 1.4.1- , posix-api >=0.3 && <0.4+ , posix-api >=0.4 && <0.5 , primitive-offset >= 0.2 && <0.3 , primitive-unlifted >= 0.1 && <0.2 , primitive-addr >= 0.1 && <0.2 , stm >= 2.4 , text >= 1.2- , byteslice >=0.1.1 && <0.2+ , byteslice >=0.1.1 && <0.3 if flag(checked) build-depends: primitive-checked >= 0.7 && <0.8 else@@ -101,7 +103,9 @@ Socket.Error Socket.EventManager Socket.IPv4+ Socket.SequencedPacket Socket.Stream+ Socket.Slab Socket.Bytes Socket.Destined.IPv4.UnmanagedBytes Socket.Destined.IPv4.MutableBytes@@ -152,7 +156,7 @@ build-depends: , base >= 4.11.1.0 && <5 , error-codes >= 0.1.0.1 && < 0.2- , posix-api >=0.3 && <0.4+ , posix-api >=0.4 && <0.5 , stm >= 2.4 , sockets-internal , sockets-buffer@@ -170,7 +174,7 @@ , base >= 4.11.1.0 && <5 , error-codes >= 0.1.0.1 && < 0.2 , stm >= 2.4- , posix-api >=0.3 && <0.4+ , posix-api >=0.4 && <0.5 , primitive-offset >= 0.2 && <0.3 , sockets-internal , sockets-buffer@@ -191,7 +195,7 @@ , base >= 4.11.1.0 && <5 , error-codes >= 0.1.0.1 && < 0.2 , stm >= 2.4- , posix-api >=0.3 && <0.4+ , posix-api >=0.4 && <0.5 , primitive-unlifted >= 0.1 && <0.2 , byteslice >= 0.1.1 , sockets-datagram-receive@@ -215,13 +219,14 @@ build-depends: , base >= 4.11.1.0 && <5 , error-codes >= 0.1.0.1 && < 0.2- , posix-api >=0.3 && <0.4+ , posix-api >=0.4 && <0.5 , stm >= 2.4 , sockets-internal , sockets-buffer , sockets-interrupt exposed-modules: Stream.Send.Indefinite+ SequencedPacket.Send.Indefinite signatures: Stream.Send hs-source-dirs: src-stream-send@@ -232,13 +237,14 @@ build-depends: , base >= 4.11.1.0 && <5 , error-codes >= 0.1.0.1 && < 0.2- , posix-api >=0.3 && <0.4+ , posix-api >=0.4 && <0.5 , stm >= 2.4 , sockets-internal , sockets-buffer , sockets-interrupt exposed-modules: Stream.Receive.Indefinite+ SequencedPacket.Receive.Indefinite signatures: Stream.Receive hs-source-dirs: src-stream-receive@@ -249,7 +255,7 @@ build-depends: , base >= 4.11.1.0 && <5 , error-codes >= 0.1.0.1 && < 0.2- , posix-api >=0.3 && <0.4+ , posix-api >=0.4 && <0.5 , stm >= 2.4 , sockets-internal , sockets-interrupt@@ -285,17 +291,24 @@ reexported-modules: , Stream.Send.Indefinite , Stream.Receive.Indefinite+ , SequencedPacket.Receive.Indefinite mixins: sockets-stream-send (Stream.Send.Indefinite)- , sockets-stream-receive (Stream.Receive.Indefinite)+ , sockets-stream-receive (Stream.Receive.Indefinite,SequencedPacket.Receive.Indefinite) default-language: Haskell2010+ ghc-options: -O2 -Wall library exposed-modules: Socket.Address Socket.Datagram.IPv4.Unconnected Socket.Datagram.IPv4.Connected+ Socket.Datagram.Unix.Unconnected+ Socket.Datagram.Unix.Connected+ Socket.SequencedPacket.Unix+ Socket.SequencedPacket.Uninterruptible.Bytes Socket.Stream.IPv4+ Socket.Stream.Unix Socket.Datagram.Interruptible.Addr Socket.Datagram.Interruptible.Bytes Socket.Datagram.Interruptible.ByteString@@ -316,8 +329,11 @@ Socket.Stream.Uninterruptible.MutableBytes Socket.Stream.Uninterruptible.Hybrid other-modules:+ Socket.Pair+ Socket.Datagram.Common Socket.Datagram.Interruptible.MutableBytes.Many Socket.Datagram.Uninterruptible.MutableBytes.Many+ Socket.Systemd mixins: sockets-datagram-receive-many (Datagram.Receive.Many.Indefinite as Socket.Datagram.Uninterruptible.MutableBytes.Receive.Many.Unit)@@ -454,7 +470,9 @@ Socket.Interrupt as Socket.Interruptible, Socket.Buffer as Socket.Bytes), sockets-stream-send- (Stream.Send.Indefinite as Socket.Stream.Uninterruptible.Bytes.Send)+ (Stream.Send.Indefinite as Socket.Stream.Uninterruptible.Bytes.Send,+ SequencedPacket.Send.Indefinite as Socket.SequencedPacket.Uninterruptible.Bytes.Send+ ) requires (Stream.Send as Socket.Bytes, Socket.Interrupt as Socket.Uninterruptible,@@ -469,7 +487,8 @@ Socket.Interrupt as Socket.Interruptible), sockets-stream-bidirectional (Stream.Send.Indefinite as Socket.Stream.Uninterruptible.MutableBytes.Send,- Stream.Receive.Indefinite as Socket.Stream.Uninterruptible.MutableBytes.Receive)+ Stream.Receive.Indefinite as Socket.Stream.Uninterruptible.MutableBytes.Receive,+ SequencedPacket.Receive.Indefinite as Socket.SequencedPacket.Uninterruptible.MutableBytes.Receive) requires (Stream.Send as Socket.MutableBytes, Stream.Receive as Socket.MutableBytes,@@ -509,11 +528,11 @@ Socket.Interrupt as Socket.Uninterruptible), build-depends: , base >= 4.11.1.0 && <5- , byteslice >=0.1.1 && <0.2- , bytestring >=0.10 && <0.11+ , byteslice >=0.1.1 && <0.3+ , bytestring >=0.10 && <0.12 , error-codes >=0.1.0.1 && <0.2 , ip >=1.4.1- , posix-api >=0.3 && <0.4+ , posix-api >=0.4 && <0.5 , primitive-addr >= 0.1 && <0.2 , primitive-offset >= 0.2 && <0.3 , primitive-unlifted >= 0.1 && <0.2@@ -599,7 +618,7 @@ if flag(example) build-depends: , base >= 4.11.1.0 && < 5- , byteslice >= 0.1.1 && < 0.2+ , byteslice >= 0.1.1 && < 0.3 , bytestring >= 0.10.8.2 , fast-logger >= 2.4.13 , ip >= 1.4.1
src-datagram-send/Datagram/Send/Indefinite.hs view
@@ -4,12 +4,13 @@ module Datagram.Send.Indefinite ( send+ , attemptSend ) where import Control.Concurrent.STM (TVar) import Datagram.Send (Peer)-import Foreign.C.Error (Errno(..), eAGAIN, eWOULDBLOCK, eACCES)-import Foreign.C.Error (eCONNREFUSED)+import Foreign.C.Error (Errno(..), eAGAIN, eWOULDBLOCK, eACCES, eMSGSIZE)+import Foreign.C.Error (eCONNREFUSED,eNOTCONN) import Foreign.C.Types (CSize) import Socket.Error (die) import Socket.EventManager (Token)@@ -41,6 +42,24 @@ Left err -> pure (Left err) Right _ -> sendLoop intr dst sock tv token0 buf +-- Never blocks.+attemptSend ::+ Peer+ -> Fd+ -> Buffer+ -> IO (Either (SendException Intr) Bool)+attemptSend !dst !sock !buf = Send.send dst sock buf >>= \case+ Left e ->+ if | e == eAGAIN || e == eWOULDBLOCK -> pure (Right False)+ | e == eACCES -> pure (Left SendBroadcasted)+ | e == eCONNREFUSED -> pure (Left SendConnectionRefused)+ | e == eNOTCONN -> pure (Left SendConnectionRefused)+ | e == eMSGSIZE -> pure (Left SendMessageSize)+ | otherwise -> die ("Socket.Datagram.send: " ++ describeErrorCode e)+ Right sz -> if csizeToInt sz == Buffer.length buf+ then pure $! Right True+ else pure $! Left $! SendTruncated $! csizeToInt sz+ sendLoop :: Interrupt -> Peer -> Fd -> TVar Token -> Token -> Buffer -> IO (Either (SendException Intr) ())@@ -55,6 +74,8 @@ Right _ -> sendLoop intr dst sock tv new buf | e == eACCES -> pure (Left SendBroadcasted) | e == eCONNREFUSED -> pure (Left SendConnectionRefused)+ | e == eNOTCONN -> pure (Left SendConnectionRefused)+ | e == eMSGSIZE -> pure (Left SendMessageSize) | otherwise -> die ("Socket.Datagram.send: " ++ describeErrorCode e) Right sz -> if csizeToInt sz == Buffer.length buf then pure $! Right ()
src-internal/Socket.hs view
@@ -5,11 +5,15 @@ {-# language DuplicateRecordFields #-} {-# language GADTs #-} {-# language KindSignatures #-}+{-# language StandaloneDeriving #-} module Socket ( SocketUnrecoverableException(..)+ , SocketException(..)+ , BindException(..) , Direction(..) , Connectedness(..)+ , Pinnedness(..) , Family(..) , Version(..) , Interruptibility(..)@@ -32,6 +36,7 @@ import Control.Exception (Exception(..)) import Data.Kind (Type)+import Data.Typeable (Typeable) import Foreign.C.Types (CInt) import qualified Data.List as L@@ -40,6 +45,18 @@ data Connectedness = Connected | Unconnected +-- | This is used as a phantom type to distinguish+-- between pinned and unpinned memory. The distinction+-- drawn here concerns the manner in which the user+-- requested the byte array. Byte arrays resulting+-- from newPinnedByteArray# are considered 'Pinned'+-- while those resulting from newByteArray# are+-- considered 'Unpinned'. Even if the runtime decides+-- to pin a byte array requested by newByteArray#+-- (e.g. because it is larger than 3KB), it is+-- considered 'Unpinned' by this classification.+data Pinnedness = Pinned | Unpinned+ data Family = Internet Version | Unix data Version = V4 | V6@@ -47,6 +64,54 @@ data Interruptibility = Interruptible | Uninterruptible data Forkedness = Forked | Unforked++data SocketException :: Type where+ -- | A limit on the number of open file descriptors has been reached.+ -- This could be the per-process limit or the system limit.+ -- (@EMFILE@ and @ENFILE@)+ SocketFileDescriptorLimit :: SocketException++deriving stock instance Show SocketException+deriving stock instance Eq SocketException+deriving anyclass instance Exception SocketException++-- | Recoverable exceptions that happen when establishing stream listeners+-- or opening datagram sockets.+--+-- ==== __Discussion__+--+-- The recoverable exceptions that we encounter with stream sockets (established+-- with @socket@-@bind@-@listen@) and datagram sockets (established with+-- @socket@-@bind@) are the exact same exceptions. Consequently, we reuse+-- the same type in both case.+data BindException :: Family -> Type where+ -- | The address is protected, and the user is not the superuser. This most+ -- commonly happens when trying to bind to a port below 1024. On Linux,+ -- When it is necessary to bind to such a port on Linux, consider using the+ -- <http://man7.org/linux/man-pages/man7/capabilities.7.html CAP_NET_BIND_SERVICE>+ -- capability instead of running the process as root. (@EACCES@)+ BindPermissionDenied :: BindException f+ -- | The given address is already in use. This can also happen if the+ -- address was recently bound to and closed. As mandated by+ -- <https://tools.ietf.org/html/rfc793 RFC 793>, such a socket remains+ -- in the @TIME-WAIT@ state for a while (commonly 30 to 120 seconds),+ -- and no new socket may be bound to the address during this period.+ -- (@EADDRINUSE@ with specified port)+ BindAddressInUse :: BindException f+ -- | The port number was specified as zero, but upon attempting to+ -- bind to an ephemeral port, it was determined that all port numbers+ -- numbers in the ephemeral port range are currently in use.+ -- (@EADDRINUSE@ with unspecified port)+ BindEphemeralPortsExhausted :: BindException ('Internet v)+ -- | A limit on the number of open file descriptors has been reached.+ -- This could be the per-process limit or the system limit.+ -- (@EMFILE@ and @ENFILE@)+ BindFileDescriptorLimit :: BindException f++deriving stock instance Show (BindException f)+deriving stock instance Eq (BindException f)+deriving anyclass instance Typeable f => Exception (BindException f)+ data SocketUnrecoverableException = SocketUnrecoverableException { modules :: String
src-internal/Socket/Connected/UnmanagedBytes.hs view
@@ -15,7 +15,7 @@ import Data.Bytes.Types (UnmanagedBytes(UnmanagedBytes)) import Socket.AddrLength (Buffer,length,advance) import Foreign.C.Error (Errno)-import Foreign.C.Types (CInt,CSize)+import Foreign.C.Types (CSize) import System.Posix.Types (Fd) import qualified Posix.Socket as S
src-internal/Socket/Datagram.hs view
@@ -24,6 +24,8 @@ -- number of bytes that were successfully copied into the -- send buffer. The datagram does still get sent when this -- happens.+ -- TODO: Can this actually happen? SendMessageSize might be+ -- what actually happens in this case. SendTruncated :: !Int -> SendException i -- | Attempted to send to a broadcast address. SendBroadcasted :: SendException i@@ -33,8 +35,16 @@ -- peer is not listening on the requested port or that the -- datagram could not be routed to the peer. (This is a -- confusing way to notify the user of a send failure, but- -- it is how Linux does it.)+ -- it is how Linux does it.) This does not happen reliably.+ -- Certain network environment block the ICMP traffic needed+ -- for the sender to learn that its messages have not been+ -- delivered. This happens with UNIX-domain sockets nothing+ -- is bound to the peer address. In that context, this exception+ -- happens reliably. (@ECONNREFUSED@ or @ENOTCONN@) SendConnectionRefused :: SendException i+ -- | The size of the message made it impossible for the message+ -- to be delivered atomically. (@EMSGSIZE@)+ SendMessageSize :: SendException i -- | STM-style interrupt (much safer than C-style interrupt) SendInterrupted :: SendException 'Interruptible
src-internal/Socket/Destined/IPv4/UnmanagedBytes.hs view
@@ -14,8 +14,7 @@ import Data.Bytes.Types (UnmanagedBytes(UnmanagedBytes)) import Foreign.C.Error (Errno)-import Foreign.C.Types (CInt,CSize)-import GHC.Exts (RealWorld)+import Foreign.C.Types (CSize) import Net.Types (IPv4(..)) import Socket.IPv4 (Peer(..)) import Socket.AddrLength (advance,length)@@ -37,9 +36,6 @@ { S.port = S.hostToNetworkShort port , S.address = S.hostToNetworkLong (getIPv4 address) }--intToCInt :: Int -> CInt-intToCInt = fromIntegral intToCSize :: Int -> CSize intToCSize = fromIntegral
src-internal/Socket/Discard.hs view
@@ -14,17 +14,23 @@ ( PeerlessSlab(..) , newPeerlessSlab , freezePeerlessSlab+ , freezePeerlessSlabAsByteString+ , replenishPeerlessSlab+ , replenishPinnedPeerlessSlab ) where import Control.Monad.Primitive (primitive,primitive_)-import Data.Primitive (ByteArray(..))-import Data.Primitive.Unlifted.Array (MutableUnliftedArray,UnliftedArray)+import Data.ByteString (ByteString)+import Data.Kind (Type)+import Data.Primitive (ByteArray(..),SmallMutableArray,SmallArray) import Data.Primitive (MutablePrimArray,MutableByteArray)-import Data.Primitive (SmallArray,SmallMutableArray)-import Data.Word (Word16)+import Data.Primitive.Unlifted.Array (MutableUnliftedArray,UnliftedArray) import Foreign.C.Types (CInt) import GHC.Exts (RealWorld,Int(I#))+import Socket (Pinnedness(Pinned,Unpinned)) import Socket.Error (die)+import Socket.Interop (fromPinned)+import Socket.Slab (replenishPinned,replenishUnpinned) import qualified GHC.Exts as Exts import qualified Data.Primitive as PM@@ -33,58 +39,112 @@ -- | A slab of memory for bulk datagram ingest (via @recvmmsg@). -- Slabs are not safe for concurrent access. This variant does -- not have space for the peer addresses.-data PeerlessSlab = PeerlessSlab- { sizes :: !(MutablePrimArray RealWorld CInt)- -- ^ Buffer for returned datagram lengths- , payloads :: !(MutableUnliftedArray RealWorld (MutableByteArray RealWorld))- -- ^ Buffers for datagram payloads, no slicing- }+data PeerlessSlab :: Pinnedness -> Type where+ PeerlessSlab ::+ -- Invariant: payload sizes are nondecreasing. An+ -- implication is that zero-size payloads come first.+ -- Invariant: All non-zero-size payloads match the+ -- phantom pinnedness. All zero-size payloads are+ -- unpinned.+ { sizes :: !(MutablePrimArray RealWorld CInt)+ -- ^ Buffer for returned datagram lengths+ , payloads :: !(MutableUnliftedArray RealWorld (MutableByteArray RealWorld))+ -- ^ Buffers for datagram payloads, no slicing+ } -> PeerlessSlab p -- | Allocate a slab that is used to receive multiple datagrams at -- the same time without receiving any information about the peer. newPeerlessSlab :: Int -- ^ maximum datagrams- -> Int -- ^ maximum size of individual datagram - -> IO PeerlessSlab-newPeerlessSlab !n !m = if n >= 1 && m >= 1+ -> IO (PeerlessSlab p)+newPeerlessSlab !n = if n >= 1 then do sizes <- PM.newPrimArray n- payloads <- PM.unsafeNewUnliftedArray n- let go !ix = if ix > (-1)- then do- writeMutableByteArrayArray payloads ix =<< PM.newByteArray m- go (ix - 1)- else pure ()- go (n - 1)+ tombstone <- PM.newByteArray 0+ payloads <- PM.newUnliftedArray n tombstone pure PeerlessSlab{sizes,payloads}- else die "newSlab"+ else die "newPeerlessSlab" -- | Freeze the specified number of messages in-place and return--- them Replaces all of the frozen messages with tombstones so that+-- them. Replaces all of the frozen messages with tombstones so that -- new buffers can be allocated before the next reception. End users -- should not need this function.-freezePeerlessSlab :: PeerlessSlab -> Int -> IO (UnliftedArray ByteArray)+freezePeerlessSlab ::+ PeerlessSlab p+ -> Int+ -> IO (UnliftedArray ByteArray) freezePeerlessSlab slab n = do msgs <- PM.unsafeNewUnliftedArray n- freezeSlabGo slab msgs (n - 1)+ tombstone <- PM.newByteArray 0+ freezeSlabGo slab tombstone msgs (n - 1) +errorThunk :: a+{-# noinline errorThunk #-}+errorThunk = error "Socket.Discard: uninitialized element"++freezePeerlessSlabAsByteString ::+ PeerlessSlab 'Pinned+ -> Int+ -> IO (SmallArray ByteString)+freezePeerlessSlabAsByteString slab n = do+ msgs <- PM.newSmallArray n errorThunk+ tombstone <- PM.newByteArray 0+ freezeSlabByteStringGo slab tombstone msgs (n - 1)++freezeSlabByteStringGo ::+ PeerlessSlab 'Pinned+ -> MutableByteArray RealWorld -- tombstone+ -> SmallMutableArray RealWorld ByteString+ -> Int+ -> IO (SmallArray ByteString)+freezeSlabByteStringGo slab@PeerlessSlab{payloads,sizes} !tombstone !arr !ix = if ix > (-1)+ then do+ !size <- PM.readPrimArray sizes ix+ -- Remove the byte array from the array of payloads, freeze it, and+ -- replace it with a tombstone.+ payloadMut <- readMutableByteArrayArray payloads ix+ writeMutableByteArrayArray payloads ix tombstone+ let sz = cintToInt size+ !payload <- PM.resizeMutableByteArray payloadMut sz+ PM.writeSmallArray arr ix $! fromPinned payload 0 sz+ freezeSlabByteStringGo slab tombstone arr (ix - 1)+ else PM.unsafeFreezeSmallArray arr+ freezeSlabGo ::- PeerlessSlab+ PeerlessSlab p+ -> MutableByteArray RealWorld -- tombstone -> MutableUnliftedArray RealWorld ByteArray -> Int -> IO (UnliftedArray ByteArray)-freezeSlabGo slab@PeerlessSlab{payloads,sizes} !arr !ix = if ix > (-1)+freezeSlabGo slab@PeerlessSlab{payloads,sizes} !tombstone !arr !ix = if ix > (-1) then do !size <- PM.readPrimArray sizes ix -- Remove the byte array from the array of payloads, freeze it, and- -- replace it with a freshly allocated byte array. + -- replace it with a tombstone. payloadMut <- readMutableByteArrayArray payloads ix- originalSize <- PM.getSizeofMutableByteArray payloadMut- writeMutableByteArrayArray payloads ix =<< PM.newByteArray originalSize+ writeMutableByteArrayArray payloads ix tombstone !payload <- PM.unsafeFreezeByteArray =<< PM.resizeMutableByteArray payloadMut (cintToInt size) writeByteArrayArray arr ix payload- freezeSlabGo slab arr (ix - 1)+ freezeSlabGo slab tombstone arr (ix - 1) else PM.unsafeFreezeUnliftedArray arr++-- | Ensure that every buffer can accomodate at least the+-- minimum number of bytes.+replenishPeerlessSlab ::+ PeerlessSlab 'Unpinned+ -> Int -- ^ Minimum Size+ -> IO () +replenishPeerlessSlab PeerlessSlab{payloads} minSz = do+ let sz = PM.sizeofMutableUnliftedArray payloads+ replenishUnpinned payloads minSz 0 sz++replenishPinnedPeerlessSlab ::+ PeerlessSlab 'Pinned+ -> Int -- ^ Minimum Size+ -> IO () +replenishPinnedPeerlessSlab PeerlessSlab{payloads} minSz = do+ let sz = PM.sizeofMutableUnliftedArray payloads+ replenishPinned payloads minSz 0 sz cintToInt :: CInt -> Int cintToInt = fromIntegral
src-internal/Socket/EventManager.hs view
@@ -31,6 +31,7 @@ import Control.Applicative (liftA2,(<|>)) import Control.Concurrent (getNumCapabilities,forkOn,rtsSupportsBoundThreads) import Control.Concurrent.STM (TVar)+import Control.Exception (mask_) import Control.Monad (when) import Control.Monad.STM (atomically) import Data.Bits (countLeadingZeros,finiteBitSize,unsafeShiftL,(.|.),(.&.))@@ -158,8 +159,10 @@ data Manager = Manager { variables :: !(MUArray (MUArray (TVar Token))) , novars :: !(MUArray (TVar Token))- -- An empty mutable array. This array is used to mark the absense of- -- a tier-two array of TVars.+ -- This array is used to mark the absense of a tier-two array+ -- of TVars. It always has a single TVar in it. This TVar is+ -- used for synchonization when growing the variables list.+ -- Zero means unlocked. Anything else means locked. , epoll :: !Fd } @@ -168,7 +171,8 @@ manager = unsafePerformIO $ do when (not rtsSupportsBoundThreads) $ do fail $ "Socket.Event.manager: threaded runtime required"- !novars <- PM.unsafeNewUnliftedArray 0+ !novars <- PM.unsafeNewUnliftedArray 1+ writeTVarArray novars 0 =<< STM.newTVarIO (Token 0) !variables <- PM.unsafeNewUnliftedArray 32 let goX !ix = if ix >= 0 then do@@ -254,11 +258,26 @@ goVars (ix - 1) else pure () goVars (len - 1)+ syncVar <- readTVarArray novars 0+ -- I do not feel great about the consistency guarantees concerning+ -- how TVars interacte with non-TVar memory.+ mask_ $ do+ -- Acquire the lock. This plays nicely with the way GHC allows+ -- retrying STM transactions to be interrupted in a mask.+ STM.atomically $ do+ Token n <- STM.readTVar syncVar+ case n of+ 0 -> STM.writeTVar syncVar (Token 1)+ _ -> STM.retry+ varsTier2' <- readMutableUnliftedArrayArray variables ixTier1+ when (PM.sameMutableUnliftedArray varsTier2' novars) $ do+ writeMutableUnliftedArrayArray variables ixTier1 varsAttempt+ STM.atomically (STM.writeTVar syncVar (Token 0)) -- We ignore the success of casUnliftedArray. It does not actually -- matter whether or not it succeeded. If it failed, some other -- thread must have initialized the tier 2 arrays.- (success,tier2) <- casMutableUnliftedArrayArray variables ixTier1 novars varsAttempt- debug ("constructivelyLookupTier1: Created tier 2 array of length " ++ show len ++ " at index " ++ show ixTier1 ++ " with success " ++ show success)+ tier2 <- readMutableUnliftedArrayArray variables ixTier1+ debug ("constructivelyLookupTier1: Created tier 2 array of length " ++ show len ++ " at index " ++ show ixTier1) pure (ixTier2,tier2) else pure (ixTier2,varsTier2) @@ -485,7 +504,7 @@ -- something less that the full number of bytes requested, we want -- to unready the token. The next time we want to @recv@, we want to wait -- for the token to be ready before even attempting a @recv@.--- 2. We want to be properly notification when the peer shuts down. Epoll+-- 2. We want to be properly notified when the peer shuts down. Epoll -- reports this as EPOLLRDHUP. -- -- The difficulty is that, without persistent readiness, performing@@ -636,7 +655,7 @@ interruptibleWaitCounting :: TVar Int -> TVar Bool -> TVar Token -> IO Token interruptibleWaitCounting !counter !interrupt !tv = atomically $ -- We cannot go to the same lengths to avoid a transaction as- -- we do in interruptibleWait. Notablely, the token check and+ -- we do in interruptibleWait. Notably, the token check and -- the counter increment must happen in a transaction together. ( do STM.check =<< STM.readTVar interrupt pure interruptToken@@ -755,22 +774,3 @@ -- -- These interpretations suggest that logical disjunction will give -- us the current readiness of the channel.--casMutableUnliftedArrayArray ::- MutableUnliftedArray RealWorld (MutableUnliftedArray RealWorld a) -- ^ array- -> Int -- ^ index- -> (MutableUnliftedArray RealWorld a) -- ^ expected old value- -> (MutableUnliftedArray RealWorld a) -- ^ new value- -> IO (Bool,MutableUnliftedArray RealWorld a)-{-# INLINE casMutableUnliftedArrayArray #-}-casMutableUnliftedArrayArray (MutableUnliftedArray arr#) (I# i#) (MutableUnliftedArray old) (MutableUnliftedArray new) =- -- All of this unsafeCoercing is really nasty business. This will go away- -- once https://github.com/ghc-proposals/ghc-proposals/pull/203 happens.- -- Also, this is unsound if the result is immidiately consumed by- -- the FFI.- IO $ \s0 ->- let !uold = (unsafeCoerce# :: MutableArrayArray# RealWorld -> Any) old- !unew = (unsafeCoerce# :: MutableArrayArray# RealWorld -> Any) new- in case casArray# ((unsafeCoerce# :: MutableArrayArray# RealWorld -> MutableArray# RealWorld Any) arr#) i# uold unew s0 of- (# s1, n, ur #) -> (# s1, (isTrue# (n ==# 0# ),MutableUnliftedArray ((unsafeCoerce# :: Any -> MutableArrayArray# RealWorld) ur)) #)-
src-internal/Socket/IPv4.hs view
@@ -18,6 +18,8 @@ , describeEndpoint , freezeIPv4Slab , newIPv4Slab+ , replenishIPv4Slab+ , replenishPinnedIPv4Slab ) where import Control.Exception (Exception)@@ -31,7 +33,9 @@ import Foreign.C.Types (CInt) import GHC.Exts (RealWorld,Int(I#)) import Net.Types (IPv4(..))+import Socket (Pinnedness(Pinned,Unpinned)) import Socket.Error (die)+import Socket.Slab (replenishPinned,replenishUnpinned) import qualified Data.Primitive as PM import qualified Data.Primitive.Unlifted.Array as PM@@ -40,7 +44,7 @@ import qualified Net.IPv4 as IPv4 import qualified Posix.Socket as S --- | An peer for an IPv4 socket, connection, or listener.+-- | A peer for an IPv4 socket, connection, or listener. -- Everything is in host byte order, and the user is not -- responsible for performing any conversions. data Peer = Peer@@ -48,7 +52,9 @@ , port :: !Word16 } deriving stock (Eq,Show) --- | A message received from a peer.+-- | A message received from a peer. The payload may be pinned+-- or unpinned depending on the reception function that produced+-- the message. data Message = Message { peer :: {-# UNPACK #-} !Peer , payload :: !ByteArray@@ -57,32 +63,32 @@ -- | A slab of memory for bulk datagram ingest (via @recvmmsg@). This -- approach cuts down on allocations. Slabs are not safe for concurrent -- access. Do not share a slab across multiple threads.-data IPv4Slab = IPv4Slab- { sizes :: !(MutablePrimArray RealWorld CInt)- -- ^ Buffer for returned datagram lengths- , peers :: !(MutablePrimArray RealWorld S.SocketAddressInternet)- -- ^ Buffer for returned addresses- , payloads :: !(MutableUnliftedArray RealWorld (MutableByteArray RealWorld))- -- ^ Buffers for datagram payloads, no slicing- }+data IPv4Slab :: Pinnedness -> Type where+ IPv4Slab ::+ -- Invariant: payload sizes are nondecreasing. An+ -- implication is that zero-size payloads come first.+ -- Invariant: All non-zero-size payloads match the+ -- phantom pinnedness. All zero-size payloads are+ -- unpinned.+ { sizes :: !(MutablePrimArray RealWorld CInt)+ -- ^ Buffer for returned datagram lengths+ , peers :: !(MutablePrimArray RealWorld S.SocketAddressInternet)+ -- ^ Buffer for returned addresses+ , payloads :: !(MutableUnliftedArray RealWorld (MutableByteArray RealWorld))+ -- ^ Buffers for datagram payloads, no slicing+ } -> IPv4Slab p -- | Allocate a slab that is used to receive multiple datagrams at -- the same time, additionally storing the IPv4 addresses of the peers. newIPv4Slab :: Int -- ^ maximum datagrams- -> Int -- ^ maximum size of individual datagram - -> IO IPv4Slab-newIPv4Slab !n !m = if n >= 1 && m >= 1+ -> IO (IPv4Slab p)+newIPv4Slab !n = if n >= 1 then do sizes <- PM.newPrimArray n peers <- PM.newPrimArray n- payloads <- PM.unsafeNewUnliftedArray n- let go !ix = if ix > (-1)- then do- writeMutableByteArrayArray payloads ix =<< PM.newByteArray m- go (ix - 1)- else pure ()- go (n - 1)+ tombstone <- PM.newByteArray 0+ payloads <- PM.newUnliftedArray n tombstone pure IPv4Slab{sizes,peers,payloads} else die "newSlabIPv4" @@ -110,7 +116,12 @@ -- <http://man7.org/linux/man-pages/man7/capabilities.7.html CAP_NET_BIND_SERVICE> -- capability instead of running the process as root. (@EACCES@) SocketPermissionDenied :: SocketException- -- | The given address is already in use. (@EADDRINUSE@ with specified port)+ -- | The given address is already in use. This can also happen if the+ -- address was recently bound to and closed. As mandated by+ -- <https://tools.ietf.org/html/rfc793 RFC 793>, such a socket remains+ -- in the @TIME-WAIT@ state for a while (commonly 30 to 120 seconds),+ -- and no new socket may be bound to the address during this period.+ -- (@EADDRINUSE@ with specified port) SocketAddressInUse :: SocketException -- | The port number was specified as zero, but upon attempting to -- bind to an ephemeral port, it was determined that all port numbers@@ -130,29 +141,53 @@ -- messages with tombstones so that new buffers can be allocated -- before the next reception. End users should not need this function. freezeIPv4Slab ::- IPv4Slab -- ^ The slab+ IPv4Slab p -- ^ The slab -> Int -- ^ Number of messages in the slab. -> IO (SmallArray Message) freezeIPv4Slab slab n = do msgs <- PM.newSmallArray n errorThunk- freezeSlabGo slab msgs (n - 1)+ tombstone <- PM.newByteArray 0+ freezeSlabGo slab tombstone msgs (n - 1) -freezeSlabGo :: IPv4Slab -> SmallMutableArray RealWorld Message -> Int -> IO (SmallArray Message)-freezeSlabGo slab@IPv4Slab{payloads,peers,sizes} !arr !ix = if ix > (-1)+freezeSlabGo ::+ IPv4Slab p+ -> MutableByteArray RealWorld -- tombstone+ -> SmallMutableArray RealWorld Message+ -> Int+ -> IO (SmallArray Message)+freezeSlabGo slab@IPv4Slab{payloads,peers,sizes} !tombstone !arr !ix = if ix > (-1) then do !size <- PM.readPrimArray sizes ix !sockaddr <- PM.readPrimArray peers ix -- Remove the byte array from the array of payloads, freeze it, and- -- replace it with a freshly allocated byte array. + -- replace it with a tombstone. payloadMut <- readMutableByteArrayArray payloads ix- originalSize <- PM.getSizeofMutableByteArray payloadMut- !payload <- PM.unsafeFreezeByteArray =<< PM.resizeMutableByteArray payloadMut (cintToInt size)- writeMutableByteArrayArray payloads ix =<< PM.newByteArray originalSize+ !payload <- PM.unsafeFreezeByteArray+ =<< PM.resizeMutableByteArray payloadMut (cintToInt size)+ writeMutableByteArrayArray payloads ix tombstone let !peer = sockAddrToPeer sockaddr !msg = Message {peer,payload} PM.writeSmallArray arr ix msg- freezeSlabGo slab arr (ix - 1)+ freezeSlabGo slab tombstone arr (ix - 1) else PM.unsafeFreezeSmallArray arr++-- | Ensure that every buffer can accomodate at least the+-- minimum number of bytes.+replenishIPv4Slab ::+ IPv4Slab 'Unpinned+ -> Int -- ^ Minimum Size+ -> IO () +replenishIPv4Slab IPv4Slab{payloads} minSz = do+ let sz = PM.sizeofMutableUnliftedArray payloads+ replenishUnpinned payloads minSz 0 sz++replenishPinnedIPv4Slab ::+ IPv4Slab 'Pinned+ -> Int -- ^ Minimum Size+ -> IO () +replenishPinnedIPv4Slab IPv4Slab{payloads} minSz = do+ let sz = PM.sizeofMutableUnliftedArray payloads+ replenishPinned payloads minSz 0 sz {-# NOINLINE errorThunk #-} errorThunk :: Message
src-internal/Socket/Interruptible.hs view
@@ -1,3 +1,4 @@+{-# language CPP #-} {-# language DataKinds #-} module Socket.Interruptible@@ -9,19 +10,45 @@ , tokenToStreamReceiveException , tokenToDatagramSendException , tokenToDatagramReceiveException+ , tokenToSequencedPacketSendException+ , tokenToSequencedPacketReceiveException ) where import Socket (Interruptibility(Interruptible)) import Socket.EventManager (Token) import Control.Concurrent.STM (TVar)-import GHC.Exts (RuntimeRep(LiftedRep)) import qualified Socket.EventManager as EM import qualified Socket.Stream as Stream import qualified Socket.Datagram as Datagram+import qualified Socket.SequencedPacket as SequencedPacket -type InterruptRep = 'LiftedRep+#if MIN_VERSION_base(4,16,0)+import GHC.Exts (RuntimeRep(BoxedRep),Levity(Lifted))+#else+import GHC.Exts (RuntimeRep(LiftedRep))+#endif++type InterruptRep = +#if MIN_VERSION_base(4,16,0)+ 'BoxedRep 'Lifted+#else+ 'LiftedRep+#endif+ type Interrupt = TVar Bool type Intr = 'Interruptible++tokenToSequencedPacketSendException :: Token -> Either (SequencedPacket.SendException 'Interruptible) ()+{-# inline tokenToSequencedPacketSendException #-}+tokenToSequencedPacketSendException t = if EM.isInterrupt t+ then Left SequencedPacket.SendInterrupted+ else Right ()++tokenToSequencedPacketReceiveException :: Token -> Either (SequencedPacket.ReceiveException 'Interruptible) ()+{-# inline tokenToSequencedPacketReceiveException #-}+tokenToSequencedPacketReceiveException t = if EM.isInterrupt t+ then Left SequencedPacket.ReceiveInterrupted+ else Right () tokenToStreamSendException :: Token -> Int -> Either (Stream.SendException 'Interruptible) () {-# inline tokenToStreamSendException #-}
+ src-internal/Socket/SequencedPacket.hs view
@@ -0,0 +1,63 @@+{-# language DataKinds #-}+{-# language DeriveAnyClass #-}+{-# language DerivingStrategies #-}+{-# language GADTs #-}+{-# language KindSignatures #-}+{-# language StandaloneDeriving #-}++module Socket.SequencedPacket+ ( ReceiveException(..)+ , SendException(..)+ ) where++import Data.Kind (Type)+import Data.Typeable (Typeable)+import Control.Exception (Exception)+import Socket (Interruptibility(..))++data ReceiveException :: Interruptibility -> Type where+ -- | The datagram did not fit in the buffer. The field is the+ -- original size of the datagram that was truncated. If+ -- this happens, the process probably needs to start using+ -- a larger receive buffer.+ ReceiveTruncated :: ReceiveException i+ -- | The peer shutdown its writing channel. (zero-length chunk)+ ReceiveShutdown :: ReceiveException i+ -- | The peer reset the connection. (@ECONNRESET@)+ ReceiveReset :: ReceiveException i+ -- | STM-style interrupt (much safer than C-style interrupt)+ -- This provides the number of bytes received before the interrupt+ -- happened. For @receiveOnce@, this will always be zero, but+ -- for @receiveExactly@ and @receiveBetween@, it may be any+ -- non-negative number less than the number of bytes the caller+ -- intended to receive.+ ReceiveInterrupted :: ReceiveException 'Interruptible++deriving stock instance Eq (ReceiveException i)+deriving stock instance Show (ReceiveException i)+deriving anyclass instance Typeable i => Exception (ReceiveException i)++data SendException :: Interruptibility -> Type where+ -- | The datagram did not fit in the buffer. The field is the+ -- number of bytes that were successfully copied into the+ -- send buffer. The datagram does still get sent when this+ -- happens.+ SendTruncated :: !Int -> SendException i+ -- | The local socket has already shutdown its writing channel.+ -- Consequently, sending is no longer possible. This can happen+ -- even if the process does not @shutdown@ the socket. If the+ -- peer decides to @close@ the connection, the local operating system+ -- will shutdown both the reading and writing channels. (@EPIPE@)+ SendShutdown :: SendException i+ -- | The peer reset the connection.+ SendReset :: SendException i+ -- | STM-style interrupt (much safer than C-style interrupt).+ -- This provides the number of bytes sent before the interrupt+ -- happened. For @sendOnce@, this will always be zero, but+ -- for @send@, it may be any non-negative number less than the+ -- number of bytes the caller intended to send.+ SendInterrupted :: SendException 'Interruptible++deriving stock instance Eq (SendException i)+deriving stock instance Show (SendException i)+deriving anyclass instance Typeable i => Exception (SendException i)
+ src-internal/Socket/Slab.hs view
@@ -0,0 +1,51 @@+{-# language BangPatterns #-}++module Socket.Slab+ ( replenishUnpinned+ , replenishPinned+ ) where++import Data.Primitive (MutableByteArray)+import Data.Primitive.Unlifted.Array (MutableUnliftedArray)+import GHC.Exts (RealWorld)++import qualified Data.Primitive as PM+import qualified Data.Primitive.Unlifted.Array as PM++-- Notice that this only replaces buffers until we+-- reach a single buffer that is of sufficient size.+-- This works because the buffer sizes are nondecreasing.+replenishUnpinned ::+ MutableUnliftedArray RealWorld (MutableByteArray RealWorld)+ -> Int+ -> Int+ -> Int+ -> IO () +replenishUnpinned !xs !minSz !ix !sz = if ix < sz+ then do+ p <- PM.readUnliftedArray xs ix+ pSz <- PM.getSizeofMutableByteArray p+ if pSz < minSz+ then do+ PM.writeUnliftedArray xs ix =<< PM.newByteArray minSz+ replenishUnpinned xs minSz (ix + 1) sz+ else pure ()+ else pure ()++replenishPinned ::+ MutableUnliftedArray RealWorld (MutableByteArray RealWorld)+ -> Int+ -> Int+ -> Int+ -> IO () +replenishPinned !xs !minSz !ix !sz = if ix < sz+ then do+ p <- PM.readUnliftedArray xs ix+ pSz <- PM.getSizeofMutableByteArray p+ if pSz < minSz+ then do+ PM.writeUnliftedArray xs ix =<< PM.newPinnedByteArray minSz+ replenishPinned xs minSz (ix + 1) sz+ else pure ()+ else pure ()+
src-internal/Socket/Stream.hs view
@@ -78,7 +78,7 @@ ConnectHostUnreachable :: ConnectException ('Internet v) i -- | All port numbers in the ephemeral port range are currently in -- use. (@EADDRNOTAVAIL@).- ConnectEphemeralPortsExhausted :: ConnectException d i+ ConnectEphemeralPortsExhausted :: ConnectException ('Internet v) i -- | No one is listening on the remote address. (@ECONNREFUSED@) ConnectRefused :: ConnectException d i -- | Timeout while attempting connection. The server may be too busy@@ -91,6 +91,9 @@ ConnectTimeout :: ConnectException d i -- | Remote socket does not match the local socket type. (@EPROTOTYPE@) ConnectProtocolType :: ConnectException 'Unix i+ -- | The pathname used as the remote address in a call to @connect@+ -- did not exist. This only happens with UNIX-domain sockets. (@ENOENT@)+ ConnectNoEntry :: ConnectException 'Unix i -- | STM-style interrupt (much safer than C-style interrupt) ConnectInterrupted :: ConnectException d 'Interruptible
src-internal/Socket/Uninterruptible.hs view
@@ -7,6 +7,8 @@ , Interrupt , Intr , wait+ , tokenToSequencedPacketSendException+ , tokenToSequencedPacketReceiveException , tokenToStreamSendException , tokenToStreamReceiveException , tokenToDatagramSendException@@ -21,12 +23,21 @@ import qualified Socket.EventManager as EM import qualified Socket.Stream as Stream import qualified Socket.Datagram as Datagram+import qualified Socket.SequencedPacket as SequencedPacket -- We use Proxy# instead of () to ensure that this interrupt type -- has no runtime cost associated with it. type InterruptRep = 'TupleRep '[] type Interrupt = Proxy# Void type Intr = 'Uninterruptible++tokenToSequencedPacketSendException :: Token -> Either (SequencedPacket.SendException 'Uninterruptible) ()+{-# inline tokenToSequencedPacketSendException #-}+tokenToSequencedPacketSendException _ = Right ()++tokenToSequencedPacketReceiveException :: Token -> Either (SequencedPacket.ReceiveException 'Uninterruptible) ()+{-# inline tokenToSequencedPacketReceiveException #-}+tokenToSequencedPacketReceiveException _ = Right () tokenToStreamSendException :: Token
src-interrupt/Socket/Interrupt.hsig view
@@ -10,6 +10,7 @@ import Socket (Interruptibility) import GHC.Exts (RuntimeRep,TYPE) import qualified Socket.Stream as Stream+import qualified Socket.SequencedPacket as SequencedPacket import qualified Socket.Datagram as Datagram data InterruptRep :: RuntimeRep@@ -18,6 +19,12 @@ wait :: Interrupt -> TVar Token -> IO Token +tokenToSequencedPacketSendException ::+ Token+ -> Either (SequencedPacket.SendException Intr) ()+tokenToSequencedPacketReceiveException ::+ Token+ -> Either (SequencedPacket.ReceiveException Intr) () tokenToStreamSendException :: Token -> Int
src-no-mmsg/Socket/Datagram/Interruptible/MutableBytes/Many.hs view
@@ -35,7 +35,7 @@ -- @'Left' 'ReceiveInterrupted'@. -> Socket c a -- ^ Socket- -> Socket.Discard.PeerlessSlab+ -> Socket.Discard.PeerlessSlab p -- ^ Buffers into which sizes and payloads are received -> IO (Either (ReceiveException 'Interruptible) Int) receiveMany intr (Socket fd) (Socket.Discard.PeerlessSlab{sizes,payloads}) =@@ -49,7 +49,7 @@ -- ^ Interrupt. On 'True', give up and return -- @'Left' 'ReceiveInterrupted'@. -> Socket 'Unconnected ('SCK.Internet 'SCK.V4) -- ^ Socket- -> Socket.IPv4.IPv4Slab+ -> Socket.IPv4.IPv4Slab p -- ^ Buffers into which sizes, addresses, and payloads -- are received -> IO (Either (ReceiveException 'Interruptible) Int)
src-no-mmsg/Socket/Datagram/Uninterruptible/MutableBytes/Many.hs view
@@ -11,7 +11,6 @@ ) where import GHC.Exts (proxy#)-import GHC.IO (IO(..)) import Socket (Interruptibility(Uninterruptible)) import Socket (Connectedness(..)) import Socket.Datagram (Socket(..),ReceiveException)@@ -33,7 +32,7 @@ receiveMany :: Socket c a -- ^ Socket- -> Socket.Discard.PeerlessSlab+ -> Socket.Discard.PeerlessSlab p -- ^ Buffers into which sizes and payloads are received -> IO (Either (ReceiveException 'Uninterruptible) Int) receiveMany (Socket fd) (Socket.Discard.PeerlessSlab{sizes,payloads}) =@@ -44,7 +43,7 @@ -- to the structure-of-arrays. receiveManyFromIPv4 :: Socket 'Unconnected ('SCK.Internet 'SCK.V4) -- ^ Socket- -> Socket.IPv4.IPv4Slab+ -> Socket.IPv4.IPv4Slab p -- ^ Buffers into which sizes, addresses, and payloads -- are received -> IO (Either (ReceiveException 'Uninterruptible) Int)
+ src-stream-receive/SequencedPacket/Receive/Indefinite.hs view
@@ -0,0 +1,85 @@+{-# language BangPatterns #-}+{-# language LambdaCase #-}+{-# language MultiWayIf #-}++module SequencedPacket.Receive.Indefinite+ ( receive+ , receiveAttempt+ ) where++import Control.Concurrent.STM (TVar)+import Foreign.C.Error (Errno(..), eAGAIN, eWOULDBLOCK, eCONNRESET)+import Foreign.C.Types (CSize)+import Socket.Error (die)+import Socket.EventManager (Token)+import Socket.SequencedPacket (ReceiveException(..))+import Socket.Buffer (Buffer)+import Socket.Interrupt (Interrupt,Intr,wait,tokenToSequencedPacketReceiveException)+import System.Posix.Types (Fd)++import qualified Foreign.C.Error.Describe as D+import qualified Socket.EventManager as EM+import qualified Socket.Buffer as Buffer+import qualified Stream.Receive as Receive+import qualified Linux.Socket as L++receive ::+ Interrupt+ -> Fd+ -> Buffer+ -> IO (Either (ReceiveException Intr) Int)+receive !intr !sock !buf = do+ let !mngr = EM.manager+ tv <- EM.reader mngr sock+ token0 <- wait intr tv+ case tokenToSequencedPacketReceiveException token0 of+ Left err -> pure (Left err)+ Right _ -> receiveLoop intr tv token0 sock buf++receiveAttempt ::+ Fd -- ^ Socket+ -> Buffer -- ^ Buffer+ -> IO (Either (ReceiveException Intr) (Maybe Int))+{-# inline receiveAttempt #-}+receiveAttempt !fd !buf = do+ e <- Receive.receiveOnce fd buf+ case e of+ Left err ->+ if | err == eWOULDBLOCK || err == eAGAIN -> pure (Right Nothing)+ | err == eCONNRESET -> pure (Left ReceiveReset)+ | otherwise -> die $ concat+ [ "Socket.SequencedPacket.receive: " + , describeErrorCode err+ ]+ Right recvSz -> do+ let !recvSzInt = csizeToInt recvSz+ -- TODO: This is sort of a hack, but it mostly works. We should+ -- use MSG_TRUNC, but I am too lazy to fix this at the moment.+ if recvSzInt == Buffer.length buf+ then pure (Left ReceiveTruncated)+ else pure (Right (Just recvSzInt))++receiveLoop ::+ Interrupt+ -> TVar Token+ -> Token+ -> Fd -- ^ Socket+ -> Buffer -- ^ Buffer+ -> IO (Either (ReceiveException Intr) Int)+receiveLoop !intr !tv !token0 !fd !buf = receiveAttempt fd buf >>= \case+ Left err -> pure (Left err)+ Right m -> case m of+ Nothing -> do+ EM.unready token0 tv+ token1 <- wait intr tv+ case tokenToSequencedPacketReceiveException token1 of+ Left err -> pure (Left err)+ Right _ -> receiveLoop intr tv token1 fd buf+ Just !r -> pure (Right r)++csizeToInt :: CSize -> Int+csizeToInt = fromIntegral++describeErrorCode :: Errno -> String+describeErrorCode err@(Errno e) = "error code " ++ D.string err ++ " (" ++ show e ++ ")"+
+ src-stream-send/SequencedPacket/Send/Indefinite.hs view
@@ -0,0 +1,82 @@+{-# language BangPatterns #-}+{-# language LambdaCase #-}+{-# language MultiWayIf #-}++module SequencedPacket.Send.Indefinite+ ( send+ , attemptSend+ ) where++import Control.Concurrent.STM (TVar)+import Foreign.C.Error (Errno(..), eAGAIN, eWOULDBLOCK)+import Foreign.C.Error (eCONNRESET,ePIPE)+import Foreign.C.Types (CSize)+import Socket.Error (die)+import Socket.EventManager (Token)+import Socket.SequencedPacket (SendException(..))+import Socket.Buffer (Buffer)+import Socket.Interrupt (Interrupt,Intr,wait,tokenToSequencedPacketSendException)+import System.Posix.Types (Fd)++import qualified Foreign.C.Error.Describe as D+import qualified Socket.EventManager as EM+import qualified Socket.Buffer as Buffer+import qualified Stream.Send as Send++-- Send the entirely of the buffer, making a single call to+-- POSIX @send@. This is used for datagram sockets. We cannot use a+-- Socket newtype here since destined and undestined sockets+-- use different newtypes.+send ::+ Interrupt+ -> Fd+ -> Buffer+ -> IO (Either (SendException Intr) ())+send !intr !sock !buf = do+ let !mngr = EM.manager+ tv <- EM.writer mngr sock+ token0 <- wait intr tv+ case tokenToSequencedPacketSendException token0 of+ Left err -> pure (Left err)+ Right _ -> sendLoop intr sock tv token0 buf++-- Never blocks.+attemptSend ::+ Fd+ -> Buffer+ -> IO (Either (SendException Intr) Bool)+attemptSend !sock !buf = Send.sendOnce sock buf >>= \case+ Left e ->+ if | e == eAGAIN || e == eWOULDBLOCK -> pure (Right False)+ | e == ePIPE -> pure (Left SendShutdown)+ | e == eCONNRESET -> pure (Left SendReset)+ | otherwise -> die ("Socket.SequencedPacket.send: " ++ describeErrorCode e)+ Right sz -> if csizeToInt sz == Buffer.length buf+ then pure $! Right True+ else pure $! Left $! SendTruncated $! csizeToInt sz++sendLoop ::+ Interrupt -> Fd -> TVar Token -> Token+ -> Buffer -> IO (Either (SendException Intr) ())+sendLoop !intr !sock !tv !old !buf =+ Send.sendOnce sock buf >>= \case+ Left e ->+ if | e == eAGAIN || e == eWOULDBLOCK -> do+ EM.unready old tv+ new <- wait intr tv+ case tokenToSequencedPacketSendException new of+ Left err -> pure (Left err)+ Right _ -> sendLoop intr sock tv new buf+ | e == ePIPE -> pure (Left SendShutdown)+ | e == eCONNRESET -> pure (Left SendReset)+ | otherwise -> die ("Socket.SequencedPacket.send: " ++ describeErrorCode e)+ Right sz -> if csizeToInt sz == Buffer.length buf+ then pure $! Right ()+ else pure $! Left $! SendTruncated $! csizeToInt sz++csizeToInt :: CSize -> Int+csizeToInt = fromIntegral++describeErrorCode :: Errno -> String+describeErrorCode err@(Errno e) = "error code " ++ D.string err ++ " (" ++ show e ++ ")"+
+ src/Socket/Datagram/Common.hs view
@@ -0,0 +1,25 @@+{-# language LambdaCase #-}++-- Functions like @close@ are the same regardless of whether+-- a socket is connected, unconnected, internet-domain, or+-- unix-domain.+module Socket.Datagram.Common+ ( close+ ) where++import Foreign.C.Error (Errno(..))+import Socket.Datagram (Socket(..))+import Socket.Error (die)+import qualified Posix.Socket as S+import qualified Foreign.C.Error.Describe as D++-- | Unbracketed function for closing a datagram socket. This must not+-- be used in conjunction with 'withSocket'.+close :: Socket c a -> IO ()+close (Socket fd) = S.uninterruptibleClose fd >>= \case+ Left err -> die ("Socket.Datagram.Common.close: " ++ describeErrorCode err)+ Right _ -> pure ()++describeErrorCode :: Errno -> String+describeErrorCode err@(Errno e) = "error code " ++ D.string err ++ " (" ++ show e ++ ")"+
src/Socket/Datagram/IPv4/Connected.hs view
@@ -19,6 +19,8 @@ , Message(..) -- * Establish , withSocket+ , open+ , close -- * Exceptions , SocketException(..) , SD.ReceiveException(..)@@ -31,13 +33,13 @@ import Data.Word (Word16) import Foreign.C.Error (Errno(..),eACCES) import Foreign.C.Error (eNFILE,eMFILE,eADDRINUSE)-import GHC.IO (IO(..)) import Net.Types (IPv4(..)) import Socket (Connectedness(..),Family(..)) import Socket.Datagram (Socket(..)) import Socket.Datagram (SocketException(..)) import Socket.IPv4 (Peer(..),Message(..)) import Socket.Error (die)+import Socket.Datagram.Common (close) import qualified Foreign.C.Error.Describe as D import qualified Linux.Socket as L@@ -46,17 +48,19 @@ import qualified Socket.EventManager as EM import qualified Socket.Datagram as SD -withSocket ::+-- | Unbracketed function for opening a socket. Be careful with+-- this function. Only call this in contexts where exceptions are+-- masked.+--+-- TODO: Rewrite this. Stop calling bind. Do not make the user+-- supply a local address.+open :: Peer- -- ^ Address and port to bind to -> Peer- -- ^ Peer address and port- -> (Socket 'Connected ('SCK.Internet 'SCK.V4) -> Word16 -> IO a)- -- ^ Callback providing the socket and the chosen port- -> IO (Either SocketException a)-withSocket local@Peer{port = specifiedPort} !peer f = mask $ \restore -> do+ -> IO (Either SocketException (Socket 'Connected ('SCK.Internet 'SCK.V4), Word16))+open local@Peer{port = specifiedPort} !peer = do -- TODO: This is mostly copied from the unconnected withSocket.- e1 <- S.uninterruptibleSocket S.internet+ e1 <- S.uninterruptibleSocket S.Internet (L.applySocketFlags (L.closeOnExec <> L.nonblocking) S.datagram) S.defaultProtocol case e1 of@@ -100,11 +104,23 @@ S.uninterruptibleConnect fd sockAddr >>= \case Left err -> die ("Socket.Datagram.IPv4.Connected.connect: " ++ describeErrorCode err)- Right (_ :: ()) -> do- a <- onException (restore (f (Socket fd) actualPort)) (S.uninterruptibleErrorlessClose fd)- S.uninterruptibleClose fd >>= \case- Left err -> die ("Socket.Datagram.IPv4.Undestined.close: " ++ describeErrorCode err)- Right _ -> pure (Right a)+ Right (_ :: ()) -> pure (Right (Socket fd, actualPort))++withSocket ::+ Peer+ -- ^ Address and port to bind to+ -> Peer+ -- ^ Peer address and port+ -> (Socket 'Connected ('SCK.Internet 'SCK.V4) -> Word16 -> IO a)+ -- ^ Callback providing the socket and the chosen port+ -> IO (Either SocketException a)+withSocket !local !peer f = mask $ \restore -> open local peer >>= \case+ Left err -> pure (Left err)+ Right (Socket fd,actualPort) -> do+ a <- onException (restore (f (Socket fd) actualPort)) (S.uninterruptibleErrorlessClose fd)+ S.uninterruptibleClose fd >>= \case+ Left err -> die ("Socket.Datagram.IPv4.Undestined.close: " ++ describeErrorCode err)+ Right _ -> pure (Right a) describeErrorCode :: Errno -> String describeErrorCode err@(Errno e) = "error code " ++ D.string err ++ " (" ++ show e ++ ")"
src/Socket/Datagram/IPv4/Unconnected.hs view
@@ -6,6 +6,8 @@ {-# language LambdaCase #-} {-# language MagicHash #-} {-# language NamedFieldPuns #-}+{-# language ScopedTypeVariables #-}+{-# language TypeApplications #-} {-# language UnboxedTuples #-} -- | Internet datagram sockets without a fixed destination.@@ -13,11 +15,16 @@ ( -- * Types Socket(..) , Family(..)+ , Version(..) , Connectedness(..) , Peer(..) , Message(..)+ , SCK.Interruptibility(..) -- * Establish , withSocket+ , open+ , openOnDevice+ , close -- * Exceptions , SocketException(..) ) where@@ -26,21 +33,96 @@ import Data.Word (Word16) import Foreign.C.Error (Errno(..),eACCES) import Foreign.C.Error (eNFILE,eMFILE,eADDRINUSE)-import GHC.IO (IO(..))+import Foreign.C.Types (CInt) import Net.Types (IPv4(..))-import Socket (Connectedness(..),Family(..))+import Socket (Connectedness(..),Family(..),Version(..)) import Socket.Datagram (Socket(..)) import Socket.Datagram (SocketException(..)) import Socket.Debug (debug) import Socket.IPv4 (Peer(..),Message(..),describeEndpoint) import Socket.Error (die)+import Socket.Datagram.Common (close)+import System.Posix.Types (Fd) import qualified Foreign.C.Error.Describe as D import qualified Linux.Socket as L import qualified Posix.Socket as S+import qualified Data.Primitive as PM import qualified Socket as SCK import qualified Socket.EventManager as EM +openOnDevice ::+ Peer -- ^ Address and port to use+ -> PM.ByteArray -- ^ Local network interface name+ -> IO (Either SocketException (Socket 'Unconnected ('SCK.Internet 'SCK.V4), Word16))+openOnDevice !local !intf = do+ e1 <- S.uninterruptibleSocket S.Internet+ (L.applySocketFlags (L.closeOnExec <> L.nonblocking) S.datagram)+ S.defaultProtocol+ case e1 of+ Left err -> handleSocketException err+ Right fd -> do+ r <- S.uninterruptibleSetSocketOptionByteArray+ fd S.levelSocket S.bindToDevice+ intf (fromIntegral @Int @CInt (PM.sizeofByteArray intf))+ -- We throw a bogus error here. TODO: fix this.+ case r of+ Left _ -> do+ S.uninterruptibleErrorlessClose fd+ pure (Left SocketPermissionDenied)+ Right (_ :: ()) -> prepareSocket local fd++open ::+ Peer+ -- ^ Address and port to use+ -> IO (Either SocketException (Socket 'Unconnected ('SCK.Internet 'SCK.V4), Word16))+open !local = do+ e1 <- S.uninterruptibleSocket S.Internet+ (L.applySocketFlags (L.closeOnExec <> L.nonblocking) S.datagram)+ S.defaultProtocol+ case e1 of+ Left err -> handleSocketException err+ Right fd -> prepareSocket local fd++prepareSocket ::+ Peer+ -> Fd+ -> IO (Either SocketException (Socket 'Unconnected ('SCK.Internet 'SCK.V4), Word16))+prepareSocket !local@Peer{port = specifiedPort} !fd = do+ let !mngr = EM.manager+ EM.register mngr fd+ e2 <- S.uninterruptibleBind fd+ (S.encodeSocketAddressInternet (endpointToSocketAddressInternet local))+ debug ("withSocket: requested binding for " ++ describeEndpoint local)+ case e2 of+ Left err -> do+ -- We intentionally discard any exceptions thrown by close.+ -- There is simply nothing that can be done with them.+ S.uninterruptibleErrorlessClose fd+ handleBindException specifiedPort err+ Right _ -> do+ eactualPort <- if specifiedPort == 0+ then S.uninterruptibleGetSocketName fd S.sizeofSocketAddressInternet >>= \case+ Left err -> do+ S.uninterruptibleErrorlessClose fd+ die ("Socket.Datagram.IPv4.Undestined.getsockname: " ++ describeErrorCode err)+ Right (sockAddrRequiredSz,sockAddr) -> if sockAddrRequiredSz == S.sizeofSocketAddressInternet+ then case S.decodeSocketAddressInternet sockAddr of+ Just S.SocketAddressInternet{port = actualPort} -> do+ let cleanPort = S.networkToHostShort actualPort+ debug ("withSocket: successfully bound " ++ describeEndpoint local ++ " and got port " ++ show cleanPort)+ pure (Right cleanPort)+ Nothing -> do+ S.uninterruptibleErrorlessClose fd+ die "Socket.Datagram.IPv4.Unconnected: non-internet socket family"+ else do+ S.uninterruptibleErrorlessClose fd+ die "Socket.Datagram.IPv4.Unconnected: socket address size"+ else pure (Right specifiedPort)+ case eactualPort of+ Left err -> pure (Left err)+ Right actualPort -> pure (Right (Socket fd, actualPort))+ -- | Open a socket and run the supplied callback on it. This closes the socket -- when the callback finishes or when an exception is thrown. Do not return -- the socket from the callback. This leads to undefined behavior. If the@@ -54,53 +136,13 @@ -> (Socket 'Unconnected ('SCK.Internet 'SCK.V4) -> Word16 -> IO a) -- ^ Callback providing the socket and the chosen port -> IO (Either SocketException a)-withSocket endpoint@Peer{port = specifiedPort} f = mask $ \restore -> do- debug ("withSocket: opening socket " ++ describeEndpoint endpoint)- e1 <- S.uninterruptibleSocket S.internet- (L.applySocketFlags (L.closeOnExec <> L.nonblocking) S.datagram)- S.defaultProtocol- debug ("withSocket: opened socket " ++ describeEndpoint endpoint)- case e1 of- Left err -> handleSocketException err- Right fd -> do- let !mngr = EM.manager- EM.register mngr fd- e2 <- S.uninterruptibleBind fd- (S.encodeSocketAddressInternet (endpointToSocketAddressInternet endpoint))- debug ("withSocket: requested binding for " ++ describeEndpoint endpoint)- case e2 of- Left err -> do- -- We intentionally discard any exceptions thrown by close.- -- There is simply nothing that can be done with them.- S.uninterruptibleErrorlessClose fd- handleBindException specifiedPort err- Right _ -> do- eactualPort <- if specifiedPort == 0- then S.uninterruptibleGetSocketName fd S.sizeofSocketAddressInternet >>= \case- Left err -> do- S.uninterruptibleErrorlessClose fd- die ("Socket.Datagram.IPv4.Undestined.getsockname: " ++ describeErrorCode err)- Right (sockAddrRequiredSz,sockAddr) -> if sockAddrRequiredSz == S.sizeofSocketAddressInternet- then case S.decodeSocketAddressInternet sockAddr of- Just S.SocketAddressInternet{port = actualPort} -> do- let cleanPort = S.networkToHostShort actualPort- debug ("withSocket: successfully bound " ++ describeEndpoint endpoint ++ " and got port " ++ show cleanPort)- pure (Right cleanPort)- Nothing -> do- S.uninterruptibleErrorlessClose fd- die "Socket.Datagram.IPv4.Undestined: non-internet socket family"- else do- S.uninterruptibleErrorlessClose fd- die "Socket.Datagram.IPv4.Undestined: socket address size"- else pure (Right specifiedPort)- case eactualPort of- Left err -> pure (Left err)- Right actualPort -> do- a <- onException (restore (f (Socket fd) actualPort)) (S.uninterruptibleErrorlessClose fd)- S.uninterruptibleClose fd >>= \case- Left err -> die ("Socket.Datagram.IPv4.Undestined.close: " ++ describeErrorCode err)- Right _ -> pure (Right a)-+withSocket local f = mask $ \restore -> open local >>= \case+ Left err -> pure (Left err)+ Right (Socket fd,actualPort) -> do+ a <- onException (restore (f (Socket fd) actualPort)) (S.uninterruptibleErrorlessClose fd)+ S.uninterruptibleClose fd >>= \case+ Left err -> die ("Socket.Datagram.IPv4.Unconnected.close: " ++ describeErrorCode err)+ Right _ -> pure (Right a) endpointToSocketAddressInternet :: Peer -> S.SocketAddressInternet endpointToSocketAddressInternet (Peer {address, port}) = S.SocketAddressInternet@@ -122,7 +164,7 @@ handleBindException !thePort !e | e == eACCES = pure (Left SocketPermissionDenied) | e == eADDRINUSE = if thePort == 0- then pure (Left SocketAddressInUse)- else pure (Left SocketEphemeralPortsExhausted)+ then pure (Left SocketEphemeralPortsExhausted)+ else pure (Left SocketAddressInUse) | otherwise = die ("Socket.Datagram.IPv4.Undestined.bind: " ++ describeErrorCode e)
src/Socket/Datagram/Interruptible/ByteString.hs view
@@ -13,6 +13,10 @@ -- * Receive , receive , receiveFromIPv4+ -- * Types+ , Peer(..)+ , ReceiveException(..)+ , SendException(..) -- * Slabs -- ** Types , PeerlessSlab(..)@@ -32,8 +36,8 @@ import GHC.Exts (Ptr(Ptr),RealWorld) import Posix.Socket (SocketAddressInternet) import Socket (Connectedness(..),Family(..),Version(..),Interruptibility(Interruptible))-import Socket.Datagram (Socket(..),SendException,ReceiveException)-import Socket.IPv4 (Peer,newIPv4Slab,IPv4Slab(..))+import Socket.Datagram (Socket(..),SendException(..),ReceiveException(..))+import Socket.IPv4 (Peer(..),newIPv4Slab,IPv4Slab(..)) import Socket.Discard (PeerlessSlab(..),newPeerlessSlab) import Socket.Interop (fromPinned)
src/Socket/Datagram/Interruptible/Bytes.hs view
@@ -13,6 +13,19 @@ -- * Receive Many , receiveMany , receiveManyFromIPv4+ , receiveManyPinnedFromIPv4+ -- * Types+ , Pinnedness(..)+ , Message(..)+ , Peer(..)+ , ReceiveException(..)+ -- * Slabs+ -- ** Types+ , PeerlessSlab(..)+ , IPv4Slab(..)+ -- ** Functions+ , newPeerlessSlab+ , newIPv4Slab ) where import Control.Concurrent.STM (TVar)@@ -20,13 +33,16 @@ import Data.Primitive (ByteArray,SmallArray) import Data.Primitive.Unlifted.Array (UnliftedArray) import Data.Primitive.PrimArray.Offset (MutablePrimArrayOffset(..))-import Socket (Connectedness(..),Family(..),Version(..),Interruptibility(Interruptible))+import Socket (Connectedness(..),Family(..),Version(..))+import Socket (Pinnedness(Pinned,Unpinned))+import Socket (Interruptibility(Interruptible)) import Socket.Address (posixToIPv4Peer)-import Socket.Datagram (Socket(..),ReceiveException)-import Socket.IPv4 (Message(..),IPv4Slab(..))+import Socket.Datagram (Socket(..),ReceiveException(..))+import Socket.IPv4 (Peer(..),Message(..),IPv4Slab(..),freezeIPv4Slab)+import Socket.IPv4 (newIPv4Slab,replenishIPv4Slab,replenishPinnedIPv4Slab)+import Socket.Discard (PeerlessSlab(..),newPeerlessSlab) import qualified Data.Primitive as PM-import qualified Socket.IPv4 import qualified Socket.Discard import qualified Socket as SCK import qualified Socket.Datagram.Interruptible.MutableBytes.Many as MM@@ -73,23 +89,43 @@ -- ^ Interrupt. On 'True', give up and return -- @'Left' 'ReceiveInterrupted'@. -> Socket 'Unconnected ('SCK.Internet 'SCK.V4) -- ^ Socket- -> Socket.IPv4.IPv4Slab -- ^ Buffers for reception+ -> Socket.IPv4.IPv4Slab 'Unpinned -- ^ Buffers for reception+ -> Int -- ^ Maximum size of single datagram -> IO (Either (ReceiveException 'Interruptible) (SmallArray Message))-receiveManyFromIPv4 intr sock slab = do+receiveManyFromIPv4 intr sock slab maxSz = do+ replenishIPv4Slab slab maxSz MM.receiveManyFromIPv4 intr sock slab >>= \case Left err -> pure (Left err) Right n -> do arr <- Socket.IPv4.freezeIPv4Slab slab n pure (Right arr) +receiveManyPinnedFromIPv4 ::+ TVar Bool+ -- ^ Interrupt. On 'True', give up and return+ -- @'Left' 'ReceiveInterrupted'@.+ -> Socket 'Unconnected ('SCK.Internet 'SCK.V4) -- ^ Socket+ -> Socket.IPv4.IPv4Slab 'Pinned -- ^ Buffers for reception+ -> Int -- ^ Maximum size of single datagram+ -> IO (Either (ReceiveException 'Interruptible) (SmallArray Message))+receiveManyPinnedFromIPv4 intr sock slab maxSz = do+ replenishPinnedIPv4Slab slab maxSz+ MM.receiveManyFromIPv4 intr sock slab >>= \case+ Left err -> pure (Left err)+ Right n -> do+ arr <- Socket.IPv4.freezeIPv4Slab slab n+ pure (Right arr)+ receiveMany :: TVar Bool -- ^ Interrupt. On 'True', give up and return -- @'Left' 'ReceiveInterrupted'@. -> Socket 'Unconnected ('SCK.Internet 'SCK.V4) -- ^ Socket- -> Socket.Discard.PeerlessSlab -- ^ Buffers for reception+ -> PeerlessSlab 'Unpinned -- ^ Buffers for reception+ -> Int -- ^ Maximum size of single datagram -> IO (Either (ReceiveException 'Interruptible) (UnliftedArray ByteArray))-receiveMany intr sock slab = do+receiveMany intr sock slab maxSz = do+ Socket.Discard.replenishPeerlessSlab slab maxSz MM.receiveMany intr sock slab >>= \case Left err -> pure (Left err) Right n -> do
src/Socket/Datagram/Uninterruptible/ByteString.hs view
@@ -13,6 +13,8 @@ -- * Receive , receive , receiveFromIPv4+ -- * Receive Many+ , receiveMany -- * Slabs -- ** Types , PeerlessSlab(..)@@ -26,21 +28,25 @@ import Data.ByteString.Unsafe (unsafeUseAsCStringLen) import Data.Bytes.Types (UnmanagedBytes(UnmanagedBytes)) import Data.Bytes.Types (MutableBytes(MutableBytes))+import Data.Primitive (SmallArray) import Data.Primitive.Addr (Addr(..)) import Data.Primitive.PrimArray.Offset (MutablePrimArrayOffset) import GHC.Exts (Ptr(Ptr),RealWorld,proxy#) import Posix.Socket (SocketAddressInternet) import Socket (Connectedness(..),Family(..),Version(..),Interruptibility(Uninterruptible))+import Socket (Pinnedness(Pinned)) import Socket.Datagram (Socket(..),SendException,ReceiveException) import Socket.IPv4 (Peer,newIPv4Slab,IPv4Slab(..)) import Socket.Discard (PeerlessSlab(..),newPeerlessSlab) import Socket.Interop (fromPinned) +import qualified Socket.Discard import qualified Data.Primitive as PM import qualified Socket.Datagram.Uninterruptible.MutableBytes.Receive.Connected as CR import qualified Socket.Datagram.Uninterruptible.Addr.Send.Connected as CS import qualified Socket.Datagram.Uninterruptible.Addr.Send.IPv4 as V4S import qualified Socket.Datagram.Uninterruptible.MutableBytes.Receive.IPv4 as V4R+import qualified Socket.Datagram.Uninterruptible.MutableBytes.Many as MM -- | Send a datagram using a socket with a pre-designated peer. This -- refers to a datagram socket for which POSIX @connect@ has locked@@ -88,3 +94,26 @@ V4R.receive proxy# sock (MutableBytes marr 0 len) addr >>= \case Left err -> pure (Left err) Right sz -> pure $! Right $! fromPinned marr 0 sz++-- | Receive multiple datagrams at the same time. This uses @recvmmsg@+-- on platforms that support it. This function is provided as a convenience+-- for users of the @bytestring@ library. A slightly more allocation-friendly+-- approach to receiving datagrams as bytestrings is to make use of+-- 'Socket.Datagram.Uninterruptible.Bytes.receiveManyPinned' from+-- @Socket.Datagram.Uninterruptible.Bytes@. Performing the conversion+-- from pinned 'ByteArray' to 'ByteString' while folding over the result+-- array may lead to fewer allocations depending on how the 'ByteString's+-- are used.+receiveMany ::+ Socket c a -- ^ Socket+ -> PeerlessSlab 'Pinned -- ^ Buffers for reception+ -> Int -- ^ Maximum size of single datagram+ -> IO (Either (ReceiveException 'Uninterruptible) (SmallArray ByteString))+receiveMany sock slab maxSz = do+ Socket.Discard.replenishPinnedPeerlessSlab slab maxSz+ MM.receiveMany sock slab >>= \case+ Left err -> pure (Left err)+ Right n -> do+ arr <- Socket.Discard.freezePeerlessSlabAsByteString slab n+ pure (Right arr)+
src/Socket/Datagram/Uninterruptible/Bytes.hs view
@@ -11,16 +11,22 @@ ( -- * Send send , sendToIPv4+ , trySend -- * Receive , receive , receiveFromIPv4+ , tryReceive -- * Receive Many , receiveMany+ , receiveManyPinned , receiveManyFromIPv4+ , receiveManyPinnedFromIPv4 -- * Types+ , Pinnedness(..) , Message(..) , Peer(..) , ReceiveException(..)+ , SendException(..) -- * Slabs -- ** Types , PeerlessSlab(..)@@ -36,10 +42,11 @@ import Data.Primitive.PrimArray.Offset (MutablePrimArrayOffset(..)) import GHC.Exts (proxy#) import Socket (Connectedness(..),Family(..),Version(..),Interruptibility(Uninterruptible))+import Socket (Pinnedness(Pinned,Unpinned)) import Socket.Address (posixToIPv4Peer)-import Socket.Datagram (Socket(..),SendException,ReceiveException(..))+import Socket.Datagram (Socket(..),SendException(..),ReceiveException(..)) import Socket.IPv4 (Peer(..),Message(..),IPv4Slab(..),freezeIPv4Slab)-import Socket.IPv4 (newIPv4Slab)+import Socket.IPv4 (newIPv4Slab,replenishIPv4Slab,replenishPinnedIPv4Slab) import Socket.Discard (PeerlessSlab(..),newPeerlessSlab) import qualified Data.Primitive as PM@@ -60,6 +67,15 @@ send (Socket !sock) !buf = CS.send proxy# () sock buf +-- | Variant of 'send' that never blocks. If the datagram cannot+-- immidiately be copied to the send queue, returns @False@.+trySend ::+ Socket 'Connected a -- ^ Socket with designated peer+ -> Bytes -- ^ Slice of a buffer+ -> IO (Either (SendException 'Uninterruptible) Bool)+trySend (Socket !sock) !buf =+ CS.attemptSend () sock buf+ sendToIPv4 :: Socket 'Unconnected ('Internet 'V4) -- ^ IPv4 socket without designated peer -> Peer -- ^ Destination@@ -83,6 +99,22 @@ pure (Right r) Left err -> pure (Left err) +-- | Variant of 'receive' that never blocks. If a datagram is not+-- immidiately available on the receive queue, returns @Nothing@.+tryReceive :: + Socket c a -- ^ Socket+ -> Int -- ^ Maximum datagram size+ -> IO (Either (ReceiveException 'Uninterruptible) (Maybe ByteArray))+tryReceive (Socket !sock) !maxSz = do+ buf <- PM.newByteArray maxSz+ CR.receiveAttempt sock (MutableBytes buf 0 maxSz) () >>= \case+ Right msz -> case msz of+ Nothing -> pure (Right Nothing)+ Just sz -> do+ r <- PM.resizeMutableByteArray buf sz >>= PM.unsafeFreezeByteArray+ pure (Right (Just r))+ Left err -> pure (Left err)+ receiveFromIPv4 :: Socket 'Unconnected ('Internet 'V4) -- ^ IPv4 socket without designated peer -> Int -- ^ Maximum datagram size@@ -97,22 +129,65 @@ pure (Right (Message (posixToIPv4Peer posixAddr) r)) Left err -> pure (Left err) +-- | Receive multiple datagrams at the same time, returning the peers'+-- addresses as well. This uses @recvmmsg@ on platforms that support it. receiveManyFromIPv4 :: Socket 'Unconnected ('Internet 'V4) -- ^ Socket- -> Socket.IPv4.IPv4Slab -- ^ Buffers for reception+ -> Socket.IPv4.IPv4Slab 'Unpinned -- ^ Buffers for reception+ -> Int -- ^ Maximum size of single datagram -> IO (Either (ReceiveException 'Uninterruptible) (SmallArray Message))-receiveManyFromIPv4 sock slab = do+receiveManyFromIPv4 sock slab maxSz = do+ replenishIPv4Slab slab maxSz MM.receiveManyFromIPv4 sock slab >>= \case Left err -> pure (Left err) Right n -> do arr <- Socket.IPv4.freezeIPv4Slab slab n pure (Right arr) -receiveMany ::+-- | Receive multiple datagrams at the same time, returning the peers'+-- addresses as well. This uses @recvmmsg@ on platforms that support it.+-- The byte arrays in the resulting 'Message's are pinned.+receiveManyPinnedFromIPv4 :: Socket 'Unconnected ('Internet 'V4) -- ^ Socket- -> Socket.Discard.PeerlessSlab -- ^ Buffers for reception+ -> Socket.IPv4.IPv4Slab 'Pinned -- ^ Buffers for reception+ -> Int -- ^ Maximum size of single datagram+ -> IO (Either (ReceiveException 'Uninterruptible) (SmallArray Message))+receiveManyPinnedFromIPv4 sock slab maxSz = do+ replenishPinnedIPv4Slab slab maxSz+ MM.receiveManyFromIPv4 sock slab >>= \case+ Left err -> pure (Left err)+ Right n -> do+ arr <- Socket.IPv4.freezeIPv4Slab slab n+ pure (Right arr)++-- | Receive multiple datagrams at the same time. This uses @recvmmsg@+-- on platforms that support it.+receiveMany ::+ Socket c a -- ^ Socket+ -> Socket.Discard.PeerlessSlab 'Unpinned -- ^ Buffers for reception+ -> Int -- ^ Maximum size of single datagram -> IO (Either (ReceiveException 'Uninterruptible) (UnliftedArray ByteArray))-receiveMany sock slab = do+receiveMany sock slab maxSz = do+ Socket.Discard.replenishPeerlessSlab slab maxSz+ -- TODO: use maxSz in receiveMany. Not terribly+ -- important for the time being.+ MM.receiveMany sock slab >>= \case+ Left err -> pure (Left err)+ Right n -> do+ arr <- Socket.Discard.freezePeerlessSlab slab n+ pure (Right arr)++-- | Receive multiple datagrams at the same time. This uses @recvmmsg@+-- on platforms that support it. All resulting byte arrays are pinned.+receiveManyPinned ::+ Socket c a -- ^ Socket+ -> Socket.Discard.PeerlessSlab 'Pinned -- ^ Buffers for reception+ -> Int -- ^ Maximum size of single datagram+ -> IO (Either (ReceiveException 'Uninterruptible) (UnliftedArray ByteArray))+receiveManyPinned sock slab maxSz = do+ Socket.Discard.replenishPinnedPeerlessSlab slab maxSz+ -- TODO: use maxSz in receiveMany. Not terribly+ -- important for the time being. MM.receiveMany sock slab >>= \case Left err -> pure (Left err) Right n -> do
src/Socket/Datagram/Uninterruptible/MutableBytes.hs view
@@ -77,4 +77,3 @@ -> IO (Either (ReceiveException 'Uninterruptible) Int) receiveFromIPv4 (Socket !sock) !buf !addr = V4R.receive proxy# sock buf addr-
+ src/Socket/Datagram/Unix/Connected.hs view
@@ -0,0 +1,162 @@+{-# language BangPatterns #-}+{-# language DataKinds #-}+{-# language DeriveAnyClass #-}+{-# language DerivingStrategies #-}+{-# language DuplicateRecordFields #-}+{-# language LambdaCase #-}+{-# language MagicHash #-}+{-# language NamedFieldPuns #-}+{-# language UnboxedTuples #-}+{-# language ScopedTypeVariables #-}+{-# language TypeApplications #-}++-- | Unix-domain datagram sockets with a fixed destination.+module Socket.Datagram.Unix.Connected+ ( -- * Types+ Socket(..)+ , Family(..)+ , Connectedness(..)+ , UnixAddress(..)+ , Message(..)+ -- * Establish+ , withSocket+ , withPair+ , connect+ , open+ , openPair+ , close+ -- * Exceptions+ , SocketException(..)+ , ConnectException(..)+ , SD.ReceiveException(..)+ , SD.SendException(..)+ -- * Examples+ -- $examples+ ) where++import Control.Exception (mask,onException)+import Data.Coerce (coerce)+import Data.Primitive (ByteArray)+import Foreign.C.Error (Errno(..),eNOENT,ePROTOTYPE)+import Foreign.C.Error (eNFILE,eMFILE,eCONNREFUSED)+import Socket (Connectedness(..),Family(..))+import Socket.Datagram (Socket(..))+import Socket (SocketException(..))+import Socket.Stream (ConnectException(..))+import Socket.Error (die)+import Socket.IPv4 (Message(..))+import Socket.Datagram.Common (close)+import Socket (Interruptibility(Uninterruptible))+import System.Posix.Types (Fd)++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.Datagram as SD+import qualified Socket.EventManager as EM+import qualified Socket.Pair as Pair++newtype UnixAddress = UnixAddress ByteArray++-- | Unbracketed function for opening a socket. Be careful with+-- this function. The resulting socket is a UNIX-domain datagram socket+-- on which @connect@ was called without first calling @bind@. This means+-- that it corresponds neither to an entry in the filesystem nor an entry+-- in the abstract socket namespace.+open ::+ UnixAddress+ -> IO (Either (ConnectException 'Unix 'Uninterruptible) (Socket 'Connected 'SCK.Unix))+open (UnixAddress remote) = do+ -- TODO: This is somewhat copied from the internet-domain+ -- socket code+ e1 <- S.uninterruptibleSocket S.Unix+ (L.applySocketFlags (L.closeOnExec <> L.nonblocking) S.datagram)+ S.defaultProtocol+ case e1 of+ Left err -> handleSocketException err+ Right fd -> do+ let !mngr = EM.manager+ EM.register mngr fd+ let sockAddr = id+ $ S.encodeSocketAddressUnix+ $ S.SocketAddressUnix+ $ remote+ S.uninterruptibleConnect fd sockAddr >>= \case+ Left err -> handleConnectException err+ Right (_ :: ()) -> pure (Right (Socket 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 (Socket 'Connected 'SCK.Unix, Socket 'Connected 'SCK.Unix))+openPair = coerce+ @(IO (Either SocketException (Fd,Fd)))+ @(IO (Either SocketException (Socket 'Connected 'SCK.Unix, Socket 'Connected 'SCK.Unix)))+ (Pair.open S.sequencedPacket)++-- | Run a callback that requires a pair of connected+-- datagram sockets. The sockets will be closed when the+-- callback completes.+withPair ::+ (Socket 'Connected 'SCK.Unix -> Socket 'Connected 'SCK.Unix -> IO a)+ -- ^ Callback providing the connected datagram sockets+ -> IO (Either SocketException a)+withPair f = mask $ \restore -> openPair >>= \case+ Left err -> pure (Left err)+ Right (Socket fdA, Socket fdB) -> do+ a <- onException+ (restore (f (Socket fdA) (Socket 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)++withSocket ::+ UnixAddress+ -- ^ Peer address (to connect to)+ -> (Socket 'Connected 'SCK.Unix -> IO a)+ -- ^ Callback providing the socket and the chosen port+ -> IO (Either (ConnectException 'Unix 'Uninterruptible) a)+withSocket !peer f = mask $ \restore -> open peer >>= \case+ Left err -> pure (Left err)+ Right (Socket fd) -> do+ a <- onException (restore (f (Socket fd))) (S.uninterruptibleErrorlessClose fd)+ S.uninterruptibleClose fd >>= \case+ Left err -> die ("Socket.Datagram.Unix.Connected.close: " ++ describeErrorCode err)+ Right _ -> pure (Right a)++-- | Set the endpoint to connect to.+connect ::+ UnixAddress+ -- ^ Peer address+ -> Socket 'Connected 'SCK.Unix+ -- ^ Unix-domain datagram socket+ -> IO (Either (ConnectException 'Unix 'Uninterruptible) ())+connect (UnixAddress remote) (Socket fd) = do+ let sockAddr = id+ $ S.encodeSocketAddressUnix+ $ S.SocketAddressUnix+ $ remote+ S.uninterruptibleConnect fd sockAddr >>= \case+ Left err -> handleConnectException err+ Right (_ :: ()) -> pure (Right ())++describeErrorCode :: Errno -> String+describeErrorCode err@(Errno e) = "error code " ++ D.string err ++ " (" ++ show e ++ ")"++handleSocketException :: Errno -> IO (Either (ConnectException 'Unix i) a)+handleSocketException e+ | e == eMFILE = pure (Left ConnectFileDescriptorLimit)+ | e == eNFILE = pure (Left ConnectFileDescriptorLimit)+ | otherwise = die+ ("Socket.Datagram.Unix.Connected.socket: " ++ 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)
+ src/Socket/Datagram/Unix/Unconnected.hs view
@@ -0,0 +1,107 @@+{-# language BangPatterns #-}+{-# language DataKinds #-}+{-# language DeriveAnyClass #-}+{-# language DerivingStrategies #-}+{-# language DuplicateRecordFields #-}+{-# language LambdaCase #-}+{-# language MagicHash #-}+{-# language NamedFieldPuns #-}+{-# language UnboxedTuples #-}++-- | Internet datagram sockets without a fixed destination.+module Socket.Datagram.Unix.Unconnected+ ( -- * Types+ Socket(..)+ , Family(..)+ , Connectedness(..)+ , UnixAddress(..)+ , Message(..)+ -- * Establish+ , withSocket+ , open+ , close+ -- * Exceptions+ , SocketException(..)+ ) where++import Control.Exception (mask,onException)+import Foreign.C.Error (Errno(..),eACCES)+import Foreign.C.Error (eNFILE,eMFILE,eADDRINUSE)+import Socket (Connectedness(..),Family(..))+import Socket.Datagram (Socket(..))+import Socket.Datagram (SocketException(..))+import Socket.IPv4 (Message(..))+import Socket.Error (die)+import Socket.Datagram.Common (close)+import Socket.Datagram.Unix.Connected (UnixAddress(..))++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++open ::+ UnixAddress+ -- ^ Address and port to use+ -> IO (Either SocketException (Socket 'Unconnected 'Unix))+open (UnixAddress path) = do+ e1 <- S.uninterruptibleSocket S.Unix+ (L.applySocketFlags (L.closeOnExec <> L.nonblocking) S.datagram)+ S.defaultProtocol+ case e1 of+ Left err -> handleSocketException err+ Right fd -> do+ let !mngr = EM.manager+ EM.register mngr fd+ let sockAddr = id+ $ S.encodeSocketAddressUnix+ $ S.SocketAddressUnix+ $ path+ e2 <- S.uninterruptibleBind fd sockAddr+ case e2 of+ Left err -> do+ -- We intentionally discard any exceptions thrown by close.+ -- There is simply nothing that can be done with them.+ S.uninterruptibleErrorlessClose fd+ handleBindException err+ Right _ -> pure (Right (Socket fd))++-- | Open a socket and run the supplied callback on it. This closes the socket+-- when the callback finishes or when an exception is thrown. Do not return +-- the socket from the callback. This leads to undefined behavior. If the+-- address @0.0.0.0@ is used, the socket receives on all network interfaces.+-- If the port 0 is used, an unused port is chosen by the operating system.+-- The callback provides the chosen port (or if the user specified a non-zero+-- port, the chosen port will be that value).+withSocket ::+ UnixAddress+ -- ^ Address and port to use+ -> (Socket 'Unconnected ('SCK.Internet 'SCK.V4) -> IO a)+ -- ^ Callback providing the socket and the chosen port+ -> IO (Either SocketException a)+withSocket local f = mask $ \restore -> open local >>= \case+ Left err -> pure (Left err)+ Right (Socket fd) -> do+ a <- onException (restore (f (Socket fd))) (S.uninterruptibleErrorlessClose fd)+ S.uninterruptibleClose fd >>= \case+ Left err -> die ("Socket.Datagram.IPv4.Unconnected.close: " ++ describeErrorCode err)+ Right _ -> pure (Right a)++describeErrorCode :: Errno -> String+describeErrorCode err@(Errno e) = "error code " ++ D.string err ++ " (" ++ show e ++ ")"++handleSocketException :: Errno -> IO (Either SocketException a)+handleSocketException e+ | e == eMFILE = pure (Left SocketFileDescriptorLimit)+ | e == eNFILE = pure (Left SocketFileDescriptorLimit)+ | otherwise = die+ ("Socket.Datagram.IPv4.Undestined.socket: " ++ describeErrorCode e)++handleBindException :: Errno -> IO (Either SocketException a)+handleBindException !e+ | e == eACCES = pure (Left SocketPermissionDenied)+ | e == eADDRINUSE = pure (Left SocketAddressInUse)+ | otherwise = die+ ("Socket.Datagram.IPv4.Undestined.bind: " ++ describeErrorCode e)+
+ src/Socket/Pair.hs view
@@ -0,0 +1,42 @@+{-# language BangPatterns #-}++module Socket.Pair+ ( open+ ) where++import Foreign.C.Error (Errno(..))+import Foreign.C.Error (eNFILE,eMFILE)+import Socket (SocketException(..))+import Socket.Error (die)+import System.Posix.Types (Fd)++import qualified Foreign.C.Error.Describe as D+import qualified Linux.Socket as L+import qualified Posix.Socket as S+import qualified Socket.EventManager as EM++-- Helper for opening a connected unix-domain socket pair. This is+-- used by Socket.Datagram.Unix.Connected and by Socket.SequencedPacket.Unix.+-- It could be used by Socket.Stream.Unix if that module is ever written.+open :: S.Type -> IO (Either SocketException (Fd, Fd))+open !typ = do+ e1 <- S.uninterruptibleSocketPair S.Unix+ (L.applySocketFlags (L.closeOnExec <> L.nonblocking) typ)+ S.defaultProtocol+ case e1 of+ Left err -> handleSocketException err+ Right (fdA,fdB) -> do+ let !mngr = EM.manager+ EM.register mngr fdA+ EM.register mngr fdB+ pure (Right (fdA, fdB))++handleSocketException :: Errno -> IO (Either SocketException a)+handleSocketException e+ | e == eMFILE = pure (Left SocketFileDescriptorLimit)+ | e == eNFILE = pure (Left SocketFileDescriptorLimit)+ | otherwise = die+ ("Socket.Pair.open: " ++ describeErrorCode e)++describeErrorCode :: Errno -> String+describeErrorCode err@(Errno e) = "error code " ++ D.string err ++ " (" ++ show e ++ ")"
+ src/Socket/SequencedPacket/Uninterruptible/Bytes.hs view
@@ -0,0 +1,81 @@+{-# language BangPatterns #-}+{-# language DataKinds #-}+{-# language LambdaCase #-}+{-# language MagicHash #-}++module Socket.SequencedPacket.Uninterruptible.Bytes+ ( send+ , trySend+ , receive+ , tryReceive+ ) where++import Socket.SequencedPacket.Unix (Connection(..))+import Data.Primitive (ByteArray)+import Data.Bytes.Types (Bytes,MutableBytes(MutableBytes))+import GHC.Exts (proxy#)+import Socket (Interruptibility(Uninterruptible))+import Socket.SequencedPacket (SendException,ReceiveException)+import Socket.SequencedPacket.Unix (Connection(..))++import qualified Data.Primitive as PM+import qualified Socket.SequencedPacket.Uninterruptible.Bytes.Send as Send+import qualified Socket.SequencedPacket.Uninterruptible.MutableBytes.Receive as Receive++-- Sadly, SEQPACKET sockets are poorly documented and, in Linux,+-- behave inconsisently depending on whether the socket is+-- internet-domain or unix-domain. More thorough coverage of+-- this is found at https://stackoverflow.com/q/3595684+--+-- Since internet-domain SEQPACKET sockets are rare and unsupported+-- by most hardware, we only support unix-domain sockets. On Linux,+-- we can do this by just reusing the send and receive functions for+-- datagram sockets.++-- | Send a message over a connection.+send ::+ Connection -- ^ Connection+ -> Bytes -- ^ Slice of a buffer+ -> IO (Either (SendException 'Uninterruptible) ())+send (Connection !sock) !buf = Send.send proxy# sock buf++-- | Variant of 'send' that never blocks. If the datagram cannot+-- immidiately be copied to the send queue, returns @False@.+trySend ::+ Connection -- ^ Connection+ -> Bytes -- ^ Slice of a buffer+ -> IO (Either (SendException 'Uninterruptible) Bool)+trySend (Connection !sock) !buf = Send.attemptSend sock buf++-- | Receive a message.+receive ::+ Connection -- ^ Connection+ -> Int -- ^ Maximum number of bytes to receive+ -> IO (Either (ReceiveException 'Uninterruptible) ByteArray)+{-# inline receive #-}+receive (Connection conn) !n = do+ !marr0 <- PM.newByteArray n+ Receive.receive 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 message. The message may be truncated if the size+-- of it is larger than the maximum allowed size.+tryReceive ::+ Connection -- ^ Connection+ -> Int -- ^ Maximum message size+ -> IO (Either (ReceiveException 'Uninterruptible) (Maybe ByteArray))+tryReceive (Connection !conn) !n = do+ !marr0 <- PM.newByteArray n+ Receive.receiveAttempt conn (MutableBytes marr0 0 n) >>= \case+ Left err -> pure (Left err)+ Right Nothing -> pure (Right Nothing)+ Right (Just sz) -> do+ marr1 <- PM.resizeMutableByteArray marr0 sz+ !arr <- PM.unsafeFreezeByteArray marr1+ pure $! Right $! Just $! arr++
+ src/Socket/SequencedPacket/Unix.hs view
@@ -0,0 +1,417 @@+{-# 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)
src/Socket/Stream/IPv4.hs view
@@ -1,6 +1,8 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE UnboxedTuples #-} {-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-}@@ -8,15 +10,18 @@ {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-} module Socket.Stream.IPv4 ( -- * Types- Listener+ Listener(..) , Connection(..) , Peer(..) -- * Bracketed -- $brackeded , withListener+ , withListenerReuse , withAccepted , withConnection , forkAccepted@@ -29,6 +34,7 @@ , SocketException(..) , AcceptException(..) , CloseException(..)+ , SystemdException(..) -- * Type Arguments , Interruptibility(..) , Family(..)@@ -36,14 +42,20 @@ -- * Unbracketed -- $unbracketed , listen+ , listenReuse , unlisten , unlisten_ , connect- -- , interruptibleConnect+ , interruptibleConnect+ , connectOnDevice+ , interruptibleConnectOnDevice+ , systemdListener , disconnect , disconnect_ , accept , interruptibleAccept+ -- * Shutdown+ , shutdown ) where import Control.Concurrent (ThreadId)@@ -51,12 +63,14 @@ import Control.Exception (mask, mask_, onException, throwIO) import Control.Monad.STM (atomically) import Control.Concurrent.STM (TVar,modifyTVar')+import Data.Coerce (coerce) import Data.Word (Word16) import Foreign.C.Error (Errno(..), eAGAIN, eINPROGRESS, eWOULDBLOCK, eNOTCONN) import Foreign.C.Error (eADDRINUSE,eHOSTUNREACH) import Foreign.C.Error (eNFILE,eMFILE,eACCES,ePERM,eCONNABORTED) import Foreign.C.Error (eTIMEDOUT,eADDRNOTAVAIL,eNETUNREACH,eCONNREFUSED) import Foreign.C.Types (CInt)+import GHC.Exts (Int#) import Net.Types (IPv4(..)) import Socket (Interruptibility(..)) import Socket (SocketUnrecoverableException(..),Family(Internet),Version(V4))@@ -67,7 +81,8 @@ import Socket.Stream (ConnectException(..),SocketException(..),AcceptException(..)) import Socket.Stream (SendException(..),ReceiveException(..),CloseException(..)) import Socket.Stream (Connection(..))-import System.Posix.Types(Fd)+import Socket.Systemd (SystemdException(..),systemdListenerInternal)+import System.Posix.Types (Fd(Fd)) import qualified Control.Concurrent.STM as STM import qualified Data.Primitive as PM@@ -94,15 +109,31 @@ -- 'withListener' unless you are writing an integration with a -- resource-management library. listen :: Peer -> IO (Either SocketException (Listener, Word16))-listen endpoint@Peer{port = specifiedPort} = do+listen !peer = listenInternal 0# peer++-- | Variant of 'listen' that sets @SO_REUSEADDR@ on the socket before+-- binding.+listenReuse :: Peer -> IO (Either SocketException (Listener, Word16))+listenReuse !peer = listenInternal 1# peer++listenInternal :: Int# -> Peer -> IO (Either SocketException (Listener, Word16))+listenInternal reuse endpoint@Peer{port = specifiedPort} = do debug ("listen: opening listen " ++ describeEndpoint endpoint)- e1 <- S.uninterruptibleSocket S.internet+ e1 <- S.uninterruptibleSocket S.Internet (L.applySocketFlags (L.closeOnExec <> L.nonblocking) S.stream) S.defaultProtocol debug ("listen: opened listen " ++ describeEndpoint endpoint) case e1 of Left err -> handleSocketListenException SCK.functionWithListener err Right fd -> do+ case reuse of+ 0# -> pure ()+ _ -> do+ r <- S.uninterruptibleSetSocketOptionInt fd S.levelSocket S.reuseAddress 1+ case r of+ Left _ -> die "listenInternal: setsockopt SO_REUSEADDR failed"+ Right (_ :: ()) -> pure ()+ -- TODO: shave off an allocation by building the sockaddr in C. e2 <- S.uninterruptibleBind fd (S.encodeSocketAddressInternet (endpointToSocketAddressInternet endpoint)) debug ("listen: requested binding for listen " ++ describeEndpoint endpoint)@@ -176,7 +207,7 @@ Peer -> (Listener -> Word16 -> IO a) -> IO (Either SocketException a)-withListener endpoint f = mask $ \restore -> do+withListener !endpoint f = mask $ \restore -> do listen endpoint >>= \case Left err -> pure (Left err) Right (sck, actualPort) -> do@@ -186,6 +217,21 @@ unlisten sck pure (Right a) +-- | Variant of 'withListener' that sets @SO_REUSEADDR@ before binding.+withListenerReuse ::+ Peer+ -> (Listener -> Word16 -> IO a)+ -> IO (Either SocketException a)+withListenerReuse !endpoint f = mask $ \restore -> do+ listenReuse endpoint >>= \case+ Left err -> pure (Left err)+ Right (sck, actualPort) -> do+ a <- onException+ (restore (f sck actualPort))+ (unlisten_ sck)+ unlisten sck+ pure (Right a)+ -- | Listen for an inbound connection. accept :: Listener -> IO (Either (AcceptException 'Uninterruptible) (Connection,Peer)) accept (Listener !fd) = do@@ -247,13 +293,16 @@ -- TODO: pull these out of the loop let !mngr = EM.manager tv <- EM.reader mngr fd- token <- EM.interruptibleWait abandon tv+ token <- EM.interruptibleWaitCounting counter abandon tv if EM.isInterrupt token then pure (Left AcceptInterrupted) else waitlessAccept fd >>= \case Left merr -> case merr of Nothing -> do EM.unready token tv+ -- Decrement the connection counter if the notification+ -- from epoll was a false alarm.+ atomically (modifyTVar' counter (subtract 1)) interruptibleAcceptCounting counter abandon (Listener fd) Just err -> pure (Left err) Right r@(Connection conn,_) -> do@@ -284,11 +333,20 @@ SCK.functionWithAccepted [SCK.cgetsockname,SCK.socketAddressSize] +-- | Close the write channel. Any attempt to write to the connection after+-- calling this will fail.+shutdown :: Connection -> IO ()+shutdown (Connection fd) = do+ S.uninterruptibleShutdown fd S.write >>= \case+ Left err -> if err == eNOTCONN+ then pure ()+ else do+ S.uninterruptibleErrorlessClose fd+ die ("Socket.Stream.shutdown: " ++ describeErrorCode err)+ Right _ -> pure ()+ 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@@ -300,30 +358,38 @@ -- have since either way we become certain that the write channel -- is closed. Left err -> if err == eNOTCONN- then gracefulCloseB tv token0 fd+ then gracefulCloseB fd else do- _ <- S.uninterruptibleClose fd- -- TODO: What about ENOTCONN? Can this happen if the remote- -- side has already closed the connection?+ S.uninterruptibleErrorlessClose fd throwIO $ SocketUnrecoverableException moduleSocketStreamIPv4 SCK.functionGracefulClose [SCK.cshutdown,describeErrorCode err]- Right _ -> gracefulCloseB tv token0 fd+ Right _ -> gracefulCloseB 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+-- In the commit after 30c0037b99517b4e665aec33ac3a479fc892cb98,+-- gracefulCloseB was changed so that it does not wait for the peer to+-- shut down its side of the connection. Although this behavior was useful,+-- it made it possible for an unresponsive peer to hold open the connection.+-- The new behavior is to make a single nonblocking attempt to read from+-- the socket. If bytes are available, then we return a CloseException.+-- Otherwise, we return Right. This is less deterministic than the+-- previous approach (network latency affects the result), but the+-- exceptions returned here are really only ever used for logging+-- purposes anyway.+gracefulCloseB :: Fd -> IO (Either CloseException ())+gracefulCloseB !fd = do !buf <- PM.newByteArray 1 -- We do not actually want to remove the bytes from the- -- receive buffer, so we use MSG_PEEK. We are be certain+ -- receive buffer, so we use MSG_PEEK. We are certain -- to send a reset when a CloseException is reported.+ -- Retrospective: Why is MSG_PEEK important? Who cares if the+ -- bytes get eaten? The receive buffer is about to get axed anyway. S.uninterruptibleReceiveMutableByteArray fd buf 0 1 S.peek >>= \case- Left err1 -> if err1 == eWOULDBLOCK || err1 == eAGAIN+ Left err1 -> if err1 == eWOULDBLOCK || err1 == eAGAIN || err1 == eNOTCONN then do- token1 <- EM.persistentUnreadyAndWait token0 tv- gracefulCloseB tv token1 fd+ _ <- S.uninterruptibleClose fd+ pure (Right ()) else do _ <- S.uninterruptibleClose fd -- We treat all @recv@ errors except for the nonblocking@@ -470,17 +536,14 @@ -> Listener -- ^ Connection listener -> (Either CloseException () -> a -> IO ())- -- ^ Callback to handle an ungraceful close. + -- ^ Callback to handle an ungraceful close. This must not+ -- throw an exception. -> (Connection -> Peer -> IO a) -- ^ Callback to consume connection. Must not return the connection. -> IO (Either (AcceptException 'Interruptible) ThreadId) interruptibleForkAcceptedUnmasked !counter !abandon !lstn consumeException cb = mask_ $ interruptibleAcceptCounting counter abandon lstn >>= \case- Left e -> do- case e of- AcceptInterrupted -> pure ()- _ -> atomically (modifyTVar' counter (subtract 1))- pure (Left e)+ Left e -> pure (Left e) Right (conn, endpoint) -> fmap Right $ forkIOWithUnmask $ \unmask -> do a <- onException (unmask (cb conn endpoint))@@ -490,6 +553,49 @@ atomically (modifyTVar' counter (subtract 1)) pure r +-- | Variant of 'connect' that allows specifying the name of the local+-- interface that should be used for the connection. Uses @SO_BINDTODEVICE@.+connectOnDevice ::+ Peer -- ^ Remote endpoint+ -> PM.ByteArray -- ^ Local network interface name+ -> IO (Either (ConnectException ('Internet 'V4) 'Uninterruptible) Connection)+connectOnDevice !remote !intf = do+ beforeEstablishment remote >>= \case+ Left err -> pure (Left err)+ Right (fd,sockAddr) -> do+ r <- S.uninterruptibleSetSocketOptionByteArray+ fd S.levelSocket S.bindToDevice+ intf (fromIntegral @Int @CInt (PM.sizeofByteArray intf))+ -- We throw a bogus error here. TODO: fix this.+ case r of+ Left _ -> do+ S.uninterruptibleErrorlessClose fd+ pure (Left ConnectEphemeralPortsExhausted)+ Right (_ :: ()) -> prepareConnection fd sockAddr++-- | Variant of 'interruptibleConnect' that allows specifying the name of the local+-- interface that should be used for the connection. Uses @SO_BINDTODEVICE@.+interruptibleConnectOnDevice ::+ TVar Bool+ -- ^ Interrupted. If this becomes 'True', give up and return+ -- @'Left' 'AcceptInterrupted'@.+ -> Peer -- ^ Remote endpoint+ -> PM.ByteArray -- ^ Local network interface name+ -> IO (Either (ConnectException ('Internet 'V4) 'Interruptible) Connection)+interruptibleConnectOnDevice !abandon !remote !intf = do+ beforeEstablishment remote >>= \case+ Left err -> pure (Left err)+ Right (fd,sockAddr) -> do+ r <- S.uninterruptibleSetSocketOptionByteArray+ fd S.levelSocket S.bindToDevice+ intf (fromIntegral @Int @CInt (PM.sizeofByteArray intf))+ -- We throw a bogus error here. TODO: fix this.+ case r of+ Left _ -> do+ S.uninterruptibleErrorlessClose fd+ pure (Left ConnectEphemeralPortsExhausted)+ Right (_ :: ()) -> interruptiblePrepareConnection abandon fd sockAddr+ -- | Open a socket and connect to a peer. Requirements: -- -- * This function may only be called in contexts where exceptions@@ -510,60 +616,88 @@ connect !remote = do beforeEstablishment remote >>= \case Left err -> pure (Left err)- Right (fd,sockAddr) -> do- 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- tv <- EM.writer mngr fd- debug ("connect: about to connect, fd=" ++ show fd)- token0 <- STM.readTVarIO tv- S.uninterruptibleConnect fd sockAddr >>= \case- Left err2 -> if err2 == eINPROGRESS- then do- debug ("connect: EINPROGRESS, fd=" ++ show fd)- -- When we receive EINPROGRESS, we have high confidence- -- the that the socket is not yet ready (keeping in mind- -- that it could have somehow become ready right after- -- C's connect returned). Immidiately diving into- -- afterEstablishment would result in a syscall to- -- getsockopt. But what a waste that would be since it- -- would almost certainly return EAGAIN. So, instead,- -- we unready the write channel (taking the usual- -- precautions) and then await readiness.- token1 <- EM.unreadyAndWait token0 tv- afterEstablishment tv token1 fd- else do- debug ("connect: failed, fd=" ++ show fd)+ Right (fd,sockAddr) -> prepareConnection fd sockAddr++prepareConnection ::+ Fd+ -> S.SocketAddress+ -> IO (Either (ConnectException ('Internet 'V4) 'Uninterruptible) Connection)+prepareConnection !fd !sockAddr = do+ 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+ tv <- EM.writer mngr fd+ debug ("connect: about to connect, fd=" ++ show fd)+ token0 <- STM.readTVarIO tv+ S.uninterruptibleConnect fd sockAddr >>= \case+ Left err2 -> if err2 == eINPROGRESS+ then do+ debug ("connect: EINPROGRESS, fd=" ++ show fd)+ -- When we receive EINPROGRESS, we have high confidence+ -- the that the socket is not yet ready (keeping in mind+ -- that it could have somehow become ready right after+ -- C's connect returned). Immidiately diving into+ -- afterEstablishment would result in a syscall to+ -- getsockopt. But what a waste that would be since it+ -- would almost certainly return EAGAIN. So, instead,+ -- we unready the write channel (taking the usual+ -- precautions) and then await readiness.+ token1 <- EM.unreadyAndWait token0 tv+ afterEstablishment tv token1 fd+ else do+ debug ("connect: failed, fd=" ++ show fd)+ S.uninterruptibleErrorlessClose fd+ handleConnectException SCK.functionWithConnection err2+ Right _ -> do+ debug ("connect: succeeded immidiately, fd=" ++ show fd)+ afterEstablishment tv token0 fd++-- See the notes in prepareConnection+interruptiblePrepareConnection ::+ TVar Bool+ -> Fd+ -> S.SocketAddress+ -> IO (Either (ConnectException ('Internet 'V4) 'Interruptible) Connection)+interruptiblePrepareConnection !abandon !fd !sockAddr = do+ let !mngr = EM.manager+ EM.register mngr fd+ tv <- EM.writer mngr fd+ debug ("connect: about to connect, fd=" ++ show fd)+ token0 <- STM.readTVarIO tv+ S.uninterruptibleConnect fd sockAddr >>= \case+ Left err2 -> if err2 == eINPROGRESS+ then do+ debug ("connect: EINPROGRESS, fd=" ++ show fd)+ EM.unready token0 tv+ token1 <- EM.interruptibleWait abandon tv+ case EM.isInterrupt token1 of+ True -> do S.uninterruptibleErrorlessClose fd- handleConnectException SCK.functionWithConnection err2- Right _ -> do- debug ("connect: succeeded immidiately, fd=" ++ show fd)- afterEstablishment tv token0 fd+ pure (Left ConnectInterrupted)+ False -> interruptibleAfterEstablishment abandon tv token1 fd+ else do+ debug ("connect: failed, fd=" ++ show fd)+ S.uninterruptibleErrorlessClose fd+ handleConnectException SCK.functionWithConnection err2+ Right _ -> do+ debug ("connect: succeeded immidiately, fd=" ++ show fd)+ interruptibleAfterEstablishment abandon tv token0 fd -- | Variant of 'connect' that is interruptible using STM-style interrupts.--- interruptibleConnect ::--- TVar Bool--- -- ^ Interrupted. If this becomes 'True', give up and return--- -- @'Left' 'AcceptInterrupted'@.--- -> Peer--- -- ^ Remote endpoint--- -> IO (Either (ConnectException 'Interruptible) Connection)--- interruptibleConnect !abandon !remote = do--- beforeEstablishment remote >>= \case--- Left err -> pure (Left err)--- Right (fd,sockAddr) -> S.uninterruptibleConnect fd sockAddr >>= \case--- Left err2 -> if err2 == eINPROGRESS--- then do--- interruptibleWaitWrite abandon fd >>= \case--- True -> afterEstablishment fd--- False -> pure (Left ConnectInterrupted)--- else do--- S.uninterruptibleErrorlessClose fd--- handleConnectException SCK.functionWithConnection err2--- Right _ -> afterEstablishment fd+interruptibleConnect ::+ TVar Bool+ -- ^ Interrupted. If this becomes 'True', give up and return+ -- @'Left' 'AcceptInterrupted'@.+ -> Peer+ -- ^ Remote endpoint+ -> IO (Either (ConnectException ('Internet 'V4) 'Interruptible) Connection)+interruptibleConnect !abandon !remote = do+ beforeEstablishment remote >>= \case+ Left err -> pure (Left err)+ Right (fd,sockAddr) -> interruptiblePrepareConnection abandon fd sockAddr -- Internal function called by both connect and interruptibleConnect -- before the connection is established. Creates the socket and prepares@@ -572,7 +706,7 @@ {-# INLINE beforeEstablishment #-} beforeEstablishment !remote = do debug ("beforeEstablishment: opening connection " ++ show remote)- e1 <- S.uninterruptibleSocket S.internet+ e1 <- S.uninterruptibleSocket S.Internet (L.applySocketFlags (L.closeOnExec <> L.nonblocking) S.stream) S.defaultProtocol debug ("beforeEstablishment: opened connection " ++ show remote)@@ -624,6 +758,48 @@ functionWithListener [SCK.cgetsockopt,connectErrorOptionValueSize] +interruptibleAfterEstablishment ::+ TVar Bool+ -> TVar EM.Token -- token tvar + -> EM.Token -- old token+ -> Fd+ -> IO (Either (ConnectException ('Internet 'V4) 'Interruptible) Connection)+interruptibleAfterEstablishment !abandon !tv !oldToken !fd = do+ debug ("interruptibleAfterEstablishment: finished waiting, fd=" ++ show fd)+ e <- S.uninterruptibleGetSocketOption fd+ S.levelSocket S.optionError (intToCInt (PM.sizeOf (undefined :: CInt)))+ case e of+ Left err -> do+ S.uninterruptibleErrorlessClose fd+ throwIO $ SocketUnrecoverableException+ moduleSocketStreamIPv4+ functionWithListener+ [SCK.cgetsockopt,describeErrorCode err]+ Right (sz,S.OptionValue val) -> if sz == intToCInt (PM.sizeOf (undefined :: CInt))+ then+ let err = PM.indexByteArray val 0 :: CInt in+ if | err == 0 -> do+ debug ("interruptibleAfterEstablishment: connection established, fd=" ++ show fd)+ pure (Right (Connection fd))+ | Errno err == eAGAIN || Errno err == eWOULDBLOCK -> do+ debug ("interruptibleAfterEstablishment: not ready yet, unreadying token and waiting, fd=" ++ show fd)+ EM.unready oldToken tv+ newToken <- EM.interruptibleWait abandon tv+ case EM.isInterrupt newToken of+ True -> do+ S.uninterruptibleErrorlessClose fd+ pure (Left ConnectInterrupted)+ False -> interruptibleAfterEstablishment abandon tv newToken fd+ | otherwise -> do+ S.uninterruptibleErrorlessClose fd+ handleConnectException SCK.functionWithConnection (Errno err)+ else do+ S.uninterruptibleErrorlessClose fd+ throwIO $ SocketUnrecoverableException+ moduleSocketStreamIPv4+ functionWithListener+ [SCK.cgetsockopt,connectErrorOptionValueSize]+ -- | 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@@ -759,8 +935,7 @@ ("Socket.Stream.IPv4.bindListen: " ++ describeErrorCode e) -- These are the exceptions that can happen as a result--- of calling @socket@ with the intent of using the socket--- to open a connection (not listen for inbound connections).+-- of calling @accept@. handleAcceptException :: Errno -> IO (Either (Maybe (AcceptException i)) a) handleAcceptException e | e == eAGAIN = pure (Left Nothing)@@ -773,6 +948,20 @@ connectErrorOptionValueSize :: String connectErrorOptionValueSize = "incorrectly sized value of SO_ERROR option"++-- | Retrieve a listener that systemd has passed to the process. This+-- may only be called once. Do not call 'unlisten' or 'unlisten_' on this+-- listener before exiting the application. There is no bracketed variant+-- of this function because the listener is expected to remain open for+-- the remainder of the application.+--+-- There are several reasons this function may return @Left@:+--+-- * @sd_listen_fds@ returned more than one file descriptor+-- * @sd_is_socket@ found that the file descriptor was not a socket or+-- that it was a socket that was not in listening mode.+systemdListener :: IO (Either SystemdException Listener)+systemdListener = coerce (systemdListenerInternal S.Internet) {- $bracketed
src/Socket/Stream/Interruptible/Bytes.hs view
@@ -6,6 +6,7 @@ ( send , receiveExactly , receiveOnce+ , receiveOncePrepended , receiveBetween ) where @@ -16,6 +17,7 @@ import Socket (Interruptibility(Interruptible)) import qualified Data.Primitive as PM+import qualified Data.Bytes as Bytes import qualified Socket.Stream.Interruptible.Bytes.Send as Send import qualified Socket.Stream.Interruptible.MutableBytes.Receive as Receive @@ -64,6 +66,28 @@ Left err -> pure (Left err) Right sz -> do marr1 <- PM.resizeMutableByteArray marr0 sz+ !arr <- PM.unsafeFreezeByteArray marr1+ pure $! Right $! arr++-- | Variant of 'receiveOnce' that returns prepends the received bytes+-- with a prefix. This can be used to attach feed leftovers back in when+-- processing line-oriented protocols.+receiveOncePrepended ::+ TVar Bool + -- ^ Interrupt. On 'True', give up and return @'Left' 'ReceiveInterrupted'@.+ -> Connection -- ^ Connection+ -> Bytes -- ^ Prefix to prepend to the received bytes+ -> Int -- ^ Maximum number of bytes to receive+ -> IO (Either (ReceiveException 'Interruptible) ByteArray)+{-# inline receiveOncePrepended #-}+receiveOncePrepended !tv !conn !prefix !n = do+ let !ix0 = Bytes.length prefix+ !marr0 <- PM.newByteArray (n + ix0)+ Bytes.unsafeCopy marr0 0 prefix+ Receive.receiveOnce tv conn (MutableBytes marr0 ix0 n) >>= \case+ Left err -> pure (Left err)+ Right sz -> do+ marr1 <- PM.resizeMutableByteArray marr0 (sz + ix0) !arr <- PM.unsafeFreezeByteArray marr1 pure $! Right $! arr
src/Socket/Stream/Uninterruptible/Bytes.hs view
@@ -12,8 +12,10 @@ -- be zero and the length would be the length of the 'ByteArray' -- payload. module Socket.Stream.Uninterruptible.Bytes- ( send+ ( -- * Send+ send , sendMany+ -- * Receive , receiveExactly , receiveOnce , receiveBetween
src/Socket/Stream/Uninterruptible/MutableBytes.hs view
@@ -3,17 +3,23 @@ {-# language MagicHash #-} module Socket.Stream.Uninterruptible.MutableBytes- ( send+ ( -- * Send+ send+ , sendMany+ -- * Receive , receiveExactly , receiveOnce , receiveBetween ) where import Data.Bytes.Types (MutableBytes)+import Data.Primitive (MutableByteArray)+import Data.Primitive.Unlifted.Array (UnliftedArray(..)) import GHC.Exts (RealWorld,proxy#) import Socket.Stream (Connection,ReceiveException,SendException) import Socket (Interruptibility(Uninterruptible)) +import qualified Socket.Stream.Uninterruptible.Bytes as SSUB import qualified Socket.Stream.Uninterruptible.MutableBytes.Send as Send import qualified Socket.Stream.Uninterruptible.MutableBytes.Receive as Receive @@ -25,6 +31,22 @@ -> IO (Either (SendException 'Uninterruptible) ()) {-# inline send #-} send = Send.send proxy#++-- | 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 (MutableByteArray RealWorld) -- ^ Byte arrays+ -> IO (Either (SendException 'Uninterruptible) ())+{-# inline sendMany #-}+sendMany conn (UnliftedArray bufs) =+ -- Effectively, what we are doing here is unsafely coercing the+ -- buffer from mutable to immutable. This ends up being sound+ -- in this context because SSUB.sendMany does not preserve any+ -- referenced to its argument byte arrays.+ SSUB.sendMany conn (UnliftedArray bufs) -- | Receive a number of bytes exactly equal to the length of the -- buffer slice. If needed, this may call @recv@ repeatedly until
+ src/Socket/Stream/Unix.hs view
@@ -0,0 +1,361 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE LambdaCase #-}++module Socket.Stream.Unix+ ( -- * Types+ Listener(..)+ , Connection(..)+ , UnixAddress(..)+ -- * Bracketed+ , withListener+ , withAccepted+ -- * Exceptions+ , SendException(..)+ , ReceiveException(..)+ , ConnectException(..)+ , SocketException(..)+ , AcceptException(..)+ , CloseException(..)+ , SystemdException(..)+ -- * Bracketed+ , interruptibleForkAcceptedUnmasked+ -- * Unbracketed+ -- $unbracketed+ , listen+ , unlisten+ , unlisten_+ , disconnect+ , disconnect_+ , accept+ , systemdListener+ ) where++import Control.Concurrent (ThreadId,forkIOWithUnmask)+import Control.Concurrent.STM (TVar,modifyTVar',atomically)+import Control.Exception (mask, mask_, onException)+import Data.Coerce (coerce)+import Foreign.C.Error (Errno(..), eAGAIN, eWOULDBLOCK, eNOTCONN)+import Foreign.C.Error (eADDRINUSE)+import Foreign.C.Error (eNFILE,eMFILE,eACCES,ePERM,eCONNABORTED)+import Socket.Datagram.Unix.Connected (UnixAddress(..))+import Socket.Error (die)+import Socket.Stream (SocketException(..),AcceptException(..),ConnectException(..))+import Socket.Stream (Connection(..))+import Socket.Stream (SendException(..),ReceiveException(..),CloseException(..))+import System.Posix.Types (Fd(Fd))+import Socket (Interruptibility(..))+import Socket.Systemd (SystemdException(..),systemdListenerInternal)+import qualified Control.Concurrent.STM as STM+import qualified Foreign.C.Error.Describe as D+import qualified Socket.EventManager as EM+import qualified Posix.Socket as S+import qualified Linux.Socket as L+import qualified Data.Primitive as PM++-- | A socket that listens for incomming connections.+newtype Listener = Listener Fd++-- | 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 SocketException Listener)+listen (UnixAddress path) = do+ e1 <- S.uninterruptibleSocket S.Unix+ (L.applySocketFlags (L.closeOnExec <> L.nonblocking) S.stream)+ S.defaultProtocol+ case e1 of+ Left err -> handleSocketListenException err+ Right fd -> do+ let sockAddr = id+ $ S.encodeSocketAddressUnix+ $ S.SocketAddressUnix+ $ path+ e2 <- S.uninterruptibleBind fd sockAddr+ case e2 of+ Left err -> do+ _ <- S.uninterruptibleClose 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+ let !mngr = EM.manager+ EM.register mngr fd+ pure (Right (Listener fd))++-- 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 SocketException a)+handleBindListenException !e+ | e == eACCES = pure (Left SocketPermissionDenied)+ | e == eADDRINUSE = pure (Left SocketAddressInUse)+ | otherwise = die+ ("Socket.Stream.Unix.bindListen: " ++ describeErrorCode e)++describeErrorCode :: Errno -> String+describeErrorCode err@(Errno e) = "error code " ++ D.string err ++ " (" ++ show e ++ ")"++-- 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 SocketException a)+handleSocketListenException e@(Errno n)+ | e == eMFILE = pure (Left SocketFileDescriptorLimit)+ | e == eNFILE = pure (Left SocketFileDescriptorLimit)+ | otherwise = die+ ("Socket.Stream.Unix.listen: " ++ D.string e ++ " (" ++ show n ++ ")")++-- | Open a socket that is used to listen for inbound connections.+withListener ::+ UnixAddress+ -> (Listener -> IO a)+ -> IO (Either SocketException 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)++-- | 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 _ -> die "Socket.Stream.Unix.unlisten"+ Right _ -> pure ()++-- | 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++-- | 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++-- We use the maybe to mean that the user needs to wait again.+waitlessAccept :: Fd -> IO (Either (Maybe (AcceptException i)) Connection)+waitlessAccept lstn = do+ L.uninterruptibleAccept4 lstn 200 (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@.+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)++-- | 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+ 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 fd+ else do+ S.uninterruptibleErrorlessClose fd+ die "Socket.Stream.Unix.gracefulCloseA"+ Right _ -> gracefulCloseB fd++gracefulCloseB :: Fd -> IO (Either CloseException ())+gracefulCloseB !fd = do+ !buf <- PM.newByteArray 1+ -- We do not actually want to remove the bytes from the+ -- receive buffer, so we use MSG_PEEK. We are certain+ -- to send a reset when a CloseException is reported.+ -- Retrospective: Why is MSG_PEEK important? Who cares if the+ -- bytes get eaten? The receive buffer is about to get axed anyway.+ S.uninterruptibleReceiveMutableByteArray fd buf 0 1 S.peek >>= \case+ Left err1 -> if err1 == eWOULDBLOCK || err1 == eAGAIN || err1 == eNOTCONN+ then do+ _ <- S.uninterruptibleClose fd+ pure (Right ())+ else do+ _ <- S.uninterruptibleClose fd+ -- We treat all @recv@ errors except for the nonblocking+ -- notices as unrecoverable.+ die "Socket.Stream.Unix.gracefulCloseB"+ Right sz -> if sz == 0+ then S.uninterruptibleClose fd >>= \case+ Left _ -> die "Socket.Stream.Unix.gracefulCloseB"+ 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@+-- and is likely to cause a TCP reset to be sent. 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)++-- | Accept a connection on the listener and run the supplied callback in+-- a new thread. The masking state is set to @Unmasked@ when running the+-- callback. Typically, @a@ is instantiated to @()@.+interruptibleForkAcceptedUnmasked ::+ TVar Int+ -- ^ Connection counter. Incremented when connection+ -- is accepted. Decremented after connection is closed.+ -> TVar Bool+ -- ^ Interrupted. If this becomes 'True' give up and return+ -- @'Left' 'AcceptInterrupted'@.+ -> Listener+ -- ^ Connection listener+ -> (Either CloseException () -> a -> IO ())+ -- ^ Callback to handle an ungraceful close. This must not+ -- throw an exception.+ -> (Connection -> IO a)+ -- ^ Callback to consume connection. Must not return the connection.+ -> IO (Either (AcceptException 'Interruptible) ThreadId)+interruptibleForkAcceptedUnmasked !counter !abandon !lstn consumeException cb =+ mask_ $ interruptibleAcceptCounting counter abandon lstn >>= \case+ Left e -> pure (Left e)+ Right conn -> fmap Right $ forkIOWithUnmask $ \unmask -> do+ a <- onException+ (unmask (cb conn))+ (disconnect_ conn *> atomically (modifyTVar' counter (subtract 1)))+ e <- disconnect conn+ r <- unmask (consumeException e a)+ atomically (modifyTVar' counter (subtract 1))+ pure r++-- Only used internally+interruptibleAcceptCounting :: + TVar Int+ -> TVar Bool+ -> Listener+ -> IO (Either (AcceptException 'Interruptible) Connection)+interruptibleAcceptCounting !counter !abandon (Listener !fd) = do+ -- TODO: pull these out of the loop+ let !mngr = EM.manager+ tv <- EM.reader mngr fd+ token <- EM.interruptibleWaitCounting counter abandon tv+ if EM.isInterrupt token+ then pure (Left AcceptInterrupted)+ else waitlessAccept fd >>= \case+ Left merr -> case merr of+ Nothing -> do+ EM.unready token tv+ -- Decrement the connection counter if the notification+ -- from epoll was a false alarm.+ atomically (modifyTVar' counter (subtract 1))+ interruptibleAcceptCounting counter abandon (Listener fd)+ Just err -> pure (Left err)+ Right r@(Connection conn) -> do+ EM.register mngr conn+ pure (Right r)++-- | Retrieve a listener that systemd has passed to the process. This+-- may only be called once. There is no bracketed variant+-- of this function because the listener is expected to remain open for+-- the remainder of the application.+--+-- There are several reasons this function may return @Left@:+--+-- * @sd_listen_fds@ returned more than one file descriptor+-- * @sd_is_socket@ found that the file descriptor was not a socket or+-- that it was a socket that was not in listening mode.+systemdListener :: IO (Either SystemdException Listener)+systemdListener = coerce (systemdListenerInternal S.Unix)
+ src/Socket/Systemd.hs view
@@ -0,0 +1,54 @@+{-# language BangPatterns #-}+{-# language DeriveAnyClass #-}+{-# language DerivingStrategies #-}+{-# LANGUAGE LambdaCase #-}++module Socket.Systemd+ ( SystemdException(..)+ , systemdListenerInternal+ ) where++import Control.Exception (Exception)+import Data.Bits ((.&.))+import Foreign.C.Types (CInt)+import Foreign.C.Error (Errno(..))+import System.Posix.Types (Fd(Fd))++import qualified Posix.File as F+import qualified Socket.EventManager as EM+import qualified Posix.Socket as S+import qualified Linux.Systemd as L++-- Internal function used for inheriting either an internet+-- stream socket or a unix-domain stream socket from systemd.+systemdListenerInternal :: S.Family -> IO (Either SystemdException Fd)+{-# noinline systemdListenerInternal #-}+systemdListenerInternal !fam = L.listenFds 1 >>= \case+ Left (Errno e) -> pure (Left (SystemdErrno e))+ Right n -> case n of+ 1 -> L.isSocket fd0 fam S.stream 1 >>= \case+ Left (Errno e) -> pure (Left (SystemdErrno e))+ Right r -> case r of+ 0 -> pure (Left SystemdDescriptorInfo)+ _ -> F.uninterruptibleGetStatusFlags fd0 >>= \case+ Left (Errno e) -> pure (Left (SystemdFnctlErrno e))+ Right status -> if F.nonblocking .&. status == mempty+ then pure (Left SystemdBlocking)+ else do+ let !mngr = EM.manager+ EM.register mngr fd0+ pure (Right fd0)+ _ -> pure (Left (SystemdDescriptorCount n))+ where+ fd0 = Fd 3++data SystemdException+ = SystemdDescriptorCount !CInt+ | SystemdDescriptorInfo+ | SystemdBlocking+ -- ^ The socket was in blocking mode. Set @NonBlocking=True@ in the systemd+ -- service to resolve this.+ | SystemdErrno !CInt+ | SystemdFnctlErrno !CInt+ deriving stock (Show,Eq)+ deriving anyclass (Exception)
test/Main.hs view
@@ -29,12 +29,16 @@ import Test.Tasty import Test.Tasty.HUnit +import qualified Data.Bytes as Bytes import qualified Data.ByteString as B import qualified Data.Primitive as PM import qualified Data.Primitive.Unlifted.Array as PM import qualified Data.Primitive.MVar as PM import qualified GHC.Exts as E import qualified Net.IPv4 as IPv4+import qualified Socket.SequencedPacket.Unix as SP+import qualified Socket.SequencedPacket.Uninterruptible.Bytes as SP+import qualified Socket.Datagram.Unix.Connected as DUC import qualified Socket.Datagram.IPv4.Unconnected as DIU import qualified Socket.Datagram.IPv4.Connected as DIC import qualified Socket.Datagram.Uninterruptible.Bytes as DUB@@ -66,7 +70,18 @@ , testCase "B" testDatagramConnectedB ] ]+ , testGroup "unix"+ [ testGroup "connected"+ [ testCase "C" testDatagramConnectedC+ ]+ ] ]+ , testGroup "seqpacket"+ [ testGroup "unix"+ [ testCase "A" testSeqpacketA+ , testCase "B" testSeqpacketB+ ]+ ] , testGroup "stream" [ testGroup "ipv4" [ testCase "A" testStreamA@@ -140,6 +155,52 @@ Right _ -> fail "testDatagramUndestinedB: received datagram" ) +-- This checks that unix-domain datagram socketpair works.+testDatagramConnectedC :: Assertion+testDatagramConnectedC = do+ ((),actual) <- either throwIO pure =<< DUC.withPair+ (\src dst -> concurrently+ ( unhandled $ DUB.send src (unsliced message) )+ ( unhandled $ DUB.receive dst sz )+ )+ message @=? actual+ where+ message = E.fromList [0,1,2,3] :: ByteArray+ sz = PM.sizeofByteArray message++-- This checks that unix-domain seqpacket socketpair works.+testSeqpacketA :: Assertion+testSeqpacketA = do+ ((),actual) <- either throwIO pure =<< SP.withPair+ (\src dst -> concurrently+ ( unhandled $ SP.send src (unsliced message) )+ ( unhandled $ SP.receive dst (sz + 1) )+ )+ message @=? actual+ where+ message = E.fromList [0,1,2,3] :: ByteArray+ sz = PM.sizeofByteArray message++testSeqpacketB :: Assertion+testSeqpacketB = do+ (m :: PM.MVar RealWorld ()) <- PM.newEmptyMVar+ ((),received) <- concurrently (sender m) (receiver m)+ received @=? message+ where+ -- Socket in abstract namespace+ addr = DUC.UnixAddress (E.fromList [0,97,98,99,100,101])+ message = E.fromList (enumFromTo 0 (100 :: Word8)) :: ByteArray+ sender :: PM.MVar RealWorld () -> IO ()+ sender m = do+ PM.takeMVar m+ unhandled $ SP.withConnection addr unhandledClose $ \conn -> do+ unhandled $ SP.send conn (Bytes.fromByteArray message)+ receiver :: PM.MVar RealWorld () -> IO ByteArray+ receiver m = unhandled $ SP.withListener addr $ \listener -> do+ PM.putMVar m ()+ unhandled $ SP.withAccepted listener unhandledClose $ \conn -> do+ unhandled $ SP.receive conn 256+ testDatagramUndestinedA :: Assertion testDatagramUndestinedA = do (m :: PM.MVar RealWorld Word16) <- PM.newEmptyMVar@@ -180,8 +241,9 @@ receiver m n = unhandled $ DIU.withSocket (DIU.Peer IPv4.loopback 0) $ \sock port -> do PM.putMVar m port PM.takeMVar n- slab <- DUB.newPeerlessSlab 2 (max sz1 sz2)- msgs <- unhandled $ DUB.receiveMany sock slab+ slab <- DUB.newPeerlessSlab 2+ let sz = max sz1 sz2+ msgs <- unhandled $ DUB.receiveMany sock slab sz let msgCount = PM.sizeofUnliftedArray msgs if msgCount == 2 then pure (PM.indexUnliftedArray msgs 0, PM.indexUnliftedArray msgs 1)@@ -222,13 +284,14 @@ receiver m n = unhandled $ DIU.withSocket (DIU.Peer IPv4.loopback 0) $ \sock port -> do PM.putMVar m port PM.takeMVar n- slab <- DUB.newIPv4Slab 3 (max sz1 (max sz2 (max sz3 sz4)))- msgsX <- unhandled $ DUB.receiveManyFromIPv4 sock slab+ let sz = max sz1 (max sz2 (max sz3 sz4))+ slab <- DUB.newIPv4Slab 3+ msgsX <- unhandled $ DUB.receiveManyFromIPv4 sock slab sz let msgCountX = PM.sizeofSmallArray msgsX (msg1,msg2,msg3) <- if PM.sizeofSmallArray msgsX == 3 then pure (PM.indexSmallArray msgsX 0, PM.indexSmallArray msgsX 1, PM.indexSmallArray msgsX 2) else fail $ "received a number of messages other than 3: " ++ show msgCountX- msgsY <- unhandled $ DUB.receiveManyFromIPv4 sock slab+ msgsY <- unhandled $ DUB.receiveManyFromIPv4 sock slab sz let msgCountY = PM.sizeofSmallArray msgsY msg4 <- if msgCountY == 1 then pure (PM.indexSmallArray msgsY 0)@@ -287,12 +350,13 @@ receiver m n = unhandled $ DIU.withSocket (DIU.Peer IPv4.loopback 0) $ \sock port -> do PM.putMVar m port PM.takeMVar n- slab <- DUB.newPeerlessSlab 1 (max sz1 (max sz2 sz3))- msgsX <- unhandled $ DUB.receiveMany sock slab+ slab <- DUB.newPeerlessSlab 1+ let sz = max sz1 (max sz2 sz3)+ msgsX <- unhandled $ DUB.receiveMany sock slab sz when (PM.sizeofUnliftedArray msgsX /= 1) $ fail "more than one message for X"- msgsY <- unhandled $ DUB.receiveMany sock slab+ msgsY <- unhandled $ DUB.receiveMany sock slab sz when (PM.sizeofUnliftedArray msgsX /= 1) $ fail "more than one message for Y"- msgsZ <- unhandled $ DUB.receiveMany sock slab+ msgsZ <- unhandled $ DUB.receiveMany sock slab sz when (PM.sizeofUnliftedArray msgsX /= 1) $ fail "more than one message for Z" pure (PM.indexUnliftedArray msgsX 0,PM.indexUnliftedArray msgsY 0,PM.indexUnliftedArray msgsZ 0) @@ -310,8 +374,8 @@ unhandled $ DUB.sendToIPv4 sock (DIU.Peer IPv4.loopback dstPort) (unsliced message) receiver m = unhandled $ DIU.withSocket (DIU.Peer IPv4.loopback 0) $ \sock port -> do PM.putMVar m port- slab <- DUB.newPeerlessSlab 1 (sz - 1)- DUB.receiveMany sock slab+ slab <- DUB.newPeerlessSlab 1+ DUB.receiveMany sock slab (sz - 1) testDatagramUndestinedG :: Assertion testDatagramUndestinedG = do