pgserve 1.1.3-rc.7 → 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 +20 -7
- 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 +142 -16
|
@@ -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
|
|
@@ -70,21 +70,34 @@ jobs:
|
|
|
70
70
|
- name: Install dependencies
|
|
71
71
|
run: bun install
|
|
72
72
|
|
|
73
|
-
# Windows: native build with
|
|
74
|
-
- name: Build for Windows (with
|
|
73
|
+
# Windows: native build with icon and proper metadata
|
|
74
|
+
- name: Build for Windows (with branding)
|
|
75
75
|
if: matrix.platform == 'windows-x64'
|
|
76
76
|
run: |
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
81
93
|
|
|
82
94
|
# Linux/macOS: native build
|
|
83
95
|
- name: Build for ${{ matrix.platform }}
|
|
84
96
|
if: matrix.platform != 'windows-x64'
|
|
85
97
|
run: |
|
|
86
98
|
mkdir -p dist
|
|
87
|
-
|
|
99
|
+
VERSION=$(node -p "require('./package.json').version")
|
|
100
|
+
bun build --compile --define BUILD_VERSION="'$VERSION'" bin/pglite-server.js --outfile dist/${{ matrix.output }}
|
|
88
101
|
ls -lh dist/
|
|
89
102
|
|
|
90
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
|
@@ -575,9 +575,11 @@ export class PostgresManager {
|
|
|
575
575
|
*/
|
|
576
576
|
async _initAdminPool() {
|
|
577
577
|
const { SQL } = await import('bun');
|
|
578
|
+
const isWindows = process.platform === 'win32';
|
|
578
579
|
|
|
579
580
|
// Bun.sql config - uses TCP connections (Unix sockets not directly supported)
|
|
580
581
|
// This is fine for admin queries (low volume, local connection)
|
|
582
|
+
// Windows needs longer timeout due to higher network stack latency
|
|
581
583
|
this.adminPool = new SQL({
|
|
582
584
|
hostname: '127.0.0.1',
|
|
583
585
|
port: this.port,
|
|
@@ -586,16 +588,38 @@ export class PostgresManager {
|
|
|
586
588
|
password: this.password,
|
|
587
589
|
max: 5, // Small pool - only for CREATE DATABASE operations
|
|
588
590
|
idleTimeout: 30,
|
|
589
|
-
connectionTimeout: 5,
|
|
591
|
+
connectionTimeout: isWindows ? 15 : 5,
|
|
590
592
|
});
|
|
591
593
|
|
|
592
|
-
// Verify connection
|
|
593
|
-
|
|
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
|
|
594
600
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
+
}
|
|
599
623
|
}
|
|
600
624
|
|
|
601
625
|
/**
|
|
@@ -667,8 +691,24 @@ export class PostgresManager {
|
|
|
667
691
|
|
|
668
692
|
let started = false;
|
|
669
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
|
|
702
|
+
|
|
703
|
+
const markReady = (method) => {
|
|
704
|
+
if (!started) {
|
|
705
|
+
started = true;
|
|
706
|
+
this.logger.info({ port: this.port, method }, 'PostgreSQL ready');
|
|
707
|
+
resolve();
|
|
708
|
+
}
|
|
709
|
+
};
|
|
670
710
|
|
|
671
|
-
// Read stderr in
|
|
711
|
+
// Read stderr - detect port binding in logs (locale-independent: just look for port number)
|
|
672
712
|
const readStream = async (stream) => {
|
|
673
713
|
const reader = stream.getReader();
|
|
674
714
|
const decoder = new TextDecoder();
|
|
@@ -680,11 +720,16 @@ export class PostgresManager {
|
|
|
680
720
|
startupOutput += message;
|
|
681
721
|
this.logger.debug({ pgOutput: message.trim() }, 'PostgreSQL output');
|
|
682
722
|
|
|
683
|
-
//
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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);
|
|
688
733
|
}
|
|
689
734
|
}
|
|
690
735
|
} catch {
|
|
@@ -698,16 +743,97 @@ export class PostgresManager {
|
|
|
698
743
|
|
|
699
744
|
// Handle process exit
|
|
700
745
|
this.process.exited.then((code) => {
|
|
746
|
+
processExited = true;
|
|
701
747
|
if (!started) {
|
|
702
748
|
reject(new Error(`PostgreSQL exited with code ${code} before starting: ${startupOutput}`));
|
|
703
749
|
}
|
|
704
750
|
this.process = null;
|
|
705
751
|
});
|
|
706
752
|
|
|
707
|
-
//
|
|
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
|
|
708
830
|
setTimeout(() => {
|
|
709
|
-
if (!started) {
|
|
710
|
-
|
|
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}`));
|
|
711
837
|
}
|
|
712
838
|
}, 30000);
|
|
713
839
|
});
|