{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Data.Conduit.Network.Unix
    ( -- * Basic utilities
      sourceSocket
    , sinkSocket
      -- * Simple server/client interface
    , Application
    , AppData
    , appSource
    , appSink
      -- ** Server
    , ServerSettings
    , serverSettings
    , serverPath
    , serverAfterBind
    , runUnixServer
      -- ** Client
    , ClientSettings
    , clientSettings
    , clientPath
    , runUnixClient
      -- * Helper utilities
    , bindPath
    , getSocket
    , acceptSafe
    ) where

import Data.Conduit
import Network.Socket (Socket)
import qualified Network.Socket as NS
import Data.Conduit.Network (sourceSocket, sinkSocket, acceptSafe)
import Data.Conduit.Network.Internal.Unix (AppData(..), ClientSettings(..), ServerSettings(..))
import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Exception (throwIO, SomeException, try, finally, bracket,
                          bracketOnError, catch)
import Control.Monad (forever, void)
import Control.Monad.Trans.Control (control)
import Control.Concurrent (forkIO)
import System.Directory (removeFile)
import System.IO.Error (isDoesNotExistError)

-- | Attempt to connect to the given Unix domain socket path.
getSocket :: FilePath -> IO Socket
getSocket path = do
    sock <- NS.socket NS.AF_UNIX NS.Stream 0
    ee <- try' $ NS.connect sock (NS.SockAddrUnix path)
    case ee of
        Left e -> NS.sClose sock >> throwIO e
        Right () -> return sock
  where
    try' :: IO a -> IO (Either SomeException a)
    try' = try

-- | Attempt to bind a listening Unix domain socket at the given path.
--
-- Since 1.0.2
bindPath :: FilePath -> IO Socket
bindPath path = do
  sock <- bracketOnError
            (NS.socket NS.AF_UNIX NS.Stream 0)
            NS.sClose
            (\sock -> do
                removeFileSafe path  -- Cannot bind if the socket file exists.
                NS.bindSocket sock (NS.SockAddrUnix path)
                return sock)
  NS.listen sock (max 2048 NS.maxListenQueue)
  return sock

removeFileSafe :: FilePath -> IO ()
removeFileSafe path =
    removeFile path `Control.Exception.catch` handleExists
  where
    handleExists e
          | isDoesNotExistError e = return ()
          | otherwise = throwIO e

-- | A simple Unix domain sockets application.
--
-- Since 1.0.2
type Application m = AppData m -> m ()

-- | Smart constructor.
--
-- Since 1.0.2
serverSettings :: Monad m
               => FilePath -- ^ path to bind to
               -> ServerSettings m
serverSettings path = ServerSettings
    { serverPath = path
    , serverAfterBind = const $ return ()
    }

-- | Run an @Application@ with the given settings. This function will create a
-- new listening socket, accept connections on it, and spawn a new thread for
-- each connection.
--
-- Since 1.0.2
runUnixServer :: (MonadIO m, MonadBaseControl IO m) => ServerSettings m -> Application m -> m ()
runUnixServer (ServerSettings path afterBind) app = control $ \run -> bracket
    (liftIO $ bindPath path)
    (liftIO . NS.sClose)
    (\socket -> run $ do
        afterBind socket
        forever $ serve socket)
  where
    serve lsocket = do
        (socket, _) <- liftIO $ acceptSafe lsocket
        let ad = AppData
                { appSource = sourceSocket socket
                , appSink = sinkSocket socket
                }
            app' run = void $ run (app ad)
            appClose run = app' run `finally` NS.sClose socket
        control $ \run -> forkIO (appClose run) >> run (return ())

-- | Smart constructor.
--
-- Since 1.0.2
clientSettings :: Monad m
               => FilePath -- ^ path to connect to
               -> ClientSettings m
clientSettings path = ClientSettings
    { clientPath = path
    }

-- | Run an @Application@ by connecting to the specified server.
--
-- Since 1.0.2
runUnixClient :: (MonadIO m, MonadBaseControl IO m) => ClientSettings m -> Application m -> m ()
runUnixClient (ClientSettings path) app = control $ \run -> bracket
    (getSocket path)
    NS.sClose
    (\sock -> run $ app AppData
        { appSource = sourceSocket sock
        , appSink = sinkSocket sock
        })