Ticket #2298 (closed bug: fixed)

Opened 5 years ago

Last modified 5 years ago

renameFile is not atomic on Windows

Reported by: duncan Owned by: simonmar
Priority: normal Milestone: 6.10.1
Component: libraries/base Version: 6.8.2
Keywords: Cc:
Operating System: Windows Architecture: x86
Type of failure: Difficulty: Unknown
Test Case: Blocked By:
Blocking: Related Tickets:

Description

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.

Attachments

MoveFileEx.dpatch Download (20.4 KB) - added by duncan 5 years ago.
patch to simplify renameFile impl on windows

Change History

Changed 5 years ago by duncan

patch to simplify renameFile impl on windows

Changed 5 years ago by igloo

  • difficulty set to Unknown
  • milestone set to 6.10.1

Changed 5 years ago by simonmar

  • owner set to simonmar

Changed 5 years ago by simonmar

  • status changed from new to closed
  • resolution set to fixed

Fixed.

libraries/base:

Mon Aug 18 08:59:50 PDT 2008  Simon Marlow <marlowsd@gmail.com>
  * remove __hscore_renameFile, it is no longer uesd

libraries/directory:

Wed Aug 20 07:01:24 PDT 2008  Simon Marlow <marlowsd@gmail.com>
  * fix #2298: use MoveFileEx() on Windows

GHC itself:

Thu Aug 21 03:04:36 PDT 2008  Simon Marlow <marlowsd@gmail.com>
  * move directory after Win32/unix

libraries/Win32:

Wed Aug 20 15:00:27 GMT Daylight Time 2008  Simon Marlow <marlowsd@gmail.com>
  * Move -DUNICODE from cc-options into HsWin32.h, where it can do less damange
Note: See TracTickets for help on using tickets.