pgserve 1.1.2 → 1.1.3-rc.2
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/bin/pgserve-wrapper.cjs +79 -8
- package/package.json +1 -1
- package/src/postgres.js +38 -1
package/bin/pgserve-wrapper.cjs
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* This wrapper enables `npx pgserve` to work without requiring
|
|
6
6
|
* users to install bun globally. The bun runtime is bundled as
|
|
7
7
|
* an npm dependency and this wrapper finds and invokes it.
|
|
8
|
+
*
|
|
9
|
+
* Windows EBUSY fix: Uses synchronous waiting and taskkill for
|
|
10
|
+
* reliable process termination and file handle cleanup.
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
|
-
const { spawn } = require('child_process');
|
|
13
|
+
const { spawn, execSync } = require('child_process');
|
|
11
14
|
const path = require('path');
|
|
12
15
|
const fs = require('fs');
|
|
13
16
|
|
|
@@ -53,6 +56,8 @@ if (!bunPath) {
|
|
|
53
56
|
const scriptPath = path.join(__dirname, 'pglite-server.js');
|
|
54
57
|
|
|
55
58
|
// Spawn bun with the actual script, inherit all stdio
|
|
59
|
+
// IMPORTANT: Do NOT use detached mode - wrapper must wait for child to fully terminate
|
|
60
|
+
// Using detached with stdio:'inherit' causes file handle inheritance issues on Windows
|
|
56
61
|
const child = spawn(bunPath, [scriptPath, ...process.argv.slice(2)], {
|
|
57
62
|
stdio: 'inherit',
|
|
58
63
|
windowsHide: true
|
|
@@ -63,19 +68,85 @@ child.on('error', (err) => {
|
|
|
63
68
|
process.exit(1);
|
|
64
69
|
});
|
|
65
70
|
|
|
66
|
-
|
|
71
|
+
// Safety timeout: force exit if 'close' event never fires after 'exit'
|
|
72
|
+
let forceExitTimeout = null;
|
|
73
|
+
|
|
74
|
+
child.on('exit', () => {
|
|
75
|
+
// Give 5 seconds for 'close' event after 'exit'
|
|
76
|
+
forceExitTimeout = setTimeout(() => {
|
|
77
|
+
console.error('Warning: Child process did not close cleanly, forcing exit');
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}, 5000);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Use 'close' event instead of 'exit' - fires AFTER all stdio streams are closed
|
|
83
|
+
// This is critical for Windows where file handles may remain locked after 'exit' fires
|
|
84
|
+
child.on('close', (code, signal) => {
|
|
85
|
+
// Clear the safety timeout
|
|
86
|
+
if (forceExitTimeout) {
|
|
87
|
+
clearTimeout(forceExitTimeout);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// On Windows, use SYNCHRONOUS delay to ensure all file handles are released
|
|
91
|
+
// This prevents EBUSY errors when npx tries to clean up the cache
|
|
92
|
+
// NOTE: async/await does NOT work in EventEmitter callbacks - Node ignores the Promise
|
|
93
|
+
if (isWindows) {
|
|
94
|
+
const delay = 200; // ms - enough for Windows kernel to release handles
|
|
95
|
+
const start = Date.now();
|
|
96
|
+
while (Date.now() - start < delay) {
|
|
97
|
+
// Synchronous busy-wait - actually blocks unlike async setTimeout
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
67
101
|
if (signal) {
|
|
68
|
-
|
|
102
|
+
// On Windows, can't reliably re-raise Unix signals
|
|
103
|
+
if (isWindows) {
|
|
104
|
+
process.exit(1);
|
|
105
|
+
} else {
|
|
106
|
+
process.kill(process.pid, signal);
|
|
107
|
+
}
|
|
69
108
|
} else {
|
|
70
109
|
process.exit(code ?? 0);
|
|
71
110
|
}
|
|
72
111
|
});
|
|
73
112
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
113
|
+
// Platform-specific signal handling
|
|
114
|
+
if (isWindows) {
|
|
115
|
+
// Windows: use taskkill for reliable process termination
|
|
116
|
+
// process.kill(pid, 'SIGINT') does NOT work properly on Windows
|
|
117
|
+
process.on('SIGINT', () => {
|
|
77
118
|
if (child.pid) {
|
|
78
|
-
|
|
119
|
+
try {
|
|
120
|
+
// /T = terminate child processes (tree), /F = force
|
|
121
|
+
execSync(`taskkill /PID ${child.pid} /T /F`, {
|
|
122
|
+
stdio: 'ignore',
|
|
123
|
+
windowsHide: true
|
|
124
|
+
});
|
|
125
|
+
} catch {
|
|
126
|
+
// Process may have already exited, ignore errors
|
|
127
|
+
}
|
|
79
128
|
}
|
|
80
129
|
});
|
|
81
|
-
|
|
130
|
+
|
|
131
|
+
// Handle Ctrl+C via readline for Windows terminal compatibility
|
|
132
|
+
// Some Windows terminals don't emit SIGINT properly
|
|
133
|
+
const readline = require('readline');
|
|
134
|
+
if (process.stdin.isTTY) {
|
|
135
|
+
const rl = readline.createInterface({
|
|
136
|
+
input: process.stdin,
|
|
137
|
+
output: process.stdout
|
|
138
|
+
});
|
|
139
|
+
rl.on('SIGINT', () => {
|
|
140
|
+
process.emit('SIGINT');
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
// Unix: forward signals to child process normally
|
|
145
|
+
['SIGINT', 'SIGTERM', 'SIGHUP'].forEach(signal => {
|
|
146
|
+
process.on(signal, () => {
|
|
147
|
+
if (child.pid) {
|
|
148
|
+
process.kill(child.pid, signal);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
package/package.json
CHANGED
package/src/postgres.js
CHANGED
|
@@ -308,6 +308,12 @@ export class PostgresManager {
|
|
|
308
308
|
// Initialize admin connection pool (for database creation operations)
|
|
309
309
|
await this._initAdminPool();
|
|
310
310
|
|
|
311
|
+
// For persistent mode, load existing databases into createdDatabases
|
|
312
|
+
// This prevents "database already exists" errors when reusing data directories
|
|
313
|
+
if (this.persistent) {
|
|
314
|
+
await this._loadExistingDatabases();
|
|
315
|
+
}
|
|
316
|
+
|
|
311
317
|
this.logger.info({
|
|
312
318
|
databaseDir: this.databaseDir,
|
|
313
319
|
port: this.port,
|
|
@@ -407,6 +413,33 @@ export class PostgresManager {
|
|
|
407
413
|
}, 'Admin connection pool initialized (Bun.sql)');
|
|
408
414
|
}
|
|
409
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Load existing databases into createdDatabases Set (for persistent mode)
|
|
418
|
+
* This allows pgserve to reuse existing data directories without
|
|
419
|
+
* attempting to CREATE DATABASE for databases that already exist.
|
|
420
|
+
*/
|
|
421
|
+
async _loadExistingDatabases() {
|
|
422
|
+
try {
|
|
423
|
+
const result = await this.adminPool`
|
|
424
|
+
SELECT datname FROM pg_database
|
|
425
|
+
WHERE datistemplate = false
|
|
426
|
+
AND datname NOT IN ('postgres', 'template0', 'template1')
|
|
427
|
+
`;
|
|
428
|
+
|
|
429
|
+
for (const row of result) {
|
|
430
|
+
this.createdDatabases.add(row.datname);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
this.logger.info({
|
|
434
|
+
databases: Array.from(this.createdDatabases),
|
|
435
|
+
count: this.createdDatabases.size
|
|
436
|
+
}, 'Loaded existing databases from persistent storage');
|
|
437
|
+
} catch (error) {
|
|
438
|
+
// Non-fatal - if we can't load existing DBs, createDatabase will handle it
|
|
439
|
+
this.logger.warn({ error: error.message }, 'Failed to load existing databases');
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
410
443
|
/**
|
|
411
444
|
* Start the PostgreSQL server process
|
|
412
445
|
* Uses Bun.spawn() for ~40% faster process startup
|
|
@@ -543,7 +576,11 @@ export class PostgresManager {
|
|
|
543
576
|
} catch (error) {
|
|
544
577
|
// Database might already exist (from previous persistent session or race condition)
|
|
545
578
|
// 42P04 = duplicate_database, 23505 = unique_violation
|
|
546
|
-
|
|
579
|
+
// Also check error.message for Bun.sql compatibility (may not expose SQLSTATE codes)
|
|
580
|
+
const isAlreadyExists = error.code === '42P04' ||
|
|
581
|
+
error.code === '23505' ||
|
|
582
|
+
error.message?.includes('already exists');
|
|
583
|
+
if (isAlreadyExists) {
|
|
547
584
|
this.createdDatabases.add(dbName);
|
|
548
585
|
this.logger.debug({ dbName }, 'Database already exists');
|
|
549
586
|
} else {
|