From 0b64d3dd344a598d3f2d6067b140330cfe28e52b Mon Sep 17 00:00:00 2001 From: Patrick Stevens <3138005+Smaug123@users.noreply.github.com> Date: Sun, 22 Jun 2025 23:04:13 +0100 Subject: [PATCH] Atomic file writes (#12) --- .envrc | 22 ++++++++++++++++++ WoofWare.Expect/Builder.fs | 2 +- WoofWare.Expect/File.fs | 32 ++++++++++++++++++++++++++ WoofWare.Expect/SnapshotUpdate.fs | 2 +- WoofWare.Expect/WoofWare.Expect.fsproj | 1 + flake.nix | 1 + 6 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 WoofWare.Expect/File.fs diff --git a/.envrc b/.envrc index 3550a30..e75d028 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,23 @@ use flake +DOTNET_PATH=$(readlink "$(which dotnet)") +SETTINGS_FILE=$(find . -maxdepth 1 -type f -name '*.sln.DotSettings.user') +MSBUILD=$(realpath "$(find "$(dirname "$DOTNET_PATH")/../share/dotnet/sdk" -maxdepth 2 -type f -name MSBuild.dll)") +if [ -f "$SETTINGS_FILE" ] ; then + xmlstarlet ed --inplace \ + -N wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" \ + -N x="http://schemas.microsoft.com/winfx/2006/xaml" \ + -N s="clr-namespace:System;assembly=mscorlib" \ + -N ss="urn:shemas-jetbrains-com:settings-storage-xaml" \ + --update "//s:String[@x:Key='/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue']" \ + --value "$(realpath "$(dirname "$DOTNET_PATH")/../share/dotnet/dotnet")" \ + "$SETTINGS_FILE" + + xmlstarlet ed --inplace \ + -N wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" \ + -N x="http://schemas.microsoft.com/winfx/2006/xaml" \ + -N s="clr-namespace:System;assembly=mscorlib" \ + -N ss="urn:shemas-jetbrains-com:settings-storage-xaml" \ + --update "//s:String[@x:Key='/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue']" \ + --value "$MSBUILD" \ + "$SETTINGS_FILE" +fi diff --git a/WoofWare.Expect/Builder.fs b/WoofWare.Expect/Builder.fs index 1470be0..91b0a70 100644 --- a/WoofWare.Expect/Builder.fs +++ b/WoofWare.Expect/Builder.fs @@ -270,7 +270,7 @@ type ExpectBuilder (mode : Mode) = let lines = File.ReadAllLines state.Caller.FilePath let oldContents = String.concat "\n" lines let lines = SnapshotUpdate.updateSnapshotAtLine lines state.Caller.LineNumber actual - File.WriteAllLines (state.Caller.FilePath, lines) + File.writeAllLines lines state.Caller.FilePath failwith ("Snapshot successfully updated. Previous contents:\n" + oldContents) match CompletedSnapshotGeneric.passesAssertion state with diff --git a/WoofWare.Expect/File.fs b/WoofWare.Expect/File.fs new file mode 100644 index 0000000..6f76232 --- /dev/null +++ b/WoofWare.Expect/File.fs @@ -0,0 +1,32 @@ +namespace WoofWare.Expect + +open System +open System.IO + +[] +module internal File = + + /// Standard attempt at an atomic file write. + /// It may fail to be atomic if the working directory somehow spans multiple volumes, + /// and of course with external network storage all bets are off. + let writeAllLines (lines : string[]) (path : string) : unit = + let file = FileInfo path + + let tempFile = + Path.Combine (file.Directory.FullName, file.Name + "." + Guid.NewGuid().ToString () + ".tmp") + + try + File.WriteAllLines (tempFile, lines) + // Atomicity guarantees are undocumented, but on Unix this is an atomic `rename` call + // https://github.com/dotnet/runtime/blob/9a4be5b56d81aa04c7ea687c02b3f4e64c83761b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs#L181 + // and on Windows this is an atomic ReplaceFile: + // https://github.com/dotnet/runtime/blob/9a4be5b56d81aa04c7ea687c02b3f4e64c83761b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs#L92 + // calls https://github.com/dotnet/runtime/blob/9a4be5b56d81aa04c7ea687c02b3f4e64c83761b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.ReplaceFile.cs#L12 + // which calls ReplaceFileW, whose atomicity guarantees are again apparently undocumented, + // but 4o-turbo, Opus 4, and Gemini 2.5 Flash all think it's atomic. + File.Replace (tempFile, path, null) + finally + try + File.Delete tempFile + with _ -> + () diff --git a/WoofWare.Expect/SnapshotUpdate.fs b/WoofWare.Expect/SnapshotUpdate.fs index 6e9dfc8..62c912b 100644 --- a/WoofWare.Expect/SnapshotUpdate.fs +++ b/WoofWare.Expect/SnapshotUpdate.fs @@ -300,5 +300,5 @@ module internal SnapshotUpdate = let newContents = updateAllLines contents sources - System.IO.File.WriteAllLines (callerFile, newContents) + File.writeAllLines newContents callerFile ) diff --git a/WoofWare.Expect/WoofWare.Expect.fsproj b/WoofWare.Expect/WoofWare.Expect.fsproj index 1807f97..102bae6 100644 --- a/WoofWare.Expect/WoofWare.Expect.fsproj +++ b/WoofWare.Expect/WoofWare.Expect.fsproj @@ -18,6 +18,7 @@ + diff --git a/flake.nix b/flake.nix index 22dea7d..fd7b113 100644 --- a/flake.nix +++ b/flake.nix @@ -65,6 +65,7 @@ pkgs.alejandra pkgs.nodePackages.markdown-link-check pkgs.shellcheck + pkgs.xmlstarlet ]; }; });