git-annex/Utility/LockFile/PidLock.hs

193 lines
5.9 KiB
Haskell
Raw Normal View History

{- 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,
LockStatus(..),
getLockStatus,
2015-11-12 20:31:34 +00:00
checkLocked,
checkSaneLock,
) 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
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
import Data.Hash.MD5
type LockFile = FilePath
2015-11-12 20:31:34 +00:00
data LockHandle = LockHandle FilePath Fd (Maybe Posix.LockHandle)
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
sidelock <- sideLockFile lockfile
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)
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"
-- | 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
let failedlock = do
2015-11-12 20:31:34 +00:00
closeFd fd
nukeFile tmp
return Nothing
2015-11-12 20:31:34 +00:00
let tooklock = return $ Just $ LockHandle lockfile fd sidelock
ifM (linkToLock tmp lockfile)
2015-11-13 16:52:24 +00:00
( do
nukeFile tmp
tooklock
, 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,
-- we know that the pidlock is
-- stale, and can take it over.
rename tmp lockfile
tooklock
_ -> failedlock
)
-- 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
]
-- | 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
-- 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
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
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
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