renameFile is not atomic on Windows
The Haskell 98 spec says about renameFile
Computation
renameFile old new
changes the name of an existing file system object from old to new. If the new object already exists, it is atomically replaced by the old object.
The renameFile
function has been tricky on Windows historically because Windows 9x did not directly support overwriting an existing file. Windows NT and later support `MoveFileEx` which has a flag MOVEFILE_REPLACE_EXISTING
.
The current __hscore_renameFile
implementation for Windows uses a complex range of tests and workarounds to allow overwriting of existing files. Of course this cannot be atomic. For Windows NT it does use MoveFileEx()
but then if that fails it goes and tries the old tricks anyway! So abandoning all promises of atomicity it goes and tries to delete the target file and then a final attempt to rename over the now-deleted target file.
As far as I can see this is bonkers. It should use MoveFileEx()
exactly once and if that fails then the whole thing fails. That way we preserve any atomicity guarantees that MoveFileEx()
might provide. Note that the MSDN documentation doesn't actually say if the rename is atomic, even in the case of two files in the same directory.
With a renameFile
that does meet the H98 requirement we can write an writeFileAtomic
function that either succeeds and replaces the target file or fails without altering the target file in any way. Additionally, other threads or processes will not see any intermediate states of the target file (though they would see the temp file created in the same directory). This is how text editors etc implement reliable file writing. Glib has a function like this g-file-set-contents.
writeFileAtomic :: FilePath -> String -> IO ()
writeFileAtomic targetFile content =
Exception.bracketOnError
(openTempFile targetDir template)
(\(tmpFile, tmpHandle) -> hClose tmpHandle
>> removeFile tmpFile)
(\(tmpFile, tmpHandle) -> hPutStr tmpHandle content
>> hClose tmpHandle
>> renameFile tmpFile targetFile)
where
template = targetName <.> "tmp"
(targetDir,targetName) = splitFileName targetFile
Indeed it's not impossible to imagine using this or something similar for the actual H98 writeFile
implementation.
Trac metadata
Trac field | Value |
---|---|
Version | 6.8.2 |
Type | Bug |
TypeOfFailure | OtherFailure |
Priority | normal |
Resolution | Unresolved |
Component | libraries/base |
Test case | |
Differential revisions | |
BlockedBy | |
Related | |
Blocking | |
CC | |
Operating system | |
Architecture |