{-# LANGUAGE ScopedTypeVariables #-} module Extra.Lock ( withLock , awaitLock ) where import Control.Exception import Control.Monad.RWS import Prelude hiding (catch) import System.Directory import System.IO import System.IO.Error hiding (try, catch) import System.Posix.Files import System.Posix.IO import System.Posix.Unistd withLock :: (MonadIO m) => FilePath -> m a -> m a withLock path task = liftIO (checkLock >> takeLock) >> task >>= \ result -> liftIO dropLock >> return result where -- Return True if file is locked by a running process, false otherwise --checkLock :: IO (Either Exception ()) checkLock = readFile path `catch` checkReadError >>= processRunning . lines checkReadError :: IOError -> IO String checkReadError e | isDoesNotExistError e = return "" checkReadError e = throw e processRunning :: [String] -> IO () processRunning (pid : _) = do exists <- doesDirectoryExist ("/proc/" ++ pid) case exists of True -> throw (lockedBy pid path) False -> breakLock processRunning [] = breakLock breakLock = removeFile path `catch` checkBreakError checkBreakError (e :: IOException) | isDoesNotExistError e = return () checkBreakError e = throw e takeLock :: IO () takeLock = -- Try to create the lock file in exclusive mode, if this -- succeeds then we have a lock. Then write the process ID -- into the lock and close. openFd path ReadWrite (Just 0o600) (defaultFileFlags {exclusive = True, trunc = True}) >>= fdToHandle >>= \ h -> processID >>= hPutStrLn h >> hClose h dropLock = removeFile path `catch` checkDrop checkDrop (e :: IOException) | isDoesNotExistError e = return () checkDrop e = throw e -- |Like withLock, but instead of giving up immediately, try n times -- with a wait between each. --awaitLock :: (MonadIO m) => Int -> Int -> FilePath -> m a -> m (Either Exception a) awaitLock tries usecs path task = attempt 0 where attempt n | n >= tries = error "awaitLock: too many failures" attempt n = withLock path task `catch` checkLockError where checkLockError e | isAlreadyInUseError e = liftIO (usleep usecs) >> attempt (n + 1) checkLockError e = throw e processID :: IO String processID = readSymbolicLink "/proc/self" lockedBy pid path = mkIOError alreadyInUseErrorType ("Locked by " ++ pid) Nothing (Just path)