pgserve 2.2.4 → 2.4.0
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 +5 -4
- package/bin/postgres-server.js +142 -631
- package/config/logrotate.d/pgserve +47 -0
- package/config/pgaudit.conf +31 -0
- package/package.json +2 -2
- package/scripts/test-npx.sh +32 -10
- package/src/cli-install.cjs +147 -77
- package/src/commands/uninstall.js +241 -0
- package/src/index.js +11 -44
- package/src/lib/admin-json.js +202 -0
- package/src/lib/pm2-args.js +119 -0
- package/src/lib/socket-dir.js +69 -0
- package/src/postgres.js +64 -5
- package/src/admin-client.js +0 -223
- package/src/audit.js +0 -168
- package/src/cluster.js +0 -654
- package/src/control-db.js +0 -330
- package/src/daemon-control.js +0 -468
- package/src/daemon-shared.js +0 -18
- package/src/daemon-tcp.js +0 -297
- package/src/daemon.js +0 -709
- package/src/dashboard.js +0 -217
- package/src/fingerprint.js +0 -479
- package/src/gc.js +0 -351
- package/src/pg-wire.js +0 -869
- package/src/protocol.js +0 -389
- package/src/restore.js +0 -574
- package/src/router.js +0 -546
- package/src/sdk.js +0 -137
- package/src/stats-collector.js +0 -453
- package/src/stats-dashboard.js +0 -401
- package/src/sync.js +0 -335
- package/src/tenancy.js +0 -75
- package/src/tokens.js +0 -102
package/bin/postgres-server.js
CHANGED
|
@@ -1,708 +1,219 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* pgserve
|
|
4
|
+
* pgserve — Embedded PostgreSQL Server (singleton, v2.4+)
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Direct postmaster supervision. The bun proxy data plane was removed in
|
|
7
|
+
* the `pgserve-singleton-no-proxy` wish (Group 2). All long-running
|
|
8
|
+
* lifecycles flow through the `postmaster` subcommand: pm2 (Tier A) and
|
|
9
|
+
* systemd-user / launchd (Tier B, separate wish) invoke this script with
|
|
10
|
+
* `pgserve postmaster --port <n> --data <dir> --socket-dir <dir>`. There
|
|
11
|
+
* is no router, no libpq protocol rewriting, no SO_PEERCRED-based
|
|
12
|
+
* startup-message rewriting, and no always-on daemon control socket.
|
|
13
|
+
*
|
|
14
|
+
* For one-off operations (`pgserve install`, `pgserve url`, …) see
|
|
15
|
+
* `bin/pgserve-wrapper.cjs` + `src/cli-install.cjs`.
|
|
8
16
|
*/
|
|
9
17
|
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import { startMultiTenantServer } from '../src/index.js';
|
|
14
|
-
import { startClusterServer } from '../src/cluster.js';
|
|
15
|
-
import { loadEffectiveConfig as loadAutopgConfig } from '../src/settings-loader.cjs';
|
|
16
|
-
import {
|
|
17
|
-
PgserveDaemon,
|
|
18
|
-
stopDaemon,
|
|
19
|
-
resolveControlSocketDir,
|
|
20
|
-
resolveControlSocketPath,
|
|
21
|
-
} from '../src/daemon.js';
|
|
22
|
-
import { createAdminClient, readAdminDiscovery } from '../src/admin-client.js';
|
|
23
|
-
import {
|
|
24
|
-
ensureMetaSchema,
|
|
25
|
-
addAllowedToken,
|
|
26
|
-
revokeAllowedToken,
|
|
27
|
-
} from '../src/control-db.js';
|
|
28
|
-
import { mintToken } from '../src/tokens.js';
|
|
29
|
-
import { audit, AUDIT_EVENTS } from '../src/audit.js';
|
|
30
|
-
|
|
31
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
import { PostgresManager } from '../src/postgres.js';
|
|
19
|
+
import { resolveSocketDir, ensureSocketDir } from '../src/lib/socket-dir.js';
|
|
20
|
+
import { createLogger } from '../src/logger.js';
|
|
32
21
|
|
|
33
|
-
// Global error handlers
|
|
34
|
-
process
|
|
22
|
+
// Global error handlers — surface unhandled rejections + uncaught errors
|
|
23
|
+
// loud so a process supervisor (pm2 / systemd-user / launchd) restarts the
|
|
24
|
+
// postmaster cleanly instead of leaving us silently stuck.
|
|
25
|
+
process.on('unhandledRejection', (reason) => {
|
|
35
26
|
console.error('Unhandled Promise Rejection:', reason);
|
|
36
27
|
});
|
|
37
|
-
|
|
38
28
|
process.on('uncaughtException', (error) => {
|
|
39
29
|
console.error('Uncaught Exception:', error);
|
|
40
30
|
process.exit(1);
|
|
41
31
|
});
|
|
42
32
|
|
|
43
|
-
// Parse CLI arguments — `pgserve daemon [stop]` is dispatched before the
|
|
44
|
-
// classic `pgserve [options]` parser so daemon-mode flags do not collide
|
|
45
|
-
// with router flags.
|
|
46
33
|
const args = process.argv.slice(2);
|
|
47
34
|
|
|
48
|
-
if (args[0] === '
|
|
49
|
-
await
|
|
35
|
+
if (args[0] === 'postmaster') {
|
|
36
|
+
await runPostmasterSubcommand(args.slice(1));
|
|
37
|
+
} else if (args[0] === 'serve') {
|
|
38
|
+
// Alias `serve` → `postmaster` for symmetry with the v2.3 alias surface.
|
|
39
|
+
// Operators land on the same direct-supervision path either way.
|
|
40
|
+
await runPostmasterSubcommand(args.slice(1));
|
|
41
|
+
} else {
|
|
42
|
+
printHelp();
|
|
43
|
+
process.exit(args.length === 0 || args[0] === '--help' || args[0] === 'help' ? 0 : 1);
|
|
50
44
|
}
|
|
51
45
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
46
|
+
/**
|
|
47
|
+
* `pgserve postmaster` — supervises an embedded PostgreSQL postmaster
|
|
48
|
+
* directly. No router, no bun proxy, no daemon control socket.
|
|
49
|
+
*
|
|
50
|
+
* The postmaster listens on the canonical Unix socket at
|
|
51
|
+
* `<socketDir>/.s.PGSQL.<port>` AND TCP `<port>` (postgres' default
|
|
52
|
+
* `listen_addresses = 'localhost'`). Operators connect with either:
|
|
53
|
+
*
|
|
54
|
+
* psql -h $XDG_RUNTIME_DIR/pgserve # Unix socket (no -p)
|
|
55
|
+
* psql -h 127.0.0.1 -p 5432 # canonical TCP
|
|
56
|
+
*/
|
|
57
|
+
async function runPostmasterSubcommand(postmasterArgs) {
|
|
58
|
+
const opts = parsePostmasterArgs(postmasterArgs);
|
|
59
|
+
const logger = createLogger({ level: opts.logLevel });
|
|
60
|
+
|
|
61
|
+
// Resolve and ensure the socket directory before postgres tries to bind.
|
|
62
|
+
// Surfaces "not writable" / "wrong owner" failures here with a clear
|
|
63
|
+
// diagnostic instead of leaving the operator to chase a libpq error.
|
|
64
|
+
let socketDir;
|
|
65
|
+
try {
|
|
66
|
+
socketDir = ensureSocketDir(opts.socketDir);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error(err.message);
|
|
72
69
|
process.exit(1);
|
|
73
70
|
}
|
|
74
71
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// `pgserve daemon` (long-running)
|
|
85
|
-
const opts = parseDaemonArgs(daemonArgs);
|
|
86
|
-
const daemon = new PgserveDaemon(opts);
|
|
72
|
+
const manager = new PostgresManager({
|
|
73
|
+
dataDir: opts.dataDir,
|
|
74
|
+
port: opts.port,
|
|
75
|
+
socketDir,
|
|
76
|
+
useRam: opts.useRam,
|
|
77
|
+
enablePgvector: opts.enablePgvector,
|
|
78
|
+
logger: logger.child({ component: 'postgres' }),
|
|
79
|
+
});
|
|
87
80
|
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
console.error(
|
|
95
|
-
`pgserve daemon: postgres backend exited unexpectedly (code=${code}); ` +
|
|
96
|
-
`the wrapper is exiting so a process supervisor can restart it.`
|
|
81
|
+
// Surface unexpected backend death so pm2 can restart us cleanly.
|
|
82
|
+
// Mirrors the daemon-mode contract from PR #49 (issue #45).
|
|
83
|
+
manager.on('backendDiedUnexpectedly', ({ code }) => {
|
|
84
|
+
logger.error(
|
|
85
|
+
{ code },
|
|
86
|
+
'pgserve postmaster: postgres backend exited unexpectedly; exiting so the supervisor can restart',
|
|
97
87
|
);
|
|
98
88
|
process.exit(1);
|
|
99
89
|
});
|
|
100
90
|
|
|
101
91
|
try {
|
|
102
|
-
await
|
|
92
|
+
await manager.start();
|
|
103
93
|
} catch (err) {
|
|
104
|
-
|
|
105
|
-
console.error(`pgserve daemon: already running, pid ${err.pid}`);
|
|
106
|
-
process.exit(1);
|
|
107
|
-
}
|
|
108
|
-
console.error('pgserve daemon: failed to start:', err.message);
|
|
94
|
+
logger.error({ err: err.message }, 'pgserve postmaster: failed to start');
|
|
109
95
|
process.exit(1);
|
|
110
96
|
}
|
|
111
|
-
const dir = resolveControlSocketDir();
|
|
112
|
-
console.log(`
|
|
113
|
-
pgserve daemon — singleton mode
|
|
114
|
-
|
|
115
|
-
Control socket: ${resolveControlSocketPath(dir)}
|
|
116
|
-
PID lock: ${path.join(dir, 'pgserve.pid')}
|
|
117
|
-
PG socket: ${daemon.pgManager.getSocketPath() || '(TCP fallback)'}
|
|
118
97
|
|
|
119
|
-
|
|
98
|
+
logger.info(
|
|
99
|
+
{ port: opts.port, socketDir, dataDir: opts.dataDir },
|
|
100
|
+
'pgserve postmaster: ready (Unix socket + TCP)',
|
|
101
|
+
);
|
|
120
102
|
|
|
121
|
-
|
|
122
|
-
|
|
103
|
+
const shutdown = async (signal) => {
|
|
104
|
+
logger.info({ signal }, 'pgserve postmaster: stopping');
|
|
105
|
+
try {
|
|
106
|
+
await manager.stop();
|
|
107
|
+
} catch (err) {
|
|
108
|
+
logger.warn({ err: err.message }, 'pgserve postmaster: error during shutdown');
|
|
109
|
+
}
|
|
110
|
+
process.exit(0);
|
|
111
|
+
};
|
|
112
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
113
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
123
114
|
|
|
124
|
-
//
|
|
115
|
+
// Park forever; the supervisor decides when to stop us.
|
|
125
116
|
await new Promise(() => {});
|
|
126
117
|
}
|
|
127
118
|
|
|
128
|
-
function
|
|
119
|
+
function parsePostmasterArgs(postmasterArgs) {
|
|
129
120
|
const opts = {
|
|
130
|
-
|
|
131
|
-
|
|
121
|
+
port: 5432,
|
|
122
|
+
dataDir: null,
|
|
123
|
+
socketDir: undefined, // resolved via resolveSocketDir() if unset
|
|
132
124
|
logLevel: 'info',
|
|
133
|
-
|
|
134
|
-
tcpListens: [],
|
|
125
|
+
useRam: false,
|
|
135
126
|
enablePgvector: false,
|
|
136
|
-
maxConnections: null,
|
|
137
127
|
};
|
|
138
|
-
for (let i = 0; i <
|
|
139
|
-
const arg =
|
|
128
|
+
for (let i = 0; i < postmasterArgs.length; i++) {
|
|
129
|
+
const arg = postmasterArgs[i];
|
|
140
130
|
switch (arg) {
|
|
131
|
+
case '--port':
|
|
132
|
+
case '-p': {
|
|
133
|
+
const raw = postmasterArgs[++i];
|
|
134
|
+
const parsed = Number.parseInt(raw, 10);
|
|
135
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
136
|
+
console.error(`pgserve postmaster: invalid --port "${raw}"`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
opts.port = parsed;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
141
142
|
case '--data':
|
|
142
143
|
case '-d':
|
|
143
|
-
opts.
|
|
144
|
+
opts.dataDir = postmasterArgs[++i];
|
|
144
145
|
break;
|
|
145
|
-
case '--
|
|
146
|
-
|
|
146
|
+
case '--socket-dir':
|
|
147
|
+
case '-k':
|
|
148
|
+
opts.socketDir = postmasterArgs[++i];
|
|
147
149
|
break;
|
|
148
150
|
case '--log':
|
|
149
151
|
case '-l':
|
|
150
|
-
opts.logLevel =
|
|
151
|
-
break;
|
|
152
|
-
case '--no-provision':
|
|
153
|
-
opts.autoProvision = false;
|
|
152
|
+
opts.logLevel = postmasterArgs[++i];
|
|
154
153
|
break;
|
|
155
|
-
case '--
|
|
156
|
-
opts.
|
|
154
|
+
case '--ram':
|
|
155
|
+
opts.useRam = true;
|
|
157
156
|
break;
|
|
158
157
|
case '--pgvector':
|
|
159
158
|
opts.enablePgvector = true;
|
|
160
159
|
break;
|
|
161
|
-
case '--max-connections': {
|
|
162
|
-
// Accept the same flag the foreground/router mode takes so callers
|
|
163
|
-
// (genie's `getOrStartDaemon`, anything that spawns `pgserve daemon`
|
|
164
|
-
// with a tuned cap) can override the postmaster's `max_connections`.
|
|
165
|
-
// The `PgserveDaemon` constructor already honors `options.maxConnections`
|
|
166
|
-
// (see src/daemon.js — defaults to 1000); we just plumb it through.
|
|
167
|
-
const raw = daemonArgs[++i];
|
|
168
|
-
const parsed = Number.parseInt(raw, 10);
|
|
169
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
170
|
-
console.error(`--max-connections: expected a positive integer, got "${raw}"`);
|
|
171
|
-
process.exit(1);
|
|
172
|
-
}
|
|
173
|
-
opts.maxConnections = parsed;
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
160
|
case '--help':
|
|
177
|
-
|
|
178
|
-
pgserve
|
|
161
|
+
process.stdout.write(`
|
|
162
|
+
pgserve postmaster — direct embedded PostgreSQL supervision (singleton v2.4)
|
|
179
163
|
|
|
180
164
|
USAGE:
|
|
181
|
-
pgserve
|
|
182
|
-
pgserve daemon stop
|
|
183
|
-
pgserve daemon issue-token --fingerprint <hex>
|
|
184
|
-
pgserve daemon revoke-token <id>
|
|
165
|
+
pgserve postmaster [options]
|
|
185
166
|
|
|
186
167
|
OPTIONS:
|
|
187
|
-
--
|
|
188
|
-
--
|
|
189
|
-
--
|
|
190
|
-
--
|
|
191
|
-
--
|
|
192
|
-
--pgvector
|
|
193
|
-
--
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
TCP peers (--listen) MUST authenticate via libpq application_name shaped
|
|
200
|
-
"?fingerprint=<12hex>&token=<bearer>". Issue tokens with
|
|
201
|
-
"pgserve daemon issue-token --fingerprint <hex>". Revoke with
|
|
202
|
-
"pgserve daemon revoke-token <id>".
|
|
168
|
+
--port, -p <n> TCP port (default: 5432)
|
|
169
|
+
--data, -d <path> Data directory (required for persistence)
|
|
170
|
+
--socket-dir, -k <p> Unix socket dir (default: $XDG_RUNTIME_DIR/pgserve)
|
|
171
|
+
--log, -l <level> Log level: error|warn|info|debug (default: info)
|
|
172
|
+
--ram Use /dev/shm (Linux only)
|
|
173
|
+
--pgvector Auto-enable pgvector on new databases
|
|
174
|
+
--help Show this help
|
|
175
|
+
|
|
176
|
+
The postmaster binds <socket-dir>/.s.PGSQL.<port> and TCP <port> on
|
|
177
|
+
localhost. This entry point is invoked by pm2/systemd-user/launchd; it has
|
|
178
|
+
no router, no bun proxy, no daemon control socket.
|
|
203
179
|
`);
|
|
204
180
|
process.exit(0);
|
|
205
181
|
// falls through (unreachable)
|
|
206
182
|
default:
|
|
207
183
|
if (arg.startsWith('-')) {
|
|
208
|
-
console.error(`
|
|
184
|
+
console.error(`pgserve postmaster: unknown option "${arg}"`);
|
|
209
185
|
process.exit(1);
|
|
210
186
|
}
|
|
211
187
|
}
|
|
212
188
|
}
|
|
189
|
+
if (opts.socketDir === undefined) opts.socketDir = resolveSocketDir();
|
|
213
190
|
return opts;
|
|
214
191
|
}
|
|
215
192
|
|
|
216
|
-
async function runIssueTokenSubcommand(args) {
|
|
217
|
-
let fingerprint = null;
|
|
218
|
-
for (let i = 0; i < args.length; i++) {
|
|
219
|
-
const arg = args[i];
|
|
220
|
-
if (arg === '--fingerprint') fingerprint = args[++i];
|
|
221
|
-
else if (arg === '--help') {
|
|
222
|
-
console.log(`
|
|
223
|
-
pgserve daemon issue-token --fingerprint <12hex>
|
|
224
|
-
|
|
225
|
-
Issues a fresh bearer token for an existing fingerprint. Prints the token
|
|
226
|
-
to stdout exactly once; only the sha256 hash is persisted. Use the printed
|
|
227
|
-
value in libpq application_name shaped "?fingerprint=<hex>&token=<bearer>".
|
|
228
|
-
`);
|
|
229
|
-
process.exit(0);
|
|
230
|
-
} else {
|
|
231
|
-
console.error(`Unknown option: ${arg}`);
|
|
232
|
-
process.exit(1);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
if (!fingerprint || !/^[0-9a-f]{12}$/.test(fingerprint)) {
|
|
236
|
-
console.error('issue-token: --fingerprint <12hex> required');
|
|
237
|
-
process.exit(1);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
let admin;
|
|
241
|
-
try {
|
|
242
|
-
const dir = resolveControlSocketDir();
|
|
243
|
-
const disc = readAdminDiscovery(dir);
|
|
244
|
-
admin = await createAdminClient({ socketDir: disc.socketDir, port: disc.port });
|
|
245
|
-
} catch (err) {
|
|
246
|
-
console.error('issue-token: cannot reach running daemon admin socket:', err.message);
|
|
247
|
-
console.error('Hint: start the daemon first with `pgserve daemon`.');
|
|
248
|
-
process.exit(1);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
try {
|
|
252
|
-
await ensureMetaSchema(admin);
|
|
253
|
-
const { id, cleartext, hash } = mintToken();
|
|
254
|
-
const result = await addAllowedToken(admin, {
|
|
255
|
-
fingerprint,
|
|
256
|
-
tokenId: id,
|
|
257
|
-
tokenHash: hash,
|
|
258
|
-
});
|
|
259
|
-
audit(AUDIT_EVENTS.TCP_TOKEN_ISSUED, {
|
|
260
|
-
fingerprint,
|
|
261
|
-
token_id: id,
|
|
262
|
-
database: result.databaseName,
|
|
263
|
-
});
|
|
264
|
-
console.log('Token issued. Save the bearer value below — it will not be shown again:');
|
|
265
|
-
console.log('');
|
|
266
|
-
console.log(` id: ${id}`);
|
|
267
|
-
console.log(` fingerprint: ${fingerprint}`);
|
|
268
|
-
console.log(` database: ${result.databaseName}`);
|
|
269
|
-
console.log(` token: ${cleartext}`);
|
|
270
|
-
console.log('');
|
|
271
|
-
console.log('Use as libpq application_name:');
|
|
272
|
-
console.log(` application_name='?fingerprint=${fingerprint}&token=${cleartext}'`);
|
|
273
|
-
process.exit(0);
|
|
274
|
-
} catch (err) {
|
|
275
|
-
if (err.code === 'EUNKNOWNFINGERPRINT') {
|
|
276
|
-
console.error(`issue-token: fingerprint ${fingerprint} not provisioned yet.`);
|
|
277
|
-
console.error('Connect once via Unix socket so pgserve creates the database first.');
|
|
278
|
-
process.exit(2);
|
|
279
|
-
}
|
|
280
|
-
console.error('issue-token failed:', err.message);
|
|
281
|
-
process.exit(1);
|
|
282
|
-
} finally {
|
|
283
|
-
try { await admin.end(); } catch { /* swallow */ }
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async function runRevokeTokenSubcommand(args) {
|
|
288
|
-
if (args.length === 0 || args[0] === '--help') {
|
|
289
|
-
console.log('Usage: pgserve daemon revoke-token <id>');
|
|
290
|
-
process.exit(args.length === 0 ? 1 : 0);
|
|
291
|
-
}
|
|
292
|
-
const tokenId = args[0];
|
|
293
|
-
|
|
294
|
-
let admin;
|
|
295
|
-
try {
|
|
296
|
-
const dir = resolveControlSocketDir();
|
|
297
|
-
const disc = readAdminDiscovery(dir);
|
|
298
|
-
admin = await createAdminClient({ socketDir: disc.socketDir, port: disc.port });
|
|
299
|
-
} catch (err) {
|
|
300
|
-
console.error('revoke-token: cannot reach running daemon admin socket:', err.message);
|
|
301
|
-
process.exit(1);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
try {
|
|
305
|
-
const affected = await revokeAllowedToken(admin, tokenId);
|
|
306
|
-
if (affected === 0) {
|
|
307
|
-
console.error(`revoke-token: no token with id ${tokenId} found`);
|
|
308
|
-
process.exit(2);
|
|
309
|
-
}
|
|
310
|
-
console.log(`Token ${tokenId} revoked (affected ${affected} row${affected === 1 ? '' : 's'})`);
|
|
311
|
-
process.exit(0);
|
|
312
|
-
} catch (err) {
|
|
313
|
-
console.error('revoke-token failed:', err.message);
|
|
314
|
-
process.exit(1);
|
|
315
|
-
} finally {
|
|
316
|
-
try { await admin.end(); } catch { /* swallow */ }
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Print usage help
|
|
322
|
-
*/
|
|
323
193
|
function printHelp() {
|
|
324
|
-
|
|
325
|
-
pgserve
|
|
326
|
-
|
|
194
|
+
process.stdout.write(`
|
|
195
|
+
pgserve — Embedded PostgreSQL Server (singleton, v2.4+)
|
|
196
|
+
=======================================================
|
|
327
197
|
|
|
328
|
-
True concurrent connections, zero config, auto-provision databases.
|
|
198
|
+
True concurrent connections, zero config, auto-provision databases. The
|
|
199
|
+
postmaster is supervised directly (pm2 Tier A, systemd-user / launchd Tier
|
|
200
|
+
B); there is no bun proxy or daemon control socket.
|
|
329
201
|
|
|
330
202
|
USAGE:
|
|
331
|
-
pgserve [
|
|
332
|
-
pgserve
|
|
333
|
-
pgserve
|
|
334
|
-
pgserve
|
|
335
|
-
pgserve
|
|
336
|
-
pgserve
|
|
337
|
-
pgserve
|
|
338
|
-
pgserve
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
--data <path> Data directory for persistence (default: in-memory)
|
|
343
|
-
--ram Use RAM storage via /dev/shm (Linux only, faster)
|
|
344
|
-
--host <host> Host to bind to (default: 127.0.0.1)
|
|
345
|
-
--log <level> Log level: error, warn, info, debug (default: info)
|
|
346
|
-
--cluster Force cluster mode (auto-enabled on multi-core systems)
|
|
347
|
-
--no-cluster Force single-process mode (disables auto-cluster)
|
|
348
|
-
--workers <n> Number of worker processes (default: CPU cores)
|
|
349
|
-
--no-provision Disable auto-provisioning of databases
|
|
350
|
-
--sync-to <url> Sync to real PostgreSQL (async replication)
|
|
351
|
-
--sync-databases Database patterns to sync (comma-separated, e.g. "myapp,tenant_*")
|
|
352
|
-
--no-stats Disable real-time stats dashboard (enabled by default)
|
|
353
|
-
--max-connections Max concurrent connections (default: 1000)
|
|
354
|
-
--pgvector Auto-enable pgvector extension on new databases
|
|
355
|
-
--help Show this help message
|
|
356
|
-
|
|
357
|
-
MODES:
|
|
358
|
-
In-memory (default): Ephemeral temp directory - data lost on restart
|
|
359
|
-
RAM mode (--ram): True RAM via /dev/shm (Linux only, fastest)
|
|
360
|
-
Persistent: Use --data to persist databases to disk
|
|
361
|
-
|
|
362
|
-
EXAMPLES:
|
|
363
|
-
# Start in memory mode (default, fast, ephemeral)
|
|
364
|
-
pgserve
|
|
365
|
-
|
|
366
|
-
# Start with persistent storage
|
|
367
|
-
pgserve --data ./data
|
|
368
|
-
|
|
369
|
-
# Custom port
|
|
370
|
-
pgserve --port 5433
|
|
371
|
-
|
|
372
|
-
# Sync to real PostgreSQL (async replication)
|
|
373
|
-
pgserve --sync-to "postgresql://user:pass@host:5432/db"
|
|
374
|
-
|
|
375
|
-
# Sync specific databases
|
|
376
|
-
pgserve --sync-to "postgresql://..." --sync-databases "myapp,tenant_*"
|
|
203
|
+
pgserve install [--port N] [--no-pm2] # one-shot register under pm2
|
|
204
|
+
pgserve uninstall # one-shot tear-down
|
|
205
|
+
pgserve url | port | status [--json] # discovery / health
|
|
206
|
+
pgserve config <subcommand> # ~/.autopg/settings.json
|
|
207
|
+
pgserve restart # pm2 restart
|
|
208
|
+
pgserve ui # autopg console UI
|
|
209
|
+
pgserve postmaster [options] # long-running supervisor entry
|
|
210
|
+
pgserve serve [options] # alias for \`postmaster\`
|
|
211
|
+
|
|
212
|
+
For postmaster options: pgserve postmaster --help
|
|
213
|
+
For install options: pgserve install --help (-> handled by wrapper)
|
|
377
214
|
|
|
378
215
|
CONNECTING:
|
|
379
|
-
#
|
|
380
|
-
|
|
381
|
-
postgresql://localhost:5432/app123 # Auto-creates "app123" database
|
|
382
|
-
|
|
383
|
-
FEATURES:
|
|
384
|
-
- TRUE concurrent connections (native PostgreSQL)
|
|
385
|
-
- Auto-provision databases on first connection
|
|
386
|
-
- Zero configuration required
|
|
387
|
-
- PostgreSQL 17 (native binaries, auto-downloaded)
|
|
388
|
-
`);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Pull daemon options from ~/.autopg/settings.json (with env overlay).
|
|
393
|
-
* Returns a partial options patch — only keys that are present in the
|
|
394
|
-
* settings file or env override the hardcoded defaults. CLI flags layer
|
|
395
|
-
* on top of this in parseArgs().
|
|
396
|
-
*
|
|
397
|
-
* Failures (missing file, bad JSON) fall through to defaults silently —
|
|
398
|
-
* the daemon must remain runnable on a brand-new install before
|
|
399
|
-
* `autopg config init` has been called.
|
|
400
|
-
*/
|
|
401
|
-
function loadSettingsOverlay() {
|
|
402
|
-
try {
|
|
403
|
-
const cpuCount = os.cpus().length;
|
|
404
|
-
const isWindows = os.platform() === 'win32';
|
|
405
|
-
const { settings } = loadAutopgConfig();
|
|
406
|
-
const s = settings.server || {};
|
|
407
|
-
const r = settings.runtime || {};
|
|
408
|
-
const sy = settings.sync || {};
|
|
409
|
-
const pg = settings.postgres || {};
|
|
410
|
-
const overlay = {};
|
|
411
|
-
if (typeof s.port === 'number') overlay.port = s.port;
|
|
412
|
-
if (typeof s.host === 'string' && s.host) overlay.host = s.host;
|
|
413
|
-
if (typeof r.dataDir === 'string' && r.dataDir) overlay.dataDir = r.dataDir;
|
|
414
|
-
if (typeof r.ramMode === 'boolean') overlay.useRam = r.ramMode;
|
|
415
|
-
if (typeof r.logLevel === 'string' && r.logLevel) overlay.logLevel = r.logLevel;
|
|
416
|
-
if (typeof r.autoProvision === 'boolean') overlay.autoProvision = r.autoProvision;
|
|
417
|
-
if (typeof r.cluster === 'string') {
|
|
418
|
-
overlay.cluster = r.cluster === 'auto'
|
|
419
|
-
? (cpuCount > 1 && !isWindows)
|
|
420
|
-
: r.cluster === 'on';
|
|
421
|
-
}
|
|
422
|
-
if (typeof r.workers === 'number' && r.workers > 0) overlay.workers = r.workers;
|
|
423
|
-
if (typeof r.statsDashboard === 'boolean') overlay.showStats = r.statsDashboard;
|
|
424
|
-
if (typeof r.enablePgvector === 'boolean') overlay.enablePgvector = r.enablePgvector;
|
|
425
|
-
if (sy.enabled && typeof sy.url === 'string' && sy.url) overlay.syncTo = sy.url;
|
|
426
|
-
if (sy.enabled && typeof sy.databases === 'string' && sy.databases) overlay.syncDatabases = sy.databases;
|
|
427
|
-
// pgserve-side connection cap mirrors the postgres GUC unless the user
|
|
428
|
-
// has explicitly diverged via CLI flag (handled in parseArgs).
|
|
429
|
-
if (typeof pg.max_connections === 'number') overlay.maxConnections = pg.max_connections;
|
|
430
|
-
return overlay;
|
|
431
|
-
} catch {
|
|
432
|
-
// First run, no settings.json yet, or file parse error. Hardcoded
|
|
433
|
-
// defaults still produce a working daemon — nothing to do here.
|
|
434
|
-
return {};
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Parse command line arguments
|
|
440
|
-
*
|
|
441
|
-
* Precedence (lowest → highest):
|
|
442
|
-
* 1. hardcoded defaults
|
|
443
|
-
* 2. ~/.autopg/settings.json (with env overlay via loadEffectiveConfig)
|
|
444
|
-
* 3. CLI flags ← explicit user intent always wins
|
|
445
|
-
*/
|
|
446
|
-
function parseArgs() {
|
|
447
|
-
// Auto-enable cluster mode on multi-core systems for best performance
|
|
448
|
-
// Note: Cluster mode uses SO_REUSEPORT which is not supported on Windows
|
|
449
|
-
const cpuCount = os.cpus().length;
|
|
450
|
-
const isWindows = os.platform() === 'win32';
|
|
451
|
-
|
|
452
|
-
const options = {
|
|
453
|
-
port: 8432,
|
|
454
|
-
host: '127.0.0.1',
|
|
455
|
-
dataDir: null, // null = memory mode
|
|
456
|
-
useRam: false, // Use /dev/shm for true RAM storage (Linux only)
|
|
457
|
-
logLevel: 'info',
|
|
458
|
-
autoProvision: true,
|
|
459
|
-
cluster: cpuCount > 1 && !isWindows, // Auto-enable on multi-core (disabled on Windows - no SO_REUSEPORT)
|
|
460
|
-
workers: null, // null = use CPU count
|
|
461
|
-
syncTo: null, // Sync target PostgreSQL URL
|
|
462
|
-
syncDatabases: null, // Database patterns to sync (comma-separated)
|
|
463
|
-
showStats: true, // Show real-time stats dashboard (default: enabled)
|
|
464
|
-
maxConnections: 1000, // Max concurrent connections (high default for multi-tenant)
|
|
465
|
-
enablePgvector: false // Auto-enable pgvector extension on new databases
|
|
466
|
-
};
|
|
467
|
-
|
|
468
|
-
// Layer settings.json + env on top of defaults. CLI flags below win.
|
|
469
|
-
Object.assign(options, loadSettingsOverlay());
|
|
470
|
-
|
|
471
|
-
for (let i = 0; i < args.length; i++) {
|
|
472
|
-
const arg = args[i];
|
|
473
|
-
|
|
474
|
-
switch (arg) {
|
|
475
|
-
case '--port':
|
|
476
|
-
case '-p':
|
|
477
|
-
options.port = parseInt(args[++i], 10);
|
|
478
|
-
break;
|
|
479
|
-
|
|
480
|
-
case '--data':
|
|
481
|
-
case '-d':
|
|
482
|
-
options.dataDir = args[++i];
|
|
483
|
-
break;
|
|
484
|
-
|
|
485
|
-
case '--ram':
|
|
486
|
-
options.useRam = true;
|
|
487
|
-
break;
|
|
488
|
-
|
|
489
|
-
case '--host':
|
|
490
|
-
case '-h':
|
|
491
|
-
options.host = args[++i];
|
|
492
|
-
break;
|
|
493
|
-
|
|
494
|
-
case '--log':
|
|
495
|
-
case '-l':
|
|
496
|
-
options.logLevel = args[++i];
|
|
497
|
-
break;
|
|
498
|
-
|
|
499
|
-
case '--cluster':
|
|
500
|
-
options.cluster = true;
|
|
501
|
-
break;
|
|
502
|
-
|
|
503
|
-
case '--no-cluster':
|
|
504
|
-
options.cluster = false;
|
|
505
|
-
break;
|
|
506
|
-
|
|
507
|
-
case '--workers':
|
|
508
|
-
options.workers = parseInt(args[++i], 10);
|
|
509
|
-
break;
|
|
510
|
-
|
|
511
|
-
case '--no-provision':
|
|
512
|
-
options.autoProvision = false;
|
|
513
|
-
break;
|
|
514
|
-
|
|
515
|
-
case '--sync-to':
|
|
516
|
-
options.syncTo = args[++i];
|
|
517
|
-
break;
|
|
518
|
-
|
|
519
|
-
case '--sync-databases':
|
|
520
|
-
options.syncDatabases = args[++i];
|
|
521
|
-
break;
|
|
522
|
-
|
|
523
|
-
case '--stats':
|
|
524
|
-
options.showStats = true;
|
|
525
|
-
break;
|
|
526
|
-
|
|
527
|
-
case '--no-stats':
|
|
528
|
-
options.showStats = false;
|
|
529
|
-
break;
|
|
530
|
-
|
|
531
|
-
case '--max-connections':
|
|
532
|
-
options.maxConnections = parseInt(args[++i], 10);
|
|
533
|
-
break;
|
|
534
|
-
|
|
535
|
-
case '--pgvector':
|
|
536
|
-
options.enablePgvector = true;
|
|
537
|
-
break;
|
|
538
|
-
|
|
539
|
-
case '--help':
|
|
540
|
-
case 'help':
|
|
541
|
-
printHelp();
|
|
542
|
-
process.exit(0);
|
|
543
|
-
// falls through (unreachable - exit above)
|
|
544
|
-
|
|
545
|
-
default:
|
|
546
|
-
if (arg.startsWith('-')) {
|
|
547
|
-
console.error(`Unknown option: ${arg}`);
|
|
548
|
-
printHelp();
|
|
549
|
-
process.exit(1);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
return options;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* Main entry point
|
|
559
|
-
*/
|
|
560
|
-
async function main() {
|
|
561
|
-
const options = parseArgs();
|
|
562
|
-
const memoryMode = !options.dataDir;
|
|
563
|
-
const storageType = options.dataDir
|
|
564
|
-
? options.dataDir
|
|
565
|
-
: (options.useRam ? '/dev/shm (RAM)' : '(temp directory)');
|
|
566
|
-
|
|
567
|
-
// Only print header if not a cluster worker (workers get PGSERVE_WORKER env)
|
|
568
|
-
if (!process.env.PGSERVE_WORKER) {
|
|
569
|
-
console.log(`
|
|
570
|
-
pgserve - Embedded PostgreSQL Server
|
|
571
|
-
=====================================
|
|
572
|
-
`);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
try {
|
|
576
|
-
let server;
|
|
577
|
-
|
|
578
|
-
if (options.cluster) {
|
|
579
|
-
// Cluster mode - multi-core scaling
|
|
580
|
-
server = await startClusterServer({
|
|
581
|
-
port: options.port,
|
|
582
|
-
host: options.host,
|
|
583
|
-
baseDir: options.dataDir,
|
|
584
|
-
useRam: options.useRam,
|
|
585
|
-
logLevel: options.logLevel,
|
|
586
|
-
autoProvision: options.autoProvision,
|
|
587
|
-
workers: options.workers,
|
|
588
|
-
maxConnections: options.maxConnections,
|
|
589
|
-
enablePgvector: options.enablePgvector
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
// Only primary process shows full startup message
|
|
593
|
-
if (server.workers) {
|
|
594
|
-
const stats = server.getStats();
|
|
595
|
-
|
|
596
|
-
console.log(`
|
|
597
|
-
Cluster started successfully!
|
|
598
|
-
|
|
599
|
-
Endpoint: postgresql://${options.host}:${options.port}/<database>
|
|
600
|
-
Mode: ${memoryMode ? (options.useRam ? 'RAM (/dev/shm)' : 'Ephemeral (temp)') : 'Persistent'} (Cluster)
|
|
601
|
-
Workers: ${stats.workers} processes
|
|
602
|
-
Data: ${storageType}
|
|
603
|
-
Auto-create: ${options.autoProvision ? 'Enabled' : 'Disabled'}
|
|
604
|
-
pgvector: ${options.enablePgvector ? 'Enabled (auto-installed on new DBs)' : 'Disabled (use --pgvector to enable)'}
|
|
605
|
-
|
|
606
|
-
Examples:
|
|
607
|
-
postgresql://${options.host}:${options.port}/myapp
|
|
608
|
-
postgresql://${options.host}:${options.port}/testdb
|
|
609
|
-
|
|
610
|
-
Press Ctrl+C to stop
|
|
216
|
+
postgres://localhost:5432/mydb # canonical TCP
|
|
217
|
+
psql -h $XDG_RUNTIME_DIR/pgserve mydb # Unix socket
|
|
611
218
|
`);
|
|
612
|
-
}
|
|
613
|
-
} else {
|
|
614
|
-
// Single process mode
|
|
615
|
-
const router = await startMultiTenantServer({
|
|
616
|
-
port: options.port,
|
|
617
|
-
host: options.host,
|
|
618
|
-
baseDir: options.dataDir,
|
|
619
|
-
useRam: options.useRam,
|
|
620
|
-
logLevel: options.logLevel,
|
|
621
|
-
autoProvision: options.autoProvision,
|
|
622
|
-
syncTo: options.syncTo,
|
|
623
|
-
syncDatabases: options.syncDatabases,
|
|
624
|
-
maxConnections: options.maxConnections,
|
|
625
|
-
enablePgvector: options.enablePgvector
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
server = router;
|
|
629
|
-
|
|
630
|
-
// Build sync status string
|
|
631
|
-
const syncStatus = options.syncTo
|
|
632
|
-
? `Enabled → ${options.syncTo.replace(/:[^:@]+@/, ':***@')}`
|
|
633
|
-
: 'Disabled';
|
|
634
|
-
|
|
635
|
-
console.log(`
|
|
636
|
-
Server started successfully!
|
|
637
|
-
|
|
638
|
-
Endpoint: postgresql://${options.host}:${options.port}/<database>
|
|
639
|
-
Mode: ${memoryMode ? (options.useRam ? 'RAM (/dev/shm)' : 'Ephemeral (temp)') : 'Persistent'}
|
|
640
|
-
Data: ${storageType}
|
|
641
|
-
PostgreSQL: Port ${router.pgPort} (internal)
|
|
642
|
-
Auto-create: ${options.autoProvision ? 'Enabled' : 'Disabled'}
|
|
643
|
-
pgvector: ${options.enablePgvector ? 'Enabled (auto-installed on new DBs)' : 'Disabled (use --pgvector to enable)'}
|
|
644
|
-
Sync: ${syncStatus}${options.syncDatabases ? ` (${options.syncDatabases})` : ''}
|
|
645
|
-
|
|
646
|
-
Examples:
|
|
647
|
-
postgresql://${options.host}:${options.port}/myapp
|
|
648
|
-
postgresql://${options.host}:${options.port}/testdb
|
|
649
|
-
|
|
650
|
-
Press Ctrl+C to stop
|
|
651
|
-
`);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// Start stats dashboard if requested (only for primary/single-process)
|
|
655
|
-
let dashboard = null;
|
|
656
|
-
if (options.showStats && !process.env.PGSERVE_WORKER) {
|
|
657
|
-
const { StatsDashboard } = await import('../src/stats-dashboard.js');
|
|
658
|
-
const { StatsCollector } = await import('../src/stats-collector.js');
|
|
659
|
-
|
|
660
|
-
// Create stats collector with appropriate sources
|
|
661
|
-
const collector = new StatsCollector({
|
|
662
|
-
router: options.cluster ? null : server,
|
|
663
|
-
pgManager: server.pgManager,
|
|
664
|
-
clusterStats: options.cluster ? () => server.getStats() : null,
|
|
665
|
-
logger: server.logger,
|
|
666
|
-
port: options.port,
|
|
667
|
-
host: options.host
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
dashboard = new StatsDashboard({
|
|
671
|
-
refreshInterval: 2000, // 2 second refresh for real-time feel
|
|
672
|
-
statsProvider: () => collector.collect()
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
dashboard.start();
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
// Graceful shutdown (only for primary/single-process, workers handle via IPC)
|
|
679
|
-
if (!process.env.PGSERVE_WORKER) {
|
|
680
|
-
const shutdown = async () => {
|
|
681
|
-
// Stop dashboard first to restore cursor
|
|
682
|
-
if (dashboard) {
|
|
683
|
-
dashboard.stop();
|
|
684
|
-
}
|
|
685
|
-
console.log('\nShutting down...');
|
|
686
|
-
try {
|
|
687
|
-
await server.stop();
|
|
688
|
-
console.log('Server stopped.');
|
|
689
|
-
} catch (err) {
|
|
690
|
-
console.error('Error during shutdown:', err.message);
|
|
691
|
-
// Still exit - best effort cleanup
|
|
692
|
-
}
|
|
693
|
-
process.exit(0);
|
|
694
|
-
};
|
|
695
|
-
|
|
696
|
-
process.on('SIGINT', shutdown);
|
|
697
|
-
process.on('SIGTERM', shutdown);
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// Keep process alive
|
|
701
|
-
await new Promise(() => {});
|
|
702
|
-
} catch (error) {
|
|
703
|
-
console.error(`Failed to start server:`, error);
|
|
704
|
-
process.exit(1);
|
|
705
|
-
}
|
|
706
219
|
}
|
|
707
|
-
|
|
708
|
-
main();
|