Skip to content

Commit 40d2b4d

Browse files
authored
fix(settings): write settings file atomically via temp + rename (#285)
## Problem `BaseSettings._save` (`sendspin/settings.py`, shared by client and serve settings) rewrites the live JSON file in place with `write_text`. A kill mid-write truncates it, and on next start the `JSONDecodeError` handler logs a warning and silently falls back to defaults -- volume, name, interface and hooks all revert with no obvious cause. ## Fix Write-temp-then-rename: content goes to a `.tmp` sibling, is fsynced, then `os.replace()` swaps it into place atomically. A crash can now only leave the previous good file behind. Same `OSError` handling as before. ## Verification - Full suite (`pytest tests/`, excluding serve + audio_devices): 123 passed - `ruff check` clean Covers both `ClientSettings` and `ServeSettings` since they share `_save`.
1 parent 2139ed7 commit 40d2b4d

1 file changed

Lines changed: 7 additions & 1 deletion

File tree

‎sendspin/settings.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,13 @@ def _save(self) -> None:
105105
return
106106
try:
107107
self._settings_file.parent.mkdir(parents=True, exist_ok=True)
108-
self._settings_file.write_text(json.dumps(self.to_dict(), indent=2) + "\n")
108+
# Write atomically: a kill mid-write must not truncate the live
109+
# file (a truncated file falls back to defaults on next load).
110+
tmp_file = self._settings_file.with_suffix(self._settings_file.suffix + ".tmp")
111+
tmp_file.write_text(json.dumps(self.to_dict(), indent=2) + "\n")
112+
with open(tmp_file, "rb") as f:
113+
os.fsync(f.fileno())
114+
os.replace(tmp_file, self._settings_file)
109115
logger.debug("Saved settings to %s", self._settings_file)
110116
except OSError as e:
111117
logger.warning("Failed to save settings to %s: %s", self._settings_file, e)

0 commit comments

Comments
 (0)