159 lines
5 KiB
Haskell
159 lines
5 KiB
Haskell
{- Pid locks, using lock pools.
|
|
-
|
|
- Copyright 2015-2021 Joey Hess <id@joeyh.name>
|
|
-
|
|
- License: BSD-2-clause
|
|
-}
|
|
|
|
module Utility.LockPool.PidLock (
|
|
P.LockFile,
|
|
LockHandle,
|
|
waitLock,
|
|
tryLock,
|
|
tryLock',
|
|
checkLocked,
|
|
getLockStatus,
|
|
LockStatus(..),
|
|
dropLock,
|
|
checkSaneLock,
|
|
) where
|
|
|
|
import qualified Utility.LockFile.PidLock as F
|
|
import Utility.LockFile.LockStatus
|
|
import qualified Utility.LockPool.STM as P
|
|
import Utility.LockPool.STM (LockFile, LockMode(..))
|
|
import Utility.LockPool.LockHandle
|
|
import Utility.ThreadScheduler
|
|
|
|
import System.IO
|
|
import System.Posix
|
|
import Control.Concurrent.STM
|
|
import Data.Maybe
|
|
import Control.Monad
|
|
import Control.Monad.Catch
|
|
import Control.Monad.IO.Class
|
|
import Control.Applicative
|
|
import Prelude
|
|
|
|
-- Does locking using a pid lock, blocking until the lock is available
|
|
-- or the Seconds timeout if the pid lock is held by another process.
|
|
--
|
|
-- There are two levels of locks. A STM lock is used to handle
|
|
-- fine-grained locking among threads, locking a specific lockfile,
|
|
-- but only in memory. The pid lock handles locking between processes.
|
|
--
|
|
-- The pid lock is only taken once, and LockShared is used for it,
|
|
-- so multiple threads can have it locked. Only the first thread
|
|
-- will create the pid lock, and it remains until all threads drop
|
|
-- their locks.
|
|
waitLock
|
|
:: (MonadIO m, MonadMask m)
|
|
=> LockFile
|
|
-> LockMode
|
|
-> Seconds
|
|
-> F.PidLockFile
|
|
-> (String -> m ())
|
|
-> m LockHandle
|
|
waitLock finelockfile lockmode timeout pidlockfile displaymessage = do
|
|
fl <- takefinelock
|
|
pl <- takepidlock
|
|
`onException` liftIO (dropLock fl)
|
|
registerPostRelease fl pl
|
|
return fl
|
|
where
|
|
takefinelock = fst <$> makeLockHandle P.lockPool finelockfile
|
|
(\p f -> P.waitTakeLock p f lockmode)
|
|
(\_ _ -> pure (stmonlyflo, ()))
|
|
-- A shared STM lock is taken for each use of the pid lock,
|
|
-- but only the first thread to take it actually creates the pid
|
|
-- lock file.
|
|
takepidlock = makeLockHandle P.lockPool pidlockfile
|
|
(\p f -> P.waitTakeLock p f LockShared)
|
|
(\f (P.FirstLock firstlock firstlocksem) -> if firstlock
|
|
then waitlock f firstlocksem
|
|
else liftIO (atomically $ readTMVar firstlocksem) >>= \case
|
|
P.FirstLockSemWaited True -> alreadylocked f
|
|
P.FirstLockSemTried True -> alreadylocked f
|
|
P.FirstLockSemWaited False -> F.waitedLock timeout f displaymessage
|
|
P.FirstLockSemTried False -> waitlock f firstlocksem
|
|
)
|
|
waitlock f firstlocksem = do
|
|
h <- F.waitLock timeout f displaymessage $
|
|
void . atomically . tryPutTMVar firstlocksem . P.FirstLockSemWaited
|
|
return (mkflo h, Just h)
|
|
alreadylocked f = do
|
|
lh <- F.alreadyLocked f
|
|
return (mkflo lh, Nothing)
|
|
|
|
registerPostRelease :: MonadIO m => LockHandle -> (LockHandle, Maybe F.LockHandle) -> m ()
|
|
registerPostRelease (LockHandle flh _) (pl@(LockHandle plh _), mpidlock) = do
|
|
-- After the fine-grained lock gets dropped (and any shared locks
|
|
-- of it are also dropped), drop the associated pid lock.
|
|
liftIO $ atomically $
|
|
P.registerPostReleaseLock flh (dropLock pl)
|
|
-- When the last thread to use the pid lock has dropped it,
|
|
-- close the pid lock file itself.
|
|
case mpidlock of
|
|
Just pidlock -> liftIO $ atomically $
|
|
P.registerPostReleaseLock plh (F.dropLock pidlock)
|
|
Nothing -> return ()
|
|
|
|
-- Tries to take a pid lock, but does not block.
|
|
tryLock :: LockFile -> LockMode -> F.PidLockFile -> IO (Maybe LockHandle)
|
|
tryLock finelockfile lockmode pidlockfile = takefinelock >>= \case
|
|
Just fl -> tryLock' pidlockfile >>= \case
|
|
Just pl -> do
|
|
registerPostRelease fl pl
|
|
return (Just fl)
|
|
Nothing -> do
|
|
dropLock fl
|
|
return Nothing
|
|
Nothing -> return Nothing
|
|
where
|
|
takefinelock = fmap fst <$> tryMakeLockHandle P.lockPool finelockfile
|
|
(\p f -> P.tryTakeLock p f lockmode)
|
|
(\_ _ -> pure (Just (stmonlyflo, ())))
|
|
|
|
tryLock' :: F.PidLockFile -> IO (Maybe (LockHandle, Maybe F.LockHandle))
|
|
tryLock' pidlockfile = tryMakeLockHandle P.lockPool pidlockfile
|
|
(\p f -> P.tryTakeLock p f LockShared)
|
|
(\f (P.FirstLock firstlock firstlocksem) -> if firstlock
|
|
then do
|
|
mlh <- F.tryLock f
|
|
void $ atomically $ tryPutTMVar firstlocksem
|
|
(P.FirstLockSemTried (isJust mlh))
|
|
case mlh of
|
|
Just lh -> return (Just (mkflo lh, Just lh))
|
|
Nothing -> return Nothing
|
|
else liftIO (atomically $ readTMVar firstlocksem) >>= \case
|
|
P.FirstLockSemWaited True -> alreadylocked f
|
|
P.FirstLockSemTried True -> alreadylocked f
|
|
P.FirstLockSemWaited False -> return Nothing
|
|
P.FirstLockSemTried False -> return Nothing
|
|
)
|
|
where
|
|
alreadylocked f = do
|
|
lh <- F.alreadyLocked f
|
|
return (Just (mkflo lh, Nothing))
|
|
|
|
checkLocked :: LockFile -> IO (Maybe Bool)
|
|
checkLocked file = P.getLockStatus P.lockPool file
|
|
(pure (Just True))
|
|
(F.checkLocked file)
|
|
|
|
getLockStatus :: LockFile -> IO LockStatus
|
|
getLockStatus file = P.getLockStatus P.lockPool file
|
|
(StatusLockedBy <$> getProcessID)
|
|
(F.getLockStatus file)
|
|
|
|
mkflo :: F.LockHandle -> FileLockOps
|
|
mkflo h = FileLockOps
|
|
{ fDropLock = return ()
|
|
, fCheckSaneLock = \f -> F.checkSaneLock f h
|
|
}
|
|
|
|
stmonlyflo :: FileLockOps
|
|
stmonlyflo = FileLockOps
|
|
{ fDropLock = return ()
|
|
, fCheckSaneLock = const (return True)
|
|
}
|