magector 2.1.9 → 2.2.1
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 +5 -5
- package/src/mcp-server.js +298 -64
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magector",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Semantic code search for Magento 2 — index, search, MCP server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/mcp-server.js",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"ruvector": "^0.1.96"
|
|
40
40
|
},
|
|
41
41
|
"optionalDependencies": {
|
|
42
|
-
"@magector/cli-darwin-arm64": "2.1
|
|
43
|
-
"@magector/cli-linux-x64": "2.1
|
|
44
|
-
"@magector/cli-linux-arm64": "2.1
|
|
45
|
-
"@magector/cli-win32-x64": "2.1
|
|
42
|
+
"@magector/cli-darwin-arm64": "2.2.1",
|
|
43
|
+
"@magector/cli-linux-x64": "2.2.1",
|
|
44
|
+
"@magector/cli-linux-arm64": "2.2.1",
|
|
45
|
+
"@magector/cli-win32-x64": "2.2.1"
|
|
46
46
|
},
|
|
47
47
|
"keywords": [
|
|
48
48
|
"magento",
|
package/src/mcp-server.js
CHANGED
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
17
17
|
import { execFileSync, spawn } from 'child_process';
|
|
18
18
|
import { createInterface } from 'readline';
|
|
19
|
-
import {
|
|
19
|
+
import { createServer as createNetServer, createConnection } from 'net';
|
|
20
|
+
import { existsSync, statSync, unlinkSync, copyFileSync, renameSync, appendFileSync, writeFileSync, readFileSync, mkdirSync, openSync, closeSync, constants as fsConstants } from 'fs';
|
|
20
21
|
import { stat } from 'fs/promises';
|
|
21
22
|
import { glob } from 'glob';
|
|
22
23
|
import path from 'path';
|
|
@@ -142,6 +143,51 @@ function extractJson(stdout) {
|
|
|
142
143
|
|
|
143
144
|
const PID_PATH = path.join(config.magentoRoot, '.magector', 'serve.pid');
|
|
144
145
|
const REINDEX_PID_PATH = path.join(config.magentoRoot, '.magector', 'reindex.pid');
|
|
146
|
+
const SOCK_PATH = path.join(config.magentoRoot, '.magector', 'serve.sock');
|
|
147
|
+
const FORMAT_CACHE_PATH = path.join(config.magentoRoot, '.magector', 'format-ok.json');
|
|
148
|
+
const PRIMARY_LOCK_PATH = path.join(config.magentoRoot, '.magector', 'primary.lock');
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Try to acquire the primary lock (O_EXCL = atomic create-or-fail).
|
|
152
|
+
* Returns true if we are the primary instance, false if another instance holds the lock.
|
|
153
|
+
*/
|
|
154
|
+
function tryAcquirePrimaryLock() {
|
|
155
|
+
try {
|
|
156
|
+
const fd = openSync(PRIMARY_LOCK_PATH, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL);
|
|
157
|
+
writeFileSync(fd, String(process.pid));
|
|
158
|
+
closeSync(fd);
|
|
159
|
+
return true;
|
|
160
|
+
} catch {
|
|
161
|
+
// Lock file exists — check if holder is alive
|
|
162
|
+
try {
|
|
163
|
+
const pid = parseInt(readFileSync(PRIMARY_LOCK_PATH, 'utf-8').trim(), 10);
|
|
164
|
+
if (pid && !isNaN(pid)) {
|
|
165
|
+
process.kill(pid, 0); // throws if dead
|
|
166
|
+
return false; // another instance is alive and primary
|
|
167
|
+
}
|
|
168
|
+
} catch { /* holder is dead, take over */ }
|
|
169
|
+
// Stale lock — reclaim
|
|
170
|
+
try { unlinkSync(PRIMARY_LOCK_PATH); } catch {}
|
|
171
|
+
try {
|
|
172
|
+
const fd = openSync(PRIMARY_LOCK_PATH, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL);
|
|
173
|
+
writeFileSync(fd, String(process.pid));
|
|
174
|
+
closeSync(fd);
|
|
175
|
+
return true;
|
|
176
|
+
} catch {
|
|
177
|
+
return false; // another instance beat us
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function releasePrimaryLock() {
|
|
183
|
+
try {
|
|
184
|
+
// Only remove if we own it
|
|
185
|
+
const content = readFileSync(PRIMARY_LOCK_PATH, 'utf-8').trim();
|
|
186
|
+
if (content === String(process.pid)) {
|
|
187
|
+
unlinkSync(PRIMARY_LOCK_PATH);
|
|
188
|
+
}
|
|
189
|
+
} catch {}
|
|
190
|
+
}
|
|
145
191
|
|
|
146
192
|
/**
|
|
147
193
|
* Write the serve process PID to disk so future instances can clean up orphans.
|
|
@@ -180,10 +226,29 @@ function getRunningReindexPid() {
|
|
|
180
226
|
}
|
|
181
227
|
}
|
|
182
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Check if an existing serve process is alive and usable.
|
|
231
|
+
* Returns the PID if alive, null if stale/missing.
|
|
232
|
+
* Does NOT kill it — multiple MCP instances can share one serve process
|
|
233
|
+
* by sending queries to it via stdin (each instance starts its own).
|
|
234
|
+
*/
|
|
235
|
+
function getExistingServePid() {
|
|
236
|
+
try {
|
|
237
|
+
if (!existsSync(PID_PATH)) return null;
|
|
238
|
+
const pid = parseInt(readFileSync(PID_PATH, 'utf-8').trim(), 10);
|
|
239
|
+
if (!pid || isNaN(pid)) return null;
|
|
240
|
+
process.kill(pid, 0); // signal 0 = existence check
|
|
241
|
+
return pid;
|
|
242
|
+
} catch {
|
|
243
|
+
removePidFile();
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
183
248
|
/**
|
|
184
249
|
* Kill any stale serve process from a previous MCP server instance.
|
|
185
|
-
*
|
|
186
|
-
*
|
|
250
|
+
* Only called during cleanup (exit/SIGTERM), not during startup —
|
|
251
|
+
* multiple concurrent MCP instances each run their own serve process.
|
|
187
252
|
*/
|
|
188
253
|
function killStaleServeProcess() {
|
|
189
254
|
try {
|
|
@@ -191,11 +256,9 @@ function killStaleServeProcess() {
|
|
|
191
256
|
const stalePid = parseInt(readFileSync(PID_PATH, 'utf-8').trim(), 10);
|
|
192
257
|
if (!stalePid || isNaN(stalePid)) return;
|
|
193
258
|
|
|
194
|
-
// Check if the process is still alive
|
|
195
259
|
try {
|
|
196
|
-
process.kill(stalePid, 0);
|
|
260
|
+
process.kill(stalePid, 0);
|
|
197
261
|
} catch {
|
|
198
|
-
// Process doesn't exist, clean up stale PID file
|
|
199
262
|
removePidFile();
|
|
200
263
|
return;
|
|
201
264
|
}
|
|
@@ -204,14 +267,11 @@ function killStaleServeProcess() {
|
|
|
204
267
|
console.error(`Killing stale serve process (PID ${stalePid})`);
|
|
205
268
|
try { process.kill(stalePid, 'SIGTERM'); } catch {}
|
|
206
269
|
|
|
207
|
-
// Give it a moment, then force kill if still alive
|
|
208
270
|
setTimeout(() => {
|
|
209
271
|
try {
|
|
210
272
|
process.kill(stalePid, 0);
|
|
211
273
|
process.kill(stalePid, 'SIGKILL');
|
|
212
|
-
} catch {
|
|
213
|
-
// Already dead, good
|
|
214
|
-
}
|
|
274
|
+
} catch {}
|
|
215
275
|
}, 2000);
|
|
216
276
|
|
|
217
277
|
removePidFile();
|
|
@@ -224,20 +284,33 @@ function killStaleServeProcess() {
|
|
|
224
284
|
|
|
225
285
|
let reindexInProgress = false;
|
|
226
286
|
let reindexProcess = null;
|
|
287
|
+
let warmupInProgress = true; // true until checkDbFormat + serve process ready
|
|
227
288
|
|
|
228
289
|
/**
|
|
229
290
|
* Check if the database file is compatible with the current binary.
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
* take 30-60s for large indexes (80k+ vectors).
|
|
291
|
+
* Uses a cached result file to avoid running stats (30-60s) on every startup.
|
|
292
|
+
* Cache key: binary path mtime + db file mtime + db size.
|
|
233
293
|
*/
|
|
234
294
|
async function checkDbFormat() {
|
|
235
295
|
if (!existsSync(config.dbPath)) return true;
|
|
236
296
|
|
|
237
297
|
try {
|
|
238
298
|
const fstat = statSync(config.dbPath);
|
|
239
|
-
if (fstat.size < 100) return true;
|
|
299
|
+
if (fstat.size < 100) return true;
|
|
300
|
+
|
|
301
|
+
// Check cached result — avoids 40s stats command on every MCP startup
|
|
302
|
+
const binaryStat = statSync(config.rustBinary);
|
|
303
|
+
const cacheKey = `${binaryStat.mtimeMs}|${fstat.mtimeMs}|${fstat.size}`;
|
|
304
|
+
try {
|
|
305
|
+
const cached = JSON.parse(readFileSync(FORMAT_CACHE_PATH, 'utf-8'));
|
|
306
|
+
if (cached.key === cacheKey) {
|
|
307
|
+
logToFile('INFO', `Format check cached: ${cached.ok ? 'compatible' : 'incompatible'}`);
|
|
308
|
+
return cached.ok;
|
|
309
|
+
}
|
|
310
|
+
} catch { /* no cache or invalid */ }
|
|
240
311
|
|
|
312
|
+
// Cache miss — run stats (expensive: loads full HNSW graph)
|
|
313
|
+
logToFile('INFO', 'Format check: running stats (this takes 30-60s for large indexes)...');
|
|
241
314
|
const result = await new Promise((resolve, reject) => {
|
|
242
315
|
const proc = spawn(config.rustBinary, ['stats', '-d', config.dbPath],
|
|
243
316
|
{ stdio: ['pipe', 'pipe', 'pipe'], env: rustEnv });
|
|
@@ -249,7 +322,11 @@ async function checkDbFormat() {
|
|
|
249
322
|
});
|
|
250
323
|
|
|
251
324
|
const vectors = parseInt(result.match(/Total vectors:\s*(\d+)/)?.[1] || '0');
|
|
252
|
-
|
|
325
|
+
const ok = vectors > 0;
|
|
326
|
+
|
|
327
|
+
// Write cache
|
|
328
|
+
try { writeFileSync(FORMAT_CACHE_PATH, JSON.stringify({ key: cacheKey, ok })); } catch {}
|
|
329
|
+
return ok;
|
|
253
330
|
} catch {
|
|
254
331
|
return false;
|
|
255
332
|
}
|
|
@@ -566,6 +643,111 @@ function startServeProcess() {
|
|
|
566
643
|
}
|
|
567
644
|
}
|
|
568
645
|
|
|
646
|
+
// ─── Singleton Socket Proxy ──────────────────────────────────────
|
|
647
|
+
// Only one serve process runs per project. Other MCP instances connect
|
|
648
|
+
// to a Unix socket proxy instead of spawning their own serve process.
|
|
649
|
+
|
|
650
|
+
let socketServer = null;
|
|
651
|
+
let isSocketClient = false; // true if we're a secondary instance using the socket
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Start a Unix socket server that proxies queries to the local serve process.
|
|
655
|
+
* Other MCP instances connect here instead of starting their own serve.
|
|
656
|
+
*/
|
|
657
|
+
function startSocketProxy() {
|
|
658
|
+
try { if (existsSync(SOCK_PATH)) unlinkSync(SOCK_PATH); } catch {}
|
|
659
|
+
socketServer = createNetServer((conn) => {
|
|
660
|
+
const rl = createInterface({ input: conn });
|
|
661
|
+
rl.on('line', async (line) => {
|
|
662
|
+
try {
|
|
663
|
+
const req = JSON.parse(line);
|
|
664
|
+
const resp = await serveQuery(req.command, req.params || {}, req.timeout || 30000);
|
|
665
|
+
conn.write(JSON.stringify(resp) + '\n');
|
|
666
|
+
} catch (err) {
|
|
667
|
+
conn.write(JSON.stringify({ ok: false, error: err.message }) + '\n');
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
conn.on('error', () => {}); // ignore client disconnect
|
|
671
|
+
});
|
|
672
|
+
socketServer.on('error', (err) => {
|
|
673
|
+
logToFile('WARN', `Socket proxy error: ${err.message}`);
|
|
674
|
+
});
|
|
675
|
+
socketServer.listen(SOCK_PATH, () => {
|
|
676
|
+
logToFile('INFO', `Socket proxy listening on ${SOCK_PATH}`);
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Try to connect to an existing socket proxy (another MCP instance owns the serve process).
|
|
682
|
+
* Returns true if connected successfully — serveQuery will route through the socket.
|
|
683
|
+
*/
|
|
684
|
+
function tryConnectSocket() {
|
|
685
|
+
return new Promise((resolve) => {
|
|
686
|
+
if (!existsSync(SOCK_PATH)) { resolve(false); return; }
|
|
687
|
+
const conn = createConnection(SOCK_PATH);
|
|
688
|
+
const timeout = setTimeout(() => { conn.destroy(); resolve(false); }, 3000);
|
|
689
|
+
conn.on('connect', () => {
|
|
690
|
+
clearTimeout(timeout);
|
|
691
|
+
// Connection works — set up socket-based serveQuery
|
|
692
|
+
const rl = createInterface({ input: conn });
|
|
693
|
+
let pendingResolve = null;
|
|
694
|
+
|
|
695
|
+
rl.on('line', (line) => {
|
|
696
|
+
try {
|
|
697
|
+
const resp = JSON.parse(line);
|
|
698
|
+
if (pendingResolve) { pendingResolve(resp); pendingResolve = null; }
|
|
699
|
+
} catch {}
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
conn.on('error', () => {
|
|
703
|
+
isSocketClient = false;
|
|
704
|
+
serveReady = false;
|
|
705
|
+
logToFile('WARN', 'Socket connection lost — falling back to cold-start');
|
|
706
|
+
});
|
|
707
|
+
conn.on('close', () => {
|
|
708
|
+
isSocketClient = false;
|
|
709
|
+
serveReady = false;
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// Override serveQuery to route through socket
|
|
713
|
+
const socketQueryQueue = [];
|
|
714
|
+
let socketBusy = false;
|
|
715
|
+
|
|
716
|
+
async function processSocketQueue() {
|
|
717
|
+
if (socketBusy || socketQueryQueue.length === 0) return;
|
|
718
|
+
socketBusy = true;
|
|
719
|
+
const { command, params, timeoutMs, resolve: qResolve, reject: qReject } = socketQueryQueue.shift();
|
|
720
|
+
const timer = setTimeout(() => { pendingResolve = null; qReject(new Error('Socket query timeout')); socketBusy = false; processSocketQueue(); }, timeoutMs);
|
|
721
|
+
pendingResolve = (resp) => { clearTimeout(timer); qResolve(resp); socketBusy = false; processSocketQueue(); };
|
|
722
|
+
try {
|
|
723
|
+
conn.write(JSON.stringify({ command, params, timeout: timeoutMs }) + '\n');
|
|
724
|
+
} catch (err) {
|
|
725
|
+
clearTimeout(timer);
|
|
726
|
+
pendingResolve = null;
|
|
727
|
+
qReject(err);
|
|
728
|
+
socketBusy = false;
|
|
729
|
+
processSocketQueue();
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Replace the global serveQuery with socket-based version
|
|
734
|
+
globalServeQuery = (command, params, timeoutMs) => new Promise((res, rej) => {
|
|
735
|
+
socketQueryQueue.push({ command, params, timeoutMs, resolve: res, reject: rej });
|
|
736
|
+
processSocketQueue();
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
isSocketClient = true;
|
|
740
|
+
serveReady = true;
|
|
741
|
+
logToFile('INFO', 'Connected to existing serve process via socket proxy');
|
|
742
|
+
resolve(true);
|
|
743
|
+
});
|
|
744
|
+
conn.on('error', () => { clearTimeout(timeout); resolve(false); });
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Global reference to serveQuery implementation (local or socket)
|
|
749
|
+
let globalServeQuery = null;
|
|
750
|
+
|
|
569
751
|
function serveQuery(command, params = {}, timeoutMs = 30000) {
|
|
570
752
|
return new Promise((resolve, reject) => {
|
|
571
753
|
const id = serveNextId++;
|
|
@@ -597,10 +779,11 @@ async function rustSearchAsync(query, limit = 10) {
|
|
|
597
779
|
await Promise.race([serveReadyPromise, new Promise(r => setTimeout(() => r(false), 10000))]);
|
|
598
780
|
}
|
|
599
781
|
|
|
600
|
-
// Try
|
|
601
|
-
|
|
782
|
+
// Try socket proxy (secondary instance) or local serve process (primary)
|
|
783
|
+
const queryFn = globalServeQuery || ((serveProcess && serveReady) ? serveQuery : null);
|
|
784
|
+
if (queryFn) {
|
|
602
785
|
try {
|
|
603
|
-
const resp = await
|
|
786
|
+
const resp = await queryFn('search', { query, limit });
|
|
604
787
|
if (resp.ok && Array.isArray(resp.data)) {
|
|
605
788
|
cacheSet(cacheKey, resp.data);
|
|
606
789
|
return resp.data;
|
|
@@ -2333,11 +2516,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2333
2516
|
const reqStart = Date.now();
|
|
2334
2517
|
logToFile('REQ', `${name}(${JSON.stringify(args || {})})`);
|
|
2335
2518
|
|
|
2336
|
-
//
|
|
2337
|
-
// If old DB is preserved, searches keep running against it during rebuild.
|
|
2519
|
+
// ── Warmup guard: index compatibility check or serve process still loading ──
|
|
2338
2520
|
const indexFreeTools = ['magento_stats', 'magento_analyze_diff', 'magento_complexity',
|
|
2339
2521
|
'magento_trace_dependency', 'magento_error_parser', 'magento_find_layout',
|
|
2340
2522
|
'magento_impact_analysis', 'magento_find_event_flow', 'magento_find_test'];
|
|
2523
|
+
if (warmupInProgress && !indexFreeTools.includes(name)) {
|
|
2524
|
+
logToFile('REQ', `${name} → blocked (warmup: loading index)`);
|
|
2525
|
+
return {
|
|
2526
|
+
content: [{
|
|
2527
|
+
type: 'text',
|
|
2528
|
+
text: 'Magector is warming up — loading the search index into memory. This takes 30-60 seconds on first startup. Please retry your query in a moment.'
|
|
2529
|
+
}],
|
|
2530
|
+
isError: true,
|
|
2531
|
+
};
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
// Block search tools only when re-indexing AND no usable old DB exists.
|
|
2535
|
+
// If old DB is preserved, searches keep running against it during rebuild.
|
|
2341
2536
|
const hasUsableDb = existsSync(config.dbPath) && (() => { try { return statSync(config.dbPath).size > 100; } catch { return false; } })();
|
|
2342
2537
|
if (reindexInProgress && !hasUsableDb && !indexFreeTools.includes(name)) {
|
|
2343
2538
|
logToFile('REQ', `${name} → blocked (re-indexing, no usable DB)`);
|
|
@@ -3217,56 +3412,89 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
3217
3412
|
});
|
|
3218
3413
|
|
|
3219
3414
|
async function main() {
|
|
3220
|
-
//
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
// Check database format compatibility before starting serve process.
|
|
3224
|
-
// With incremental saves, a partial but valid index should be kept — don't
|
|
3225
|
-
// force a full re-index just because the previous session didn't finish.
|
|
3226
|
-
if (existsSync(config.dbPath)) {
|
|
3227
|
-
if (!(await checkDbFormat())) {
|
|
3228
|
-
logToFile('WARN', 'Database format incompatible — scheduling background re-index');
|
|
3229
|
-
startBackgroundReindex();
|
|
3230
|
-
} else {
|
|
3231
|
-
logToFile('INFO', 'Existing database is compatible — reusing index');
|
|
3232
|
-
}
|
|
3233
|
-
} else if (config.magentoRoot && existsSync(config.magentoRoot)) {
|
|
3234
|
-
// No DB file at all — need initial index
|
|
3235
|
-
logToFile('INFO', 'No index database found — scheduling background index');
|
|
3236
|
-
startBackgroundReindex();
|
|
3237
|
-
}
|
|
3415
|
+
// Don't kill existing serve processes — other MCP instances may be using them.
|
|
3416
|
+
// Each instance starts its own serve process; cleanup happens on exit.
|
|
3238
3417
|
|
|
3239
|
-
//
|
|
3240
|
-
//
|
|
3241
|
-
const
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3418
|
+
// Connect MCP transport FIRST so tools can return "warming up" messages
|
|
3419
|
+
// instead of the client hanging during index load.
|
|
3420
|
+
const transport = new StdioServerTransport();
|
|
3421
|
+
await server.connect(transport);
|
|
3422
|
+
logToFile('INFO', 'Magector MCP server connected (warming up...)');
|
|
3423
|
+
console.error('Magector MCP server connected (warming up...)');
|
|
3424
|
+
|
|
3425
|
+
// ── Singleton serve: one serve process per project ──────────────
|
|
3426
|
+
// 1. Try socket → secondary (instant, no CPU)
|
|
3427
|
+
// 2. Try lock → primary (start serve + socket proxy)
|
|
3428
|
+
// 3. Lock failed → wait for socket (another instance is starting)
|
|
3429
|
+
try {
|
|
3430
|
+
let role = 'secondary';
|
|
3431
|
+
|
|
3432
|
+
const connected = await tryConnectSocket();
|
|
3433
|
+
if (connected) {
|
|
3434
|
+
logToFile('INFO', 'Joined existing serve process via socket (secondary)');
|
|
3435
|
+
console.error('Joined existing serve process (secondary)');
|
|
3436
|
+
} else if (tryAcquirePrimaryLock()) {
|
|
3437
|
+
role = 'primary';
|
|
3438
|
+
logToFile('INFO', 'Acquired primary lock — this instance owns the serve process');
|
|
3439
|
+
|
|
3440
|
+
// Check DB format (uses cache → instant if already validated)
|
|
3441
|
+
if (existsSync(config.dbPath)) {
|
|
3442
|
+
if (!(await checkDbFormat())) {
|
|
3443
|
+
logToFile('WARN', 'Database format incompatible — scheduling background re-index');
|
|
3444
|
+
startBackgroundReindex();
|
|
3253
3445
|
} else {
|
|
3254
|
-
logToFile('
|
|
3255
|
-
console.error('Serve process not ready in time, will use fallback');
|
|
3446
|
+
logToFile('INFO', 'Existing database is compatible — reusing index');
|
|
3256
3447
|
}
|
|
3448
|
+
} else if (config.magentoRoot && existsSync(config.magentoRoot)) {
|
|
3449
|
+
logToFile('INFO', 'No index database found — scheduling background index');
|
|
3450
|
+
startBackgroundReindex();
|
|
3257
3451
|
}
|
|
3258
|
-
} catch {
|
|
3259
|
-
// Non-fatal: falls back to execFileSync per query
|
|
3260
|
-
}
|
|
3261
|
-
}
|
|
3262
3452
|
|
|
3263
|
-
|
|
3264
|
-
|
|
3453
|
+
const canStartServe = !reindexInProgress || (existsSync(config.dbPath) && (() => { try { return statSync(config.dbPath).size > 100; } catch { return false; } })());
|
|
3454
|
+
if (canStartServe) {
|
|
3455
|
+
try {
|
|
3456
|
+
startServeProcess();
|
|
3457
|
+
if (serveReadyPromise) {
|
|
3458
|
+
const ready = await Promise.race([
|
|
3459
|
+
serveReadyPromise,
|
|
3460
|
+
new Promise(r => setTimeout(() => r(false), 60000))
|
|
3461
|
+
]);
|
|
3462
|
+
if (ready) {
|
|
3463
|
+
logToFile('INFO', 'Serve process ready (primary)');
|
|
3464
|
+
console.error('Serve process ready (primary)');
|
|
3465
|
+
startSocketProxy();
|
|
3466
|
+
} else {
|
|
3467
|
+
logToFile('WARN', 'Serve process not ready in time, will use fallback');
|
|
3468
|
+
console.error('Serve process not ready in time, will use fallback');
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
} catch {
|
|
3472
|
+
// Non-fatal: falls back to execFileSync per query
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
} else {
|
|
3476
|
+
// Another instance is starting up — wait for its socket to appear
|
|
3477
|
+
logToFile('INFO', 'Another instance is primary — waiting for socket...');
|
|
3478
|
+
console.error('Waiting for primary instance to start serve process...');
|
|
3479
|
+
for (let i = 0; i < 12; i++) { // wait up to 60s
|
|
3480
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
3481
|
+
if (await tryConnectSocket()) {
|
|
3482
|
+
logToFile('INFO', 'Connected to socket after waiting (secondary)');
|
|
3483
|
+
console.error('Joined existing serve process (secondary)');
|
|
3484
|
+
break;
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
if (!globalServeQuery) {
|
|
3488
|
+
logToFile('WARN', 'Socket not available after waiting — using cold-start fallback');
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3265
3491
|
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3492
|
+
await loadDescriptions();
|
|
3493
|
+
} finally {
|
|
3494
|
+
warmupInProgress = false;
|
|
3495
|
+
logToFile('INFO', 'Warmup complete — all tools available');
|
|
3496
|
+
console.error('Warmup complete — all tools available');
|
|
3497
|
+
}
|
|
3270
3498
|
}
|
|
3271
3499
|
|
|
3272
3500
|
// Cleanup on exit — kill all child processes and remove PID file
|
|
@@ -3282,6 +3510,12 @@ function cleanup(reason) {
|
|
|
3282
3510
|
try { reindexProcess.kill(); } catch {}
|
|
3283
3511
|
reindexProcess = null;
|
|
3284
3512
|
}
|
|
3513
|
+
if (socketServer) {
|
|
3514
|
+
try { socketServer.close(); } catch {}
|
|
3515
|
+
try { if (existsSync(SOCK_PATH)) unlinkSync(SOCK_PATH); } catch {}
|
|
3516
|
+
socketServer = null;
|
|
3517
|
+
}
|
|
3518
|
+
releasePrimaryLock();
|
|
3285
3519
|
removePidFile();
|
|
3286
3520
|
removeReindexPidFile();
|
|
3287
3521
|
}
|