@@ -85,11 +85,12 @@ module Distribution.Utils.Generic
8585import Distribution.Compat.Prelude
8686import Prelude ()
8787
88- import Data.Char ( isAsciiLower , isAsciiUpper )
89-
88+ import Control.Concurrent ( threadDelay )
89+ import qualified Control.Exception as Exception
9090import Data.Bits (shiftL , (.&.) , (.|.) )
9191import qualified Data.ByteString as SBS
9292import qualified Data.ByteString.Lazy as LBS
93+ import Data.Char (isAsciiLower , isAsciiUpper )
9394import Data.List
9495 ( isInfixOf
9596 )
@@ -99,8 +100,7 @@ import qualified Data.Text.Encoding as T
99100import qualified Data.Text.Encoding.Error as T
100101import qualified Data.Text.Lazy as TL
101102import qualified Data.Text.Lazy.Encoding as TL
102-
103- import qualified Control.Exception as Exception
103+ import GHC.IO.Exception (IOErrorType (UnsupportedOperation ))
104104import 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