fix: stop settings.json from being wiped on launch

Electron main, ssh-manager, and the embedded web server all write the
same settings.json. readJsonFile/readJsonRoot silently coerced any read
failure (including mid-write parse errors) to {}, and writes were plain
fs.writeFile. A partial read during a concurrent write let the reader's
next read-modify-write overwrite the whole file with only the field it
just set — wiping projects, desktopDefaultHostId, and more. Next launch
showed the welcome chooser because defaultHostId was gone, and the
sidebar was empty because projects were gone.

- Switch all writers to atomic tmp+rename so readers never see partial
  JSON.
- Add mutateSettingsRoot() in Electron main to serialize read-modify-
  write pairs across its own call sites (hosts config, window state,
  desktop port, ssh instances, vibrancy).
- Keep read-on-error returning {} to avoid crashing startup callers,
  but log loudly now so we can catch it if it ever happens again.
- useProjectsStore: don't clobber a populated cache with empty incoming
  settings. If settings ever do come back empty, the sidebar stays
  intact until a real, non-empty sync lands.
This commit is contained in:
Bohdan Triapitsyn
2026-04-20 22:28:32 +03:00
parent f1b84e3291
commit 70fd6aaacc
4 changed files with 104 additions and 46 deletions
@@ -440,7 +440,13 @@ export const createSettingsRuntime = (deps) => {
const writeSettingsToDisk = async (settings) => {
try {
await fsPromises.mkdir(path.dirname(SETTINGS_FILE_PATH), { recursive: true });
await fsPromises.writeFile(SETTINGS_FILE_PATH, JSON.stringify(settings, null, 2), 'utf8');
// Atomic write: Electron main and ssh-manager read this file via plain
// readFile + JSON.parse and silently coerce parse errors to {}. A
// partial read during a non-atomic writeFile would make their next
// read-modify-write wipe the settings file.
const tmp = `${SETTINGS_FILE_PATH}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
await fsPromises.writeFile(tmp, JSON.stringify(settings, null, 2), 'utf8');
await fsPromises.rename(tmp, SETTINGS_FILE_PATH);
} catch (error) {
console.warn('Failed to write settings file:', error);
throw error;