Skip to content

Commit c5c2072

Browse files
authored
Merge pull request #11588 from haskell/renameFile-with-retry
Make renameFile in writeFileAtomic more robust
2 parents 46acb86 + ab21844 commit c5c2072

1 file changed

Lines changed: 34 additions & 10 deletions

File tree

Cabal-syntax/src/Distribution/Utils/Generic.hs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,12 @@ module Distribution.Utils.Generic
8585
import Distribution.Compat.Prelude
8686
import Prelude ()
8787

88-
import Data.Char (isAsciiLower, isAsciiUpper)
89-
88+
import Control.Concurrent (threadDelay)
89+
import qualified Control.Exception as Exception
9090
import Data.Bits (shiftL, (.&.), (.|.))
9191
import qualified Data.ByteString as SBS
9292
import qualified Data.ByteString.Lazy as LBS
93+
import Data.Char (isAsciiLower, isAsciiUpper)
9394
import Data.List
9495
( isInfixOf
9596
)
@@ -99,8 +100,7 @@ import qualified Data.Text.Encoding as T
99100
import qualified Data.Text.Encoding.Error as T
100101
import qualified Data.Text.Lazy as TL
101102
import qualified Data.Text.Lazy.Encoding as TL
102-
103-
import qualified Control.Exception as Exception
103+
import GHC.IO.Exception (IOErrorType (UnsupportedOperation))
104104
import System.Directory
105105
( copyFile
106106
, getTemporaryDirectory
@@ -119,6 +119,7 @@ import System.IO
119119
, withBinaryFile
120120
, withFile
121121
)
122+
import System.IO.Error (ioeGetErrorType)
122123

123124
-- -----------------------------------------------------------------------------
124125
-- Helper functions
@@ -197,14 +198,37 @@ writeFileAtomic targetPath content = do
197198
( \(tmpPath, handle) -> do
198199
LBS.hPut handle content
199200
hClose handle
200-
Exception.catch
201-
(renameFile tmpPath targetPath)
202-
( \(_ :: Exception.SomeException) -> do
203-
copyFile tmpPath targetPath
204-
removeFile tmpPath
205-
)
201+
renameFileWithRetry tmpPath targetPath
206202
)
207203

204+
-- | A robust 'renameFile' which retries with delay and
205+
-- switches to 'copyFile' as a backup implementation.
206+
renameFileWithRetry :: FilePath -> FilePath -> IO ()
207+
renameFileWithRetry srcPath targetPath =
208+
renameFile srcPath targetPath `Exception.catch` retryRename 3
209+
where
210+
retryRename :: Word -> IOException -> IO ()
211+
-- If no retries left then throw whatever exception we ended up with.
212+
retryRename 0 exception = throwIO exception
213+
retryRename retriesLeft exception
214+
-- UnsupportedOperation means EXDEV from rename(2) with source and target
215+
-- on different devices. In such case we resort to copying instead of renaming.
216+
| ioeGetErrorType exception == UnsupportedOperation =
217+
copyFile srcPath targetPath `Exception.catch` retryCopy (retriesLeft - 1)
218+
| otherwise = do
219+
-- Wait 1ms between retries: maybe device is busy, maybe antivirus locked srcPath.
220+
threadDelay 1000
221+
renameFile srcPath targetPath `Exception.catch` retryRename (retriesLeft - 1)
222+
223+
retryCopy :: Word -> IOException -> IO ()
224+
retryCopy 0 exception = throwIO exception
225+
retryCopy retriesLeft _ = do
226+
-- Wait 1ms between retries: maybe device is busy, maybe antivirus locked srcPath.
227+
threadDelay 1000
228+
copyFile srcPath targetPath `Exception.catch` retryCopy retriesLeft
229+
-- It's nice to clean up, but not critical, so ignoring any exceptions.
230+
removeFile srcPath `Exception.catch` (\(_ :: IOException) -> pure ())
231+
208232
-- ------------------------------------------------------------
209233

210234
-- * Unicode stuff

0 commit comments

Comments
 (0)