I have a 'safe' config file saving routine that tries to be atomic when writing user config data to the disk, avoiding disk caching etc.
The code goes something like this:
public static void WriteAllTextSafe(string path, string contents)
{
// generate a temp filename
var tempPath = Path.GetTempFileName();
// create the backup name
var backup = path + ".backup";
// delete any existing backups
if (File.Exists(backup))
File.Delete(backup);
// get the bytes
var data = Encoding.UTF8.GetBytes(contents);
if (File.Exists(path))
{
// write the data to a temp file
using (var tempFile = File.Create(tempPath, 4096, FileOptions.WriteThrough))
tempFile.Write(data, 0, data.Length);
// replace the contents
File.Replace(tempPath, path, backup, );
}
else
{
// if the file doesn't exist we can't replace so just write it
using (var tempFile = File.Create(path, 4096, FileOptions.WriteThrough))
tempFile.Write(data, 0, data.Length);
}
}
On most systems this works perfectly and I have not had any reports of issues, but for some users each time my program calls this function they get the following error:
System.IO.IOException: 置換するファイルを置換されるファイルに移動できません。置換されるファイルの名前は、元のままです。
at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
at System.IO.File.InternalReplace(String sourceFileName, String destinationFileName, String destinationBackupFileName, Boolean ignoreMetadataErrors)
at download.ninja.BO.FileExtensions.WriteAllTextSafe(String path, String contents)
at download.ninja.BO.FileExtensions.SaveConfig(String path, Object toSave)
After a bit of investigation and with the help of Google Translate I've found that the actual error is thrown from the wine32 ReplaceFile function:
ERROR_UNABLE_TO_MOVE_REPLACEMENT 1176 (0x498)
The replacement file could not be renamed. If lpBackupFileName was specified, the replaced and
replacement files retain their original file names. Otherwise, the replaced file no longer exists
and the replacement file exists under its original name.
http://msdn.microsoft.com/en-us/library/windows/desktop/aa365512(v=vs.85).aspx
The problem is that I have no idea why Windows is throwing this error.. I have tried setting the file to readonly locally but that throws an Unauthorized exception rather than a IOException so I don't believe that is causing the problem.
My second guess is that the the is somehow locked, but I only have one read function that is used for reading all config files and that should be closing off all file handles when it finsihes
public static T LoadJson<T>(string path)
{
try
{
// load the values from the file
using (var r = new StreamReader(path))
{
string json = r.ReadToEnd();
T result = JsonConvert.DeserializeObject<T>(json);
if (result == null)
return default(T);
return result;
}
}
catch (Exception)
{
}
return default(T);
}
I have also tried throwing fake exceptions in the LoadJson function to try and lock the file but I can't seem to do it.
Even then I have tried simulating a file lock by opening the file in a different process and running the save code while it is still open, and that generates a different (expected) error:
System.IO.IOException: The process cannot access the file because it is being used by another process.
at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
at System.IO.File.InternalReplace(String sourceFileName, String destinationFileName, String destinationBackupFileName, Boolean ignoreMetadataErrors)
at download.ninja.BO.FileExtensions.WriteAllTextSafe(String path, String contents)
So the question is.. what is causing Windows to throw this ERROR_UNABLE_TO_MOVE_REPLACEMENT error on some systems
NOTE: This error is thrown EVERY TIME my program attempt to replace the file on an affected machine.. not just occasionally.
OK, so found the problem. It seems that files passed into ReplaceFile must be on the SAME DRIVE
In this case the user had changed their temp folder to d:\tmp\
So File.Replace looked something like this:
File.Replace(@"D:\tmp\sometempfile", @"c:\AppData\DN\app-config", @"c:\AppData\DN\app-config.backup");
This difference in drives between the source file and the desitnation file in File.Replace
seems to be what caused the problem.
I have modified my WriteAllTextSafe
function as follows and my user reports the problem has been resolved!
public static void WriteAllTextSafe(string path, string contents)
{
// DISABLED: User temp folder might be on different drive
// var tempPath = Path.GetTempFileName();
// use the same folder so that they are always on the same drive!
var tempPath = Path.Combine(Path.GetDirectoryName(path), Guid.NewGuid().ToString());
....
}
As far as I can find there is no documentation stating that the two files need to be on the same drive but I can reproduce the problem by manually entering different drives (yet valid paths) into File.Replace