feat(desktop): Windows Postgres lifecycle — quit prompt and uninstaller cleanup

Closing the Windows desktop app now asks whether to also shut down the
embedded PostgreSQL server ('leave it running' keeps serving other
Fusion processes; programmatic restarts never prompt and keep the full
stop). The NSIS uninstaller stops the postmaster from postmaster.pid,
removes materialized runtime binaries, and asks before deleting the
database cluster — silent and auto-update uninstalls never touch data.
build/uninstaller.nsh force-added like entitlements.mac.plist (the
build/ ignore targets generated icons).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-18 00:14:54 -07:00
parent c7b1529ef8
commit 755063715a
5 changed files with 75 additions and 6 deletions

View File

@@ -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."

View File

@@ -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

View File

@@ -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}"

View File

@@ -567,12 +567,12 @@ export class LocalRuntimeManager {
}
}
async stopLocal(): Promise<DesktopRuntimeStatus> {
async stopLocal(options: { keepEmbeddedPostgres?: boolean } = {}): Promise<DesktopRuntimeStatus> {
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<DesktopRuntimeStatus> {
private async stopInternal(options: { keepEmbeddedPostgres?: boolean } = {}): Promise<DesktopRuntimeStatus> {
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<void> }).__backendShutdown;
if (backendShutdown) {
if (backendShutdown && !options.keepEmbeddedPostgres) {
await backendShutdown().catch(() => undefined);
} else {
runtime.store.close();

View File

@@ -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<void> {
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 });
}
});