Skip to content

Commit c1562d5

Browse files
committed
Add new compat code for OS file locking
This code is backported from base from ghc-8.2. It will be used as part of the concurrent store updates. It may also be used in future to work around the lack of file locking in ghc-pkg prior to version 8.2.
1 parent 4780c0a commit c1562d5

File tree

2 files changed

+207
-0
lines changed

2 files changed

+207
-0
lines changed
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
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 */

cabal-install/cabal-install.cabal

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ library
223223
Distribution.Client.World
224224
Distribution.Client.Win32SelfUpgrade
225225
Distribution.Client.Compat.ExecutablePath
226+
Distribution.Client.Compat.FileLock
226227
Distribution.Client.Compat.FilePerms
227228
Distribution.Client.Compat.Prelude
228229
Distribution.Client.Compat.Process
@@ -395,6 +396,8 @@ executable cabal
395396
zlib >= 0.5.3 && < 0.7,
396397
hackage-security >= 0.5.2.2 && < 0.6
397398

399+
other-modules: Distribution.Client.Compat.FileLock
400+
398401
if flag(old-bytestring)
399402
build-depends: bytestring < 0.10.2, bytestring-builder >= 0.10 && < 1
400403
else

0 commit comments

Comments
 (0)