aff37fc208
This makes annexFileMode be just an application of setAnnexPerm', which avoids having 2 functions that do different versions of the same thing. Fixes some buggy behavior for some combinations of core.sharedRepository and umask. Sponsored-by: Jack Hill on Patreon
332 lines
11 KiB
Haskell
332 lines
11 KiB
Haskell
{- pid-based lock files
|
|
-
|
|
- Copyright 2015-2021 Joey Hess <id@joeyh.name>
|
|
-
|
|
- License: BSD-2-clause
|
|
-}
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
module Utility.LockFile.PidLock (
|
|
PidLockFile,
|
|
LockHandle,
|
|
tryLock,
|
|
waitLock,
|
|
waitedLock,
|
|
alreadyLocked,
|
|
dropLock,
|
|
LockStatus(..),
|
|
getLockStatus,
|
|
checkLocked,
|
|
checkSaneLock,
|
|
pidLockEnv,
|
|
pidLockEnvValue,
|
|
) where
|
|
|
|
import Utility.PartialPrelude
|
|
import Utility.Exception
|
|
import Utility.Applicative
|
|
import Utility.Directory
|
|
import Utility.Monad
|
|
import Utility.Path.AbsRel
|
|
import Utility.FileMode
|
|
import Utility.LockFile.LockStatus
|
|
import Utility.ThreadScheduler
|
|
import Utility.Hash
|
|
import Utility.FileSystemEncoding
|
|
import Utility.Env
|
|
import Utility.Env.Set
|
|
import Utility.Tmp
|
|
import qualified Utility.LockFile.Posix as Posix
|
|
|
|
import System.IO
|
|
import System.Posix.Types
|
|
import System.Posix.IO.ByteString
|
|
import System.Posix.Files.ByteString
|
|
import System.Posix.Process
|
|
import Control.Monad
|
|
import Control.Monad.IO.Class (liftIO, MonadIO)
|
|
import qualified System.FilePath.ByteString as P
|
|
import Data.Maybe
|
|
import Data.List
|
|
import Network.BSD
|
|
import System.FilePath
|
|
import Control.Applicative
|
|
import Prelude
|
|
|
|
type PidLockFile = RawFilePath
|
|
|
|
data LockHandle
|
|
= LockHandle PidLockFile FileStatus SideLockHandle
|
|
| ParentLocked
|
|
|
|
type SideLockHandle = Maybe (RawFilePath, Posix.LockHandle)
|
|
|
|
data PidLock = PidLock
|
|
{ lockingPid :: ProcessID
|
|
, lockingHost :: HostName
|
|
}
|
|
deriving (Eq, Read, Show)
|
|
|
|
mkPidLock :: IO PidLock
|
|
mkPidLock = PidLock
|
|
<$> getProcessID
|
|
<*> getHostName
|
|
|
|
readPidLock :: PidLockFile -> IO (Maybe PidLock)
|
|
readPidLock lockfile = (readish =<<)
|
|
<$> catchMaybeIO (readFile (fromRawFilePath lockfile))
|
|
|
|
-- To avoid races when taking over a stale pid lock, a side lock is used.
|
|
-- This is a regular posix exclusive lock.
|
|
trySideLock :: PidLockFile -> (SideLockHandle -> IO a) -> IO a
|
|
trySideLock lockfile a = do
|
|
sidelock <- sideLockFile lockfile
|
|
mlck <- catchDefaultIO Nothing $
|
|
Posix.tryLockExclusive (Just modesetter) sidelock
|
|
-- Check the lock we just took, in case we opened a side lock file
|
|
-- belonging to another process that will have since deleted it.
|
|
case mlck of
|
|
Nothing -> a Nothing
|
|
Just lck -> ifM (Posix.checkSaneLock sidelock lck)
|
|
( a (Just (sidelock, lck))
|
|
, a Nothing
|
|
)
|
|
where
|
|
-- Let all users write to the lock file in /dev/shm or /tmp,
|
|
-- so that other users can reuse it to take the lock.
|
|
-- Since /dev/shm and /tmp are sticky dirs, a user cannot
|
|
-- delete another user's lock file there, so could not
|
|
-- delete a stale lock.
|
|
mode = combineModes (readModes ++ writeModes)
|
|
modesetter = ModeSetter mode (\f -> modifyFileMode f (const mode))
|
|
|
|
dropSideLock :: SideLockHandle -> IO ()
|
|
dropSideLock Nothing = return ()
|
|
dropSideLock (Just (f, h)) = do
|
|
-- Delete the file first, to ensure that any process that is trying
|
|
-- to take the side lock will only succeed once the file is
|
|
-- deleted, and so will be able to immediately see that it's taken
|
|
-- a stale lock.
|
|
_ <- tryIO $ removeFile (fromRawFilePath f)
|
|
Posix.dropLock h
|
|
|
|
-- 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. /tmp is used as a fallback.
|
|
sideLockFile :: PidLockFile -> IO RawFilePath
|
|
sideLockFile lockfile = do
|
|
f <- fromRawFilePath <$> absPath lockfile
|
|
let base = intercalate "_" (splitDirectories (makeRelative "/" f))
|
|
let shortbase = reverse $ take 32 $ reverse base
|
|
let md5sum = if base == shortbase
|
|
then ""
|
|
else toRawFilePath $ show (md5 (encodeBL base))
|
|
dir <- ifM (doesDirectoryExist "/dev/shm")
|
|
( return "/dev/shm"
|
|
, return "/tmp"
|
|
)
|
|
return $ dir P.</> md5sum <> toRawFilePath shortbase <> ".lck"
|
|
|
|
-- | Tries to take a lock; does not block when the lock is already held.
|
|
--
|
|
-- 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.
|
|
--
|
|
-- If a parent process is holding the lock, determined by a
|
|
-- "PIDLOCK_lockfile" environment variable, does not block either.
|
|
tryLock :: PidLockFile -> IO (Maybe LockHandle)
|
|
tryLock lockfile = do
|
|
abslockfile <- absPath lockfile
|
|
lockenv <- pidLockEnv abslockfile
|
|
getEnv lockenv >>= \case
|
|
Nothing -> trySideLock lockfile (go abslockfile)
|
|
_ -> return (Just ParentLocked)
|
|
where
|
|
go abslockfile sidelock = do
|
|
let abslockfile' = fromRawFilePath abslockfile
|
|
(tmp, h) <- openTmpFileIn (takeDirectory abslockfile') "locktmp"
|
|
let tmp' = toRawFilePath tmp
|
|
setFileMode tmp' (combineModes readModes)
|
|
hPutStr h . show =<< mkPidLock
|
|
hClose h
|
|
let failedlock = do
|
|
dropSideLock sidelock
|
|
removeWhenExistsWith removeLink tmp'
|
|
return Nothing
|
|
let tooklock st = return $ Just $ LockHandle abslockfile st sidelock
|
|
linkToLock sidelock tmp' abslockfile >>= \case
|
|
Just lckst -> do
|
|
removeWhenExistsWith removeLink tmp'
|
|
tooklock lckst
|
|
Nothing -> do
|
|
v <- readPidLock abslockfile
|
|
hn <- getHostName
|
|
tmpst <- getFileStatus tmp'
|
|
case v of
|
|
Just pl | isJust sidelock && hn == lockingHost pl -> do
|
|
-- Since we have the sidelock,
|
|
-- and are on the same host that
|
|
-- the pidlock was taken on,
|
|
-- we know that the pidlock is
|
|
-- stale, and can take it over.
|
|
rename tmp' abslockfile
|
|
tooklock tmpst
|
|
_ -> failedlock
|
|
|
|
-- Linux's open(2) man page recommends linking a pid lock into place,
|
|
-- as the most portable atomic operation that will fail if
|
|
-- it already exists.
|
|
--
|
|
-- open(2) suggests that link can sometimes appear to fail
|
|
-- on NFS but have actually succeeded, and the way to find out is to stat
|
|
-- the file and check its link count etc.
|
|
--
|
|
-- However, not all filesystems support hard links. So, first probe
|
|
-- to see if they are supported. If not, use open with O_EXCL.
|
|
linkToLock :: SideLockHandle -> RawFilePath -> RawFilePath -> IO (Maybe FileStatus)
|
|
linkToLock Nothing _ _ = return Nothing
|
|
linkToLock (Just _) src dest = do
|
|
let probe = src <> ".lnk"
|
|
v <- tryIO $ createLink src probe
|
|
removeWhenExistsWith removeLink probe
|
|
case v of
|
|
Right _ -> do
|
|
_ <- tryIO $ createLink src dest
|
|
ifM (catchBoolIO checklinked)
|
|
( ifM (catchBoolIO $ not <$> checkInsaneLustre dest)
|
|
( catchMaybeIO $ getFileStatus dest
|
|
, return Nothing
|
|
)
|
|
, return Nothing
|
|
)
|
|
Left _ -> catchMaybeIO $ do
|
|
let setup = do
|
|
fd <- openFd dest WriteOnly
|
|
(Just $ combineModes readModes)
|
|
(defaultFileFlags {exclusive = True})
|
|
fdToHandle fd
|
|
let cleanup = hClose
|
|
let go h = readFile (fromRawFilePath src) >>= hPutStr h
|
|
bracket setup cleanup go
|
|
getFileStatus dest
|
|
where
|
|
checklinked = do
|
|
x <- getSymbolicLinkStatus src
|
|
y <- getSymbolicLinkStatus dest
|
|
return $ and
|
|
[ deviceID x == deviceID y
|
|
, fileID x == fileID y
|
|
, fileMode x == fileMode y
|
|
, fileOwner x == fileOwner y
|
|
, fileGroup x == fileGroup y
|
|
, fileSize x == fileSize y
|
|
, modificationTime x == modificationTime y
|
|
, isRegularFile x == isRegularFile y
|
|
, linkCount x == linkCount y
|
|
, linkCount x == 2
|
|
]
|
|
|
|
-- On a Lustre filesystem, link has been observed to incorrectly *succeed*,
|
|
-- despite the dest already existing. A subsequent stat of the dest
|
|
-- looked like it had been replaced with the src. The process proceeded to
|
|
-- run and then deleted the dest, and after the process was done, the
|
|
-- original file was observed to still be in place.
|
|
--
|
|
-- We can detect this insanity by getting the directory contents after
|
|
-- making the link, and checking to see if 2 copies of the dest file,
|
|
-- with the SAME FILENAME exist.
|
|
checkInsaneLustre :: RawFilePath -> IO Bool
|
|
checkInsaneLustre dest = do
|
|
let dest' = fromRawFilePath dest
|
|
fs <- dirContents (takeDirectory dest')
|
|
case length (filter (== dest') fs) of
|
|
1 -> return False -- whew!
|
|
0 -> return True -- wtf?
|
|
_ -> do
|
|
-- Try to clean up the extra copy we made
|
|
-- that has the same name. Egads.
|
|
_ <- tryIO $ removeFile dest'
|
|
return True
|
|
|
|
-- | Waits as necessary to take a lock.
|
|
--
|
|
-- Uses a 1 second wait-loop, retrying until a timeout.
|
|
--
|
|
-- After the first second waiting, runs the callback to display a message,
|
|
-- so the user knows why it's stalled.
|
|
waitLock :: MonadIO m => Seconds -> PidLockFile -> (String -> m ()) -> (Bool -> IO ()) -> m LockHandle
|
|
waitLock (Seconds timeout) lockfile displaymessage sem = go timeout
|
|
where
|
|
go n
|
|
| n > 0 = liftIO (tryLock lockfile) >>= \case
|
|
Nothing -> do
|
|
when (n == pred timeout) $
|
|
displaymessage $ "waiting for pid lock file " ++ fromRawFilePath lockfile ++ " which is held by another process (or may be stale)"
|
|
liftIO $ threadDelaySeconds (Seconds 1)
|
|
go (pred n)
|
|
Just lckh -> do
|
|
liftIO $ sem True
|
|
return lckh
|
|
| otherwise = do
|
|
liftIO $ sem False
|
|
waitedLock (Seconds timeout) lockfile displaymessage
|
|
|
|
waitedLock :: MonadIO m => Seconds -> PidLockFile -> (String -> m ()) -> m a
|
|
waitedLock (Seconds timeout) lockfile displaymessage = do
|
|
displaymessage $ show timeout ++ " second timeout exceeded while waiting for pid lock file " ++ fromRawFilePath lockfile
|
|
giveup $ "Gave up waiting for pid lock file " ++ fromRawFilePath lockfile
|
|
|
|
-- | Use when the pid lock has already been taken by another thread of the
|
|
-- same process.
|
|
alreadyLocked :: MonadIO m => PidLockFile -> m LockHandle
|
|
alreadyLocked lockfile = liftIO $ do
|
|
abslockfile <- absPath lockfile
|
|
st <- getFileStatus abslockfile
|
|
return $ LockHandle abslockfile st Nothing
|
|
|
|
dropLock :: LockHandle -> IO ()
|
|
dropLock (LockHandle lockfile _ sidelock) = do
|
|
-- Drop side lock first, at which point the pid lock will be
|
|
-- considered stale.
|
|
dropSideLock sidelock
|
|
removeWhenExistsWith removeLink lockfile
|
|
dropLock ParentLocked = return ()
|
|
|
|
getLockStatus :: PidLockFile -> IO LockStatus
|
|
getLockStatus = maybe StatusUnLocked (StatusLockedBy . lockingPid) <$$> readPidLock
|
|
|
|
checkLocked :: PidLockFile -> 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 :: PidLockFile -> LockHandle -> IO Bool
|
|
checkSaneLock lockfile (LockHandle _ st _) =
|
|
go =<< catchMaybeIO (getFileStatus lockfile)
|
|
where
|
|
go Nothing = return False
|
|
go (Just st') = return $
|
|
deviceID st == deviceID st' && fileID st == fileID st'
|
|
checkSaneLock _ ParentLocked = return True
|
|
|
|
-- | A parent process that is using pid locking can set this to 1 before
|
|
-- starting a child, to communicate to the child that it's holding the pid
|
|
-- lock and that the child can skip trying to take it, and not block
|
|
-- on the pid lock its parent is holding.
|
|
--
|
|
-- The parent process should keep running as long as the child
|
|
-- process is running, since the child inherits the environment and will
|
|
-- not see unsetLockEnv.
|
|
pidLockEnv :: RawFilePath -> IO String
|
|
pidLockEnv lockfile = do
|
|
abslockfile <- fromRawFilePath <$> absPath lockfile
|
|
return $ "PIDLOCK_" ++ filter legalInEnvVar abslockfile
|
|
|
|
pidLockEnvValue :: String
|
|
pidLockEnvValue = "1"
|