Always ask before stopping running sites on quit - #3350
Merged
Conversation
Collaborator
📊 Performance Test ResultsComparing a64b303 vs trunk app-size
site-editor
site-startup
Results are median values from multiple test runs. Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff) |
youknowriad
approved these changes
May 6, 2026
fredrikekelund
approved these changes
May 6, 2026
fredrikekelund
left a comment
Contributor
There was a problem hiding this comment.
For fixing the use case where the wp-studio CLI has been installed from npm, but the bundled CLI hasn't been installed, this seems like an OK fix to me 👍
How AI was used in this PR
When AI is also used to write the PR description, please acknowledge this.
3 tasks
fredrikekelund
pushed a commit
that referenced
this pull request
May 29, 2026
## Related issues - Fixes [STU-1750](https://linear.app/a8c/issue/STU-1750/studio-reopens-multiple-times-when-quitting-during-site-startup) ## How AI was used in this PR Claude Code investigated the bug, identified the root cause (create-if-missing branch in `getMainWindow()` racing with async IPC fan-out during the `will-quit` shutdown window), proposed the minimal gate at `sendIpcEventToRenderer`, and applied the change. I reviewed the diff and confirmed both the fix and its placement before committing. ## Proposed Changes - Add an `isAppQuitting` flag in `apps/studio/src/ipc-utils.ts` with an exported `markAppQuitting()` setter. While set, `sendIpcEventToRenderer` returns early without calling `getMainWindow()`. - Call `markAppQuitting()` at the start of the `will-quit` handler in `apps/studio/src/index.ts`, before any other cleanup. ### Why this works When the user confirms "Stop sites" on quit, `will-quit` calls `event.preventDefault()` and waits up to 6 s for `stopAllServers()` to drain. During that window, in-flight async callbacks (site-startup events, snapshot events, sync progress, etc.) keep firing through `sendIpcEventToRenderer()`. Each call routes through `getMainWindow()` — and because the original window has typically already been closed by Electron's shutdown sequence, `getMainWindow()` falls through to `createMainWindow()` and spawns a fresh `BrowserWindow`. One pending event = one ghost window. With ~6 sites starting simultaneously on a Linux VM, you reliably see ~6 windows reopen. Blocking the fan-out at the `sendIpcEventToRenderer` chokepoint is the smallest fix: it catches every async-IPC path without changing `getMainWindow`'s signature or touching its 15+ other call sites (menus, deeplinks, update dialog, etc.), which are user-triggered and can't fire post-`will-quit` anyway. `will-quit` (not `before-quit`) is the right gate point: `before-quit` can still be cancelled by the sync-in-progress dialog or the "Cancel" button on the running-sites dialog. Once `will-quit` fires, the app is committed. ### Regression origin The mechanic was introduced in #86 (May 2024) for OAuth deep-link handling. It became today's user-visible bug in #849 (Jan 2025), which routed `sendIpcEventToRenderer` through `getMainWindow()`. The bug got more reproducible recently due to Linux VM testing (slower shutdown widens the race window) plus PR #3350 removing an accidental escape hatch that bypassed the quit dialog when the CLI wasn't installed. ## Testing Instructions Reproduce on `trunk` first, then verify the fix on this branch. **Repro on trunk (Linux/Windows easier; macOS rarer):** 1. Have several Studio sites created (~6 makes it obvious). 2. Quit Studio so all sites stop. 3. Re-launch Studio. 4. Start all sites at once. 5. **Before** they finish booting, quit Studio. 6. Observe: Studio reopens 4–6 times in quick succession. **Verify on this branch:** 1. Repeat steps 1–5 above. 2. Studio should quit cleanly with no ghost windows. ## Pre-merge Checklist - [x] Have you checked for TypeScript, React or other console errors? - [x] Manual repro on Linux (with several sites) - [x] Manual repro on Windows (with several sites)
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related issues
How AI was used in this PR
Proposed Changes
isStudioCliInstalled()gate around the "running sites on quit" confirmation dialog inapps/studio/src/index.ts. The dialog now always shows when sites are running (unless the user has saved a preference, or when running E2E tests).isStudioCliInstalledimport.Why drop the CLI gate
isStudioCliInstalled()only returnstruewhen the CLI symlink points to the packaged CLI installed via the app. Users who installedstudioindependently (e.g. via npm) failed this check, so the dialog was silently skipped andshouldStopSitesOnQuitdefaulted totrue— sites were stopped on quit without any confirmation.Sites are actually managed by a long-lived detached daemon (
apps/cli/lib/daemon-client.ts, spawned withdetached: true,stdio: 'ignore',daemonProcess.unref()) that survives the desktop app quit completely independently of CLI install state. Picking "Leave running" works regardless of whether the CLI is installed through the app, via npm, or not at all — so the gate was hiding a useful choice from users.The gate's original intent (in #2314) was to spare GUI-only users from a new prompt, on the assumption that they wouldn't care about keeping sites running. But the capability is the same for everyone: closing the app shouldn't kill background processes the user started, and the dialog is also how users discover that sites can keep running.
Why default to "Leave running"
Quitting the app is rarely a deliberate "stop everything" action — it's often Cmd+Q to free RAM, declutter, or by accident. Defaulting to the destructive option (
Stop sites) means a single Enter keypress can terminate work that took non-trivial time to set up (long-running imports, in-progress dev sessions).Leave runningis reversible — the user can alwaysstudio site stop --alllater — whileStop sitesis not.Testing Instructions
ps/Activity Monitor that the daemon process under~/.studio/daemon/is still alive, and that the site URL still responds.stopSitesOnQuitfrom~/.studio/app.jsonif needed.)Pre-merge Checklist