Fix Cmd+Q on macOS leaving the desktop app alive
The before-quit handler preventDefaulted on every quit and re-fired app.quit() from .finally. That re-entered Electron through the window-all-closed gate, which on macOS is a no-op listener that vetoes the quit — so the window closed but the process stayed up. Use app.exit(0) to bypass the close → window-all-closed → will-quit chain entirely. Also drop the daemon-stop NSAlert: with no parent window it ran app-modal and stalled the child-process pipes for 'paseo daemon stop', so daemon shutdown waited on the user dismissing the dialog. Replace it with an in-app overlay rendered in the renderer via a paseo:event:quitting IPC event. Stop opening DevTools on dev boot.
This commit is contained in:
@@ -75,9 +75,9 @@ describe("quit-lifecycle", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("gates quit until the stop decision completes, then lets the next quit pass through", async () => {
|
||||
it("preventDefaults the first quit, runs the async stop decision, then exits hard", async () => {
|
||||
let resolveStopDecision: (() => void) | null = null;
|
||||
const app = { quit: vi.fn() };
|
||||
const app = { exit: vi.fn() };
|
||||
const closeTransportSessions = vi.fn();
|
||||
const onStopError = vi.fn();
|
||||
const preventDefault = vi.fn();
|
||||
@@ -99,20 +99,20 @@ describe("quit-lifecycle", () => {
|
||||
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||
expect(closeTransportSessions).toHaveBeenCalledTimes(1);
|
||||
expect(app.quit).not.toHaveBeenCalled();
|
||||
expect(app.exit).not.toHaveBeenCalled();
|
||||
expect(resolveStopDecision).not.toBeNull();
|
||||
|
||||
resolveStopDecision?.();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(app.quit).toHaveBeenCalledTimes(1);
|
||||
expect(app.exit).toHaveBeenCalledWith(0);
|
||||
expect(onStopError).not.toHaveBeenCalled();
|
||||
|
||||
handleBeforeQuit({ preventDefault: secondPreventDefault });
|
||||
|
||||
expect(secondPreventDefault).not.toHaveBeenCalled();
|
||||
expect(closeTransportSessions).toHaveBeenCalledTimes(2);
|
||||
expect(app.quit).toHaveBeenCalledTimes(1);
|
||||
expect(app.exit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ interface BeforeQuitEvent {
|
||||
}
|
||||
|
||||
interface BeforeQuitApp {
|
||||
quit(): void;
|
||||
exit(code: number): void;
|
||||
}
|
||||
|
||||
export interface StopOnQuitDeps {
|
||||
@@ -53,30 +53,25 @@ export function createBeforeQuitHandler({
|
||||
stopDesktopManagedDaemonIfNeeded: () => Promise<boolean>;
|
||||
onStopError: (error: unknown) => void;
|
||||
}): (event: BeforeQuitEvent) => void {
|
||||
let allowingQuitToContinue = false;
|
||||
let quittingInProgress = false;
|
||||
// We always preventDefault on first quit so we can run the async stop
|
||||
// decision, then call app.exit(0) — which bypasses Electron's
|
||||
// close → window-all-closed → will-quit chain. The window-all-closed
|
||||
// listener is a darwin no-op (macOS convention) and would otherwise
|
||||
// veto a re-fired app.quit().
|
||||
let quitting = false;
|
||||
|
||||
return (event) => {
|
||||
closeTransportSessions();
|
||||
|
||||
if (allowingQuitToContinue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (quitting) return;
|
||||
quitting = true;
|
||||
event.preventDefault();
|
||||
if (quittingInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
quittingInProgress = true;
|
||||
void stopDesktopManagedDaemonIfNeeded()
|
||||
.catch((error) => {
|
||||
onStopError(error);
|
||||
})
|
||||
.finally(() => {
|
||||
allowingQuitToContinue = true;
|
||||
quittingInProgress = false;
|
||||
app.quit();
|
||||
app.exit(0);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user