tricorder-0.1.0.0: src/Tricorder/Effects/UnixSocket.hs
module Tricorder.Effects.UnixSocket
( -- * Effect
UnixSocket
, bindSocket
, acceptHandle
, withConnection
, readLine
, sendLine
, closeHandle
, removeSocketFile
, socketFileExists
-- * Interpreters
, runUnixSocketIO
, runUnixSocketScripted
, SocketScript (..)
) where
import Atelier.Effects.File (BufferMode (..), File, Handle)
import Atelier.Exception (trySyncIO)
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 (..)
, accept
, bind
, defaultProtocol
, listen
, socket
, socketToHandle
)
import System.Directory (doesPathExist, removeFile)
import System.IO (hSetEncoding, utf8)
import Atelier.Effects.File qualified as File
import Network.Socket qualified as Net
data UnixSocket :: Effect where
-- | Create, bind, and listen on a Unix socket at the given path.
BindSocket :: FilePath -> UnixSocket m Socket
-- | Accept the next incoming connection and return a line-buffered 'Handle'.
AcceptHandle :: 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
-- | Read a line from a connected handle.
ReadLine :: Handle -> UnixSocket m Text
-- | Write a line to a connected handle and flush.
SendLine :: Handle -> Text -> UnixSocket m ()
-- | Close a connected handle.
CloseHandle :: Handle -> UnixSocket m ()
-- | Remove the socket file, ignoring errors (e.g. file not found).
RemoveSocketFile :: FilePath -> UnixSocket m ()
-- | Check whether the socket file exists.
SocketFileExists :: FilePath -> UnixSocket m Bool
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.
runUnixSocketIO :: (File :> es, IOE :> es) => Eff (UnixSocket : es) a -> Eff es a
runUnixSocketIO eff = interpretWith eff \env -> \case
BindSocket path -> liftIO do
sock <- socket AF_UNIX Stream defaultProtocol
bind sock (SockAddrUnix path)
listen sock 5
pure sock
AcceptHandle sock -> do
h <- liftIO do
(conn, _) <- accept sock
h <- 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 AF_UNIX Stream defaultProtocol
Net.connect sock (SockAddrUnix sockPath)
h <- socketToHandle sock ReadWriteMode
hSetEncoding h utf8
pure h
File.hSetBuffering h LineBuffering
unlift (callback h) `finally` File.hClose h
ReadLine h -> File.hGetLine h
SendLine h line -> File.hPutTextLn h line >> File.hFlush h
CloseHandle h -> File.hClose h
RemoveSocketFile path ->
liftIO $ void $ trySyncIO $ removeFile path
SocketFileExists path ->
liftIO $ doesPathExist path
-- | 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.
runUnixSocketScripted :: (File :> es, IOE :> es) => [SocketScript] -> Eff (UnixSocket : es) a -> Eff es a
runUnixSocketScripted script = reinterpret (evalState script) \env -> \case
BindSocket _ ->
liftIO $ Net.socket AF_UNIX Stream defaultProtocol
AcceptHandle _ ->
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"
ReadLine _ ->
get >>= \case
NextReadLine line : rest -> put rest >> pure line
_ -> error "UnixSocketScripted: expected NextReadLine but queue was empty or mismatched"
SendLine _ _ -> pure ()
CloseHandle _ -> pure ()
RemoveSocketFile _ -> pure ()
SocketFileExists _ ->
get >>= \case
NextFileCheck b : rest -> put rest >> pure b
_ -> error "UnixSocketScripted: expected NextFileCheck but queue was empty or mismatched"