Skip to content

fix: guard before-quit against the closeVault race - #30

Open
GFrancV wants to merge 3 commits into
mainfrom
fix/before-quit-close-vault-race
Open

GFrancV wants to merge 3 commits into
mainfrom
fix/before-quit-close-vault-race

Conversation

@GFrancV

@GFrancV GFrancV commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Summary

before-quit never called event.preventDefault(), so Electron didn't wait for closeVault() to finish before the process could die, and window-all-closed could trigger a second, parallel closeVault() via app.quit(). This closes both gaps with a two-flag reentrancy guard (isQuitting / readyToQuit) in src/main/index.ts:

  • preventDefault() now runs synchronously on every before-quit pass until the close has actually settled.
  • window-all-closed only calls app.quit() on Windows/Linux, leaving before-quit as the sole owner of the shutdown sequence; macOS keeps its dock-aware closeVault() call (safe even mid-shutdown, since closeVault() is serialized via withVaultLock and idempotent).

Why a two-flag guard instead of one

The original single-flag design (isQuitting only) closes the bug described in the issue, but leaves a narrower window open: a genuine second quit signal (e.g. a duplicate Cmd+Q, or an OS logoff) arriving while the first closeVault() is still in flight would skip preventDefault() entirely and let Electron's default action (terminate) proceed — reintroducing the same class of data-loss bug the issue reports, just requiring two quit signals instead of one. This surfaced during review, not from the original issue text, so readyToQuit was added to distinguish "a close is in progress" from "the close has settled and it's safe to let the real quit through."

Why there's a test beyond what the issue specifies

The issue only describes the single-signal race. The extra test (a duplicate before-quit signal while closeVault() is still in flight is still prevented) exists to pin down the two-flag fix above — it reproduces the narrower race by holding closeVault() unresolved and firing a second before-quit while it's pending, asserting that signal still gets preventDefault()'d. Confirmed this test fails against the original single-flag guard before restoring the fix, so it's a real regression guard, not incidental coverage.

Test plan

  • pnpm check:task (typecheck, lint, format, depcruise, test:vault) passes clean
  • All tests/lifecycle.test.ts cases pass, including the new duplicate-signal test (confirmed it fails against the pre-fix single-flag guard)
  • Full vitest run (including the real SQLCipher/Argon2id vault.test.ts suite) passes with no regressions
  • Manual: quit the app with unsynced notes and confirm no stale .nvx or orphaned temp DB in os.tmpdir() (not automatable without a running GUI)

Closes #18

before-quit never called event.preventDefault(), so Electron didn't
wait for closeVault() to finish, and window-all-closed could trigger
a second, parallel closeVault() via app.quit(). Adds a two-flag
reentrancy guard: preventDefault() runs synchronously on every pass
until the close settles (isQuitting guards against starting a second
closeVault() while one is in flight; readyToQuit is set only in the
finally, once closeVault() has settled, and is what finally lets a
before-quit signal through). This also closes a narrower window a
single-flag guard would leave open: a genuine second quit signal
(e.g. a duplicate Cmd+Q) arriving while the first closeVault() is
still in flight would otherwise slip through unprevented.

window-all-closed now only calls app.quit() on Windows/Linux, leaving
before-quit as the sole owner of the shutdown sequence; macOS keeps
its existing dock-aware closeVault() call — safe even when a real
quit is already in progress (e.g. during autoUpdater.quitAndInstall(),
which closes windows before before-quit fires) because closeVault()
is serialized (withVaultLock) and idempotent, not because no quit
could be in progress.
…lt()

Proves the readyToQuit guard actually blocks a genuine second quit
signal arriving before the first closeVault() settles, not just the
internal re-entrant app.quit() from the finally. Confirmed this test
fails against the old single-flag guard before restoring the fix.
@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit f265480.

@GFrancV GFrancV changed the title fix: guard before-quit against the closeVault race (#18) fix: guard before-quit against the closeVault race Sep 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

before-quit does not await closeVault(): the final checkpoint and pack race process exit

1 participant