tricorder-0.6.0.0: src/Tricorder/Socket/UnixSocket.hs
module Tricorder.Socket.UnixSocket
( -- * Effect
UnixSocket
, bind
, accept
, withConnection
-- * Interpreters
, runIO
, runScripted
, SocketScript (..)
)
where
import Atelier.Effects.File (BufferMode (..), File, Handle)
import Effectful (Effect, IOE)
import Effectful.Dispatch.Dynamic (interpretWith, localSeqUnlift, reinterpret)
import Effectful.Exception (finally)
import Effectful.State.Static.Shared (evalState, get, put)
import Effectful.TH (makeEffect)
import Network.Socket
( Family (..)
, SockAddr (..)
, Socket
, SocketType (..)
)
import System.IO (hSetEncoding, utf8)
import Atelier.Effects.File qualified as File
import Network.Socket qualified as Net
import Network.Socket qualified as Socket
data UnixSocket :: Effect where
-- | Create, bind, and listen on a Unix socket at the given path.
Bind :: FilePath -> UnixSocket m Socket
-- | Accept the next incoming connection and return a line-buffered 'Handle'.
Accept :: Socket -> UnixSocket m Handle
-- | Connect to a Unix socket and run the callback with the resulting handle.
-- The handle is closed when the callback returns.
WithConnection :: FilePath -> (Handle -> m a) -> UnixSocket m a
makeEffect ''UnixSocket
-- | Production interpreter backed by real Unix sockets.
--
-- Socket creation and encoding setup require raw IO (no @File@ equivalent),
-- but the handle-level reads, writes, buffering and closing go through the
-- 'File' effect.
runIO :: (File :> es, IOE :> es) => Eff (UnixSocket : es) a -> Eff es a
runIO eff = interpretWith eff \env -> \case
Bind path -> liftIO do
sock <- Socket.socket AF_UNIX Stream Socket.defaultProtocol
Socket.bind sock (SockAddrUnix path)
Socket.listen sock 5
pure sock
Accept sock -> do
h <- liftIO do
(conn, _) <- Socket.accept sock
h <- Socket.socketToHandle conn ReadWriteMode
hSetEncoding h utf8
pure h
File.hSetBuffering h LineBuffering
pure h
WithConnection sockPath callback ->
localSeqUnlift env \unlift -> do
h <- liftIO do
sock <- Socket.socket AF_UNIX Stream Socket.defaultProtocol
Net.connect sock (SockAddrUnix sockPath)
h <- Socket.socketToHandle sock ReadWriteMode
hSetEncoding h utf8
pure h
File.hSetBuffering h LineBuffering
unlift (callback h) `finally` File.hClose h
-- | Script element for the test interpreter.
data SocketScript
= -- | Return this 'Handle' for the next 'acceptHandle' call.
NextAccept Handle
| -- | Return this 'Bool' for the next 'socketFileExists' call.
NextFileCheck Bool
| -- | Use this 'Handle' for the next 'withConnection' call.
NextConnect Handle
| -- | Return this text for the next 'readLine' call.
NextReadLine Text
-- | Scripted interpreter for testing.
--
-- 'bindSocket' creates a real (unbound) socket so that the returned 'Socket'
-- is a valid value, but does not actually bind to the filesystem.
-- 'acceptHandle' pops the next 'NextAccept' entry from the queue and sets
-- line buffering on it. 'removeSocketFile' is always a no-op.
-- 'socketFileExists' pops the next 'NextFileCheck' entry.
-- 'withConnection' pops the next 'NextConnect' entry and passes it to the callback.
runScripted
:: (File :> es, IOE :> es) => [SocketScript] -> Eff (UnixSocket : es) a -> Eff es a
runScripted script = reinterpret (evalState script) \env -> \case
Bind _ ->
liftIO $ Net.socket AF_UNIX Stream Socket.defaultProtocol
Accept _ ->
get >>= \case
NextAccept h : rest -> do
put rest
File.hSetBuffering h LineBuffering
pure h
_ -> error "UnixSocketScripted: expected NextAccept but queue was empty or mismatched"
WithConnection _ callback ->
get >>= \case
NextConnect h : rest -> do
put rest
localSeqUnlift env \unlift -> unlift (callback h)
_ -> error "UnixSocketScripted: expected NextConnect but queue was empty or mismatched"