Atomic File Writes on Windows
The other day a few people reported an issue with one of my open source applications, in that a forced shutdown could leave the config file full of zeros. The culprit was almost certainly the fact that the application would try to write to its config file just after the main window was closed (in order to save the window’s size and location), but that write was being cut off after the file had been zero’d, but before some interal cache had been flushed to disk, resulting in lots of NULLs.
So, atomic file writes are in order. Easy on Linux, less easy on Windows. Where to start?
My first resource was an MSDN blog post titled How to do atomic writes in a file, which lists an 11-step algorithm ensuring atomic file writes. Painful. Browse the comments, however, and someone mentions TxF, which it turns out stands for Transactional NTFS, and is a component introduced in Windows Vista to bring atomic file transactions to NTFS. It turns out that you can have a single transaction spanning the filesystem, a database, etc, potentially across multiple computers, all of which are either committed together, or none are committed at all. Sounds amazing! Where do we start?
There’s no native C# support, but a quick Google brings some useful links: TxF .NET is a C# wrapper, and a guy called Bart de Smet has a three part blog series where he walks through a bunch of stuff. I’m feel like I’m well on my way towards a nice solution…
However open the MSDN page for CreateFileTransacted
(or another TxF function) and a lovely warning greets you:
Microsoft strongly recommends developers utilize alternative means to achieve your application’s needs. Many scenarios that TxF was developed for can be achieved through simpler and more readily available techniques. Furthermore, TxF may not be available in future versions of Microsoft Windows. For more information, and alternatives to TxF, please see Alternatives to using Transactional NTFS.
Great.
More digging, and it turns out there are two potential functions available: ReplaceFile
, or MoveFileEx
with MOVEFILE_REPLACE_EXISTING
and MOVEFILE_WRITE_THROUGH
. Some random comment on the MSDN page for MoveFileEx
seemed convinced that it would be atomic if possible (i.e. not across filesystem boundaries, etc), which seems to be supported by the fact that the MOVEFILE_COPY_ALLOWED
flag exists.
And that’s largely where I ended up: make a copy of the file (if the FileMode
says to and it exists), write to it, and move it back with MoveFileEx
.
I wrapped this concept up in a little class. WARNING I haven’t tested this heavily at all. Use at your own risk! It will copy filename.ext
to filename.ext.tmp
if suitable, and open a FileStream
on filename.ext.tmp
. When that FileStream
is closed, it will replace filename.ext
with filename.ext.tmp
using MoveFileEx
.
using System;
using System.IO;
using System.Runtime.InteropServices;
public class AtomicFileStream : FileStream
{
private readonly string path;
private readonly string tempPath;
public static AtomicFileStream Open(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options)
{
return Open(path, path + ".tmp", mode, access, share, bufferSize, options);
}
public static AtomicFileStream Open(string path, string tempPath, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options)
{
if (access == FileAccess.Read)
throw new ArgumentException("If you're just opening the file for reading, AtomicFileStream won't help you at all");
if (File.Exists(tempPath))
File.Delete(tempPath);
if (File.Exists(path) && (mode == FileMode.Append || mode == FileMode.Open || mode == FileMode.OpenOrCreate))
File.Copy(path, tempPath);
return new AtomicFileStream(path, tempPath, mode, access, share, bufferSize, options);
}
private AtomicFileStream(string path, string tempPath, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options)
: base(tempPath, mode, access, share, bufferSize, options)
{
if (path == null)
throw new ArgumentNullException("path");
if (tempPath == null)
throw new ArgumentNullException("tempPath");
this.path = path;
this.tempPath = tempPath;
}
public override void Close()
{
base.Close();
bool success = NativeMethods.MoveFileEx(this.tempPath, this.path, MoveFileFlags.ReplaceExisting | MoveFileFlags.WriteThrough);
if (!success)
Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
}
[Flags]
private enum MoveFileFlags
{
None = 0,
ReplaceExisting = 1,
CopyAllowed = 2,
DelayUntilReboot = 4,
WriteThrough = 8,
CreateHardlink = 16,
FailIfNotTrackable = 32,
}
private static class NativeMethods
{
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool MoveFileEx(
[In] string lpExistingFileName,
[In] string lpNewFileName,
[In] MoveFileFlags dwFlags);
}
}