{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE NoImplicitPrelude #-}
module System.IO.LockFile.Internal
(
lock
, unlock
, LockingParameters(..)
, RetryStrategy(..)
, LockingException(..)
)
where
import Prelude ((-), fromIntegral)
import Control.Applicative ((*>), pure)
import Control.Concurrent (threadDelay)
import Control.Exception
( Exception
, IOException
, ioError
, mask
, onException
, catch
, throw
)
import Control.Monad ((>>=), when)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Bits ((.|.))
import Data.Data (Data)
import Data.Eq (Eq, (/=))
import Data.Ord ((>))
import Data.Function ((.), ($))
import Data.Functor ((<$>))
import Data.Maybe (Maybe(Just, Nothing))
import Data.Typeable (Typeable)
import Data.Word (Word8, Word64)
import Foreign.C (eEXIST, errnoToIOError, getErrno)
import GHC.Generics (Generic)
import GHC.IO.Handle.FD (fdToHandle)
import System.IO (FilePath, Handle, IO, hClose, hFlush, hPutStr)
import System.Posix.Internals
( c_close
, c_getpid
, c_open
, o_BINARY
, o_CREAT
, o_EXCL
, o_NOCTTY
, o_NONBLOCK
, o_RDWR
, withFilePath
)
import Text.Read (Read)
import Text.Show (Show(showsPrec), show, shows, showString)
import Control.Monad.Catch (MonadMask)
import Data.Default.Class (Default(def))
import System.Directory (removeFile)
data RetryStrategy
= No
| Indefinitely
| NumberOfTimes {-# UNPACK #-} !Word8
deriving (Data, Eq, Generic, Read, Show, Typeable)
instance Default RetryStrategy where
def = Indefinitely
data LockingParameters = LockingParameters
{ retryToAcquireLock :: !RetryStrategy
, sleepBetweenRetries :: {-# UNPACK #-} !Word64
}
deriving (Data, Eq, Generic, Read, Show, Typeable)
instance Default LockingParameters where
def = LockingParameters
{ retryToAcquireLock = def
, sleepBetweenRetries = 8000000
}
data LockingException
= UnableToAcquireLockFile FilePath
| CaughtIOException IOException
deriving (Typeable)
instance Show LockingException where
showsPrec _ e = case e of
UnableToAcquireLockFile fp -> shows' "Unable to acquire lock file" fp
CaughtIOException ioe -> shows' "Caught IO exception" ioe
where
shows' str x = showString str . showString ": " . shows x
instance Exception LockingException
io :: (MonadMask m, MonadIO m) => IO a -> m a
io m = liftIO $ mask $ \restore ->
restore m `catch` (throw . CaughtIOException)
lock
:: (MonadMask m, MonadIO m)
=> LockingParameters
-> FilePath
-> m Handle
lock params = lock' $ case retryToAcquireLock params of
NumberOfTimes 0 -> params{retryToAcquireLock = No}
_ -> params
where
openLockFile lockFileName = io $ do
fd <- withFilePath lockFileName $ \fp -> c_open fp openFlags 0o644
if fd > 0
then Just <$> fdToHandle fd `onException` c_close fd
else do
errno <- getErrno
when (errno /= eEXIST) . ioError
. errnoToIOError "lock" errno Nothing $ Just lockFileName
pure Nothing
where
openFlags = o_NONBLOCK .|. o_NOCTTY .|. o_RDWR .|. o_CREAT .|. o_EXCL
.|. o_BINARY
lock' params' lockFileName = case retryToAcquireLock params' of
NumberOfTimes 0 -> failedToAcquireLockFile
_ -> do
lockFileHandle <- openLockFile lockFileName
case lockFileHandle of
Just h -> io $ do
c_getpid >>= hPutStr h . show
hFlush h
pure h
Nothing ->
case retryToAcquireLock params' of
No -> failedToAcquireLockFile
_ -> do
io $ threadDelay sleepBetweenRetries'
lock' paramsDecRetries lockFileName
where
sleepBetweenRetries' = fromIntegral $ sleepBetweenRetries params'
failedToAcquireLockFile = throw $ UnableToAcquireLockFile lockFileName
paramsDecRetries = case retryToAcquireLock params' of
NumberOfTimes n ->
params'{retryToAcquireLock = NumberOfTimes $ n - 1}
_ -> params'
unlock
:: (MonadMask m, MonadIO m)
=> FilePath
-> Handle
-> m ()
unlock lockFileName lockFileHandle =
io $ hClose lockFileHandle *> removeFile lockFileName