{-# LANGUAGE CPP #-}

-- | This module provides a portable interface to file locks as a mechanism for
-- inter-process synchronization.
--
-- Each file lock is associated with a file. When taking a lock, the assiciated
-- file is created if it's not present, then the file is locked in an
-- OS-dependent way. While the lock is being held, no other process or
-- thread can take it, unless the specified 'SharedExclusive' values
-- allow it.
--
-- All locks held by a process are released when the process exits. They can
-- also be explicitly released using 'unlockFile'.
--
-- It is not recommended to open or otherwise use lock files for other
-- purposes, because it tends to expose differences between operating systems.
-- For example, on Windows 'System.IO.openFile' for a lock file will fail when
-- the lock is held, but on Unix it won't.
--
-- Note on the implementation: currently the module uses flock(2) on non-Windows
-- platforms, and LockFileEx on Windows.
--
-- On non-Windows platforms, @InterruptibleFFI@ is used in the implementation to
-- ensures that blocking lock calls can be correctly interrupted by async
-- exceptions (e.g. functions like `timeout`).  This has been tested on Linux.
module System.FileLock
  ( FileLock
  , SharedExclusive(..)
  , lockFile
  , tryLockFile
  , unlockFile
  , withFileLock
  , withTryFileLock
  ) where

import Control.Applicative
import qualified Control.Exception as E
import Control.Monad
import Data.IORef
import Prelude

#ifdef USE_FLOCK
import qualified System.FileLock.Internal.Flock as I
#elif USE_LOCKFILEEX
import qualified System.FileLock.Internal.LockFileEx as I
#else
#error No backend is available
#endif

-- | A token that represents ownership of a lock.
data FileLock = Lock
  {-# UNPACk #-} !I.Lock
  {-# UNPACk #-} !(IORef Bool) -- alive?

instance Eq FileLock where
  Lock _ x == Lock _ y = x == y

newLock :: I.Lock -> IO FileLock
newLock x = Lock x <$> newIORef True

-- | A type of lock to be taken.
data SharedExclusive
  = Shared -- ^ Other process can hold a shared lock at the same time.
  | Exclusive -- ^ No other process can hold a lock, shared or exclusive.
  deriving (Show, Eq)

-- | Take a lock. This function blocks until the lock is available.
lockFile :: FilePath -> SharedExclusive -> IO FileLock
lockFile path mode = newLock =<< I.lock path (mode == Exclusive)

-- | Try to take a lock. This function does not block. If the lock is not
-- immediately available, it returns Nothing.
tryLockFile :: FilePath -> SharedExclusive -> IO (Maybe FileLock)
tryLockFile path mode = traverse newLock =<< I.tryLock path (mode == Exclusive)

-- | Release the lock.
unlockFile :: FileLock -> IO ()
unlockFile (Lock l ref) = do
  wasAlive <- atomicModifyIORef ref $ \old -> (False, old)
  when wasAlive $ I.unlock l

-- | Perform some action with a lock held. Blocks until the lock is available.
withFileLock :: FilePath -> SharedExclusive -> (FileLock -> IO a) -> IO a
withFileLock path mode = E.bracket (lockFile path mode) unlockFile

-- | Perform some action with a lock held. Non-blocking.
withTryFileLock :: FilePath -> SharedExclusive -> (FileLock -> IO a) -> IO (Maybe a)
withTryFileLock path mode f = E.bracket (tryLockFile path mode) (traverse unlockFile) (traverse f)
