Skip to content
Open
2 changes: 1 addition & 1 deletion src/Tasks.UnitTests/WriteLinesToFile_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ public void TransactionalModeHandlesConcurrentWritesSuccessfully()
}

[Fact]
public void TransactionalModePreservesAllData()
public void TransactionalModeSucceedsWithConcurrentOverwrites()
{
using (var testEnv = TestEnvironment.Create(_output))
{
Expand Down
24 changes: 21 additions & 3 deletions src/Tasks/FileIO/WriteLinesToFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,27 @@ private bool SaveAtomically(AbsolutePath filePath, string contentsAsString, Enco
}
catch (IOException moveEx)
{
// Move failed, log and return
string lockedFileMessage = LockCheck.GetLockedFileMessage(filePath);
Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath.OriginalValue, moveEx.Message, lockedFileMessage);
// Only retry with Replace if the destination now exists (concurrent write race).
if (System.IO.File.Exists(filePath))
{
try
{
System.IO.File.Replace(temporaryFilePath, filePath, null, true);
temporaryFilePath = null; // Mark as successfully replaced
return !Log.HasLoggedErrors;
}
catch (IOException replaceEx)
{
// Both attempts failed; log the original move error as the root cause.
string lockedFileMessage = LockCheck.GetLockedFileMessage(filePath);
Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath.OriginalValue, moveEx.Message + " " + replaceEx.Message, lockedFileMessage);
return !Log.HasLoggedErrors;
}
}

// Destination doesn't exist; move failed for a different reason.
string lockedFileDiagnostics = LockCheck.GetLockedFileMessage(filePath);
Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath.OriginalValue, moveEx.Message, lockedFileDiagnostics);
return !Log.HasLoggedErrors;
}
}
Expand Down