fix(core): boot embedded Postgres under non-admin user on elevated Windows (#2117)

## Summary

Windows embedded Postgres verification (CI `windows-latest` and elevated
desktop) fails because PostgreSQL refuses to run under an administrative
token:

> Execution of PostgreSQL by a user with administrative permissions is
not permitted.

GitHub Actions runners execute as `runneradmin` elevated, so the
existing `test:embedded-postgres` smoke (and any elevated Local-mode
desktop launch) cannot start the server.

### Fix

- When `isWindowsElevatedAdmin()` is true, **initdb / clients stay as
the launcher**, but the **postgres server** is started as a dedicated
non-admin local user (`fusion-pg`) via PowerShell `Start-Process
-Credential`.
- Readiness waits on the postgres log line `database system is ready to
accept connections` with a lightweight poll (no per-iteration
`tasklist`).
- Real-process vitest cases use a **180s** timeout on Windows (package
default is 15s, which killed healthy boots mid-start).
- Builds on top of the packaged-desktop asar materialization work
already on main (#2106).

## Test plan

- [x] `pnpm --filter @fusion/core test:embedded-postgres` on macOS
(33/33)
- [ ] `desktop-windows.yml` on `feature/win-pg-verify`:
  - [ ] Smoke embedded Postgres on Windows
  - [ ] Build + package Windows EXE
  - [ ] Verify app.asar assets
- [ ] Optional: download portable EXE and manual Local mode smoke on a
Windows host

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Bug Fixes**
- Improved embedded PostgreSQL startup on Windows when Fusion runs with
elevated administrator privileges.
- When elevated, the embedded database now boots under a dedicated
non-administrator local account, with more reliable readiness detection,
logging, and shutdown cleanup.
- Enhanced database provisioning and now prefers `127.0.0.1` for Windows
connection addressing.

- **Tests**
- Added coverage for Windows elevation detection without starting
embedded PostgreSQL.
- Increased platform-dependent timeouts for embedded real-process tests
to avoid premature failures on Windows.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
gsxdsm
2026-07-15 00:15:10 -07:00
committed by GitHub
parent 97172fdcf2
commit 6c008418fe
4 changed files with 927 additions and 119 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Embedded Postgres now boots on Windows when Fusion runs elevated, fixing the Windows installer build.
category: fix
dev: On elevated Windows the postgres server is booted under a dedicated non-admin local user via Start-Process -Credential (packages/core embedded-lifecycle.ts + embedded-windows-admin.ts); initdb and the pg client still run as the launching process.

View File

@@ -34,6 +34,7 @@ import {
EmbeddedStartTimeoutError,
DEFAULT_START_TIMEOUT_MS,
isDataDirInitialized,
isWindowsElevatedAdmin,
normalizeMacosEmbeddedPostgresDylibSymlinks,
readPortFromPostmasterPid,
__setEmbeddedPostgresCtorForTests,
@@ -108,6 +109,18 @@ describe("embedded-lifecycle: isDataDirInitialized (PG_VERSION marker)", () => {
});
});
describe("embedded-lifecycle: Windows elevation probe (no process)", () => {
it("isWindowsElevatedAdmin is false on non-Windows platforms", () => {
// FNXC:WindowsDesktopPackaging 2026-07-15-04:55:
// The non-admin boot path is Windows-only; other OSes must never claim elevation.
if (process.platform !== "win32") {
expect(isWindowsElevatedAdmin()).toBe(false);
} else {
expect(typeof isWindowsElevatedAdmin()).toBe("boolean");
}
});
});
describe("embedded-lifecycle: constructor + URL helpers (no process)", () => {
it("builds a connection URL with credentials for the configured database", () => {
const lifecycle = new EmbeddedPostgresLifecycle({
@@ -510,134 +523,172 @@ describe("embedded-lifecycle: macOS dylib compatibility links", () => {
});
});
/*
* FNXC:WindowsDesktopPackaging 2026-07-15-04:55:
* Package default testTimeout is 15s (packages/core/vitest.config.ts). On
* elevated Windows (GitHub windows-latest = runneradmin) the non-admin boot
* path + initdb regularly takes 60–90s before "ready to accept connections".
* The lifecycle startTimeout is 120s; the vitest wrapper must not kill earlier
* or CI reports false timeouts while postgres is still healthy (and orphans the
* non-admin postmaster). Use a per-test budget that covers elevated CI.
*
* FNXC:WindowsDesktopPackaging 2026-07-14-23:10:
* Double-start tests (VAL-CONN-006 reuse, already-initialized log) need room
* for two elevated boots. A 180s wall was tight: first start ~90s left the
* second start racing the vitest budget, which timed out mid-readiness and
* left EBUSY orphans on the data dir. 6 minutes covers 2×120s startTimeout
* plus stop/teardown margin under loaded windows-latest runners.
*/
const REAL_PROCESS_TEST_TIMEOUT_MS = process.platform === "win32" ? 360_000 : 60_000;
embeddedDescribe("embedded-lifecycle: real process (VAL-CONN-001, VAL-CONN-006, VAL-CONN-007)", () => {
it("first start runs initdb, ensures DB exists, and serves traffic (VAL-CONN-001)", async () => {
const dataDir = makeDataDir();
const lifecycle = new EmbeddedPostgresLifecycle(baseOptions(dataDir));
tracked.push({ lifecycle, dataDir });
it(
"first start runs initdb, ensures DB exists, and serves traffic (VAL-CONN-001)",
async () => {
const dataDir = makeDataDir();
const lifecycle = new EmbeddedPostgresLifecycle(baseOptions(dataDir));
tracked.push({ lifecycle, dataDir });
// Before start, the dir is not initialized.
expect(isDataDirInitialized(dataDir)).toBe(false);
// Before start, the dir is not initialized.
expect(isDataDirInitialized(dataDir)).toBe(false);
const backend = await lifecycle.start();
const backend = await lifecycle.start();
// After start, PG_VERSION exists (initdb ran).
expect(isDataDirInitialized(dataDir)).toBe(true);
// After start, PG_VERSION exists (initdb ran).
expect(isDataDirInitialized(dataDir)).toBe(true);
// Backend is embedded mode with a resolved runtime URL.
expect(backend.mode).toBe("embedded");
expect(backend.runtimeUrl).not.toBeNull();
expect(backend.runtimeUrl).toContain("/fusion");
// Backend is embedded mode with a resolved runtime URL.
expect(backend.mode).toBe("embedded");
expect(backend.runtimeUrl).not.toBeNull();
expect(backend.runtimeUrl).toContain("/fusion");
// The port was assigned (free-port discovery).
expect(lifecycle.getPort()).toBeGreaterThan(0);
// The port was assigned (free-port discovery).
expect(lifecycle.getPort()).toBeGreaterThan(0);
// Traffic is served: connect via postgres.js and query.
const sql = postgres(lifecycle.getConnectionUrl(), { max: 1 });
try {
const rows = await sql`SELECT current_database() AS db`;
expect(rows[0].db).toBe("fusion");
} finally {
await sql.end({ timeout: 5 });
}
});
// Traffic is served: connect via postgres.js and query.
const sql = postgres(lifecycle.getConnectionUrl(), { max: 1 });
try {
const rows = await sql`SELECT current_database() AS db`;
expect(rows[0].db).toBe("fusion");
} finally {
await sql.end({ timeout: 5 });
}
},
REAL_PROCESS_TEST_TIMEOUT_MS,
);
it("second start reuses the existing data directory without re-initdb (VAL-CONN-006)", async () => {
const dataDir = makeDataDir();
it(
"second start reuses the existing data directory without re-initdb (VAL-CONN-006)",
async () => {
const dataDir = makeDataDir();
// First lifecycle: start, write a marker row, stop.
const first = new EmbeddedPostgresLifecycle(baseOptions(dataDir));
await first.start();
const sql1 = postgres(first.getConnectionUrl(), { max: 1 });
try {
await sql1`CREATE TABLE persistence_marker (id int PRIMARY KEY, note text)`;
await sql1`INSERT INTO persistence_marker (id, note) VALUES (1, 'persisted')`;
} finally {
await sql1.end({ timeout: 5 });
}
await first.stop();
// First lifecycle: start, write a marker row, stop.
const first = new EmbeddedPostgresLifecycle(baseOptions(dataDir));
await first.start();
const sql1 = postgres(first.getConnectionUrl(), { max: 1 });
try {
await sql1`CREATE TABLE persistence_marker (id int PRIMARY KEY, note text)`;
await sql1`INSERT INTO persistence_marker (id, note) VALUES (1, 'persisted')`;
} finally {
await sql1.end({ timeout: 5 });
}
await first.stop();
// The data dir is still initialized after stop (persistent).
expect(isDataDirInitialized(dataDir)).toBe(true);
// The data dir is still initialized after stop (persistent).
expect(isDataDirInitialized(dataDir)).toBe(true);
// Second lifecycle: start against the SAME dir.
const second = new EmbeddedPostgresLifecycle(baseOptions(dataDir));
tracked.push({ lifecycle: second, dataDir });
// Second lifecycle: start against the SAME dir.
const second = new EmbeddedPostgresLifecycle(baseOptions(dataDir));
tracked.push({ lifecycle: second, dataDir });
await second.start();
const sql2 = postgres(second.getConnectionUrl(), { max: 1 });
try {
const rows = await sql2`SELECT note FROM persistence_marker WHERE id = 1`;
expect(rows[0].note).toBe("persisted");
} finally {
await sql2.end({ timeout: 5 });
}
});
await second.start();
const sql2 = postgres(second.getConnectionUrl(), { max: 1 });
try {
const rows = await sql2`SELECT note FROM persistence_marker WHERE id = 1`;
expect(rows[0].note).toBe("persisted");
} finally {
await sql2.end({ timeout: 5 });
}
},
REAL_PROCESS_TEST_TIMEOUT_MS,
);
it("ensureDatabase is idempotent: re-starting and ensuring the same DB does not error", async () => {
const dataDir = makeDataDir();
const lifecycle = new EmbeddedPostgresLifecycle(baseOptions(dataDir));
tracked.push({ lifecycle, dataDir });
it(
"ensureDatabase is idempotent: re-starting and ensuring the same DB does not error",
async () => {
const dataDir = makeDataDir();
const lifecycle = new EmbeddedPostgresLifecycle(baseOptions(dataDir));
tracked.push({ lifecycle, dataDir });
await lifecycle.start();
// Calling ensureDatabase again on the already-created DB should not throw.
await lifecycle.ensureDatabase();
await lifecycle.ensureDatabase();
});
await lifecycle.start();
// Calling ensureDatabase again on the already-created DB should not throw.
await lifecycle.ensureDatabase();
await lifecycle.ensureDatabase();
},
REAL_PROCESS_TEST_TIMEOUT_MS,
);
it("graceful shutdown stops the Postgres process; no orphan remains (VAL-CONN-007)", async () => {
const dataDir = makeDataDir();
const lifecycle = new EmbeddedPostgresLifecycle(baseOptions(dataDir));
tracked.push({ lifecycle, dataDir });
it(
"graceful shutdown stops the Postgres process; no orphan remains (VAL-CONN-007)",
async () => {
const dataDir = makeDataDir();
const lifecycle = new EmbeddedPostgresLifecycle(baseOptions(dataDir));
tracked.push({ lifecycle, dataDir });
await lifecycle.start();
const port = lifecycle.getPort()!;
expect(port).toBeGreaterThan(0);
await lifecycle.start();
const port = lifecycle.getPort()!;
expect(port).toBeGreaterThan(0);
// Confirm the port is accepting connections before shutdown.
const probeBefore = postgres(
`postgresql://postgres:password@localhost:${port}/fusion`,
{ max: 1, connect_timeout: 5 },
);
await probeBefore`SELECT 1`;
await probeBefore.end({ timeout: 5 });
// Confirm the port is accepting connections before shutdown.
const probeBefore = postgres(
`postgresql://postgres:password@localhost:${port}/fusion`,
{ max: 1, connect_timeout: 5 },
);
await probeBefore`SELECT 1`;
await probeBefore.end({ timeout: 5 });
await lifecycle.stop();
expect(lifecycle.isRunning()).toBe(false);
await lifecycle.stop();
expect(lifecycle.isRunning()).toBe(false);
// After shutdown, the port should refuse new connections.
const probeAfter = postgres(
`postgresql://postgres:password@localhost:${port}/fusion`,
{ max: 1, connect_timeout: 3 },
);
await expect(probeAfter`SELECT 1`).rejects.toThrow();
await probeAfter.end({ timeout: 5 }).catch(() => {});
// After shutdown, the port should refuse new connections.
const probeAfter = postgres(
`postgresql://postgres:password@localhost:${port}/fusion`,
{ max: 1, connect_timeout: 3 },
);
await expect(probeAfter`SELECT 1`).rejects.toThrow();
await probeAfter.end({ timeout: 5 }).catch(() => {});
// Remove from tracked cleanup since we already stopped.
const idx = tracked.findIndex((t) => t.lifecycle === lifecycle);
if (idx >= 0) tracked.splice(idx, 1);
});
// Remove from tracked cleanup since we already stopped.
const idx = tracked.findIndex((t) => t.lifecycle === lifecycle);
if (idx >= 0) tracked.splice(idx, 1);
},
REAL_PROCESS_TEST_TIMEOUT_MS,
);
it("start reports already-initialized reuse via the log when the dir exists", async () => {
const dataDir = makeDataDir();
const reuseLogLines: string[] = [];
const opts: EmbeddedLifecycleOptions = {
...baseOptions(dataDir),
onLog: (msg) => reuseLogLines.push(msg),
};
it(
"start reports already-initialized reuse via the log when the dir exists",
async () => {
const dataDir = makeDataDir();
const reuseLogLines: string[] = [];
const opts: EmbeddedLifecycleOptions = {
...baseOptions(dataDir),
onLog: (msg) => reuseLogLines.push(msg),
};
const first = new EmbeddedPostgresLifecycle(opts);
await first.start();
await first.stop();
const first = new EmbeddedPostgresLifecycle(opts);
await first.start();
await first.stop();
reuseLogLines.length = 0;
const second = new EmbeddedPostgresLifecycle(opts);
tracked.push({ lifecycle: second, dataDir });
await second.start();
expect(
reuseLogLines.some((l) => /existing data directory/i.test(l)),
).toBe(true);
});
reuseLogLines.length = 0;
const second = new EmbeddedPostgresLifecycle(opts);
tracked.push({ lifecycle: second, dataDir });
await second.start();
expect(
reuseLogLines.some((l) => /existing data directory/i.test(l)),
).toBe(true);
},
REAL_PROCESS_TEST_TIMEOUT_MS,
);
});
describe("embedded-lifecycle: startup timeout (P1 #24)", () => {

View File

@@ -67,6 +67,19 @@ import { createRequire, syncBuiltinESMExports } from "node:module";
import { createLogger } from "../logger.js";
import { redactConnectionString } from "./credential-redact.js";
import type { ResolvedBackend } from "./backend-resolver.js";
import {
isWindowsElevatedAdmin,
startServerAsNonAdminUser,
type NonAdminServerHandle,
} from "./embedded-windows-admin.js";
// FNXC:WindowsDesktopPackaging 2026-07-14-22:53:
// Static import so tsup/esbuild bundles postgres.js into packages/cli/dist/bin.js.
// A runtime require("postgres") resolved via the CLI createRequire banner against
// packages/cli/dist and failed boot-smoke with "Cannot find module 'postgres'"
// because @runfusion/fusion does not list postgres as a direct dependency.
import postgres from "postgres";
export { isWindowsElevatedAdmin } from "./embedded-windows-admin.js";
const require = createRequire(import.meta.url);
@@ -442,10 +455,18 @@ export type EmbeddedPostgresCtor = new (opts: Record<string, unknown>) => {
/** Instance type produced by the embedded-postgres constructor. */
type EmbeddedPostgresInstance = InstanceType<EmbeddedPostgresCtor>;
let embeddedPostgresCtorCache: EmbeddedPostgresCtor | null = null;
/**
* FNXC:WindowsDesktopPackaging 2026-07-14-22:53:
* True while tests inject a mock EmbeddedPostgres ctor. Elevated Windows CI
* would otherwise take the real non-admin boot path and ignore the mock's
* delayed start() used by cancellation coverage.
*/
let embeddedPostgresCtorIsTestOverride = false;
/** Test-only constructor seam for deterministic lifecycle cancellation coverage. */
export function __setEmbeddedPostgresCtorForTests(ctor: EmbeddedPostgresCtor | null): void {
embeddedPostgresCtorCache = ctor;
embeddedPostgresCtorIsTestOverride = ctor !== null;
}
function getEmbeddedPostgresCtor(): EmbeddedPostgresCtor {
@@ -674,6 +695,29 @@ function resolveMacosEmbeddedPostgresNativeRoot(): string | null {
return resolveGenericEmbeddedPostgresNativeRoot();
}
/**
* FNXC:WindowsDesktopPackaging 2026-07-14-21:40:
* Resolve the bundled @embedded-postgres/windows-x64 native root (.../native).
* Used to stage binaries for the non-admin server boot path under elevation.
* Prefer the host-local materialization when available so elevated Windows
* desktop launches never spawn postgres.exe from app.asar.
*/
function resolveWindowsEmbeddedPostgresNativeRoot(): string | null {
if (process.platform !== "win32") return null;
const nativeRoot = resolveGenericEmbeddedPostgresNativeRoot();
if (!nativeRoot) return null;
if (nativeRoot.includes(`${sep}app.asar`)) {
try {
return materializeEmbeddedPostgresRuntimeBinaries(
resolveElectronAsarUnpackedPath(nativeRoot),
);
} catch {
return resolveElectronAsarUnpackedPath(nativeRoot);
}
}
return nativeRoot;
}
function normalizeBundledMacosDylibs(onLog: (message: string) => void): void {
const nativeRoot = resolveMacosEmbeddedPostgresNativeRoot();
if (!nativeRoot) return;
@@ -811,6 +855,13 @@ export class EmbeddedPostgresLifecycle {
// is false and stop() is a no-op (the owning instance handles shutdown).
private ownsProcess = true;
private shutdownHookInstalled = false;
/**
* FNXC:WindowsDesktopPackaging 2026-07-14-21:40:
* When the process is an elevated Windows admin, the server is booted under a
* dedicated non-admin user (see embedded-windows-admin.ts) and this holds the
* stop handle. Null for normal (non-elevated / non-Windows) launches.
*/
private nonAdminHandle: NonAdminServerHandle | null = null;
/**
* FNXC:PostgresEmbedded 2026-06-26-16:20 (fix migration-review P1 #24):
* Active start() timeout timer, retained so it can be cleared on success or
@@ -874,7 +925,13 @@ export class EmbeddedPostgresLifecycle {
}
private buildUrl(port: number, database: string): string {
return `postgresql://${encodeURIComponent(this.options.user)}:${encodeURIComponent(this.options.password)}@localhost:${port}/${encodeURIComponent(database)}`;
// FNXC:WindowsDesktopPackaging 2026-07-15-05:00:
// Prefer 127.0.0.1 on Windows. `localhost` can resolve to ::1 first; the
// non-admin postmaster path and some Windows loopback policies made IPv6
// connects hang while IPv4 was fine, which blocked ensureDatabase after the
// cluster was already ready.
const host = process.platform === "win32" ? "127.0.0.1" : "localhost";
return `postgresql://${encodeURIComponent(this.options.user)}:${encodeURIComponent(this.options.password)}@${host}:${port}/${encodeURIComponent(database)}`;
}
/**
@@ -1016,7 +1073,48 @@ export class EmbeddedPostgresLifecycle {
throw new EmbeddedStartCancelledError(this.options.dataDir);
}
await pg.start();
// FNXC:WindowsDesktopPackaging 2026-07-14-21:40:
// Under an elevated Windows admin token, postgres refuses to inherit the
// process token ("Execution of PostgreSQL by a user with administrative
// permissions is not permitted"). initdb + the pg client above ran as the
// launching (admin) process and work unchanged; only the SERVER start is
// re-homed under a dedicated non-admin local user. Normal (non-elevated /
// non-Windows) launches use the inherited-token path as before.
// FNXC:WindowsDesktopPackaging 2026-07-15-05:20:
// Pass AbortSignal so outer start() timeout can cancel a still-polling
// non-admin launch and kill the wrapper before readiness assigns a handle.
// FNXC:WindowsDesktopPackaging 2026-07-14-22:53:
// Skip the real non-admin path when tests inject a mock ctor so delayed
// start/cancellation coverage exercises pg.start() even on elevated CI.
if (isWindowsElevatedAdmin() && !embeddedPostgresCtorIsTestOverride) {
const nativeRoot = resolveWindowsEmbeddedPostgresNativeRoot();
if (!nativeRoot) {
throw new Error(
"embedded postgres: the process is running elevated on Windows, where " +
"PostgreSQL refuses to start under an administrative token, and the " +
"non-admin boot path could not locate the bundled " +
"@embedded-postgres/windows-x64 native binaries to stage. Run Fusion " +
"non-elevated, or ensure the embedded-postgres platform package is installed.",
);
}
this.nonAdminHandle = await startServerAsNonAdminUser({
nativeRoot,
dataDir: this.options.dataDir,
port,
postgresFlags: this.options.postgresFlags,
onLog: this.options.onLog,
onError: this.options.onError,
startTimeoutMs: this.options.startTimeoutMs,
signal,
// Assign handle as soon as the wrapper PID is known so outer start()
// timeout cleanup can taskkill orphans mid-readiness poll.
onLaunched: (handle) => {
this.nonAdminHandle = handle;
},
});
} else {
await pg.start();
}
/*
FNXC:PostgresResourceLifecycle 2026-07-14-18:42:
Promise.race does not cancel the losing embedded-postgres startup. Check the cooperative cancellation signal after every delayed phase and stop the exact late instance before it can publish running state, registry ownership, or process hooks. A timeout may already have attempted stop while pg.start() was pending, so the post-resolution stop is intentionally repeated to catch a postmaster that appeared after that first cleanup.
@@ -1065,6 +1163,20 @@ export class EmbeddedPostgresLifecycle {
}
private async settleCancelledStart(pg: EmbeddedPostgresInstance): Promise<void> {
// FNXC:WindowsDesktopPackaging 2026-07-15-05:20:
// Prefer stopping a non-admin handle (if already assigned) before asking
// embedded-postgres to stop a process it never started.
if (this.nonAdminHandle) {
try {
await this.nonAdminHandle.stop();
} catch (error) {
this.options.onError(
`embedded postgres: cancelled non-admin cleanup failed: ${String(error)}`,
);
} finally {
this.nonAdminHandle = null;
}
}
try {
await pg.stop();
} catch (error) {
@@ -1083,35 +1195,72 @@ export class EmbeddedPostgresLifecycle {
* Idempotent: queries `pg_database` first and only issues `CREATE DATABASE`
* when the database is missing. `embedded-postgres.createDatabase()` throws on
* an existing database, so this guard is required for safe re-starts.
*
* FNXC:WindowsDesktopPackaging 2026-07-15-05:00:
* Do not call embedded-postgres.createDatabase() when the server was started
* under the elevated-Windows non-admin path: that library requires
* `this.process` (set only by its own .start()), so createDatabase throws
* "cluster must be running" even though postgres is healthy. Use a direct
* SQL connection with a bounded connect timeout instead.
*/
async ensureDatabase(): Promise<void> {
if (!this.pg || !this.running) {
if (!this.running || this.getPort() === undefined) {
throw new Error(
"Cannot ensure database: the embedded cluster is not running. Call start() first.",
);
}
const exists = await this.databaseExists(this.options.database);
if (exists) return;
await this.pg.createDatabase(this.options.database);
const sql = this.openMaintenanceSql();
try {
const safeName = this.options.database.replace(/"/g, '""');
await sql.unsafe(`CREATE DATABASE "${safeName}"`);
} finally {
await sql.end({ timeout: 5 }).catch(() => {});
}
}
/** Check whether a database with the given name exists on the cluster. */
private async databaseExists(name: string): Promise<boolean> {
if (!this.pg) return false;
// Use the maintenance client (connects to the default "postgres" db).
const client = this.pg.getPgClient("postgres", "localhost");
if (!this.running || this.getPort() === undefined) return false;
const sql = this.openMaintenanceSql();
try {
await client.connect();
const result = await client.query(
"SELECT 1 FROM pg_database WHERE datname = $1",
[name],
);
return (result.rowCount ?? 0) > 0;
const rows = await sql`SELECT 1 AS one FROM pg_database WHERE datname = ${name}`;
return rows.length > 0;
} catch {
return false;
} finally {
await client.end().catch(() => {});
await sql.end({ timeout: 5 }).catch(() => {});
}
}
/**
* Open a short-lived maintenance connection to the embedded cluster's
* built-in `postgres` database (for CREATE DATABASE / existence checks).
*
* FNXC:WindowsDesktopPackaging 2026-07-14-22:53:
* Uses the statically imported postgres.js client (bundled into CLI) rather
* than embedded-postgres getPgClient, which requires this.process set by
* library start() — unavailable on the elevated Windows non-admin path.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private openMaintenanceSql(): any {
const port = this.getPort();
if (port === undefined) {
throw new Error("openMaintenanceSql: no port assigned");
}
const host = process.platform === "win32" ? "127.0.0.1" : "localhost";
return postgres({
host,
port,
user: this.options.user,
password: this.options.password,
database: "postgres",
max: 1,
connect_timeout: 10,
});
}
/**
* Stop the embedded PostgreSQL process. Safe to call multiple times.
* After stop, the data directory is preserved (persistent), so a subsequent
@@ -1128,6 +1277,24 @@ export class EmbeddedPostgresLifecycle {
return;
}
// FNXC:WindowsDesktopPackaging 2026-07-14-21:40:
// Elevated Windows path: the postmaster was started under a dedicated
// non-admin user; stop it via the handle instead of embedded-postgres
// (which never called .start() and has no process handle).
if (this.nonAdminHandle) {
try {
await this.nonAdminHandle.stop();
} catch (err) {
this.options.onError(`embedded postgres: error during non-admin stop: ${String(err)}`);
} finally {
this.nonAdminHandle = null;
this.pg = null;
this.running = false;
runningInstances.delete(this.options.dataDir);
}
return;
}
if (!this.pg) {
this.running = false;
// Clean up the registry even if pg is null

View File

@@ -0,0 +1,583 @@
// FNXC:WindowsDesktopPackaging 2026-07-14-21:30:
// Embedded PostgreSQL refuses to start under a Windows process token whose
// Administrators group is ENABLED (a high-integrity / elevated token). It exits
// immediately with "Execution of PostgreSQL by a user with administrative
// permissions is not permitted." The bundled embedded-postgres server is
// spawned as a DIRECT child of the Node process (see embedded-postgres
// dist/index.js start()), so it inherits that elevated token and cannot boot.
// This only affects ELEVATED launches: GitHub windows-latest runners execute
// jobs as `runneradmin` with a fully elevated token (the smoke build fails
// here), and an end user who explicitly "Run as administrator" hits the same
// refusal. A normal Electron asInvoker launch uses a filtered/medium token
// which Postgres accepts.
//
// Fix: when the current process is elevated, boot the postgres SERVER process
// (only) under a freshly-created NON-ADMIN local user via Start-Process
// -Credential (CreateProcessWithLogonW). That user's token has no enabled
// Administrators group, so Postgres accepts it. initdb / the pg client /
// createDatabase still run as the (admin) launching process and work unchanged;
// only the server start is re-homed. Proven on the windows-2025-vs2026 runner:
// postgres reached "database system is ready to accept connections" under the
// dedicated non-admin user (broker diagnostic run 29382479266, job 87248898326).
//
// Access model: Windows grants "Bypass traverse checking" to Everyone by
// default, so the non-admin user does NOT need permission on parent dirs — only
// on the target dirs themselves. We grant the user RX on the native binary root
// and full control on the data dir.
import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { createConnection } from "node:net";
import { join, dirname } from "node:path";
/** Handle returned by {@link startServerAsNonAdminUser}; call stop() to kill it. */
export interface NonAdminServerHandle {
/**
* Best-effort OS pid of the running postgres server (from postmaster.pid when
* available). May be the cmd wrapper pid until postmaster.pid appears.
*/
readonly postgresPid: number;
/** Stop the non-admin postgres process (taskkill). Safe to call once. */
stop(): Promise<void>;
}
export interface NonAdminStartOptions {
/** .../native dir containing bin/postgres.exe + lib + share. */
readonly nativeRoot: string;
/** The initialized PG data directory. */
readonly dataDir: string;
/** TCP port postgres should listen on. */
readonly port: number;
/** Extra flags forwarded to postgres.exe (same semantics as embedded-postgres). */
readonly postgresFlags: readonly string[];
readonly onLog: (message: string) => void;
readonly onError: (messageOrError: string | Error | unknown) => void;
/** Hard timeout (ms) on reaching "ready to accept connections". */
readonly startTimeoutMs: number;
/**
* FNXC:WindowsDesktopPackaging 2026-07-15-05:20:
* Cooperative cancellation from EmbeddedPostgresLifecycle.start()'s AbortController.
* When aborted during readiness polling, kill the wrapper/postmaster immediately.
*/
readonly signal?: AbortSignal;
/**
* Invoked as soon as the cmd wrapper PID is known (before readiness) so the
* lifecycle can stop orphans if the outer start() timeout wins the race.
*/
readonly onLaunched?: (handle: NonAdminServerHandle) => void;
}
let elevatedCache: boolean | null = null;
/**
* True only on Windows when the current process holds an elevated admin token.
* `net session` succeeds (exit 0) exclusively under an elevated admin token, so
* it is a reliable elevation probe that does not depend on UAC EnableLUA.
*/
export function isWindowsElevatedAdmin(): boolean {
if (process.platform !== "win32") return false;
if (elevatedCache !== null) return elevatedCache;
const r = spawnSync("net", ["session"], { encoding: "utf8", shell: true });
elevatedCache = r.status === 0;
return elevatedCache;
}
const DEDICATED_USER = "fusion-pg";
let dedicatedPassword: string | null = null;
/**
* FNXC:WindowsDesktopPackaging 2026-07-15-05:25:
* Fully randomized password with all four complexity classes and no fixed
* prefix/suffix (review feedback: constant frames reduce entropy). Avoids the
* account-name token so Windows complexity policy accepts it.
*/
function generatePassword(): string {
const upper = "ABCDEFGHJKLMNPQRSTUVWXYZ";
const lower = "abcdefghijkmnopqrstuvwxyz";
const digits = "23456789";
const symbols = "!@#$%^*_-+=?";
const all = upper + lower + digits + symbols;
const seed =
spawnSync(
"powershell",
["-NoProfile", "-Command", "[BitConverter]::ToString([guid]::NewGuid().ToByteArray()) + [BitConverter]::ToString([guid]::NewGuid().ToByteArray())"],
{ encoding: "utf8" },
).stdout ?? Math.random().toString(36) + Math.random().toString(36);
const required = [
upper[Math.floor(Math.random() * upper.length)]!,
lower[Math.floor(Math.random() * lower.length)]!,
digits[Math.floor(Math.random() * digits.length)]!,
symbols[Math.floor(Math.random() * symbols.length)]!,
];
let body = "";
for (const ch of seed) {
if (/[a-zA-Z0-9]/.test(ch)) {
const idx = parseInt(ch.toLowerCase(), 16);
if (Number.isFinite(idx)) body += all[idx % all.length]!;
}
if (body.length >= 20) break;
}
while (body.length < 20) body += all[Math.floor(Math.random() * all.length)]!;
const chars = [...required, ...body.split("")];
for (let i = chars.length - 1; i > 0; i -= 1) {
const j = Math.floor(Math.random() * (i + 1));
const tmp = chars[i]!;
chars[i] = chars[j]!;
chars[j] = tmp;
}
return chars.join("");
}
/**
* Ensure the dedicated non-admin local user exists and we know its password.
* Idempotent: creates the user if absent, or resets its password if present
* (so a leftover account from a prior run still works). Always strips
* Administrators membership so a reused account cannot stay elevated.
*/
function ensureNonAdminUser(): { user: string; password: string } {
if (dedicatedPassword) return { user: DEDICATED_USER, password: dedicatedPassword };
const password = generatePassword();
const add = spawnSync("net", ["user", DEDICATED_USER, password, "/add", "/y"], {
encoding: "utf8",
});
if (add.status !== 0) {
// Likely already exists from a prior run: reset its password so we can log on.
const reset = spawnSync("net", ["user", DEDICATED_USER, password, "/y"], {
encoding: "utf8",
});
if (reset.status !== 0) {
throw new Error(
`embedded postgres: could not create/reset non-admin user '${DEDICATED_USER}' ` +
`(net user add status=${add.status}: ${(add.stderr || "").trim()}; ` +
`reset status=${reset.status}: ${(reset.stderr || "").trim()}).`,
);
}
}
// FNXC:WindowsDesktopPackaging 2026-07-15-05:25:
// A leftover fusion-pg that was manually promoted to Administrators would
// still be refused by postgres. Demote and fail closed unless the account is
// already not a member (review: ignore silent demote failures).
// FNXC:WindowsDesktopPackaging 2026-07-14-22:53:
// net localgroup /delete status 0 = removed; non-zero is OK only when the
// account was already not in Administrators ("not a member" / "could not find").
const demote = spawnSync(
"net",
["localgroup", "Administrators", DEDICATED_USER, "/delete"],
{ encoding: "utf8" },
);
if (demote.status !== 0) {
const demoteOut = `${demote.stdout || ""}\n${demote.stderr || ""}`.toLowerCase();
const alreadyNotMember =
demoteOut.includes("not a member") ||
demoteOut.includes("could not find") ||
demoteOut.includes("no such") ||
demoteOut.includes("does not exist");
if (!alreadyNotMember) {
throw new Error(
`embedded postgres: failed to remove '${DEDICATED_USER}' from Administrators ` +
`(net localgroup status=${demote.status}): ` +
`${(demote.stderr || demote.stdout || "").trim().slice(0, 400)}. ` +
"PostgreSQL refuses to start under an administrative token; demote the " +
"account or run Fusion non-elevated.",
);
}
}
dedicatedPassword = password;
return { user: DEDICATED_USER, password };
}
/**
* FNXC:WindowsDesktopPackaging 2026-07-14-22:30:
* Grant traverse (RX) on each ancestor dir of `leaf` up to the drive root, so
* the non-admin user can reach `leaf` even when "Bypass traverse checking" is
* restricted (the windows-2025 runner) or a profile ACL would deny traversal.
* RX is applied folder-by-folder as a non-inheriting ACE so sibling contents
* are not over-granted. Best-effort: ancestors that already allow traverse
* (e.g. C:\) reject harmlessly, and a blocked path surfaces later via the
* Start-Process error (which carries the full stderr).
*/
function grantTraverseChain(user: string, leaf: string): void {
let dir = dirname(leaf);
for (let depth = 0; depth < 16; depth += 1) {
const parent = dirname(dir);
if (parent === dir) break; // drive root reached
if (existsSync(dir)) {
spawnSync("icacls", [dir, "/grant", `${user}:(RX)`, "/C"], { encoding: "utf8" });
}
dir = parent;
}
}
/**
* Grant the non-admin user full control on the data dir (postgres writes
* there), read+execute on the native binary root, and traverse on each parent
* ancestor of both so the user can reach them. F/RX grants fail fast; the
* traverse walk is best-effort. /T applies the F/RX grants recursively; /C
* keeps going on non-fatal errors (e.g. unreadable sibling files).
*/
function grantNonAdminAccess(user: string, nativeRoot: string, dataDir: string): void {
for (const [target, perm] of [
[dataDir, "(OI)(CI)F"],
[nativeRoot, "(OI)(CI)RX"],
] as const) {
if (!existsSync(target)) {
throw new Error(`embedded postgres: non-admin grant target does not exist: ${target}`);
}
const r = spawnSync("icacls", [target, "/grant", `${user}:${perm}`, "/T", "/C"], {
encoding: "utf8",
});
if (r.status !== 0) {
throw new Error(
`embedded postgres: failed to grant '${user}' ${perm} on ${target} ` +
`(icacls status=${r.status}): ${(r.stderr || "").trim().slice(0, 400)}`,
);
}
}
grantTraverseChain(user, dataDir);
grantTraverseChain(user, nativeRoot);
}
function readPostgresPid(dataDir: string): number | null {
try {
const lines = readFileSync(join(dataDir, "postmaster.pid"), "utf-8").split("\n");
const pid = parseInt((lines[0] ?? "").trim(), 10);
return Number.isFinite(pid) && pid > 0 ? pid : null;
} catch {
return null;
}
}
function readTail(file: string, max: number): string {
try {
const content = readFileSync(file, "utf-8");
return content.length > max ? "…" + content.slice(-max) : content;
} catch {
return "(no log file)";
}
}
/**
* FNXC:WindowsDesktopPackaging 2026-07-15-05:25:
* Reject postgresFlags that would break cmd.exe quoting or enable injection
* when embedded into launch.bat (review: arbitrary flags with % " & | etc.).
*/
function sanitizePostgresFlags(flags: readonly string[]): string[] {
const safe: string[] = [];
for (const flag of flags) {
if (typeof flag !== "string" || flag.length === 0) {
throw new Error(`embedded postgres: invalid postgresFlags entry (empty/non-string)`);
}
if (/[\r\n"%&|<>^!]/.test(flag)) {
throw new Error(
`embedded postgres: postgresFlags entry contains cmd.exe-sensitive characters: ${JSON.stringify(flag)}`,
);
}
safe.push(flag);
}
return safe;
}
/** Quote a path for cmd.exe double-quoted args (escape embedded quotes). */
function cmdQuote(value: string): string {
return `"${value.replace(/"/g, '""')}"`;
}
let pwshCache: string | null | undefined;
/**
* FNXC:WindowsDesktopPackaging 2026-07-14-22:10:
* Resolve the PowerShell binary used to launch the non-admin server. Prefer
* PowerShell 7 (`pwsh`): the windows-2025 runner runs Windows PowerShell 5.1
* (`powershell.exe`) in Constrained Language Mode, where the
* Microsoft.PowerShell.Security module cannot load (ConvertTo-SecureString
* fails). pwsh runs unconstrained and is what the proven broker diagnostic
* used. Fall back to powershell.exe for end-user boxes that only have 5.1 in
* Full Language Mode.
*/
function resolvePowerShell(): string {
if (pwshCache !== undefined) return pwshCache as string;
const pf = process.env.PROGRAMFILES;
const pf86 = process.env["ProgramFiles(x86)"];
const candidates = [
pf ? join(pf, "PowerShell", "7", "pwsh.exe") : null,
pf86 ? join(pf86, "PowerShell", "7", "pwsh.exe") : null,
].filter((v): v is string => v !== null);
for (const c of candidates) {
if (existsSync(c)) {
pwshCache = c;
return c;
}
}
const where = spawnSync("where", ["pwsh"], { encoding: "utf8", shell: true });
if (where.status === 0) {
const found = (where.stdout || "")
.split(/\r?\n/)
.map((s) => s.trim())
.find(Boolean);
if (found) {
pwshCache = found;
return found;
}
}
pwshCache = "powershell.exe";
return pwshCache;
}
/**
* Start postgres.exe under the dedicated non-admin user and resolve once it is
* accepting connections. Rejects with a clear error (including the postgres log
* tail) on timeout or early exit. The returned handle's stop() kills the server.
*/
export async function startServerAsNonAdminUser(
opts: NonAdminStartOptions,
): Promise<NonAdminServerHandle> {
const { user, password } = ensureNonAdminUser();
grantNonAdminAccess(user, opts.nativeRoot, opts.dataDir);
const pgExe = join(opts.nativeRoot, "bin", "postgres.exe");
const runDir = join(opts.dataDir, ".pgrunner");
mkdirSync(runDir, { recursive: true });
const logFile = join(runDir, "postgres.log");
// FNXC:WindowsDesktopPackaging 2026-07-14-22:50:
// Separate wrapper log: the bat echoes whoami / cwd / the exact postgres
// command / the exit code here (cmd's own output), while postgres's output
// goes to logFile. This distinguishes "bat never ran", "postgres exited",
// and "postgres running but not listening" — postgres.log alone can be empty
// when the bat never reaches the postgres command.
const wrapperLog = join(runDir, "wrapper.log");
// FNXC:WindowsDesktopPackaging 2026-07-15-05:05:
// Truncate logs each launch. The bat appends (>>) so a prior stop that wrote
// `exit=1` would make the readiness poll throw "exited before becoming ready"
// on the next start against a reused data directory (VAL-CONN-006).
writeFileSync(logFile, "", "utf8");
writeFileSync(wrapperLog, "", "utf8");
const bat = join(runDir, "launch.bat");
const safeFlags = sanitizePostgresFlags(opts.postgresFlags);
const args = ["-D", opts.dataDir, "-p", String(opts.port), ...safeFlags];
// Set TMP/TEMP inside the granted data dir so the non-admin postgres process
// never writes outside an accessible location.
const argStr = args.map((a) => cmdQuote(a)).join(" ");
// FNXC:WindowsDesktopPackaging 2026-07-15-05:25:
// UTF-8 + chcp 65001 so non-ASCII profile paths (e.g. C:\Users\José) are not
// corrupted when cmd.exe reads the bat (review: ASCII encoding broke paths).
writeFileSync(
bat,
[
"@echo off",
"chcp 65001 >nul",
`set "TMP=${runDir}"`,
`set "TEMP=${runDir}"`,
`call :main >> ${cmdQuote(wrapperLog)} 2>&1`,
"exit /b",
":main",
"echo launch-start",
"whoami",
"cd",
`echo cmd: ${cmdQuote(pgExe)} ${argStr}`,
`${cmdQuote(pgExe)} ${argStr} > ${cmdQuote(logFile)} 2>&1`,
"echo exit=%ERRORLEVEL%",
"",
].join("\r\n"),
"utf8",
);
const computerName = process.env.COMPUTERNAME ?? "";
const domainUser = computerName ? `${computerName}\\${user}` : user;
// Launch detached under the non-admin credential. Start-Process returns at
// once with a process object (postgres keeps running in the background).
//
// FNXC:WindowsDesktopPackaging 2026-07-14-21:50:
// Use a parametrized launcher .ps1 invoked with -File + params, NOT an inline
// -Command string. The bat path contains backslashes (literal in a PS
// single-quoted string — doubling them would corrupt it to C:\\...) and the
// password contains ! and #; passing each as a discrete argv token via -File
// params is robust across Node's Windows arg escaping and PowerShell parsing.
const launcherPs1 = join(runDir, "launch.ps1");
writeFileSync(
launcherPs1,
[
"param([string]$User,[string]$Password,[string]$DomainUser,[string]$Bat)",
"$ErrorActionPreference='Stop'",
// FNXC:WindowsDesktopPackaging 2026-07-14-22:15:
// Build the SecureString char-by-char instead of ConvertTo-SecureString,
// which lives in Microsoft.PowerShell.Security — a module that fails to
// load under Windows PowerShell 5.1 Constrained Language Mode. System.
// Security.SecureString + PSCredential are core SMA/.NET types available
// without that module.
"$s = New-Object System.Security.SecureString",
"foreach ($ch in $Password.ToCharArray()) { [void]$s.AppendChar($ch) }",
"$s.MakeReadOnly()",
"$c = New-Object System.Management.Automation.PSCredential($DomainUser,$s)",
"$p = Start-Process -FilePath 'cmd.exe' -ArgumentList '/c',$Bat -Credential $c -WindowStyle Hidden -PassThru",
"Write-Output $p.Id",
"",
].join("\r\n"),
"utf8",
);
const powerShell = resolvePowerShell();
const launch = spawnSync(
powerShell,
[
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
launcherPs1,
"-User",
user,
"-Password",
password,
"-DomainUser",
domainUser,
"-Bat",
bat,
],
{ encoding: "utf8" },
);
const wrapperPid = parseInt((launch.stdout || "").trim(), 10);
if (!Number.isFinite(wrapperPid)) {
throw new Error(
`embedded postgres: failed to launch non-admin postgres ` +
`(${powerShell} status=${launch.status} ` +
`stdout=${(launch.stdout || "").trim().slice(0, 500)} ` +
`stderr=${(launch.stderr || "").trim().slice(0, 2000)}).`,
);
}
let stopped = false;
const killAll = (): void => {
if (stopped) return;
stopped = true;
const pid = readPostgresPid(opts.dataDir);
if (pid) spawnSync("taskkill", ["/pid", String(pid), "/f", "/t"], { encoding: "utf8" });
spawnSync("taskkill", ["/pid", String(wrapperPid), "/f", "/t"], { encoding: "utf8" });
};
// FNXC:WindowsDesktopPackaging 2026-07-15-05:20:
// Publish a stop handle immediately so lifecycle timeout cleanup can kill the
// wrapper even while readiness is still polling (review: orphan on timeout).
const handle: NonAdminServerHandle = {
get postgresPid() {
return readPostgresPid(opts.dataDir) ?? wrapperPid;
},
async stop() {
killAll();
},
};
opts.onLaunched?.(handle);
opts.onLog(
`embedded postgres: launched postgres as non-admin user '${user}' (wrapper pid ${wrapperPid}); ` +
`waiting for port ${opts.port}`,
);
// Poll for readiness until the server accepts connections or the timeout hits.
// FNXC:WindowsDesktopPackaging 2026-07-14-22:53:
// startTimeoutMs <= 0 means unbounded (matches outer lifecycle: 0 disables
// the start timeout). Math.max(..., 1000) previously forced a 1s deadline and
// killed elevated boots when callers disabled the timeout (review feedback).
const hasDeadline =
opts.startTimeoutMs > 0 && Number.isFinite(opts.startTimeoutMs);
const deadline = hasDeadline
? Date.now() + opts.startTimeoutMs
: Number.POSITIVE_INFINITY;
let ready = false;
let lastSnapshot = "";
while (Date.now() < deadline) {
if (opts.signal?.aborted) {
killAll();
throw new Error(
`embedded postgres: non-admin launch cancelled before ready (wrapper pid ${wrapperPid}).`,
);
}
// FNXC:WindowsDesktopPackaging 2026-07-14-23:05:
// Lightweight poll: readFileSync only. Do NOT spawn tasklist/probePort in
// the hot loop — a synchronous tasklist per iteration blocked ~16s between
// polls on windows-2025, blowing the test's 15s budget before postgres's
// "ready" marker was observed (and orphaning servers when start() never
// returned). Readiness = the postgres log "ready to accept connections"
// marker (the same one embedded-postgres watches). Exit = the wrapper bat's
// "exit=" line (written only once postgres returns). Errors = a FATAL in
// the postgres log. Logs are emitted only on change to avoid per-poll spam.
const tail = readTail(logFile, 3000);
const wrapperTail = readTail(wrapperLog, 1500);
const snapshot = `${wrapperTail}\u0000${tail.slice(-400)}`;
if (snapshot !== lastSnapshot) {
lastSnapshot = snapshot;
opts.onLog(`non-admin poll wrapper={${wrapperTail}} pg={${tail.slice(-400)}}`);
}
if (/database system is ready to accept connections/.test(tail)) {
// FNXC:WindowsDesktopPackaging 2026-07-15-05:00:
// Log readiness alone is not enough: confirm TCP accept on 127.0.0.1 so
// ensureDatabase cannot hang on a connect that never completes (IPv6 /
// cross-session loopback quirks). Probe is async and cheap.
if (await probeTcpPort(opts.port, 500)) {
ready = true;
break;
}
}
if (/\bFATAL\b|\bPANIC\b|could not (bind|start|create|access|connect|load)|not permitted|Permission denied|is not the owner/i.test(tail)) {
killAll();
throw new Error(
`embedded postgres: non-admin postgres reported a startup error before opening the port.\n${tail}`,
);
}
if (/^exit=/m.test(wrapperTail)) {
killAll();
throw new Error(
`embedded postgres: non-admin postgres exited before becoming ready.\nwrapper={${wrapperTail}}\npg={${tail}}`,
);
}
// Avoid Promise.withResolvers (needs lib es2024); package tsconfig stays on es2022.
await new Promise<void>((resolve) => {
setTimeout(resolve, 200);
});
}
if (!ready) {
const tail = readTail(logFile, 1500);
killAll();
throw new Error(
`embedded postgres: non-admin postgres did not become ready` +
(hasDeadline ? ` within ${opts.startTimeoutMs}ms` : "") +
`.\n${tail}`,
);
}
if (opts.signal?.aborted) {
killAll();
throw new Error(
`embedded postgres: non-admin launch cancelled after ready (wrapper pid ${wrapperPid}).`,
);
}
const postgresPid = readPostgresPid(opts.dataDir);
if (!postgresPid) {
opts.onError("embedded postgres: started but could not read postmaster.pid");
}
const resolvedPid = postgresPid ?? wrapperPid;
opts.onLog(
`embedded postgres: non-admin server ready on 127.0.0.1:${opts.port} (pid ${resolvedPid})`,
);
return handle;
}
/** True when a TCP accept is available on 127.0.0.1:port within timeoutMs. */
function probeTcpPort(port: number, timeoutMs: number): Promise<boolean> {
return new Promise((resolve) => {
const socket = createConnection({ host: "127.0.0.1", port });
let settled = false;
const finish = (ok: boolean) => {
if (settled) return;
settled = true;
socket.removeAllListeners();
socket.destroy();
resolve(ok);
};
socket.setTimeout(timeoutMs);
socket.once("connect", () => finish(true));
socket.once("timeout", () => finish(false));
socket.once("error", () => finish(false));
});
}