|
| 1 | +{-# LANGUAGE CPP #-} |
| 2 | +{-# LANGUAGE InterruptibleFFI #-} |
| 3 | +{-# LANGUAGE LambdaCase #-} |
| 4 | +{-# LANGUAGE MultiWayIf #-} |
| 5 | +{-# LANGUAGE DeriveDataTypeable #-} |
| 6 | + |
| 7 | +-- | This compat module can be removed once base-4.10 (ghc-8.2) is the minimum |
| 8 | +-- required version. Though note that the locking functionality is not in |
| 9 | +-- public modules in base-4.10, just in the "GHC.IO.Handle.Lock" module. |
| 10 | +module Distribution.Client.Compat.FileLock ( |
| 11 | + FileLockingNotSupported(..) |
| 12 | + , LockMode(..) |
| 13 | + , hLock |
| 14 | + , hTryLock |
| 15 | + ) where |
| 16 | + |
| 17 | +#if MIN_VERSION_base(4,10,0) |
| 18 | + |
| 19 | +import GHC.IO.Handle.Lock |
| 20 | + |
| 21 | +#else |
| 22 | + |
| 23 | +-- The remainder of this file is a modified copy |
| 24 | +-- of GHC.IO.Handle.Lock from ghc-8.2.x |
| 25 | +-- |
| 26 | +-- The modifications were just to the imports and the CPP, since we do not have |
| 27 | +-- access to the HAVE_FLOCK from the ./configure script. We approximate the |
| 28 | +-- lack of HAVE_FLOCK with defined(solaris2_HOST_OS) instead since that is the |
| 29 | +-- only known major Unix platform lacking flock(). |
| 30 | + |
| 31 | +import Control.Exception (Exception) |
| 32 | +import Data.Typeable |
| 33 | + |
| 34 | +#if defined(solaris2_HOST_OS) |
| 35 | + |
| 36 | +import Control.Exception (throwIO) |
| 37 | +import System.IO (Handle) |
| 38 | + |
| 39 | +#else |
| 40 | + |
| 41 | +import Data.Bits |
| 42 | +import Data.Function |
| 43 | +import Control.Concurrent.MVar |
| 44 | + |
| 45 | +import Foreign.C.Error |
| 46 | +import Foreign.C.Types |
| 47 | + |
| 48 | +import GHC.IO.Handle.Types |
| 49 | +import GHC.IO.FD |
| 50 | +import GHC.IO.Exception |
| 51 | + |
| 52 | +#if defined(mingw32_HOST_OS) |
| 53 | + |
| 54 | +#if defined(i386_HOST_ARCH) |
| 55 | +## define WINDOWS_CCONV stdcall |
| 56 | +#elif defined(x86_64_HOST_ARCH) |
| 57 | +## define WINDOWS_CCONV ccall |
| 58 | +#else |
| 59 | +# error Unknown mingw32 arch |
| 60 | +#endif |
| 61 | + |
| 62 | +#include <windows.h> |
| 63 | + |
| 64 | +import Foreign.Marshal.Alloc |
| 65 | +import Foreign.Marshal.Utils |
| 66 | +import Foreign.Ptr |
| 67 | +import GHC.Windows |
| 68 | + |
| 69 | +#else /* !defined(mingw32_HOST_OS), so assume unix with flock() */ |
| 70 | + |
| 71 | +#include <sys/file.h> |
| 72 | + |
| 73 | +#endif /* !defined(mingw32_HOST_OS) */ |
| 74 | + |
| 75 | +#endif /* !defined(solaris2_HOST_OS) */ |
| 76 | + |
| 77 | +#endif /* MIN_VERSION_base */ |
| 78 | + |
| 79 | + |
| 80 | +-- | Exception thrown by 'hLock' on non-Windows platforms that don't support |
| 81 | +-- 'flock'. |
| 82 | +data FileLockingNotSupported = FileLockingNotSupported |
| 83 | + deriving (Typeable, Show) |
| 84 | + |
| 85 | +instance Exception FileLockingNotSupported |
| 86 | + |
| 87 | +#if !(MIN_VERSION_base(4,10,0)) |
| 88 | + |
| 89 | +-- | Indicates a mode in which a file should be locked. |
| 90 | +data LockMode = SharedLock | ExclusiveLock |
| 91 | + |
| 92 | +-- | If a 'Handle' references a file descriptor, attempt to lock contents of the |
| 93 | +-- underlying file in appropriate mode. If the file is already locked in |
| 94 | +-- incompatible mode, this function blocks until the lock is established. The |
| 95 | +-- lock is automatically released upon closing a 'Handle'. |
| 96 | +-- |
| 97 | +-- Things to be aware of: |
| 98 | +-- |
| 99 | +-- 1) This function may block inside a C call. If it does, in order to be able |
| 100 | +-- to interrupt it with asynchronous exceptions and/or for other threads to |
| 101 | +-- continue working, you MUST use threaded version of the runtime system. |
| 102 | +-- |
| 103 | +-- 2) The implementation uses 'LockFileEx' on Windows and 'flock' otherwise, |
| 104 | +-- hence all of their caveats also apply here. |
| 105 | +-- |
| 106 | +-- 3) On non-Windows plaftorms that don't support 'flock' (e.g. Solaris) this |
| 107 | +-- function throws 'FileLockingNotImplemented'. We deliberately choose to not |
| 108 | +-- provide fcntl based locking instead because of its broken semantics. |
| 109 | +-- |
| 110 | +-- @since 4.10.0.0 |
| 111 | +hLock :: Handle -> LockMode -> IO () |
| 112 | +hLock h mode = lockImpl h "hLock" mode True >> return () |
| 113 | + |
| 114 | +-- | Non-blocking version of 'hLock'. |
| 115 | +-- |
| 116 | +-- @since 4.10.0.0 |
| 117 | +hTryLock :: Handle -> LockMode -> IO Bool |
| 118 | +hTryLock h mode = lockImpl h "hTryLock" mode False |
| 119 | + |
| 120 | +---------------------------------------- |
| 121 | + |
| 122 | +#if defined(solaris2_HOST_OS) |
| 123 | + |
| 124 | +-- | No-op implementation. |
| 125 | +lockImpl :: Handle -> String -> LockMode -> Bool -> IO Bool |
| 126 | +lockImpl _ _ _ _ = throwIO FileLockingNotSupported |
| 127 | + |
| 128 | +#else /* !defined(solaris2_HOST_OS) */ |
| 129 | + |
| 130 | +#if defined(mingw32_HOST_OS) |
| 131 | + |
| 132 | +lockImpl :: Handle -> String -> LockMode -> Bool -> IO Bool |
| 133 | +lockImpl h ctx mode block = do |
| 134 | + FD{fdFD = fd} <- handleToFd h |
| 135 | + wh <- throwErrnoIf (== iNVALID_HANDLE_VALUE) ctx $ c_get_osfhandle fd |
| 136 | + allocaBytes sizeof_OVERLAPPED $ \ovrlpd -> do |
| 137 | + fillBytes ovrlpd (fromIntegral sizeof_OVERLAPPED) 0 |
| 138 | + let flags = cmode .|. (if block then 0 else #{const LOCKFILE_FAIL_IMMEDIATELY}) |
| 139 | + -- We want to lock the whole file without looking up its size to be |
| 140 | + -- consistent with what flock does. According to documentation of LockFileEx |
| 141 | + -- "locking a region that goes beyond the current end-of-file position is |
| 142 | + -- not an error", however e.g. Windows 10 doesn't accept maximum possible |
| 143 | + -- value (a pair of MAXDWORDs) for mysterious reasons. Work around that by |
| 144 | + -- trying 2^32-1. |
| 145 | + fix $ \retry -> c_LockFileEx wh flags 0 0xffffffff 0x0 ovrlpd >>= \case |
| 146 | + True -> return True |
| 147 | + False -> getLastError >>= \err -> if |
| 148 | + | not block && err == #{const ERROR_LOCK_VIOLATION} -> return False |
| 149 | + | err == #{const ERROR_OPERATION_ABORTED} -> retry |
| 150 | + | otherwise -> failWith ctx err |
| 151 | + where |
| 152 | + sizeof_OVERLAPPED = #{size OVERLAPPED} |
| 153 | + |
| 154 | + cmode = case mode of |
| 155 | + SharedLock -> 0 |
| 156 | + ExclusiveLock -> #{const LOCKFILE_EXCLUSIVE_LOCK} |
| 157 | + |
| 158 | +-- https://msdn.microsoft.com/en-us/library/aa297958.aspx |
| 159 | +foreign import ccall unsafe "_get_osfhandle" |
| 160 | + c_get_osfhandle :: CInt -> IO HANDLE |
| 161 | + |
| 162 | +-- https://msdn.microsoft.com/en-us/library/windows/desktop/aa365203.aspx |
| 163 | +foreign import WINDOWS_CCONV interruptible "LockFileEx" |
| 164 | + c_LockFileEx :: HANDLE -> DWORD -> DWORD -> DWORD -> DWORD -> Ptr () -> IO BOOL |
| 165 | + |
| 166 | +#else /* !defined(mingw32_HOST_OS), so assume unix with flock() */ |
| 167 | + |
| 168 | +lockImpl :: Handle -> String -> LockMode -> Bool -> IO Bool |
| 169 | +lockImpl h ctx mode block = do |
| 170 | + FD{fdFD = fd} <- handleToFd h |
| 171 | + let flags = cmode .|. (if block then 0 else #{const LOCK_NB}) |
| 172 | + fix $ \retry -> c_flock fd flags >>= \case |
| 173 | + 0 -> return True |
| 174 | + _ -> getErrno >>= \errno -> if |
| 175 | + | not block && errno == eWOULDBLOCK -> return False |
| 176 | + | errno == eINTR -> retry |
| 177 | + | otherwise -> ioException $ errnoToIOError ctx errno (Just h) Nothing |
| 178 | + where |
| 179 | + cmode = case mode of |
| 180 | + SharedLock -> #{const LOCK_SH} |
| 181 | + ExclusiveLock -> #{const LOCK_EX} |
| 182 | + |
| 183 | +foreign import ccall interruptible "flock" |
| 184 | + c_flock :: CInt -> CInt -> IO CInt |
| 185 | + |
| 186 | +#endif /* !defined(mingw32_HOST_OS) */ |
| 187 | + |
| 188 | +-- | Turn an existing Handle into a file descriptor. This function throws an |
| 189 | +-- IOError if the Handle does not reference a file descriptor. |
| 190 | +handleToFd :: Handle -> IO FD |
| 191 | +handleToFd h = case h of |
| 192 | + FileHandle _ mv -> do |
| 193 | + Handle__{haDevice = dev} <- readMVar mv |
| 194 | + case cast dev of |
| 195 | + Just fd -> return fd |
| 196 | + Nothing -> throwErr "not a file descriptor" |
| 197 | + DuplexHandle{} -> throwErr "not a file handle" |
| 198 | + where |
| 199 | + throwErr msg = ioException $ IOError (Just h) |
| 200 | + InappropriateType "handleToFd" msg Nothing Nothing |
| 201 | + |
| 202 | +#endif /* defined(solaris2_HOST_OS) */ |
| 203 | + |
| 204 | +#endif /* MIN_VERSION_base */ |
0 commit comments