diff --git a/src/PlanViewer.App/AboutWindow.axaml.cs b/src/PlanViewer.App/AboutWindow.axaml.cs
index d5ce313..89ac6d7 100644
--- a/src/PlanViewer.App/AboutWindow.axaml.cs
+++ b/src/PlanViewer.App/AboutWindow.axaml.cs
@@ -56,7 +56,7 @@ private void SaveMcpSettings()
}, new JsonSerializerOptions { WriteIndented = true });
Directory.CreateDirectory(settingsDir);
- File.WriteAllText(settingsFile, json);
+ Services.AtomicFile.WriteAllText(settingsFile, json);
}
private void GitHubLink_Click(object? sender, PointerPressedEventArgs e) => OpenUrl(GitHubUrl);
diff --git a/src/PlanViewer.App/Services/AppSettingsService.cs b/src/PlanViewer.App/Services/AppSettingsService.cs
index 46b374b..afff7fa 100644
--- a/src/PlanViewer.App/Services/AppSettingsService.cs
+++ b/src/PlanViewer.App/Services/AppSettingsService.cs
@@ -59,7 +59,7 @@ public static void Save(AppSettings settings)
{
Directory.CreateDirectory(SettingsDir);
var json = JsonSerializer.Serialize(settings, JsonOptions);
- File.WriteAllText(SettingsPath, json);
+ AtomicFile.WriteAllText(SettingsPath, json);
}
catch
{
diff --git a/src/PlanViewer.App/Services/AtomicFile.cs b/src/PlanViewer.App/Services/AtomicFile.cs
new file mode 100644
index 0000000..c2c2447
--- /dev/null
+++ b/src/PlanViewer.App/Services/AtomicFile.cs
@@ -0,0 +1,27 @@
+using System.IO;
+
+namespace PlanViewer.App.Services;
+
+///
+/// Helper for atomic text-file writes: write to a sibling .tmp and rename
+/// into place so a crash mid-write can't truncate the target file. Callers
+/// are responsible for creating the parent directory first.
+///
+internal static class AtomicFile
+{
+ ///
+ /// Writes to atomically
+ /// with respect to process crashes. If the process dies before the rename,
+ /// keeps its previous contents and a stray
+ /// .tmp sibling is left behind (cleaned up on the next call).
+ ///
+ public static void WriteAllText(string path, string contents)
+ {
+ var tmp = path + ".tmp";
+ File.WriteAllText(tmp, contents);
+ // File.Move with overwrite:true maps to MoveFileEx(MOVEFILE_REPLACE_EXISTING)
+ // on Windows and rename(2) on Unix — both atomic when source and destination
+ // live on the same filesystem, which is always the case here.
+ File.Move(tmp, path, overwrite: true);
+ }
+}
diff --git a/src/PlanViewer.App/Services/ConnectionStore.cs b/src/PlanViewer.App/Services/ConnectionStore.cs
index ae56194..9d0e8b9 100644
--- a/src/PlanViewer.App/Services/ConnectionStore.cs
+++ b/src/PlanViewer.App/Services/ConnectionStore.cs
@@ -39,7 +39,7 @@ public void Save(List connections)
{
Directory.CreateDirectory(ConfigDir);
var json = JsonSerializer.Serialize(connections, JsonOptions);
- File.WriteAllText(ConfigFile, json);
+ AtomicFile.WriteAllText(ConfigFile, json);
}
public void AddOrUpdate(ServerConnection connection)
diff --git a/src/PlanViewer.App/Services/SqlFormatSettingsService.cs b/src/PlanViewer.App/Services/SqlFormatSettingsService.cs
index 23e8cc5..179e908 100644
--- a/src/PlanViewer.App/Services/SqlFormatSettingsService.cs
+++ b/src/PlanViewer.App/Services/SqlFormatSettingsService.cs
@@ -123,7 +123,7 @@ public static bool Save(SqlFormatSettings settings, out string? error)
{
Directory.CreateDirectory(SettingsDir);
var json = JsonSerializer.Serialize(settings, JsonOptions);
- File.WriteAllText(SettingsPath, json);
+ AtomicFile.WriteAllText(SettingsPath, json);
return true;
}
catch (Exception ex)