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)
 | 
						|
	}
 |