semaphore-compat-2.0.1: src/System/Semaphore/Internal/Posix.hs
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
-- | POSIX backend: semaphores implemented on top of Unix domain sockets.
module System.Semaphore.Internal.Posix
( ClientSemaphore(..), ServerSemaphore(..)
, SemaphoreToken(..)
, waitOnSemaphore, tryWaitOnSemaphore
, releaseSemaphoreToken
, destroyClientSemaphore, destroyServerSemaphore
, getSemaphoreValue
, create_sem, open_sem_raw
, get_time_seed
) where
-- base
import Control.Concurrent
( ThreadId, forkIOWithUnmask, killThread )
import Control.Concurrent.MVar
( MVar, mkWeakMVar, newEmptyMVar, newMVar, putMVar
, takeMVar, tryTakeMVar )
import Control.Exception
( IOException, SomeException
, finally, mask_, onException, throw, try, uninterruptibleMask_
)
import Control.Monad ( void )
import Data.Bits ( xor )
import GHC.Clock ( getMonotonicTimeNSec )
import GHC.Stack ( HasCallStack )
-- directory
import System.Directory ( doesPathExist )
-- stm
import Control.Concurrent.STM
( TVar, newTVarIO, readTVarIO )
-- unix
import System.Posix.IO ( closeFd, createPipe, setFdOption, FdOption(CloseOnExec) )
import System.Posix.Files ( removeLink )
import System.Posix.Types ( Fd )
import System.Posix.Process ( getProcessID )
-- semaphore-compat
import System.Semaphore.Internal.Version
import System.Semaphore.Internal.DomainSocket
( connectDomainSocket, listenDomainSocket
, fdReadByte, fdWriteByte
, fdShutdown )
import System.Semaphore.Internal.Posix.Server
( serverLoop
, pattern CmdWait, pattern CmdTryWait, pattern CmdRelease
, pattern RspOk, pattern RspFail
)
--------------------------------------------------------------------------------
-- A client is just an identity (name + socket path); each operation that needs
-- a connection opens one internally.
data ClientSemaphore =
ClientSemaphore
{ clientSemaphoreName :: !SemaphoreName
, semSocketPath :: !FilePath
}
-- The fd is held in the 'MVar' so 'releaseSemaphoreToken' takes ownership
-- atomically: a second (erroneous) release is a safe no-op.
newtype SemaphoreToken = SemaphoreToken
{ tokenFdLock :: MVar Fd
-- NB: the 'Fd' is held in an 'MVar' to allow 'releaseSemaphoreToken'
-- to takes ownership of the 'Fd' atomically. This ensures that a
-- second (erroneous) release is a safe no-op (avoiding Fd double-close bugs).
}
data ServerSemaphore = ServerSemaphore
{ serverClientSemaphore :: !ClientSemaphore
, serverThreadId :: !ThreadId
, serverPool :: !(TVar Int)
, serverState :: !(MVar ServerState)
-- NB: the MVar is emptied when resources/fds are freed to prevent double close
}
data ServerState = ServerState
{ serverListenFd :: !Fd
, serverCancelFd :: !Fd
-- Write end of the cancel pipe. Writing a byte signals the
-- server loop to exit its poll-accept.
}
create_sem :: SemaphoreName -> Int -> IO (Either SemaphoreError ServerSemaphore)
create_sem sem_nm init_toks = do
mb_res <- try @IOException $ mask_ $ do
socketPath <- getSemaphoreSocketPath sem_nm
listenFd <- listenDomainSocket socketPath
-- ^^ creates the socket file, unlink it too on failure.
let cleanupListen = do
void $ try @IOException $ closeFd listenFd
void $ try @IOException $ removeLink socketPath
flip onException cleanupListen $ do
pool <- newTVarIO init_toks
(cancelRd, cancelWr) <- createPipe
setFdOption cancelRd CloseOnExec True
setFdOption cancelWr CloseOnExec True
tid <- forkIOWithUnmask $ \unmask ->
unmask (serverLoop pool listenFd cancelRd)
`finally` closeFd cancelRd
stateVar <- newMVar ServerState
{ serverListenFd = listenFd
, serverCancelFd = cancelWr
}
return ServerSemaphore
{ serverClientSemaphore = ClientSemaphore { clientSemaphoreName = sem_nm
, semSocketPath = socketPath }
, serverThreadId = tid
, serverPool = pool
, serverState = stateVar
}
return $ case mb_res of
Left err -> Left $ SemaphoreOtherError err
Right sem -> Right sem
open_sem_raw :: SemaphoreName -> IO (Either SemaphoreError ClientSemaphore)
open_sem_raw nm = do
mb_res <- try @IOException $ do
socketPath <- getSemaphoreSocketPath nm
exists <- doesPathExist socketPath
return (socketPath, exists)
return $ case mb_res of
Left err -> Left $ SemaphoreOtherError err
Right (_, False) -> Left $ SemaphoreDoesNotExist (semaphoreIdentifier nm)
Right (socketPath, _) -> Right $
ClientSemaphore
{ clientSemaphoreName = nm
, semSocketPath = socketPath
}
waitOnSemaphore :: HasCallStack => ClientSemaphore -> IO SemaphoreToken
waitOnSemaphore sem = do
resultVar <- newEmptyMVar
-- Mask exceptions until we get to the interruptible takeMVar
mask_ $ do
fd <- connectDomainSocket (semSocketPath sem)
-- The read() runs in a forked thread
workerTid <- forkIOWithUnmask $ \_ -> do
res <- try @SomeException $ do
fdWriteByte fd CmdWait
fdReadByte fd
putMVar resultVar res
-- uninterruptibleMask_: killThread is interruptible, and a
-- second async between killThread and closeFd would leak fd.
let cleanup = uninterruptibleMask_ $ do
-- shutdown(SHUT_RDWR) causes the worker's read() to return EOF immediately
void $ try @IOException $ fdShutdown fd
-- Wait for the worker to exit before closing the fd.
-- this prevents double close bugs
killThread workerTid
-- Finally close the fd
void $ try @IOException $ closeFd fd
-- We achieve interruptiblity by relying on the interruptiblity of takeMVar
-- The worker thread is blocked on read(), fdShutdown in cleanup interrupts
-- it if we are interrupt by an async exception here.
res <- takeMVar resultVar `onException` cleanup
case res of
Right resp
| resp == RspOk -> mkToken fd
| otherwise -> do void $ try @IOException $ closeFd fd
fail $ "semaphore-compat: unexpected response in waitOnSemaphore: " ++ show resp
Left e -> do void $ try @IOException $ closeFd fd
throw e
tryWaitOnSemaphore :: HasCallStack => ClientSemaphore -> IO (Maybe SemaphoreToken)
tryWaitOnSemaphore sem =
mask_ $ do
fd <- connectDomainSocket (semSocketPath sem)
resp <- flip onException (closeFd fd) $ do
fdWriteByte fd CmdTryWait
fdReadByte fd
case resp of
RspOk -> Just <$> mkToken fd
_ -> do void $ try @IOException $ closeFd fd
return Nothing
mkToken :: Fd -> IO SemaphoreToken
mkToken fd = do
fdVar <- newMVar fd
_ <- mkWeakMVar fdVar $ do
mb <- tryTakeMVar fdVar
case mb of
Nothing -> return () -- already released
-- Closing the fd triggers the server's disconnect handling,
-- which returns the token to the pool.
Just fd' -> void $ try @IOException $ closeFd fd'
return (SemaphoreToken fdVar)
releaseSemaphoreToken :: HasCallStack => SemaphoreToken -> IO ()
releaseSemaphoreToken (SemaphoreToken fdVar) =
mask_ $ do
mb <- tryTakeMVar fdVar
case mb of
Nothing -> return () -- already released
Just fd -> do
resp <- (do fdWriteByte fd CmdRelease
fdReadByte fd
) `finally` (void $ try @IOException $ closeFd fd)
case resp of
RspOk -> return ()
-- myCount <= 0 on the server means the token is effectively
-- already released; stay idempotent.
RspFail -> return ()
_ -> fail "semaphore-compat: unexpected response in releaseSemaphoreToken"
-- On POSIX this is a no-op: 'ClientSemaphore' holds no live connection.
destroyClientSemaphore :: ClientSemaphore -> IO ()
destroyClientSemaphore _ = return ()
destroyServerSemaphore :: ServerSemaphore -> IO ()
destroyServerSemaphore server = uninterruptibleMask_ $ do
-- we justify the uninterruptibleMask_ here by analysis of the server, which must exit
-- in a bounded amount of time once it receives the cancel signal on serverCancelFd
-- It has a bounded number of child threads it needs to cleanup before returning,
-- but otherwise it should exit promptly.
--
-- Without uninterruptibleMask_, we potentially leak serverListenFd and path if an
-- exception arrives when we are in `killThread`.
mbState <- tryTakeMVar (serverState server)
case mbState of
Nothing -> return () -- already destroyed
Just ServerState{..} -> do
let path = semSocketPath $ serverClientSemaphore server
-- Signal the server loop to exit pollAcceptSocket, then wait for it.
void $ try @IOException $ fdWriteByte serverCancelFd 0
void $ try @IOException $ closeFd serverCancelFd
killThread (serverThreadId server)
void $ try @IOException $ closeFd serverListenFd
void $ try @IOException $ removeLink path
getSemaphoreValue :: ServerSemaphore -> IO Int
getSemaphoreValue server = readTVarIO (serverPool server)
get_time_seed :: IO Int
get_time_seed = do
ns <- getMonotonicTimeNSec
pid <- getProcessID
return $ fromIntegral ns `xor` fromIntegral pid