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)