jsql-neo 5.4.2 → 5.5.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/LICENSE +202 -202
- package/README.md +8097 -7971
- package/bin/jsql +320 -320
- package/bin/jsql-server +90 -90
- package/bin/jsql-server-control.js +177 -177
- package/index.d.ts +385 -357
- package/index.js +124 -118
- package/lib/ast.js +214 -0
- package/lib/btree.js +413 -413
- package/lib/cache.js +328 -328
- package/lib/client.js +125 -125
- package/lib/database.js +1744 -1744
- package/lib/date-types.js +182 -182
- package/lib/errors.js +89 -89
- package/lib/jsql_format.js +497 -497
- package/lib/migrate.js +254 -254
- package/lib/mod.js +292 -292
- package/lib/mongo_server.js +607 -607
- package/lib/multiserver.js +140 -140
- package/lib/mysql_compat.js +413 -413
- package/lib/mysql_server.js +1237 -1237
- package/lib/native_client.js +732 -732
- package/lib/nedb_compat.js +506 -506
- package/lib/pg_server.js +864 -864
- package/lib/plugin.js +34 -34
- package/lib/query.js +815 -815
- package/lib/redis_server.js +571 -571
- package/lib/sql.js +3460 -2925
- package/lib/sqlite_compat.js +323 -323
- package/lib/sqlite_worker.js +264 -264
- package/lib/table.js +1304 -1304
- package/lib/tui.js +502 -502
- package/lib/wasm.js +1 -1
- package/lib/wasm_client.js +718 -718
- package/lib/web_ui.js +257 -257
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/Cargo.lock +455 -455
- package/nativesrc/jsql-neo-core/Cargo.toml +24 -24
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +455 -455
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +168 -168
- package/nativesrc/jsql-neo-core/src/engine/mod.rs +62 -62
- package/nativesrc/jsql-neo-core/src/engine/table.rs +700 -700
- package/nativesrc/jsql-neo-core/src/lib.rs +2 -2
- package/nativesrc/jsql-neo-core/src/storage/mod.rs +2 -2
- package/nativesrc/jsql-neo-core/src/storage/persistent.rs +1 -1
- package/nativesrc/jsql-neo-core/src/storage/wal.rs +85 -85
- package/nativesrc/jsql-neo-core/src/types.rs +93 -93
- package/nativesrc/jsql-neo-native/Cargo.lock +606 -606
- package/nativesrc/jsql-neo-native/Cargo.toml +16 -16
- package/nativesrc/jsql-neo-native/build.rs +3 -3
- package/nativesrc/jsql-neo-native/package.json +7 -7
- package/nativesrc/jsql-neo-native/src/lib.rs +269 -269
- package/nativesrc/jsql-neo-wasm/Cargo.lock +465 -465
- package/nativesrc/jsql-neo-wasm/Cargo.toml +15 -15
- package/nativesrc/jsql-neo-wasm/src/lib.rs +447 -447
- package/package.json +87 -86
- package/postinstall.js +158 -158
- package/test/btree.test.js +128 -128
- package/test/coverage.js +187 -187
- package/test/join.test.js +110 -110
- package/test/native.test.js +158 -158
- package/test/regress-5.1.0.js +271 -271
- package/test/smoke.js +58 -58
- package/test/sql-parser.test.js +273 -0
- package/test/wasm.test.js +110 -110
- package/wasm/browser.d.ts +89 -89
- package/wasm/browser.mjs +569 -569
- package/wasm/browser_bg.mjs +597 -597
- package/wasm/jsql_neo_wasm.d.ts +38 -38
- package/wasm/jsql_neo_wasm.js +520 -520
- package/wasm/jsql_neo_wasm_bg.wasm.d.ts +26 -26
- package/wasm/package.json +11 -11
package/bin/jsql-server
CHANGED
|
@@ -1,90 +1,90 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// © Vexify 2026 All Rights Reserved.
|
|
3
|
-
// jsql-neo 后台常驻服务入口 — 由 `jsql start` 以 detached 子进程方式拉起。
|
|
4
|
-
// 用法: node bin/jsql-server --config <config.json>
|
|
5
|
-
// 支持 SIGTERM/SIGINT 优雅关闭。
|
|
6
|
-
|
|
7
|
-
const path = require('path');
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
const { createMysqlServer } = require('../lib/mysql_server');
|
|
10
|
-
|
|
11
|
-
function parseArgs(argv) {
|
|
12
|
-
const args = { _: [] };
|
|
13
|
-
for (let i = 0; i < argv.length; i++) {
|
|
14
|
-
const a = argv[i];
|
|
15
|
-
if (a === '--config') { args.config = argv[++i]; }
|
|
16
|
-
else if (a.startsWith('--config=')) { args.config = a.slice(9); }
|
|
17
|
-
else args._.push(a);
|
|
18
|
-
}
|
|
19
|
-
return args;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function loadConfig(configPath) {
|
|
23
|
-
const cfg = { port: 3306, host: '127.0.0.1' };
|
|
24
|
-
if (configPath && fs.existsSync(configPath)) {
|
|
25
|
-
Object.assign(cfg, JSON.parse(fs.readFileSync(configPath, 'utf8')));
|
|
26
|
-
}
|
|
27
|
-
return cfg;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function buildAuth(cfg) {
|
|
31
|
-
if (cfg.auth) {
|
|
32
|
-
if (typeof cfg.auth !== 'object' || Array.isArray(cfg.auth)) {
|
|
33
|
-
throw new Error('config.auth must be an object map { user: "password" | { password, databases } }');
|
|
34
|
-
}
|
|
35
|
-
return cfg.auth;
|
|
36
|
-
}
|
|
37
|
-
if (cfg.user != null || cfg.password != null) {
|
|
38
|
-
const auth = {};
|
|
39
|
-
auth[cfg.user || 'root'] = cfg.password || '';
|
|
40
|
-
return auth;
|
|
41
|
-
}
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function main() {
|
|
46
|
-
const args = parseArgs(process.argv.slice(2));
|
|
47
|
-
const cfg = loadConfig(args.config);
|
|
48
|
-
const auth = buildAuth(cfg);
|
|
49
|
-
const options = {
|
|
50
|
-
port: cfg.port || 3306,
|
|
51
|
-
host: cfg.host || '127.0.0.1',
|
|
52
|
-
dataDir: cfg.dataDir || null,
|
|
53
|
-
maxConnections: cfg.maxConnections,
|
|
54
|
-
idleTimeout: cfg.idleTimeout,
|
|
55
|
-
handshakeTimeout: cfg.handshakeTimeout,
|
|
56
|
-
safety: cfg.safety !== false,
|
|
57
|
-
allowComments: cfg.allowComments === true,
|
|
58
|
-
noAuth: cfg.noAuth === true,
|
|
59
|
-
};
|
|
60
|
-
if (auth) options.auth = auth;
|
|
61
|
-
if (cfg.onSecurityEvent) options.onSecurityEvent = cfg.onSecurityEvent;
|
|
62
|
-
|
|
63
|
-
const server = createMysqlServer(options);
|
|
64
|
-
|
|
65
|
-
let closing = false;
|
|
66
|
-
const shutdown = (code) => {
|
|
67
|
-
if (closing) return;
|
|
68
|
-
closing = true;
|
|
69
|
-
server.close(() => process.exit(code));
|
|
70
|
-
setTimeout(() => process.exit(code), 2000).unref();
|
|
71
|
-
};
|
|
72
|
-
process.on('SIGTERM', () => shutdown(0));
|
|
73
|
-
process.on('SIGINT', () => shutdown(0));
|
|
74
|
-
|
|
75
|
-
server.listen(err => {
|
|
76
|
-
if (err) {
|
|
77
|
-
console.error(`[jsql-server] failed to start: ${err.message}`);
|
|
78
|
-
process.exit(1);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
const addr = server.address;
|
|
82
|
-
console.log(`[jsql-server] listening on ${options.host}:${addr ? addr.port : options.port} (dataDir: ${options.dataDir || 'in-memory'})`);
|
|
83
|
-
console.log(`[jsql-server] pid ${process.pid}`);
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
main().catch(e => {
|
|
88
|
-
console.error(`[jsql-server] ${e.message}`);
|
|
89
|
-
process.exit(1);
|
|
90
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// © Vexify 2026 All Rights Reserved.
|
|
3
|
+
// jsql-neo 后台常驻服务入口 — 由 `jsql start` 以 detached 子进程方式拉起。
|
|
4
|
+
// 用法: node bin/jsql-server --config <config.json>
|
|
5
|
+
// 支持 SIGTERM/SIGINT 优雅关闭。
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const { createMysqlServer } = require('../lib/mysql_server');
|
|
10
|
+
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
const args = { _: [] };
|
|
13
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14
|
+
const a = argv[i];
|
|
15
|
+
if (a === '--config') { args.config = argv[++i]; }
|
|
16
|
+
else if (a.startsWith('--config=')) { args.config = a.slice(9); }
|
|
17
|
+
else args._.push(a);
|
|
18
|
+
}
|
|
19
|
+
return args;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function loadConfig(configPath) {
|
|
23
|
+
const cfg = { port: 3306, host: '127.0.0.1' };
|
|
24
|
+
if (configPath && fs.existsSync(configPath)) {
|
|
25
|
+
Object.assign(cfg, JSON.parse(fs.readFileSync(configPath, 'utf8')));
|
|
26
|
+
}
|
|
27
|
+
return cfg;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildAuth(cfg) {
|
|
31
|
+
if (cfg.auth) {
|
|
32
|
+
if (typeof cfg.auth !== 'object' || Array.isArray(cfg.auth)) {
|
|
33
|
+
throw new Error('config.auth must be an object map { user: "password" | { password, databases } }');
|
|
34
|
+
}
|
|
35
|
+
return cfg.auth;
|
|
36
|
+
}
|
|
37
|
+
if (cfg.user != null || cfg.password != null) {
|
|
38
|
+
const auth = {};
|
|
39
|
+
auth[cfg.user || 'root'] = cfg.password || '';
|
|
40
|
+
return auth;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function main() {
|
|
46
|
+
const args = parseArgs(process.argv.slice(2));
|
|
47
|
+
const cfg = loadConfig(args.config);
|
|
48
|
+
const auth = buildAuth(cfg);
|
|
49
|
+
const options = {
|
|
50
|
+
port: cfg.port || 3306,
|
|
51
|
+
host: cfg.host || '127.0.0.1',
|
|
52
|
+
dataDir: cfg.dataDir || null,
|
|
53
|
+
maxConnections: cfg.maxConnections,
|
|
54
|
+
idleTimeout: cfg.idleTimeout,
|
|
55
|
+
handshakeTimeout: cfg.handshakeTimeout,
|
|
56
|
+
safety: cfg.safety !== false,
|
|
57
|
+
allowComments: cfg.allowComments === true,
|
|
58
|
+
noAuth: cfg.noAuth === true,
|
|
59
|
+
};
|
|
60
|
+
if (auth) options.auth = auth;
|
|
61
|
+
if (cfg.onSecurityEvent) options.onSecurityEvent = cfg.onSecurityEvent;
|
|
62
|
+
|
|
63
|
+
const server = createMysqlServer(options);
|
|
64
|
+
|
|
65
|
+
let closing = false;
|
|
66
|
+
const shutdown = (code) => {
|
|
67
|
+
if (closing) return;
|
|
68
|
+
closing = true;
|
|
69
|
+
server.close(() => process.exit(code));
|
|
70
|
+
setTimeout(() => process.exit(code), 2000).unref();
|
|
71
|
+
};
|
|
72
|
+
process.on('SIGTERM', () => shutdown(0));
|
|
73
|
+
process.on('SIGINT', () => shutdown(0));
|
|
74
|
+
|
|
75
|
+
server.listen(err => {
|
|
76
|
+
if (err) {
|
|
77
|
+
console.error(`[jsql-server] failed to start: ${err.message}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const addr = server.address;
|
|
82
|
+
console.log(`[jsql-server] listening on ${options.host}:${addr ? addr.port : options.port} (dataDir: ${options.dataDir || 'in-memory'})`);
|
|
83
|
+
console.log(`[jsql-server] pid ${process.pid}`);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
main().catch(e => {
|
|
88
|
+
console.error(`[jsql-server] ${e.message}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
});
|
|
@@ -1,177 +1,177 @@
|
|
|
1
|
-
// © Vexify 2026 All Rights Reserved.
|
|
2
|
-
// jsql server start/stop/status — 后台常驻服务控制(对标 `net mysql80 start`)。
|
|
3
|
-
// 状态文件放在 ~/.jsql/ 下:server.pid / server.log / server.json(保存启动配置)。
|
|
4
|
-
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
const { spawn } = require('child_process');
|
|
9
|
-
|
|
10
|
-
const STATE_DIR = path.join(os.homedir(), '.jsql');
|
|
11
|
-
const PID_FILE = path.join(STATE_DIR, 'server.pid');
|
|
12
|
-
const LOG_FILE = path.join(STATE_DIR, 'server.log');
|
|
13
|
-
const CONF_FILE = path.join(STATE_DIR, 'server.json');
|
|
14
|
-
|
|
15
|
-
function ensureStateDir() {
|
|
16
|
-
fs.mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function readPid() {
|
|
20
|
-
try {
|
|
21
|
-
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10);
|
|
22
|
-
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
23
|
-
} catch (e) {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isAlive(pid) {
|
|
29
|
-
if (!pid) return false;
|
|
30
|
-
try {
|
|
31
|
-
process.kill(pid, 0);
|
|
32
|
-
return true;
|
|
33
|
-
} catch (e) {
|
|
34
|
-
return e.code === 'EPERM';
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function isPortOpen(port, host) {
|
|
39
|
-
return new Promise(resolve => {
|
|
40
|
-
const net = require('net');
|
|
41
|
-
const sock = net.connect({ port, host: host || '127.0.0.1' });
|
|
42
|
-
sock.once('connect', () => { sock.destroy(); resolve(true); });
|
|
43
|
-
sock.once('error', () => resolve(false));
|
|
44
|
-
setTimeout(() => { sock.destroy(); resolve(false); }, 800);
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function parseAuthSpecs(list) {
|
|
49
|
-
const auth = {};
|
|
50
|
-
for (const spec of list) {
|
|
51
|
-
const idx = spec.indexOf(':');
|
|
52
|
-
if (idx <= 0) {
|
|
53
|
-
auth[spec] = '';
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
const user = spec.slice(0, idx);
|
|
57
|
-
const rest = spec.slice(idx + 1);
|
|
58
|
-
const idx2 = rest.indexOf(':');
|
|
59
|
-
const password = idx2 === -1 ? rest : rest.slice(0, idx2);
|
|
60
|
-
const dbPart = idx2 === -1 ? '' : rest.slice(idx2 + 1);
|
|
61
|
-
const dbs = dbPart
|
|
62
|
-
? dbPart.split(',').map(s => s.trim()).filter(Boolean)
|
|
63
|
-
: null;
|
|
64
|
-
auth[user] = dbs ? { password, databases: dbs } : password;
|
|
65
|
-
}
|
|
66
|
-
return auth;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function start(opts = {}) {
|
|
70
|
-
ensureStateDir();
|
|
71
|
-
const existing = readPid();
|
|
72
|
-
if (existing && isAlive(existing)) {
|
|
73
|
-
return `jsql server is already running (pid ${existing}).`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const configPath = opts.config || CONF_FILE;
|
|
77
|
-
let cfg = { port: 3306, host: '127.0.0.1' };
|
|
78
|
-
if (fs.existsSync(configPath)) {
|
|
79
|
-
try { cfg = Object.assign(cfg, JSON.parse(fs.readFileSync(configPath, 'utf8'))); } catch (e) {}
|
|
80
|
-
}
|
|
81
|
-
if (opts.port != null) cfg.port = opts.port;
|
|
82
|
-
if (opts.host != null) cfg.host = opts.host;
|
|
83
|
-
if (opts.dataDir != null) cfg.dataDir = path.resolve(opts.dataDir);
|
|
84
|
-
if (opts.noAuth) {
|
|
85
|
-
cfg.noAuth = true;
|
|
86
|
-
delete cfg.auth;
|
|
87
|
-
delete cfg.user;
|
|
88
|
-
delete cfg.password;
|
|
89
|
-
} else {
|
|
90
|
-
cfg.noAuth = false;
|
|
91
|
-
}
|
|
92
|
-
if (opts.auth) cfg.auth = parseAuthSpecs(Array.isArray(opts.auth) ? opts.auth : [opts.auth]);
|
|
93
|
-
fs.writeFileSync(CONF_FILE, JSON.stringify(cfg, null, 2), 'utf8');
|
|
94
|
-
|
|
95
|
-
const entry = path.join(__dirname, 'jsql-server');
|
|
96
|
-
const logFd = fs.openSync(LOG_FILE, 'a');
|
|
97
|
-
const child = spawn(process.execPath, [entry, '--config', CONF_FILE], {
|
|
98
|
-
detached: true,
|
|
99
|
-
stdio: ['ignore', logFd, logFd],
|
|
100
|
-
env: Object.assign({}, process.env, { JSQL_SERVER_MAIN: '1' }),
|
|
101
|
-
});
|
|
102
|
-
child.once('error', err => {
|
|
103
|
-
fs.closeSync(logFd);
|
|
104
|
-
throw err;
|
|
105
|
-
});
|
|
106
|
-
child.unref();
|
|
107
|
-
|
|
108
|
-
fs.writeFileSync(PID_FILE, String(child.pid), 'utf8');
|
|
109
|
-
|
|
110
|
-
const ready = await waitReady(cfg.port, cfg.host, 5000);
|
|
111
|
-
if (!ready) {
|
|
112
|
-
if (isAlive(child.pid)) {
|
|
113
|
-
try { process.kill(child.pid, 'SIGTERM'); } catch (e) {}
|
|
114
|
-
fs.unlinkSync(PID_FILE);
|
|
115
|
-
}
|
|
116
|
-
throw new Error(`server failed to start (see ${LOG_FILE})`);
|
|
117
|
-
}
|
|
118
|
-
return `jsql server started (pid ${child.pid}, ${cfg.host}:${cfg.port}, dataDir: ${cfg.dataDir || 'in-memory'}). Log: ${LOG_FILE}`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function waitReady(port, host, timeoutMs) {
|
|
122
|
-
const deadline = Date.now() + timeoutMs;
|
|
123
|
-
return new Promise(resolve => {
|
|
124
|
-
const tick = async () => {
|
|
125
|
-
if (await isPortOpen(port, host)) return resolve(true);
|
|
126
|
-
if (Date.now() > deadline) return resolve(false);
|
|
127
|
-
setTimeout(tick, 200);
|
|
128
|
-
};
|
|
129
|
-
tick();
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async function stop(opts = {}) {
|
|
134
|
-
const pid = readPid();
|
|
135
|
-
if (!pid) return 'jsql server is not running.';
|
|
136
|
-
if (!isAlive(pid)) {
|
|
137
|
-
fs.unlinkSync(PID_FILE);
|
|
138
|
-
return 'jsql server is not running.';
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
process.kill(pid, 'SIGTERM');
|
|
142
|
-
} catch (e) {
|
|
143
|
-
throw new Error(`failed to stop server (pid ${pid}): ${e.message}`);
|
|
144
|
-
}
|
|
145
|
-
const deadline = Date.now() + 5000;
|
|
146
|
-
while (Date.now() < deadline) {
|
|
147
|
-
if (!isAlive(pid)) break;
|
|
148
|
-
await new Promise(r => setTimeout(r, 150));
|
|
149
|
-
}
|
|
150
|
-
if (isAlive(pid)) {
|
|
151
|
-
try { process.kill(pid, 'SIGKILL'); } catch (e) {}
|
|
152
|
-
}
|
|
153
|
-
fs.unlinkSync(PID_FILE);
|
|
154
|
-
return `jsql server stopped (pid ${pid}).`;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function status(opts = {}) {
|
|
158
|
-
const pid = readPid();
|
|
159
|
-
if (!pid || !isAlive(pid)) {
|
|
160
|
-
return 'jsql server is not running.';
|
|
161
|
-
}
|
|
162
|
-
let cfg = {};
|
|
163
|
-
try { cfg = JSON.parse(fs.readFileSync(CONF_FILE, 'utf8')); } catch (e) {}
|
|
164
|
-
const port = (opts.port != null ? opts.port : cfg.port) || 3306;
|
|
165
|
-
const host = (opts.host != null ? opts.host : cfg.host) || '127.0.0.1';
|
|
166
|
-
const up = await isPortOpen(port, host);
|
|
167
|
-
const lines = [
|
|
168
|
-
`jsql server is running (pid ${pid}).`,
|
|
169
|
-
` ${host}:${port} ${up ? 'listening' : 'NOT responding'}`,
|
|
170
|
-
` dataDir: ${cfg.dataDir || 'in-memory'}`,
|
|
171
|
-
` log: ${LOG_FILE}`,
|
|
172
|
-
];
|
|
173
|
-
if (cfg.auth) lines.push(` users: ${Object.keys(cfg.auth).join(', ')}`);
|
|
174
|
-
return lines.join('\n');
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
module.exports = { serverControl: { start, stop, status }, STATE_DIR, PID_FILE, LOG_FILE, CONF_FILE };
|
|
1
|
+
// © Vexify 2026 All Rights Reserved.
|
|
2
|
+
// jsql server start/stop/status — 后台常驻服务控制(对标 `net mysql80 start`)。
|
|
3
|
+
// 状态文件放在 ~/.jsql/ 下:server.pid / server.log / server.json(保存启动配置)。
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const STATE_DIR = path.join(os.homedir(), '.jsql');
|
|
11
|
+
const PID_FILE = path.join(STATE_DIR, 'server.pid');
|
|
12
|
+
const LOG_FILE = path.join(STATE_DIR, 'server.log');
|
|
13
|
+
const CONF_FILE = path.join(STATE_DIR, 'server.json');
|
|
14
|
+
|
|
15
|
+
function ensureStateDir() {
|
|
16
|
+
fs.mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readPid() {
|
|
20
|
+
try {
|
|
21
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10);
|
|
22
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isAlive(pid) {
|
|
29
|
+
if (!pid) return false;
|
|
30
|
+
try {
|
|
31
|
+
process.kill(pid, 0);
|
|
32
|
+
return true;
|
|
33
|
+
} catch (e) {
|
|
34
|
+
return e.code === 'EPERM';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isPortOpen(port, host) {
|
|
39
|
+
return new Promise(resolve => {
|
|
40
|
+
const net = require('net');
|
|
41
|
+
const sock = net.connect({ port, host: host || '127.0.0.1' });
|
|
42
|
+
sock.once('connect', () => { sock.destroy(); resolve(true); });
|
|
43
|
+
sock.once('error', () => resolve(false));
|
|
44
|
+
setTimeout(() => { sock.destroy(); resolve(false); }, 800);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseAuthSpecs(list) {
|
|
49
|
+
const auth = {};
|
|
50
|
+
for (const spec of list) {
|
|
51
|
+
const idx = spec.indexOf(':');
|
|
52
|
+
if (idx <= 0) {
|
|
53
|
+
auth[spec] = '';
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const user = spec.slice(0, idx);
|
|
57
|
+
const rest = spec.slice(idx + 1);
|
|
58
|
+
const idx2 = rest.indexOf(':');
|
|
59
|
+
const password = idx2 === -1 ? rest : rest.slice(0, idx2);
|
|
60
|
+
const dbPart = idx2 === -1 ? '' : rest.slice(idx2 + 1);
|
|
61
|
+
const dbs = dbPart
|
|
62
|
+
? dbPart.split(',').map(s => s.trim()).filter(Boolean)
|
|
63
|
+
: null;
|
|
64
|
+
auth[user] = dbs ? { password, databases: dbs } : password;
|
|
65
|
+
}
|
|
66
|
+
return auth;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function start(opts = {}) {
|
|
70
|
+
ensureStateDir();
|
|
71
|
+
const existing = readPid();
|
|
72
|
+
if (existing && isAlive(existing)) {
|
|
73
|
+
return `jsql server is already running (pid ${existing}).`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const configPath = opts.config || CONF_FILE;
|
|
77
|
+
let cfg = { port: 3306, host: '127.0.0.1' };
|
|
78
|
+
if (fs.existsSync(configPath)) {
|
|
79
|
+
try { cfg = Object.assign(cfg, JSON.parse(fs.readFileSync(configPath, 'utf8'))); } catch (e) {}
|
|
80
|
+
}
|
|
81
|
+
if (opts.port != null) cfg.port = opts.port;
|
|
82
|
+
if (opts.host != null) cfg.host = opts.host;
|
|
83
|
+
if (opts.dataDir != null) cfg.dataDir = path.resolve(opts.dataDir);
|
|
84
|
+
if (opts.noAuth) {
|
|
85
|
+
cfg.noAuth = true;
|
|
86
|
+
delete cfg.auth;
|
|
87
|
+
delete cfg.user;
|
|
88
|
+
delete cfg.password;
|
|
89
|
+
} else {
|
|
90
|
+
cfg.noAuth = false;
|
|
91
|
+
}
|
|
92
|
+
if (opts.auth) cfg.auth = parseAuthSpecs(Array.isArray(opts.auth) ? opts.auth : [opts.auth]);
|
|
93
|
+
fs.writeFileSync(CONF_FILE, JSON.stringify(cfg, null, 2), 'utf8');
|
|
94
|
+
|
|
95
|
+
const entry = path.join(__dirname, 'jsql-server');
|
|
96
|
+
const logFd = fs.openSync(LOG_FILE, 'a');
|
|
97
|
+
const child = spawn(process.execPath, [entry, '--config', CONF_FILE], {
|
|
98
|
+
detached: true,
|
|
99
|
+
stdio: ['ignore', logFd, logFd],
|
|
100
|
+
env: Object.assign({}, process.env, { JSQL_SERVER_MAIN: '1' }),
|
|
101
|
+
});
|
|
102
|
+
child.once('error', err => {
|
|
103
|
+
fs.closeSync(logFd);
|
|
104
|
+
throw err;
|
|
105
|
+
});
|
|
106
|
+
child.unref();
|
|
107
|
+
|
|
108
|
+
fs.writeFileSync(PID_FILE, String(child.pid), 'utf8');
|
|
109
|
+
|
|
110
|
+
const ready = await waitReady(cfg.port, cfg.host, 5000);
|
|
111
|
+
if (!ready) {
|
|
112
|
+
if (isAlive(child.pid)) {
|
|
113
|
+
try { process.kill(child.pid, 'SIGTERM'); } catch (e) {}
|
|
114
|
+
fs.unlinkSync(PID_FILE);
|
|
115
|
+
}
|
|
116
|
+
throw new Error(`server failed to start (see ${LOG_FILE})`);
|
|
117
|
+
}
|
|
118
|
+
return `jsql server started (pid ${child.pid}, ${cfg.host}:${cfg.port}, dataDir: ${cfg.dataDir || 'in-memory'}). Log: ${LOG_FILE}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function waitReady(port, host, timeoutMs) {
|
|
122
|
+
const deadline = Date.now() + timeoutMs;
|
|
123
|
+
return new Promise(resolve => {
|
|
124
|
+
const tick = async () => {
|
|
125
|
+
if (await isPortOpen(port, host)) return resolve(true);
|
|
126
|
+
if (Date.now() > deadline) return resolve(false);
|
|
127
|
+
setTimeout(tick, 200);
|
|
128
|
+
};
|
|
129
|
+
tick();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function stop(opts = {}) {
|
|
134
|
+
const pid = readPid();
|
|
135
|
+
if (!pid) return 'jsql server is not running.';
|
|
136
|
+
if (!isAlive(pid)) {
|
|
137
|
+
fs.unlinkSync(PID_FILE);
|
|
138
|
+
return 'jsql server is not running.';
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
process.kill(pid, 'SIGTERM');
|
|
142
|
+
} catch (e) {
|
|
143
|
+
throw new Error(`failed to stop server (pid ${pid}): ${e.message}`);
|
|
144
|
+
}
|
|
145
|
+
const deadline = Date.now() + 5000;
|
|
146
|
+
while (Date.now() < deadline) {
|
|
147
|
+
if (!isAlive(pid)) break;
|
|
148
|
+
await new Promise(r => setTimeout(r, 150));
|
|
149
|
+
}
|
|
150
|
+
if (isAlive(pid)) {
|
|
151
|
+
try { process.kill(pid, 'SIGKILL'); } catch (e) {}
|
|
152
|
+
}
|
|
153
|
+
fs.unlinkSync(PID_FILE);
|
|
154
|
+
return `jsql server stopped (pid ${pid}).`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function status(opts = {}) {
|
|
158
|
+
const pid = readPid();
|
|
159
|
+
if (!pid || !isAlive(pid)) {
|
|
160
|
+
return 'jsql server is not running.';
|
|
161
|
+
}
|
|
162
|
+
let cfg = {};
|
|
163
|
+
try { cfg = JSON.parse(fs.readFileSync(CONF_FILE, 'utf8')); } catch (e) {}
|
|
164
|
+
const port = (opts.port != null ? opts.port : cfg.port) || 3306;
|
|
165
|
+
const host = (opts.host != null ? opts.host : cfg.host) || '127.0.0.1';
|
|
166
|
+
const up = await isPortOpen(port, host);
|
|
167
|
+
const lines = [
|
|
168
|
+
`jsql server is running (pid ${pid}).`,
|
|
169
|
+
` ${host}:${port} ${up ? 'listening' : 'NOT responding'}`,
|
|
170
|
+
` dataDir: ${cfg.dataDir || 'in-memory'}`,
|
|
171
|
+
` log: ${LOG_FILE}`,
|
|
172
|
+
];
|
|
173
|
+
if (cfg.auth) lines.push(` users: ${Object.keys(cfg.auth).join(', ')}`);
|
|
174
|
+
return lines.join('\n');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = { serverControl: { start, stop, status }, STATE_DIR, PID_FILE, LOG_FILE, CONF_FILE };
|