bingocode 1.1.60 → 1.1.62
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/package.json +1 -1
- package/src/server/ensureSingletonLocalServer.ts +256 -169
package/package.json
CHANGED
|
@@ -1,169 +1,256 @@
|
|
|
1
|
-
// server/ensureSingletonLocalServer.ts
|
|
2
|
-
import { spawn, spawnSync } from 'child_process';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import fsp from 'fs/promises';
|
|
5
|
-
import path from 'path';
|
|
6
|
-
import os from 'os';
|
|
7
|
-
import axios from 'axios';
|
|
8
|
-
|
|
9
|
-
const LOG_FILE = path.join(os.homedir(), '.claude-cli', 'runtime', 'boot.log');
|
|
10
|
-
function log(msg: string) {
|
|
11
|
-
try {
|
|
12
|
-
const line = `[${new Date().toISOString()}] ${msg}\n`;
|
|
13
|
-
fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
|
|
14
|
-
fs.appendFileSync(LOG_FILE, line);
|
|
15
|
-
} catch {}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
type Handle = {
|
|
19
|
-
baseUrl: string;
|
|
20
|
-
stopIfLast: () => Promise<void>;
|
|
21
|
-
pid?: number;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const RUNTIME_DIR = path.join(os.homedir(), '.claude-cli', 'runtime');
|
|
25
|
-
const LOCK_JSON = path.join(RUNTIME_DIR, 'server.lock.json');
|
|
26
|
-
const BOOT_LOCK = path.join(RUNTIME_DIR, 'server.boot.lock');
|
|
27
|
-
const LEASES_DIR = path.join(RUNTIME_DIR, 'leases');
|
|
28
|
-
|
|
29
|
-
const DEFAULT_HOST = '127.0.0.1';
|
|
30
|
-
const DEFAULT_PORT = Number(process.env.SERVER_PORT || 3456);
|
|
31
|
-
const HEALTH_TIMEOUT_MS = Number(process.env.HEALTH_TIMEOUT_MS || 20000);
|
|
32
|
-
const HEALTH_RETRY_MS = 300;
|
|
33
|
-
|
|
34
|
-
function mkdirp(p: string) { fs.mkdirSync(p, { recursive: true }); }
|
|
35
|
-
function atomicCreate(p: string): boolean {
|
|
36
|
-
try { const fd = fs.openSync(p, 'wx'); fs.closeSync(fd); return true; } catch { return false; }
|
|
37
|
-
}
|
|
38
|
-
function rmSafe(p: string) { try { fs.rmSync(p, { force: true, recursive: true }); } catch {} }
|
|
39
|
-
function readJson<T>(p: string): T | null { try { return JSON.parse(fs.readFileSync(p, 'utf-8')) as T; } catch { return null; } }
|
|
40
|
-
function writeJson(p: string, data: any) { fs.writeFileSync(p, JSON.stringify(data, null, 2), 'utf-8'); }
|
|
41
|
-
function isPidAlive(pid: number): boolean { try { process.kill(pid, 0); return true; } catch { return false; } }
|
|
42
|
-
async function waitHealthy(baseUrl: string, timeoutMs: number) {
|
|
43
|
-
const start = Date.now(); let lastErr: any = null;
|
|
44
|
-
while (Date.now() - start < timeoutMs) {
|
|
45
|
-
try {
|
|
46
|
-
const r = await axios.get(baseUrl.replace(/\/+$/, '') + '/health', { timeout: 1500 });
|
|
47
|
-
if (r.status === 200 && r.data?.status === 'ok') return;
|
|
48
|
-
} catch (e) { lastErr = e; }
|
|
49
|
-
await new Promise(r => setTimeout(r, HEALTH_RETRY_MS));
|
|
50
|
-
}
|
|
51
|
-
throw new Error(`健康检查超时:${lastErr?.message || 'unknown'}`);
|
|
52
|
-
}
|
|
53
|
-
function resolveBunPath() {
|
|
54
|
-
const fromEnv = process.env.BUN_PATH;
|
|
55
|
-
if (fromEnv) return fromEnv;
|
|
56
|
-
const r = spawnSync('bun', ['--version'], { stdio: 'ignore' });
|
|
57
|
-
if (r.status === 0) return 'bun';
|
|
58
|
-
throw new Error('未检测到 bun,请安装 https://bun.sh 或设置 BUN_PATH');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function acquireLease(): Promise<string> {
|
|
62
|
-
mkdirp(LEASES_DIR);
|
|
63
|
-
const lease = path.join(LEASES_DIR, `${process.pid}.json`);
|
|
64
|
-
await fsp.writeFile(lease, JSON.stringify({ pid: process.pid, ts: Date.now() }));
|
|
65
|
-
const cleanup = () => rmSafe(lease);
|
|
66
|
-
process.once('exit', cleanup);
|
|
67
|
-
process.once('SIGINT', () => { cleanup(); process.exit(130); });
|
|
68
|
-
process.once('SIGTERM', () => { cleanup(); process.exit(143); });
|
|
69
|
-
return lease;
|
|
70
|
-
}
|
|
71
|
-
function countValidLeases(): number {
|
|
72
|
-
try {
|
|
73
|
-
const files = fs.readdirSync(LEASES_DIR);
|
|
74
|
-
let alive = 0;
|
|
75
|
-
for (const f of files) {
|
|
76
|
-
const p = path.join(LEASES_DIR, f);
|
|
77
|
-
const data = readJson<{pid:number, ts:number}>(p);
|
|
78
|
-
if (data?.pid && isPidAlive(data.pid)) alive++;
|
|
79
|
-
else rmSafe(p);
|
|
80
|
-
}
|
|
81
|
-
return alive;
|
|
82
|
-
} catch { return 0; }
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export async function ensureSingletonLocalServer(opts: {
|
|
86
|
-
serverEntry: string;
|
|
87
|
-
host?: string;
|
|
88
|
-
port?: number;
|
|
89
|
-
baseUrlEnv?: string;
|
|
90
|
-
passEnv?: Record<string, string>;
|
|
91
|
-
}): Promise<Handle> {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
1
|
+
// server/ensureSingletonLocalServer.ts
|
|
2
|
+
import { spawn, spawnSync } from 'child_process';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import fsp from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
import axios from 'axios';
|
|
8
|
+
|
|
9
|
+
const LOG_FILE = path.join(os.homedir(), '.claude-cli', 'runtime', 'boot.log');
|
|
10
|
+
function log(msg: string) {
|
|
11
|
+
try {
|
|
12
|
+
const line = `[${new Date().toISOString()}] ${msg}\n`;
|
|
13
|
+
fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
|
|
14
|
+
fs.appendFileSync(LOG_FILE, line);
|
|
15
|
+
} catch {}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type Handle = {
|
|
19
|
+
baseUrl: string;
|
|
20
|
+
stopIfLast: () => Promise<void>;
|
|
21
|
+
pid?: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const RUNTIME_DIR = path.join(os.homedir(), '.claude-cli', 'runtime');
|
|
25
|
+
const LOCK_JSON = path.join(RUNTIME_DIR, 'server.lock.json');
|
|
26
|
+
const BOOT_LOCK = path.join(RUNTIME_DIR, 'server.boot.lock');
|
|
27
|
+
const LEASES_DIR = path.join(RUNTIME_DIR, 'leases');
|
|
28
|
+
|
|
29
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
30
|
+
const DEFAULT_PORT = Number(process.env.SERVER_PORT || 3456);
|
|
31
|
+
const HEALTH_TIMEOUT_MS = Number(process.env.HEALTH_TIMEOUT_MS || 20000);
|
|
32
|
+
const HEALTH_RETRY_MS = 300;
|
|
33
|
+
|
|
34
|
+
function mkdirp(p: string) { fs.mkdirSync(p, { recursive: true }); }
|
|
35
|
+
function atomicCreate(p: string): boolean {
|
|
36
|
+
try { const fd = fs.openSync(p, 'wx'); fs.closeSync(fd); return true; } catch { return false; }
|
|
37
|
+
}
|
|
38
|
+
function rmSafe(p: string) { try { fs.rmSync(p, { force: true, recursive: true }); } catch {} }
|
|
39
|
+
function readJson<T>(p: string): T | null { try { return JSON.parse(fs.readFileSync(p, 'utf-8')) as T; } catch { return null; } }
|
|
40
|
+
function writeJson(p: string, data: any) { fs.writeFileSync(p, JSON.stringify(data, null, 2), 'utf-8'); }
|
|
41
|
+
function isPidAlive(pid: number): boolean { try { process.kill(pid, 0); return true; } catch { return false; } }
|
|
42
|
+
async function waitHealthy(baseUrl: string, timeoutMs: number) {
|
|
43
|
+
const start = Date.now(); let lastErr: any = null;
|
|
44
|
+
while (Date.now() - start < timeoutMs) {
|
|
45
|
+
try {
|
|
46
|
+
const r = await axios.get(baseUrl.replace(/\/+$/, '') + '/health', { timeout: 1500 });
|
|
47
|
+
if (r.status === 200 && r.data?.status === 'ok') return;
|
|
48
|
+
} catch (e) { lastErr = e; }
|
|
49
|
+
await new Promise(r => setTimeout(r, HEALTH_RETRY_MS));
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`健康检查超时:${lastErr?.message || 'unknown'}`);
|
|
52
|
+
}
|
|
53
|
+
function resolveBunPath() {
|
|
54
|
+
const fromEnv = process.env.BUN_PATH;
|
|
55
|
+
if (fromEnv) return fromEnv;
|
|
56
|
+
const r = spawnSync('bun', ['--version'], { stdio: 'ignore' });
|
|
57
|
+
if (r.status === 0) return 'bun';
|
|
58
|
+
throw new Error('未检测到 bun,请安装 https://bun.sh 或设置 BUN_PATH');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function acquireLease(): Promise<string> {
|
|
62
|
+
mkdirp(LEASES_DIR);
|
|
63
|
+
const lease = path.join(LEASES_DIR, `${process.pid}.json`);
|
|
64
|
+
await fsp.writeFile(lease, JSON.stringify({ pid: process.pid, ts: Date.now() }));
|
|
65
|
+
const cleanup = () => rmSafe(lease);
|
|
66
|
+
process.once('exit', cleanup);
|
|
67
|
+
process.once('SIGINT', () => { cleanup(); process.exit(130); });
|
|
68
|
+
process.once('SIGTERM', () => { cleanup(); process.exit(143); });
|
|
69
|
+
return lease;
|
|
70
|
+
}
|
|
71
|
+
function countValidLeases(): number {
|
|
72
|
+
try {
|
|
73
|
+
const files = fs.readdirSync(LEASES_DIR);
|
|
74
|
+
let alive = 0;
|
|
75
|
+
for (const f of files) {
|
|
76
|
+
const p = path.join(LEASES_DIR, f);
|
|
77
|
+
const data = readJson<{pid:number, ts:number}>(p);
|
|
78
|
+
if (data?.pid && isPidAlive(data.pid)) alive++;
|
|
79
|
+
else rmSafe(p);
|
|
80
|
+
}
|
|
81
|
+
return alive;
|
|
82
|
+
} catch { return 0; }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function ensureSingletonLocalServer(opts: {
|
|
86
|
+
serverEntry: string;
|
|
87
|
+
host?: string;
|
|
88
|
+
port?: number;
|
|
89
|
+
baseUrlEnv?: string;
|
|
90
|
+
passEnv?: Record<string, string>;
|
|
91
|
+
}): Promise<Handle> {
|
|
92
|
+
log(`=== ensureSingletonLocalServer 启动 ===`);
|
|
93
|
+
log(`serverEntry: ${opts.serverEntry}`);
|
|
94
|
+
log(`serverEntry exists: ${fs.existsSync(opts.serverEntry)}`);
|
|
95
|
+
log(`process.cwd: ${process.cwd()}`);
|
|
96
|
+
log(`process.execPath: ${process.execPath}`);
|
|
97
|
+
log(`platform: ${process.platform} arch: ${process.arch}`);
|
|
98
|
+
log(`node version: ${process.version}`);
|
|
99
|
+
|
|
100
|
+
const preset = (opts.baseUrlEnv || process.env.BASE_API_URL || '').trim();
|
|
101
|
+
if (preset) {
|
|
102
|
+
log(`使用预设 baseUrl: ${preset}`);
|
|
103
|
+
try { await waitHealthy(preset, 3000); } catch {}
|
|
104
|
+
await acquireLease();
|
|
105
|
+
return { baseUrl: preset.replace(/\/+$/, ''), stopIfLast: async () => {} };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const host = opts.host || DEFAULT_HOST;
|
|
109
|
+
const port = Number(opts.port ?? DEFAULT_PORT);
|
|
110
|
+
const baseUrl = `http://${host}:${port}`;
|
|
111
|
+
log(`目标 baseUrl: ${baseUrl}`);
|
|
112
|
+
mkdirp(RUNTIME_DIR);
|
|
113
|
+
|
|
114
|
+
const lock = readJson<{ pid:number, port:number }>(LOCK_JSON);
|
|
115
|
+
log(`现有锁文件: ${JSON.stringify(lock)}`);
|
|
116
|
+
if (lock && lock.port === port) {
|
|
117
|
+
const healthy = await (async () => { try { await waitHealthy(baseUrl, 1200); return true; } catch { return false; } })();
|
|
118
|
+
log(`已有锁 pid=${lock.pid} healthy=${healthy} alive=${lock.pid ? isPidAlive(lock.pid) : false}`);
|
|
119
|
+
if (healthy && isPidAlive(lock.pid)) {
|
|
120
|
+
await acquireLease();
|
|
121
|
+
return { baseUrl, stopIfLast: makeStopIfLast(lock.pid, baseUrl), pid: lock.pid };
|
|
122
|
+
}
|
|
123
|
+
log(`锁文件无效,清除`);
|
|
124
|
+
rmSafe(LOCK_JSON);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const iAmSpawner = atomicCreate(BOOT_LOCK);
|
|
128
|
+
log(`iAmSpawner: ${iAmSpawner}`);
|
|
129
|
+
if (!iAmSpawner) {
|
|
130
|
+
log(`等待其他进程启动服务...`);
|
|
131
|
+
try { await waitHealthy(baseUrl, HEALTH_TIMEOUT_MS); } catch {
|
|
132
|
+
log(`等待超时,清除 boot lock 后重试`);
|
|
133
|
+
rmSafe(BOOT_LOCK); rmSafe(LOCK_JSON);
|
|
134
|
+
return await ensureSingletonLocalServer(opts);
|
|
135
|
+
}
|
|
136
|
+
await acquireLease();
|
|
137
|
+
const live = readJson<{pid:number}>(LOCK_JSON);
|
|
138
|
+
return { baseUrl, stopIfLast: makeStopIfLast(live?.pid || 0, baseUrl), pid: live?.pid };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let child: any = null;
|
|
142
|
+
try {
|
|
143
|
+
let bun: string;
|
|
144
|
+
try {
|
|
145
|
+
bun = resolveBunPath();
|
|
146
|
+
log(`bun 路径: ${bun}`);
|
|
147
|
+
} catch (e: any) {
|
|
148
|
+
log(`resolveBunPath 失败: ${e?.message}`);
|
|
149
|
+
throw e;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 检查 serverEntry 文件是否存在
|
|
153
|
+
if (!fs.existsSync(opts.serverEntry)) {
|
|
154
|
+
log(`错误:serverEntry 文件不存在: ${opts.serverEntry}`);
|
|
155
|
+
// 尝试列出父目录内容帮助诊断
|
|
156
|
+
try {
|
|
157
|
+
const parentDir = path.dirname(opts.serverEntry);
|
|
158
|
+
log(`父目录 ${parentDir} 存在: ${fs.existsSync(parentDir)}`);
|
|
159
|
+
if (fs.existsSync(parentDir)) {
|
|
160
|
+
const files = fs.readdirSync(parentDir);
|
|
161
|
+
log(`父目录内容: ${files.join(', ')}`);
|
|
162
|
+
}
|
|
163
|
+
// 列出祖父目录
|
|
164
|
+
const grandParentDir = path.dirname(parentDir);
|
|
165
|
+
log(`祖父目录 ${grandParentDir} 存在: ${fs.existsSync(grandParentDir)}`);
|
|
166
|
+
if (fs.existsSync(grandParentDir)) {
|
|
167
|
+
const files2 = fs.readdirSync(grandParentDir);
|
|
168
|
+
log(`祖父目录内容: ${files2.join(', ')}`);
|
|
169
|
+
}
|
|
170
|
+
} catch (dirErr: any) {
|
|
171
|
+
log(`目录列举失败: ${dirErr?.message}`);
|
|
172
|
+
}
|
|
173
|
+
throw new Error(`服务器入口文件不存在: ${opts.serverEntry}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const spawnArgs = [opts.serverEntry, '--host', host, '--port', String(port)];
|
|
177
|
+
const spawnEnv = { ...process.env, SERVER_AUTH_REQUIRED: '0', ...(opts.passEnv || {}) };
|
|
178
|
+
// cwd 设为包根目录(serverEntry 的两级上级:src/server -> src -> 包根),
|
|
179
|
+
// 确保 bun 能从正确位置查找 node_modules,无论用户在哪个目录启动
|
|
180
|
+
const serverCwd = path.dirname(path.dirname(path.dirname(opts.serverEntry)));
|
|
181
|
+
log(`spawn: ${bun} ${spawnArgs.join(' ')}`);
|
|
182
|
+
log(`spawn cwd: ${serverCwd}`);
|
|
183
|
+
|
|
184
|
+
child = spawn(
|
|
185
|
+
bun,
|
|
186
|
+
spawnArgs,
|
|
187
|
+
{
|
|
188
|
+
env: spawnEnv,
|
|
189
|
+
cwd: serverCwd,
|
|
190
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
191
|
+
detached: false,
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
log(`子进程 pid: ${child.pid}`);
|
|
196
|
+
|
|
197
|
+
// 收集子进程输出用于诊断
|
|
198
|
+
let childStdout = '';
|
|
199
|
+
let childStderr = '';
|
|
200
|
+
child.stdout?.on('data', (d: Buffer) => {
|
|
201
|
+
const s = d.toString();
|
|
202
|
+
childStdout += s;
|
|
203
|
+
log(`[child stdout] ${s.trim()}`);
|
|
204
|
+
});
|
|
205
|
+
child.stderr?.on('data', (d: Buffer) => {
|
|
206
|
+
const s = d.toString();
|
|
207
|
+
childStderr += s;
|
|
208
|
+
log(`[child stderr] ${s.trim()}`);
|
|
209
|
+
});
|
|
210
|
+
child.on('exit', (code: number | null, signal: string | null) => {
|
|
211
|
+
log(`子进程退出: code=${code} signal=${signal}`);
|
|
212
|
+
log(`子进程 stdout 总计: ${childStdout.slice(0, 500)}`);
|
|
213
|
+
log(`子进程 stderr 总计: ${childStderr.slice(0, 500)}`);
|
|
214
|
+
});
|
|
215
|
+
child.on('error', (err: Error) => {
|
|
216
|
+
log(`子进程 spawn error: ${err.message}`);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
log(`开始健康检查,超时 ${HEALTH_TIMEOUT_MS}ms`);
|
|
220
|
+
await waitHealthy(baseUrl, HEALTH_TIMEOUT_MS);
|
|
221
|
+
log(`健康检查通过`);
|
|
222
|
+
writeJson(LOCK_JSON, { pid: child.pid, port, startedAt: new Date().toISOString() });
|
|
223
|
+
} catch (e: any) {
|
|
224
|
+
log(`启动失败: ${e?.message}`);
|
|
225
|
+
if (child?.pid) {
|
|
226
|
+
log(`清理子进程 pid=${child.pid}`);
|
|
227
|
+
try {
|
|
228
|
+
if (process.platform === 'win32') spawn('taskkill', ['/PID', String(child.pid), '/T', '/F']);
|
|
229
|
+
else process.kill(child.pid, 'SIGTERM');
|
|
230
|
+
} catch {}
|
|
231
|
+
}
|
|
232
|
+
throw e;
|
|
233
|
+
} finally {
|
|
234
|
+
rmSafe(BOOT_LOCK);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
await acquireLease();
|
|
238
|
+
log(`启动完成 pid=${child.pid} baseUrl=${baseUrl}`);
|
|
239
|
+
return { baseUrl, stopIfLast: makeStopIfLast(child.pid, baseUrl), pid: child.pid };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function makeStopIfLast(serverPid: number, baseUrl: string) {
|
|
243
|
+
return async () => {
|
|
244
|
+
const rest = countValidLeases();
|
|
245
|
+
if (rest > 0) return;
|
|
246
|
+
let healthy = false;
|
|
247
|
+
try { await waitHealthy(baseUrl, 800); healthy = true; } catch {}
|
|
248
|
+
if (healthy && serverPid && isPidAlive(serverPid)) {
|
|
249
|
+
try {
|
|
250
|
+
if (process.platform === 'win32') spawn('taskkill', ['/PID', String(serverPid), '/T', '/F']);
|
|
251
|
+
else process.kill(serverPid, 'SIGTERM');
|
|
252
|
+
} catch {}
|
|
253
|
+
}
|
|
254
|
+
rmSafe(LOCK_JSON);
|
|
255
|
+
};
|
|
256
|
+
}
|