packages feed

semaphore-compat-2.0.0: src/System/Semaphore/Internal/DomainSocket.hs

{-# LANGUAGE ForeignFunctionInterface #-}

module System.Semaphore.Internal.DomainSocket
  ( connectDomainSocket
  , listenDomainSocket
  , pollAcceptSocket, AcceptResult(..)
  , fdReadByte, fdWriteByte
  , fdShutdown
  ) where

-- base
import Data.Word ( Word8 )
import Foreign.C.Error ( throwErrnoIfMinus1Retry, throwErrnoIfMinus1Retry_, throwErrno )
import Foreign.C.String ( CString, withCString )
import Foreign.C.Types ( CInt(..), CSize(..) )
import Foreign.Marshal.Alloc ( allocaBytes )
import Foreign.Ptr ( Ptr )
import Foreign.Storable ( peek, poke )
import GHC.IO.Exception ( IOErrorType(EOF), IOException(..) )
import GHC.Stack ( HasCallStack, callStack, prettyCallStack )

-- unix
import System.Posix.Types ( Fd(..) )

foreign import ccall safe "hs_connect_domain_socket"
    c_connectDomainSocket :: CString -> IO CInt

connectDomainSocket :: FilePath -> IO Fd
connectDomainSocket path =
    withCString path $ fmap Fd . throwErrnoIfMinus1Retry "connectDomainSocket" . c_connectDomainSocket

foreign import ccall safe "hs_listen_domain_socket"
    c_listenDomainSocket :: CString -> IO CInt

-- | Open a socket in non blocking mode (O_NONBLOCK)
listenDomainSocket :: FilePath -> IO Fd
listenDomainSocket path =
    withCString path $ fmap Fd . throwErrnoIfMinus1Retry "listenDomainSocket" . c_listenDomainSocket

foreign import ccall safe "read"
    c_read :: CInt -> Ptr Word8 -> CSize -> IO CInt

foreign import ccall safe "write"
    c_write :: CInt -> Ptr Word8 -> CSize -> IO CInt

-- | Read a single byte from a file descriptor.
-- Throws an EOF 'IOError' if the peer has disconnected.
fdReadByte :: HasCallStack => Fd -> IO Word8
fdReadByte (Fd fd) =
    allocaBytes 1 $ \buf -> do
        rc <- throwErrnoIfMinus1Retry ("fdReadByte(fd=" ++ show fd ++ ")") $
            c_read fd buf 1
        if rc == 0
          then ioError $ IOError Nothing EOF
                  (prettyCallStack callStack)
                  ("fd=" ++ show fd)
                  Nothing Nothing
          else peek buf

-- | Write a single byte to a file descriptor.
fdWriteByte :: HasCallStack => Fd -> Word8 -> IO ()
fdWriteByte (Fd fd) byte =
    allocaBytes 1 $ \buf -> do
        poke buf byte
        _ <- throwErrnoIfMinus1Retry ("fdWriteByte(fd=" ++ show fd ++ ")") $
            c_write fd buf 1
        return ()

foreign import ccall safe "shutdown"
    c_shutdown :: CInt -> CInt -> IO CInt

-- | Shut down a socket for both reading and writing.
-- A concurrent 'fdReadByte' on the same fd will return immediately
-- Used to cancel threads blocked in read()
fdShutdown :: Fd -> IO ()
fdShutdown (Fd fd) =
    throwErrnoIfMinus1Retry_ "fdShutdown" $ c_shutdown fd 2  -- SHUT_RDWR

-- | Result of 'pollAcceptSocket'.
data AcceptResult
  = AcceptedFd !Fd     -- ^ A client connected; here is the fd.
  | AcceptCancelled    -- ^ The cancel pipe was signalled.

foreign import ccall safe "hs_poll_accept"
    c_pollAccept :: CInt -> CInt -> IO CInt

-- | Block until either a client connects or the cancel fd is written to.
--
-- Relies on cooperative cancellation implemented in hs_poll_accept using
-- @poll(2)@ + @accept(2)@ via safe FFI to avoid GHC #27110 and #27113.
--
-- Must be called from a masked context.  The caller is responsible for
-- installing an exception handler that closes all 3 fds (inputs and outputs).
pollAcceptSocket :: Fd -> Fd -> IO AcceptResult
pollAcceptSocket (Fd listenFd) (Fd cancelFd) = do
    r <- c_pollAccept listenFd cancelFd
    if r == -2
      then return AcceptCancelled
      else if r == -1
        then throwErrno "pollAcceptSocket"
        else return (AcceptedFd (Fd r))