2015-11-12 19:38:02 +00:00
|
|
|
{- pid-based lock files
|
|
|
|
-
|
|
|
|
- Copyright 2015 Joey Hess <id@joeyh.name>
|
|
|
|
-
|
|
|
|
- License: BSD-2-clause
|
|
|
|
-}
|
|
|
|
|
|
|
|
module Utility.LockFile.PidLock (
|
|
|
|
LockHandle,
|
|
|
|
tryLock,
|
|
|
|
waitLock,
|
2015-11-12 20:31:34 +00:00
|
|
|
dropLock,
|
2015-11-12 19:38:02 +00:00
|
|
|
LockStatus(..),
|
|
|
|
getLockStatus,
|
2015-11-12 20:31:34 +00:00
|
|
|
checkLocked,
|
|
|
|
checkSaneLock,
|
2015-11-12 19:38:02 +00:00
|
|
|
) where
|
|
|
|
|
|
|
|
import Utility.PartialPrelude
|
|
|
|
import Utility.Exception
|
|
|
|
import Utility.Applicative
|
|
|
|
import Utility.Directory
|
|
|
|
import Utility.Monad
|
|
|
|
import Utility.Path
|
|
|
|
import Utility.FileMode
|
|
|
|
import Utility.LockFile.LockStatus
|
2015-11-12 21:12:54 +00:00
|
|
|
import Utility.ThreadScheduler
|
2015-11-12 19:38:02 +00:00
|
|
|
import qualified Utility.LockFile.Posix as Posix
|
|
|
|
|
|
|
|
import System.IO
|
|
|
|
import System.Posix
|
|
|
|
import Data.Maybe
|
|
|
|
import Data.List
|
|
|
|
import Control.Applicative
|
|
|
|
import Network.BSD
|
|
|
|
import System.FilePath
|
2015-11-13 18:04:29 +00:00
|
|
|
import Data.Hash.MD5
|
2015-11-12 19:38:02 +00:00
|
|
|
|
|
|
|
type LockFile = FilePath
|
|
|
|
|
2015-11-12 20:31:34 +00:00
|
|
|
data LockHandle = LockHandle FilePath Fd (Maybe Posix.LockHandle)
|
2015-11-12 19:38:02 +00:00
|
|
|
|
|
|
|
data PidLock = PidLock
|
|
|
|
{ lockingPid :: ProcessID
|
|
|
|
, lockingHost :: HostName
|
|
|
|
}
|
|
|
|
deriving (Eq, Read, Show)
|
|
|
|
|
|
|
|
mkPidLock :: IO PidLock
|
|
|
|
mkPidLock = PidLock
|
|
|
|
<$> getProcessID
|
|
|
|
<*> getHostName
|
|
|
|
|
|
|
|
readPidLock :: LockFile -> IO (Maybe PidLock)
|
|
|
|
readPidLock lockfile = (readish =<<) <$> catchMaybeIO (readFile lockfile)
|
|
|
|
|
|
|
|
-- To avoid races when taking over a stale pid lock, a side lock is used.
|
|
|
|
-- This is a regular posix exclusive lock. The side lock is put in
|
|
|
|
-- /dev/shm. This will work on most any Linux system, even if its whole
|
|
|
|
-- root filesystem doesn't support posix locks.
|
|
|
|
trySideLock :: LockFile -> (Maybe Posix.LockHandle -> IO a) -> IO a
|
|
|
|
trySideLock lockfile a = do
|
2015-11-13 18:04:29 +00:00
|
|
|
sidelock <- sideLockFile lockfile
|
2015-11-12 19:38:02 +00:00
|
|
|
mlck <- catchDefaultIO Nothing $
|
|
|
|
withUmask nullFileMode $
|
|
|
|
Posix.tryLockExclusive (Just mode) sidelock
|
|
|
|
a mlck
|
|
|
|
where
|
|
|
|
-- Let all users write to the lock file in /dev/shm,
|
|
|
|
-- so that other users can reuse it to take the lock.
|
|
|
|
-- Since /dev/shm is sticky, a user cannot delete another user's
|
|
|
|
-- lock file there, so could not delete a stale lock.
|
|
|
|
mode = combineModes (readModes ++ writeModes)
|
|
|
|
|
2015-11-13 18:04:29 +00:00
|
|
|
sideLockFile :: LockFile -> IO LockFile
|
|
|
|
sideLockFile lockfile = do
|
|
|
|
f <- absPath lockfile
|
|
|
|
let base = intercalate "_" (splitDirectories (makeRelative "/" f))
|
|
|
|
let shortbase = reverse $ take 32 $ reverse base
|
|
|
|
let md5 = if base == shortbase then "" else md5s (Str base)
|
|
|
|
return $ "/dev/shm" </> md5 ++ shortbase ++ ".lck"
|
|
|
|
|
2015-11-12 19:38:02 +00:00
|
|
|
-- | Tries to take a lock; does not block when the lock is already held.
|
|
|
|
--
|
|
|
|
-- The method used is atomic even on NFS without needing O_EXCL support.
|
|
|
|
--
|
|
|
|
-- Note that stale locks are automatically detected and broken.
|
|
|
|
-- However, if the lock file is on a networked file system, and was
|
|
|
|
-- created on a different host than the current host (determined by hostname),
|
|
|
|
-- this can't be done and stale locks may persist.
|
|
|
|
tryLock :: LockFile -> IO (Maybe LockHandle)
|
|
|
|
tryLock lockfile = trySideLock lockfile $ \sidelock -> do
|
|
|
|
(tmp, h) <- openTempFile (takeDirectory lockfile) "locktmp"
|
|
|
|
setFileMode tmp (combineModes readModes)
|
|
|
|
hPutStr h . show =<< mkPidLock
|
2015-11-12 20:31:34 +00:00
|
|
|
fd <- handleToFd h
|
2015-11-12 19:38:02 +00:00
|
|
|
let failedlock = do
|
2015-11-12 20:31:34 +00:00
|
|
|
closeFd fd
|
2015-11-12 19:38:02 +00:00
|
|
|
nukeFile tmp
|
|
|
|
return Nothing
|
2015-11-12 20:31:34 +00:00
|
|
|
let tooklock = return $ Just $ LockHandle lockfile fd sidelock
|
2015-11-13 17:22:45 +00:00
|
|
|
ifM (linkToLock tmp lockfile)
|
2015-11-13 16:52:24 +00:00
|
|
|
( do
|
|
|
|
nukeFile tmp
|
|
|
|
tooklock
|
2015-11-12 19:38:02 +00:00
|
|
|
, do
|
|
|
|
v <- readPidLock lockfile
|
|
|
|
hn <- getHostName
|
|
|
|
case v of
|
|
|
|
Just pl | isJust sidelock && hn == lockingHost pl -> do
|
|
|
|
-- Since we have the sidelock,
|
|
|
|
-- and are on the same host that
|
2015-11-12 21:12:54 +00:00
|
|
|
-- the pidlock was taken on,
|
2015-11-12 19:38:02 +00:00
|
|
|
-- we know that the pidlock is
|
|
|
|
-- stale, and can take it over.
|
|
|
|
rename tmp lockfile
|
|
|
|
tooklock
|
|
|
|
_ -> failedlock
|
|
|
|
)
|
|
|
|
|
2015-11-13 17:22:45 +00:00
|
|
|
-- Linux man pages recommend linking a pid lock into place,
|
|
|
|
-- as the most portable atomic operation that will fail if
|
|
|
|
-- it already exists. However, on some network filesystems,
|
|
|
|
-- link will return success sometimes despite having failed,
|
|
|
|
-- so we have to stat both files to check if it actually worked.
|
|
|
|
linkToLock :: FilePath -> FilePath -> IO Bool
|
|
|
|
linkToLock src dest = ifM (isJust <$> catchMaybeIO (createLink src dest))
|
|
|
|
( catchDefaultIO False checklink
|
|
|
|
, return False
|
|
|
|
)
|
|
|
|
where
|
|
|
|
checklink = do
|
|
|
|
x <- getSymbolicLinkStatus src
|
|
|
|
y <- getSymbolicLinkStatus dest
|
|
|
|
return $ and
|
|
|
|
[ deviceID x == deviceID y
|
|
|
|
, fileID x == fileID y
|
|
|
|
, fileMode x == fileMode y
|
|
|
|
, linkCount x == linkCount y
|
|
|
|
, fileOwner x == fileOwner y
|
|
|
|
, fileGroup x == fileGroup y
|
|
|
|
, specialDeviceID x == specialDeviceID y
|
|
|
|
, fileSize x == fileSize y
|
|
|
|
, modificationTime x == modificationTime y
|
|
|
|
, isRegularFile x == isRegularFile y
|
|
|
|
]
|
|
|
|
|
2015-11-12 19:38:02 +00:00
|
|
|
-- | Waits as necessary to take a lock.
|
|
|
|
--
|
|
|
|
-- Uses a 1 second wait-loop.
|
|
|
|
--
|
2015-11-12 21:12:54 +00:00
|
|
|
-- May wait untie timeout if the lock file is stale and is on a network file
|
2015-11-12 19:38:02 +00:00
|
|
|
-- system, or on a system where the side lock cannot be taken.
|
2015-11-12 21:12:54 +00:00
|
|
|
waitLock :: Seconds -> LockFile -> IO LockHandle
|
|
|
|
waitLock (Seconds timeout) lockfile = go timeout
|
2015-11-12 19:38:02 +00:00
|
|
|
where
|
2015-11-12 21:12:54 +00:00
|
|
|
go n
|
|
|
|
| n > 0 = maybe (threadDelaySeconds (Seconds 1) >> go (pred n)) return
|
|
|
|
=<< tryLock lockfile
|
|
|
|
| otherwise = do
|
|
|
|
hPutStrLn stderr $ show timeout ++ " second timeout exceeded while waiting for pid lock file " ++ lockfile
|
|
|
|
error $ "Gave up waiting for possibly stale pid lock file " ++ lockfile
|
2015-11-12 19:38:02 +00:00
|
|
|
|
|
|
|
dropLock :: LockHandle -> IO ()
|
2015-11-13 16:36:37 +00:00
|
|
|
dropLock (LockHandle lockfile fd sidelock) = do
|
|
|
|
-- Drop side lock first, at which point the pid lock will be
|
|
|
|
-- considered stale.
|
|
|
|
-- The side lock file cannot be deleted because another process may
|
|
|
|
-- have it open and be waiting to lock it.
|
|
|
|
maybe noop Posix.dropLock sidelock
|
2015-11-12 20:31:34 +00:00
|
|
|
closeFd fd
|
2015-11-12 19:38:02 +00:00
|
|
|
nukeFile lockfile
|
2015-11-12 20:31:34 +00:00
|
|
|
|
|
|
|
getLockStatus :: LockFile -> IO LockStatus
|
|
|
|
getLockStatus = maybe StatusUnLocked (StatusLockedBy . lockingPid) <$$> readPidLock
|
|
|
|
|
|
|
|
checkLocked :: LockFile -> IO (Maybe Bool)
|
|
|
|
checkLocked lockfile = conv <$> getLockStatus lockfile
|
|
|
|
where
|
|
|
|
conv (StatusLockedBy _) = Just True
|
|
|
|
conv _ = Just False
|
|
|
|
|
|
|
|
-- Checks that the lock file still exists, and is the same file that was
|
|
|
|
-- locked to get the LockHandle.
|
|
|
|
checkSaneLock :: LockFile -> LockHandle -> IO Bool
|
|
|
|
checkSaneLock lockfile (LockHandle _ fd _) =
|
|
|
|
go =<< catchMaybeIO (getFileStatus lockfile)
|
|
|
|
where
|
|
|
|
go Nothing = return False
|
|
|
|
go (Just st) = do
|
|
|
|
fdst <- getFdStatus fd
|
|
|
|
return $ deviceID fdst == deviceID st && fileID fdst == fileID st
|