pgserve 1.1.3-rc.6 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/context/windows-debug.md +119 -0
- package/.github/workflows/build-all-platforms.yml +21 -23
- package/assets/icon.ico +0 -0
- package/bin/pglite-server.js +3 -1
- package/eslint.config.js +2 -0
- package/package.json +1 -1
- package/src/cluster.js +7 -1
- package/src/dashboard.js +10 -4
- package/src/postgres.js +232 -75
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Windows Debug Context
|
|
2
|
+
|
|
3
|
+
## RESOLVED (2025-12-12)
|
|
4
|
+
|
|
5
|
+
The Windows router binding issue has been fixed. See "Solution Applied" below.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Original Issue
|
|
10
|
+
PostgreSQL starts successfully on port 9432, but the router/proxy on port 8432 was NOT listening.
|
|
11
|
+
Clients could not connect because 8432 wasn't bound.
|
|
12
|
+
|
|
13
|
+
## Root Causes Found
|
|
14
|
+
|
|
15
|
+
### 1. `reusePort: true` - Linux-only feature (PRIMARY CAUSE)
|
|
16
|
+
**Location:** `src/cluster.js:75`
|
|
17
|
+
|
|
18
|
+
The cluster mode uses `Bun.listen({ reusePort: true })` which maps to `SO_REUSEPORT`.
|
|
19
|
+
This socket option is Linux-only and **silently fails on Windows**.
|
|
20
|
+
|
|
21
|
+
### 2. Auto-enabled cluster mode on multi-core Windows systems
|
|
22
|
+
**Location:** `bin/pglite-server.js:106`
|
|
23
|
+
|
|
24
|
+
Windows systems with multiple CPU cores automatically entered cluster mode,
|
|
25
|
+
triggering the `reusePort` failure.
|
|
26
|
+
|
|
27
|
+
### 3. TCP port opens before PostgreSQL ready for protocol handshakes
|
|
28
|
+
**Location:** `src/postgres.js:803-821`
|
|
29
|
+
|
|
30
|
+
PostgreSQL was marked "ready" when TCP port opened, but it wasn't actually
|
|
31
|
+
ready for protocol-level handshakes. Admin pool connection timed out.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Solution Applied
|
|
36
|
+
|
|
37
|
+
### Fix 1: Disable cluster mode on Windows
|
|
38
|
+
**File:** `bin/pglite-server.js:98-108`
|
|
39
|
+
```javascript
|
|
40
|
+
const isWindows = os.platform() === 'win32';
|
|
41
|
+
cluster: cpuCount > 1 && !isWindows,
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Fix 2: Add platform check to reusePort
|
|
45
|
+
**File:** `src/cluster.js:72-76`
|
|
46
|
+
```javascript
|
|
47
|
+
const isWindows = os.platform() === 'win32';
|
|
48
|
+
this.server = Bun.listen({
|
|
49
|
+
reusePort: !isWindows, // SO_REUSEPORT only works on Linux/macOS
|
|
50
|
+
...
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Fix 3: Add port binding verification
|
|
55
|
+
**File:** `src/cluster.js:99-102`
|
|
56
|
+
```javascript
|
|
57
|
+
if (!this.server || !this.server.port) {
|
|
58
|
+
throw new Error(`Failed to bind to port ${this.port} - reusePort may not be supported`);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Fix 4: Add Windows readiness delay
|
|
63
|
+
**File:** `src/postgres.js:813-817`
|
|
64
|
+
```javascript
|
|
65
|
+
if (isWindows) {
|
|
66
|
+
await Bun.sleep(2000); // 2 second delay for Windows
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Fix 5: Increase admin pool retry for Windows
|
|
71
|
+
**File:** `src/postgres.js:598-599`
|
|
72
|
+
```javascript
|
|
73
|
+
const maxRetries = isWindows ? 10 : 5;
|
|
74
|
+
const baseDelay = isWindows ? 2000 : 1000;
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Verification
|
|
80
|
+
|
|
81
|
+
After applying fixes, server runs correctly on Windows:
|
|
82
|
+
- Router: `127.0.0.1:7432` ✅
|
|
83
|
+
- PostgreSQL: `127.0.0.1:8432` ✅
|
|
84
|
+
- Database auto-provisioning: ✅
|
|
85
|
+
- Query execution: ✅
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
Server started successfully!
|
|
89
|
+
|
|
90
|
+
Endpoint: postgresql://127.0.0.1:7432/<database>
|
|
91
|
+
Mode: In-memory (ephemeral)
|
|
92
|
+
PostgreSQL: Port 8432 (internal)
|
|
93
|
+
Auto-create: Enabled
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Test Commands (Run from Windows)
|
|
99
|
+
```cmd
|
|
100
|
+
# Build binary
|
|
101
|
+
bun build --compile bin/pglite-server.js --outfile dist/pgserve-windows-x64.exe
|
|
102
|
+
|
|
103
|
+
# Start server
|
|
104
|
+
dist\pgserve-windows-x64.exe
|
|
105
|
+
|
|
106
|
+
# Check ports are listening (should see BOTH)
|
|
107
|
+
netstat -an | findstr "LISTEN" | findstr "8432 9432"
|
|
108
|
+
|
|
109
|
+
# Test connection
|
|
110
|
+
psql postgresql://localhost:8432/testdb
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Related Files
|
|
116
|
+
- `bin/pglite-server.js` - Entry point, cluster mode decision
|
|
117
|
+
- `src/cluster.js` - Cluster mode with reusePort
|
|
118
|
+
- `src/router.js` - Single-process mode (works on Windows)
|
|
119
|
+
- `src/postgres.js` - PostgreSQL startup and admin pool
|
|
@@ -45,17 +45,14 @@ jobs:
|
|
|
45
45
|
include:
|
|
46
46
|
# Linux x64
|
|
47
47
|
- platform: linux-x64
|
|
48
|
-
pg_pkg: linux-x64
|
|
49
48
|
output: pgserve-linux-x64
|
|
50
49
|
os: ubuntu-latest
|
|
51
|
-
# macOS ARM64 (Apple Silicon)
|
|
50
|
+
# macOS ARM64 (Apple Silicon)
|
|
52
51
|
- platform: darwin-arm64
|
|
53
|
-
pg_pkg: darwin-arm64
|
|
54
52
|
output: pgserve-darwin-arm64
|
|
55
53
|
os: macos-latest
|
|
56
54
|
# Windows x64 - must build on Windows for --windows-icon
|
|
57
55
|
- platform: windows-x64
|
|
58
|
-
pg_pkg: windows-x64
|
|
59
56
|
output: pgserve-windows-x64.exe
|
|
60
57
|
os: windows-latest
|
|
61
58
|
|
|
@@ -73,33 +70,34 @@ jobs:
|
|
|
73
70
|
- name: Install dependencies
|
|
74
71
|
run: bun install
|
|
75
72
|
|
|
76
|
-
#
|
|
77
|
-
- name:
|
|
78
|
-
run: |
|
|
79
|
-
echo "Installing @embedded-postgres/${{ matrix.pg_pkg }}..."
|
|
80
|
-
npm install @embedded-postgres/${{ matrix.pg_pkg }}
|
|
81
|
-
mkdir -p embedded-postgres
|
|
82
|
-
cp -r node_modules/@embedded-postgres/${{ matrix.pg_pkg }}/native/* embedded-postgres/
|
|
83
|
-
echo "Bundled PostgreSQL binaries:"
|
|
84
|
-
ls -la embedded-postgres/
|
|
85
|
-
ls -la embedded-postgres/bin/ || true
|
|
86
|
-
shell: bash
|
|
87
|
-
|
|
88
|
-
# Windows: native build with custom icon
|
|
89
|
-
- name: Build for Windows (with icon)
|
|
73
|
+
# Windows: native build with icon and proper metadata
|
|
74
|
+
- name: Build for Windows (with branding)
|
|
90
75
|
if: matrix.platform == 'windows-x64'
|
|
91
76
|
run: |
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
77
|
+
New-Item -ItemType Directory -Force -Path dist | Out-Null
|
|
78
|
+
$RAW_VERSION = node -p "require('./package.json').version"
|
|
79
|
+
# Convert 1.1.3-rc.10 to 1.1.3.10 (Windows requires X.Y.Z.W format)
|
|
80
|
+
$WIN_VERSION = $RAW_VERSION -replace '-rc\.', '.'
|
|
81
|
+
Write-Host "Raw version: $RAW_VERSION -> Windows version: $WIN_VERSION"
|
|
82
|
+
bun build --compile `
|
|
83
|
+
--define BUILD_VERSION="'$RAW_VERSION'" `
|
|
84
|
+
--windows-icon=assets/icon.ico `
|
|
85
|
+
--windows-title="pgserve" `
|
|
86
|
+
--windows-publisher="Namastex Labs" `
|
|
87
|
+
--windows-description="Embedded PostgreSQL Server - Zero config, auto-provision, unlimited connections" `
|
|
88
|
+
--windows-version="$WIN_VERSION" `
|
|
89
|
+
--windows-copyright="Copyright (c) 2025 Namastex Labs" `
|
|
90
|
+
bin/pglite-server.js --outfile dist/${{ matrix.output }}
|
|
91
|
+
Get-ChildItem dist/
|
|
92
|
+
shell: pwsh
|
|
96
93
|
|
|
97
94
|
# Linux/macOS: native build
|
|
98
95
|
- name: Build for ${{ matrix.platform }}
|
|
99
96
|
if: matrix.platform != 'windows-x64'
|
|
100
97
|
run: |
|
|
101
98
|
mkdir -p dist
|
|
102
|
-
|
|
99
|
+
VERSION=$(node -p "require('./package.json').version")
|
|
100
|
+
bun build --compile --define BUILD_VERSION="'$VERSION'" bin/pglite-server.js --outfile dist/${{ matrix.output }}
|
|
103
101
|
ls -lh dist/
|
|
104
102
|
|
|
105
103
|
- name: Upload artifact
|
package/assets/icon.ico
CHANGED
|
Binary file
|
package/bin/pglite-server.js
CHANGED
|
@@ -94,7 +94,9 @@ FEATURES:
|
|
|
94
94
|
*/
|
|
95
95
|
function parseArgs() {
|
|
96
96
|
// Auto-enable cluster mode on multi-core systems for best performance
|
|
97
|
+
// Note: Cluster mode uses SO_REUSEPORT which is not supported on Windows
|
|
97
98
|
const cpuCount = os.cpus().length;
|
|
99
|
+
const isWindows = os.platform() === 'win32';
|
|
98
100
|
|
|
99
101
|
const options = {
|
|
100
102
|
port: 8432,
|
|
@@ -103,7 +105,7 @@ function parseArgs() {
|
|
|
103
105
|
useRam: false, // Use /dev/shm for true RAM storage (Linux only)
|
|
104
106
|
logLevel: 'info',
|
|
105
107
|
autoProvision: true,
|
|
106
|
-
cluster: cpuCount > 1, // Auto-enable on multi-core (
|
|
108
|
+
cluster: cpuCount > 1 && !isWindows, // Auto-enable on multi-core (disabled on Windows - no SO_REUSEPORT)
|
|
107
109
|
workers: null, // null = use CPU count
|
|
108
110
|
syncTo: null, // Sync target PostgreSQL URL
|
|
109
111
|
syncDatabases: null // Database patterns to sync (comma-separated)
|
package/eslint.config.js
CHANGED
package/package.json
CHANGED
package/src/cluster.js
CHANGED
|
@@ -69,10 +69,11 @@ class ClusterRouter extends EventEmitter {
|
|
|
69
69
|
|
|
70
70
|
// Create TCP server using Bun.listen() for 2-3x throughput
|
|
71
71
|
const router = this;
|
|
72
|
+
const isWindows = os.platform() === 'win32';
|
|
72
73
|
this.server = Bun.listen({
|
|
73
74
|
hostname: this.host,
|
|
74
75
|
port: this.port,
|
|
75
|
-
reusePort:
|
|
76
|
+
reusePort: !isWindows, // SO_REUSEPORT for multi-worker port sharing (Linux/macOS only)
|
|
76
77
|
socket: {
|
|
77
78
|
data(socket, data) {
|
|
78
79
|
router.handleSocketData(socket, data);
|
|
@@ -95,6 +96,11 @@ class ClusterRouter extends EventEmitter {
|
|
|
95
96
|
}
|
|
96
97
|
});
|
|
97
98
|
|
|
99
|
+
// Verify port actually bound (detect silent failures on Windows)
|
|
100
|
+
if (!this.server || !this.server.port) {
|
|
101
|
+
throw new Error(`Failed to bind to port ${this.port} - reusePort may not be supported on this platform`);
|
|
102
|
+
}
|
|
103
|
+
|
|
98
104
|
this.emit('listening');
|
|
99
105
|
}
|
|
100
106
|
|
package/src/dashboard.js
CHANGED
|
@@ -13,12 +13,18 @@ import { readFileSync } from 'fs';
|
|
|
13
13
|
import { join, dirname } from 'path';
|
|
14
14
|
import { fileURLToPath } from 'url';
|
|
15
15
|
|
|
16
|
-
// Get package version
|
|
16
|
+
// Get package version - BUILD_VERSION is injected at compile time via --define
|
|
17
|
+
// Falls back to reading package.json for development mode
|
|
17
18
|
let version = '1.0.0';
|
|
18
19
|
try {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// Check for build-time injected version first
|
|
21
|
+
if (typeof BUILD_VERSION !== 'undefined') {
|
|
22
|
+
version = BUILD_VERSION;
|
|
23
|
+
} else {
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
26
|
+
version = pkg.version;
|
|
27
|
+
}
|
|
22
28
|
} catch {
|
|
23
29
|
// Fallback version
|
|
24
30
|
}
|
package/src/postgres.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* - No locale dependency (works on any system)
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
/* global fetch, Bun */
|
|
16
17
|
import os from 'os';
|
|
17
18
|
import path from 'path';
|
|
18
19
|
import fs from 'fs';
|
|
@@ -45,71 +46,105 @@ function getBinaryCacheDir() {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* should be bundled alongside the executable.
|
|
51
|
-
* @returns {string|null} Path to bundled binaries or null if not found
|
|
52
|
-
*/
|
|
53
|
-
function getBundledBinaryDir() {
|
|
54
|
-
// Check for embedded-postgres folder relative to the module
|
|
55
|
-
// This works when binaries are bundled during build
|
|
56
|
-
const bundledDir = path.join(import.meta.dirname, '..', 'embedded-postgres');
|
|
57
|
-
if (fs.existsSync(path.join(bundledDir, 'bin'))) {
|
|
58
|
-
return bundledDir;
|
|
59
|
-
}
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Extract bundled PostgreSQL binaries to cache directory.
|
|
65
|
-
* Bun-compiled binaries can read embedded files but cannot execute them directly.
|
|
66
|
-
* We extract to ~/.pgserve/bin/{platform}/ for execution.
|
|
49
|
+
* Download and extract PostgreSQL binaries on first run.
|
|
50
|
+
* Downloads from npm registry (@embedded-postgres packages).
|
|
67
51
|
*
|
|
68
|
-
* @returns {Promise<string>} Path to extracted
|
|
52
|
+
* @returns {Promise<string>} Path to extracted directory
|
|
69
53
|
*/
|
|
70
|
-
async function
|
|
54
|
+
async function downloadPostgresBinaries() {
|
|
71
55
|
const platform = os.platform();
|
|
72
56
|
const cacheDir = getBinaryCacheDir();
|
|
73
57
|
const cacheBinDir = path.join(cacheDir, 'bin');
|
|
74
58
|
const initdbName = platform === 'win32' ? 'initdb.exe' : 'initdb';
|
|
75
59
|
const postgresName = platform === 'win32' ? 'postgres.exe' : 'postgres';
|
|
76
60
|
|
|
77
|
-
// Check if already
|
|
61
|
+
// Check if already downloaded
|
|
78
62
|
if (fs.existsSync(path.join(cacheBinDir, initdbName)) &&
|
|
79
63
|
fs.existsSync(path.join(cacheBinDir, postgresName))) {
|
|
80
64
|
return cacheDir;
|
|
81
65
|
}
|
|
82
66
|
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
67
|
+
const platformKey = getPlatformKey();
|
|
68
|
+
const pkgName = `@embedded-postgres/${platformKey}`;
|
|
69
|
+
const pkgVersion = '17.7.0-beta.15';
|
|
70
|
+
|
|
71
|
+
console.log(`[pgserve] PostgreSQL binaries not found.`);
|
|
72
|
+
console.log(`[pgserve] Downloading ${pkgName}@${pkgVersion}...`);
|
|
73
|
+
|
|
74
|
+
// Get tarball URL from npm registry
|
|
75
|
+
const registryUrl = `https://registry.npmjs.org/${pkgName}`;
|
|
76
|
+
const registryRes = await fetch(registryUrl);
|
|
77
|
+
if (!registryRes.ok) {
|
|
78
|
+
throw new Error(`Failed to fetch package info: ${registryRes.status}`);
|
|
79
|
+
}
|
|
80
|
+
const pkgInfo = await registryRes.json();
|
|
81
|
+
const tarballUrl = pkgInfo.versions[pkgVersion]?.dist?.tarball;
|
|
82
|
+
|
|
83
|
+
if (!tarballUrl) {
|
|
84
|
+
throw new Error(`Version ${pkgVersion} not found for ${pkgName}`);
|
|
87
85
|
}
|
|
88
86
|
|
|
89
|
-
|
|
87
|
+
// Download tarball
|
|
88
|
+
console.log(`[pgserve] Downloading from npm registry...`);
|
|
89
|
+
const tarballRes = await fetch(tarballUrl);
|
|
90
|
+
if (!tarballRes.ok) {
|
|
91
|
+
throw new Error(`Failed to download tarball: ${tarballRes.status}`);
|
|
92
|
+
}
|
|
90
93
|
|
|
91
|
-
|
|
94
|
+
const tarballBuffer = await tarballRes.arrayBuffer();
|
|
95
|
+
const tarballSize = (tarballBuffer.byteLength / 1024 / 1024).toFixed(1);
|
|
96
|
+
console.log(`[pgserve] Downloaded ${tarballSize} MB`);
|
|
97
|
+
|
|
98
|
+
// Create temp file for tarball
|
|
99
|
+
const tempDir = path.join(os.tmpdir(), `pgserve-download-${Date.now()}`);
|
|
100
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
101
|
+
const tarballPath = path.join(tempDir, 'package.tgz');
|
|
102
|
+
fs.writeFileSync(tarballPath, Buffer.from(tarballBuffer));
|
|
103
|
+
|
|
104
|
+
// Extract tarball using tar (available on all platforms via bun/node)
|
|
105
|
+
console.log(`[pgserve] Extracting binaries...`);
|
|
92
106
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
93
107
|
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
108
|
+
// Use Bun.spawn for extraction
|
|
109
|
+
const extractProc = Bun.spawn(['tar', '-xzf', tarballPath, '-C', tempDir], {
|
|
110
|
+
stdout: 'pipe',
|
|
111
|
+
stderr: 'pipe'
|
|
112
|
+
});
|
|
113
|
+
await extractProc.exited;
|
|
114
|
+
|
|
115
|
+
// Copy native/* to cache dir
|
|
116
|
+
const nativeDir = path.join(tempDir, 'package', 'native');
|
|
117
|
+
if (!fs.existsSync(nativeDir)) {
|
|
118
|
+
throw new Error('Extracted package does not contain native/ directory');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Copy files recursively
|
|
122
|
+
await copyDirRecursive(nativeDir, cacheDir);
|
|
97
123
|
|
|
98
124
|
// Make executables executable (Unix only)
|
|
99
125
|
if (platform !== 'win32') {
|
|
100
126
|
const binDir = path.join(cacheDir, 'bin');
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
127
|
+
if (fs.existsSync(binDir)) {
|
|
128
|
+
const files = fs.readdirSync(binDir);
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
const filePath = path.join(binDir, file);
|
|
131
|
+
try {
|
|
132
|
+
fs.chmodSync(filePath, 0o755);
|
|
133
|
+
} catch {
|
|
134
|
+
// Ignore
|
|
135
|
+
}
|
|
108
136
|
}
|
|
109
137
|
}
|
|
110
138
|
}
|
|
111
139
|
|
|
112
|
-
|
|
140
|
+
// Cleanup temp
|
|
141
|
+
try {
|
|
142
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
143
|
+
} catch {
|
|
144
|
+
// Ignore cleanup errors
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(`[pgserve] PostgreSQL binaries installed to ${cacheDir}`);
|
|
113
148
|
return cacheDir;
|
|
114
149
|
}
|
|
115
150
|
|
|
@@ -118,7 +153,7 @@ async function extractBundledBinaries() {
|
|
|
118
153
|
* @param {string} src - Source directory
|
|
119
154
|
* @param {string} dest - Destination directory
|
|
120
155
|
*/
|
|
121
|
-
async function
|
|
156
|
+
async function copyDirRecursive(src, dest) {
|
|
122
157
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
123
158
|
|
|
124
159
|
for (const entry of entries) {
|
|
@@ -127,11 +162,9 @@ async function copyRecursive(src, dest) {
|
|
|
127
162
|
|
|
128
163
|
if (entry.isDirectory()) {
|
|
129
164
|
fs.mkdirSync(destPath, { recursive: true });
|
|
130
|
-
await
|
|
165
|
+
await copyDirRecursive(srcPath, destPath);
|
|
131
166
|
} else {
|
|
132
|
-
|
|
133
|
-
const content = fs.readFileSync(srcPath);
|
|
134
|
-
fs.writeFileSync(destPath, content);
|
|
167
|
+
fs.copyFileSync(srcPath, destPath);
|
|
135
168
|
}
|
|
136
169
|
}
|
|
137
170
|
}
|
|
@@ -233,22 +266,20 @@ async function getBinaryPaths() {
|
|
|
233
266
|
return { initdb: cachedInitdb, postgres: cachedPostgres, binDir: cacheBinDir, libDir };
|
|
234
267
|
}
|
|
235
268
|
|
|
236
|
-
// Priority 2:
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
ensureLibrarySymlinks(libDir, platform);
|
|
249
|
-
}
|
|
250
|
-
return { initdb: extractedInitdb, postgres: extractedPostgres, binDir: extractedBinDir, libDir };
|
|
269
|
+
// Priority 2: Download binaries if not found (standalone exe mode)
|
|
270
|
+
// This downloads from npm registry on first run
|
|
271
|
+
const downloadedDir = await downloadPostgresBinaries();
|
|
272
|
+
if (downloadedDir) {
|
|
273
|
+
const downloadedBinDir = path.join(downloadedDir, 'bin');
|
|
274
|
+
const downloadedInitdb = path.join(downloadedBinDir, 'initdb' + exeSuffix);
|
|
275
|
+
const downloadedPostgres = path.join(downloadedBinDir, 'postgres' + exeSuffix);
|
|
276
|
+
|
|
277
|
+
if (fs.existsSync(downloadedInitdb) && fs.existsSync(downloadedPostgres)) {
|
|
278
|
+
const libDir = path.join(downloadedDir, 'lib');
|
|
279
|
+
if ((platform === 'linux' || platform === 'darwin') && fs.existsSync(libDir)) {
|
|
280
|
+
ensureLibrarySymlinks(libDir, platform);
|
|
251
281
|
}
|
|
282
|
+
return { initdb: downloadedInitdb, postgres: downloadedPostgres, binDir: downloadedBinDir, libDir };
|
|
252
283
|
}
|
|
253
284
|
}
|
|
254
285
|
|
|
@@ -544,9 +575,11 @@ export class PostgresManager {
|
|
|
544
575
|
*/
|
|
545
576
|
async _initAdminPool() {
|
|
546
577
|
const { SQL } = await import('bun');
|
|
578
|
+
const isWindows = process.platform === 'win32';
|
|
547
579
|
|
|
548
580
|
// Bun.sql config - uses TCP connections (Unix sockets not directly supported)
|
|
549
581
|
// This is fine for admin queries (low volume, local connection)
|
|
582
|
+
// Windows needs longer timeout due to higher network stack latency
|
|
550
583
|
this.adminPool = new SQL({
|
|
551
584
|
hostname: '127.0.0.1',
|
|
552
585
|
port: this.port,
|
|
@@ -555,16 +588,38 @@ export class PostgresManager {
|
|
|
555
588
|
password: this.password,
|
|
556
589
|
max: 5, // Small pool - only for CREATE DATABASE operations
|
|
557
590
|
idleTimeout: 30,
|
|
558
|
-
connectionTimeout: 5,
|
|
591
|
+
connectionTimeout: isWindows ? 15 : 5,
|
|
559
592
|
});
|
|
560
593
|
|
|
561
|
-
// Verify connection
|
|
562
|
-
|
|
594
|
+
// Verify connection with retry logic
|
|
595
|
+
// TCP port being open doesn't mean PostgreSQL protocol is ready
|
|
596
|
+
// This handles the race condition on Windows where the port binds
|
|
597
|
+
// before the server is fully ready to accept protocol handshakes
|
|
598
|
+
const maxRetries = isWindows ? 10 : 5; // More retries on Windows
|
|
599
|
+
const baseDelay = isWindows ? 2000 : 1000; // Longer delay on Windows
|
|
563
600
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
601
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
602
|
+
try {
|
|
603
|
+
await this.adminPool`SELECT 1`;
|
|
604
|
+
this.logger.debug({
|
|
605
|
+
host: '127.0.0.1',
|
|
606
|
+
maxConnections: 5,
|
|
607
|
+
attempt
|
|
608
|
+
}, 'Admin connection pool initialized (Bun.sql)');
|
|
609
|
+
return; // Success
|
|
610
|
+
} catch (err) {
|
|
611
|
+
if (attempt === maxRetries) {
|
|
612
|
+
throw new Error(`Failed to initialize admin pool after ${maxRetries} attempts: ${err.message}`);
|
|
613
|
+
}
|
|
614
|
+
this.logger.debug({
|
|
615
|
+
attempt,
|
|
616
|
+
maxRetries,
|
|
617
|
+
error: err.message
|
|
618
|
+
}, 'Admin pool connection failed, retrying...');
|
|
619
|
+
// Exponential backoff: 1s, 2s, 3s, 4s
|
|
620
|
+
await new Promise(resolve => setTimeout(resolve, baseDelay * attempt));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
568
623
|
}
|
|
569
624
|
|
|
570
625
|
/**
|
|
@@ -636,8 +691,24 @@ export class PostgresManager {
|
|
|
636
691
|
|
|
637
692
|
let started = false;
|
|
638
693
|
let startupOutput = '';
|
|
694
|
+
let processExited = false;
|
|
695
|
+
let portBindingSeen = false;
|
|
696
|
+
const portStr = this.port.toString();
|
|
697
|
+
|
|
698
|
+
// Hybrid startup detection:
|
|
699
|
+
// 1. TCP connection polling (works on Linux/macOS)
|
|
700
|
+
// 2. Log-based detection (fallback for Windows where Bun.connect may fail)
|
|
701
|
+
// Whichever succeeds first wins
|
|
639
702
|
|
|
640
|
-
|
|
703
|
+
const markReady = (method) => {
|
|
704
|
+
if (!started) {
|
|
705
|
+
started = true;
|
|
706
|
+
this.logger.info({ port: this.port, method }, 'PostgreSQL ready');
|
|
707
|
+
resolve();
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
// Read stderr - detect port binding in logs (locale-independent: just look for port number)
|
|
641
712
|
const readStream = async (stream) => {
|
|
642
713
|
const reader = stream.getReader();
|
|
643
714
|
const decoder = new TextDecoder();
|
|
@@ -649,11 +720,16 @@ export class PostgresManager {
|
|
|
649
720
|
startupOutput += message;
|
|
650
721
|
this.logger.debug({ pgOutput: message.trim() }, 'PostgreSQL output');
|
|
651
722
|
|
|
652
|
-
//
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
723
|
+
// Detect port binding - look for our port number in log output
|
|
724
|
+
// This is locale-independent (numbers are universal)
|
|
725
|
+
if (!portBindingSeen && message.includes(portStr)) {
|
|
726
|
+
portBindingSeen = true;
|
|
727
|
+
// Give PostgreSQL 500ms after port binding to finish startup
|
|
728
|
+
setTimeout(() => {
|
|
729
|
+
if (!started && !processExited) {
|
|
730
|
+
markReady('log-port-binding');
|
|
731
|
+
}
|
|
732
|
+
}, 500);
|
|
657
733
|
}
|
|
658
734
|
}
|
|
659
735
|
} catch {
|
|
@@ -667,16 +743,97 @@ export class PostgresManager {
|
|
|
667
743
|
|
|
668
744
|
// Handle process exit
|
|
669
745
|
this.process.exited.then((code) => {
|
|
746
|
+
processExited = true;
|
|
670
747
|
if (!started) {
|
|
671
748
|
reject(new Error(`PostgreSQL exited with code ${code} before starting: ${startupOutput}`));
|
|
672
749
|
}
|
|
673
750
|
this.process = null;
|
|
674
751
|
});
|
|
675
752
|
|
|
676
|
-
//
|
|
753
|
+
// Method 1: TCP connection polling (preferred, works on Linux/macOS)
|
|
754
|
+
const tryConnect = () => {
|
|
755
|
+
return new Promise((resolveConn, rejectConn) => {
|
|
756
|
+
let resolved = false;
|
|
757
|
+
const timeout = setTimeout(() => {
|
|
758
|
+
if (!resolved) {
|
|
759
|
+
resolved = true;
|
|
760
|
+
rejectConn(new Error('Connection timeout'));
|
|
761
|
+
}
|
|
762
|
+
}, 500);
|
|
763
|
+
|
|
764
|
+
Bun.connect({
|
|
765
|
+
hostname: '127.0.0.1',
|
|
766
|
+
port: this.port,
|
|
767
|
+
socket: {
|
|
768
|
+
open(socket) {
|
|
769
|
+
if (!resolved) {
|
|
770
|
+
resolved = true;
|
|
771
|
+
clearTimeout(timeout);
|
|
772
|
+
socket.end();
|
|
773
|
+
resolveConn(true);
|
|
774
|
+
}
|
|
775
|
+
},
|
|
776
|
+
connectError(_socket, error) {
|
|
777
|
+
if (!resolved) {
|
|
778
|
+
resolved = true;
|
|
779
|
+
clearTimeout(timeout);
|
|
780
|
+
rejectConn(error);
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
error(_socket, error) {
|
|
784
|
+
if (!resolved) {
|
|
785
|
+
resolved = true;
|
|
786
|
+
clearTimeout(timeout);
|
|
787
|
+
rejectConn(error);
|
|
788
|
+
}
|
|
789
|
+
},
|
|
790
|
+
data() {},
|
|
791
|
+
close() {},
|
|
792
|
+
},
|
|
793
|
+
}).catch((err) => {
|
|
794
|
+
if (!resolved) {
|
|
795
|
+
resolved = true;
|
|
796
|
+
clearTimeout(timeout);
|
|
797
|
+
rejectConn(err);
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
const pollConnection = async () => {
|
|
804
|
+
const startTime = Date.now();
|
|
805
|
+
const timeoutMs = 30000;
|
|
806
|
+
const isWindows = process.platform === 'win32';
|
|
807
|
+
|
|
808
|
+
while (Date.now() - startTime < timeoutMs && !started) {
|
|
809
|
+
if (processExited) return;
|
|
810
|
+
|
|
811
|
+
try {
|
|
812
|
+
await tryConnect();
|
|
813
|
+
// On Windows, TCP port opens before PostgreSQL is fully ready for protocol handshakes
|
|
814
|
+
// Add delay to let PostgreSQL complete its startup sequence
|
|
815
|
+
if (isWindows) {
|
|
816
|
+
await Bun.sleep(2000); // 2 second delay for Windows
|
|
817
|
+
}
|
|
818
|
+
markReady('tcp');
|
|
819
|
+
return;
|
|
820
|
+
} catch {
|
|
821
|
+
await Bun.sleep(200);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
// Start TCP polling (log detection is handled inline above via setTimeout)
|
|
827
|
+
pollConnection();
|
|
828
|
+
|
|
829
|
+
// Overall timeout with helpful error for Windows firewall
|
|
677
830
|
setTimeout(() => {
|
|
678
|
-
if (!started) {
|
|
679
|
-
|
|
831
|
+
if (!started && !processExited) {
|
|
832
|
+
const isWindows = process.platform === 'win32';
|
|
833
|
+
const hint = isWindows
|
|
834
|
+
? '\n\nOn Windows, this may be caused by Windows Firewall blocking localhost connections.\nTry: netsh advfirewall firewall add rule name="pgserve" dir=in action=allow protocol=TCP localport=' + this.port
|
|
835
|
+
: '';
|
|
836
|
+
reject(new Error(`PostgreSQL startup timed out after 30s.${hint}\n\nOutput: ${startupOutput}`));
|
|
680
837
|
}
|
|
681
838
|
}, 30000);
|
|
682
839
|
});
|