packages feed

pqi-native-0.0.1.0: src/transport/Pqi/Native/Transport.hs

-- | The byte-level transport: a TCP socket with a read buffer, plus the framing
-- that turns the stream into discrete @[type byte][Int32 length][body]@
-- messages.
module Pqi.Native.Transport
  ( Transport,
    connect,
    unconnected,
    close,
    send,
    receiveFrame,
    socketFd,
    peerIp,
    readUntilClosed,
  )
where

import qualified Data.ByteString as ByteString
import qualified Data.ByteString.Char8 as ByteString.Char8
import Data.IORef
import qualified Network.Socket as Socket
import qualified Network.Socket.ByteString as Socket.ByteString
import Pqi.Native.Transport.Prelude
import qualified PtrPoker.Write as Poker
import System.IO.Error (eofErrorType, mkIOError)

-- | An open connection's byte transport.
data Transport = Transport
  { socket :: Socket.Socket,
    readBuffer :: IORef ByteString
  }

-- | Open a TCP connection to the given host and port.
connect :: ByteString -> Int -> IO Transport
connect host port = do
  let hints = Socket.defaultHints {Socket.addrSocketType = Socket.Stream}
  addresses <-
    Socket.getAddrInfo (Just hints) (Just (ByteString.Char8.unpack host)) (Just (show port))
  case addresses of
    [] -> ioError (userError ("Could not resolve host: " <> ByteString.Char8.unpack host))
    address : _ -> do
      sock <- Socket.socket (Socket.addrFamily address) (Socket.addrSocketType address) (Socket.addrProtocol address)
      Socket.connect sock (Socket.addrAddress address)
      buffer <- newIORef ByteString.empty
      pure Transport {socket = sock, readBuffer = buffer}

-- | An unconnected transport, for representing a \"null\" connection. Its
-- socket is allocated but never connected; it must not be used for I\/O.
unconnected :: IO Transport
unconnected = do
  sock <- Socket.socket Socket.AF_INET Socket.Stream Socket.defaultProtocol
  buffer <- newIORef ByteString.empty
  pure Transport {socket = sock, readBuffer = buffer}

-- | Close the connection.
close :: Transport -> IO ()
close transport = Socket.close transport.socket

-- | The underlying socket file descriptor.
socketFd :: Transport -> IO Int32
socketFd transport = fromIntegral <$> Socket.unsafeFdSocket transport.socket

-- | Send a serialized message.
send :: Transport -> Poker.Write -> IO ()
send transport write = Socket.ByteString.sendAll transport.socket (Poker.toByteString write)

-- | Read exactly @n@ bytes, looping over @recv@ (which yields up to @n@) and
-- buffering any overshoot. Throws on EOF before @n@ bytes arrive.
receiveExactly :: Transport -> Int -> IO ByteString
receiveExactly transport n = do
  buffered <- readIORef transport.readBuffer
  go buffered
  where
    go accumulated
      | ByteString.length accumulated >= n = do
          let (result, rest) = ByteString.splitAt n accumulated
          writeIORef transport.readBuffer rest
          pure result
      | otherwise = do
          chunk <- Socket.ByteString.recv transport.socket (max 4096 (n - ByteString.length accumulated))
          if ByteString.null chunk
            then ioError (mkIOError eofErrorType "pqi-native: connection closed by server" Nothing Nothing)
            else go (accumulated <> chunk)

-- | Receive one framed message: its type byte and its body (the length prefix,
-- which counts itself, is consumed).
receiveFrame :: Transport -> IO (Word8, ByteString)
receiveFrame transport = do
  header <- receiveExactly transport 5
  let typeByte = ByteString.head header
      frameLength = decodeInt32BE (ByteString.drop 1 header)
      bodyLength = frameLength - 4
  body <-
    if bodyLength > 0
      then receiveExactly transport bodyLength
      else pure ByteString.empty
  pure (typeByte, body)

-- | Read bytes until the peer closes the connection (EOF), discarding them.
-- Used by the cancel path to mirror libpq's behaviour: keep the socket open
-- until the server has read the cancel request and closed its end.
readUntilClosed :: Transport -> IO ()
readUntilClosed transport = go
  where
    go = do
      chunk <- Socket.ByteString.recv transport.socket 4096
      if ByteString.null chunk
        then pure ()
        else go

-- | The numeric IP address of the connected peer (e.g. @\"::1\"@ or
-- @\"127.0.0.1\"@). Throws if the socket has no peer (unconnected).
peerIp :: Transport -> IO ByteString
peerIp transport = do
  addr <- Socket.getPeerName transport.socket
  (Just ip, _) <- Socket.getNameInfo [Socket.NI_NUMERICHOST] True False addr
  pure (ByteString.Char8.pack ip)

decodeInt32BE :: ByteString -> Int
decodeInt32BE = ByteString.foldl' (\acc w -> acc * 256 + fromIntegral w) 0 . ByteString.take 4