diff --git a/.changeset/windows-quit-postgres-prompt.md b/.changeset/windows-quit-postgres-prompt.md new file mode 100644 index 0000000000..9e303a2f48 --- /dev/null +++ b/.changeset/windows-quit-postgres-prompt.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Closing the Windows desktop app now asks whether to also shut down the embedded PostgreSQL server. +category: feature +dev: "User-initiated window close on win32 shows a sync dialog (default: shut down); 'leave it running' skips only the embedded-cluster teardown in stopLocal({keepEmbeddedPostgres}) so pools/runtime still close. Programmatic quits (dashboard restart) never prompt and keep the full stop." diff --git a/packages/desktop/build/uninstaller.nsh b/packages/desktop/build/uninstaller.nsh new file mode 100644 index 0000000000..41f0c7377c --- /dev/null +++ b/packages/desktop/build/uninstaller.nsh @@ -0,0 +1,19 @@ +; FNXC:WindowsDesktopPackaging 2026-07-18-05:20: +; Operator request: uninstalling Fusion on Windows must also shut down the +; embedded PostgreSQL server and remove Fusion's materialized runtime binaries +; (~\.fusion\embedded-postgres\runtime-bin, recreated on demand). The database +; cluster (~\.fusion\embedded-postgres\default) is USER DATA: interactive +; uninstalls ask before deleting it, silent uninstalls always keep it, and +; auto-update reinstalls (${isUpdated}) touch nothing so updates never kill a +; running server or prompt. +!macro customUnInstall + ${ifNot} ${isUpdated} + ; Stop the postmaster recorded in postmaster.pid (best effort; the PID is + ; the file's first line). usebackq tolerates spaces in the profile path. + nsExec::ExecToLog 'cmd /c for /f "usebackq" %i in ("$PROFILE\.fusion\embedded-postgres\default\postmaster.pid") do taskkill /PID %i /F /T' + RMDir /r "$PROFILE\.fusion\embedded-postgres\runtime-bin" + IfSilent +3 0 + MessageBox MB_YESNO|MB_ICONQUESTION "Also delete the embedded PostgreSQL database (all local Fusion data) at $PROFILE\.fusion\embedded-postgres?" IDNO +2 + RMDir /r "$PROFILE\.fusion\embedded-postgres" + ${endIf} +!macroend diff --git a/packages/desktop/electron-builder.yml b/packages/desktop/electron-builder.yml index fd8fdfe944..6e6fcb7e2d 100644 --- a/packages/desktop/electron-builder.yml +++ b/packages/desktop/electron-builder.yml @@ -205,6 +205,11 @@ nsis: perMachine: false allowElevation: false allowToChangeInstallationDirectory: true + # FNXC:WindowsDesktopPackaging 2026-07-18-05:20: + # Uninstall must stop the embedded postmaster and remove Fusion-materialized + # runtime binaries; the database is user data and is only deleted after an + # interactive YES (never on silent or auto-update uninstalls). + include: build/uninstaller.nsh portable: artifactName: "${productName}-${version}-${os}-${arch}-portable.${ext}" diff --git a/packages/desktop/src/local-runtime.ts b/packages/desktop/src/local-runtime.ts index 26ac5854b8..e5ea5fb683 100644 --- a/packages/desktop/src/local-runtime.ts +++ b/packages/desktop/src/local-runtime.ts @@ -567,12 +567,12 @@ export class LocalRuntimeManager { } } - async stopLocal(): Promise { + async stopLocal(options: { keepEmbeddedPostgres?: boolean } = {}): Promise { if (this.stopPromise) { return this.stopPromise; } - this.stopPromise = this.stopInternal(); + this.stopPromise = this.stopInternal(options); try { return await this.stopPromise; } finally { @@ -580,7 +580,7 @@ export class LocalRuntimeManager { } } - private async stopInternal(): Promise { + private async stopInternal(options: { keepEmbeddedPostgres?: boolean } = {}): Promise { if (this.runtime) { const runtime = this.runtime; this.runtime = null; @@ -595,8 +595,15 @@ export class LocalRuntimeManager { // Release the backend connection pool / embedded PG cluster if the store // was booted via the startup factory. store.close() already closes the // AsyncDataLayer pool; this adds embedded-cluster teardown. Best-effort. + /* + FNXC:DesktopClosePolicy 2026-07-18-05:00: + keepEmbeddedPostgres is the operator's Windows quit-prompt answer: close + the pools and the Fusion runtime but leave the embedded postmaster + running for other Fusion processes. Default (false) preserves the full + teardown for programmatic restarts and every non-prompted path. + */ const backendShutdown = (runtime.store as TaskStoreLike & { __backendShutdown?: () => Promise }).__backendShutdown; - if (backendShutdown) { + if (backendShutdown && !options.keepEmbeddedPostgres) { await backendShutdown().catch(() => undefined); } else { runtime.store.close(); diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts index 1b5e1d57a0..86ea1ea88e 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, nativeImage, screen, shell, Tray } from "electron"; +import { dialog, app, BrowserWindow, nativeImage, screen, shell, Tray } from "electron"; import { join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import os from "node:os"; @@ -130,6 +130,13 @@ function isInternalDesktopNavigation(url: URL, rendererOrigin: string | null): b return false; } +/* +FNXC:DesktopClosePolicy 2026-07-18-05:00: +Answer from the Windows close prompt, consumed once by before-quit teardown. +false (default) = full stop including the embedded PostgreSQL cluster. +*/ +let keepEmbeddedPostgresOnQuit = false; + async function resetLaunchModeAndReload(window: BrowserWindow): Promise { try { const settings = await readShellSettings(); @@ -237,6 +244,30 @@ export function createMainWindow(state?: WindowState, launchTargetUrl?: string): Windows Desktop close is a shutdown request, not a tray-minimize request. Let the BrowserWindow close so Electron emits window-all-closed and before-quit, which stops the embedded local Fusion runtime; keep macOS/non-Windows close-to-tray semantics for dock/tray restoration. */ if (process.platform === "win32") { + /* + FNXC:DesktopClosePolicy 2026-07-18-05:00: + Operator request: shutting down Fusion on Windows must ASK whether to also + shut down the embedded PostgreSQL server (it can serve other Fusion + processes such as the CLI). Asked only on a USER-initiated window close — + programmatic quits (dashboard-requested restart via app.relaunch/app.quit) + never pass through this handler, so automation stays prompt-free and + defaults to a full stop. The synchronous dialog is required: the close + event cannot await, and the answer must exist before before-quit tears the + runtime down. + */ + if (localRuntimeManager?.getStatus().state === "running") { + const choice = dialog.showMessageBoxSync(window, { + type: "question", + title: "Fusion", + message: "Also shut down the embedded PostgreSQL server?", + detail: "Other Fusion processes (like the fn CLI) can keep using it if you leave it running. It will be reused on the next start either way.", + buttons: ["Shut down PostgreSQL", "Leave it running"], + defaultId: 0, + cancelId: 0, + noLink: true, + }); + keepEmbeddedPostgresOnQuit = choice === 1; + } return; } @@ -482,7 +513,7 @@ export function run(): void { } if (localRuntimeManager) { - void localRuntimeManager.stopLocal(); + void localRuntimeManager.stopLocal({ keepEmbeddedPostgres: keepEmbeddedPostgresOnQuit }); } });