magector 2.2.0 → 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.
Files changed (2) hide show
  1. package/package.json +5 -5
  2. package/src/mcp-server.js +76 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magector",
3
- "version": "2.2.0",
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.2.0",
43
- "@magector/cli-linux-x64": "2.2.0",
44
- "@magector/cli-linux-arm64": "2.2.0",
45
- "@magector/cli-win32-x64": "2.2.0"
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
@@ -17,7 +17,7 @@ import {
17
17
  import { execFileSync, spawn } from 'child_process';
18
18
  import { createInterface } from 'readline';
19
19
  import { createServer as createNetServer, createConnection } from 'net';
20
- import { existsSync, statSync, unlinkSync, copyFileSync, renameSync, appendFileSync, writeFileSync, readFileSync, mkdirSync } from 'fs';
20
+ import { existsSync, statSync, unlinkSync, copyFileSync, renameSync, appendFileSync, writeFileSync, readFileSync, mkdirSync, openSync, closeSync, constants as fsConstants } from 'fs';
21
21
  import { stat } from 'fs/promises';
22
22
  import { glob } from 'glob';
23
23
  import path from 'path';
@@ -145,6 +145,49 @@ const PID_PATH = path.join(config.magentoRoot, '.magector', 'serve.pid');
145
145
  const REINDEX_PID_PATH = path.join(config.magentoRoot, '.magector', 'reindex.pid');
146
146
  const SOCK_PATH = path.join(config.magentoRoot, '.magector', 'serve.sock');
147
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
+ }
148
191
 
149
192
  /**
150
193
  * Write the serve process PID to disk so future instances can clean up orphans.
@@ -3379,15 +3422,22 @@ async function main() {
3379
3422
  logToFile('INFO', 'Magector MCP server connected (warming up...)');
3380
3423
  console.error('Magector MCP server connected (warming up...)');
3381
3424
 
3382
- // ── Singleton serve: try connecting to existing instance first ──
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)
3383
3429
  try {
3384
- // Try to connect to an existing serve process via Unix socket
3430
+ let role = 'secondary';
3431
+
3385
3432
  const connected = await tryConnectSocket();
3386
3433
  if (connected) {
3387
- logToFile('INFO', 'Joined existing serve process (singleton mode)');
3388
- console.error('Joined existing serve process (singleton mode)');
3389
- } else {
3390
- // We are the primary instance — check DB format and start serve
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)
3391
3441
  if (existsSync(config.dbPath)) {
3392
3442
  if (!(await checkDbFormat())) {
3393
3443
  logToFile('WARN', 'Database format incompatible — scheduling background re-index');
@@ -3407,12 +3457,11 @@ async function main() {
3407
3457
  if (serveReadyPromise) {
3408
3458
  const ready = await Promise.race([
3409
3459
  serveReadyPromise,
3410
- new Promise(r => setTimeout(() => r(false), 15000))
3460
+ new Promise(r => setTimeout(() => r(false), 60000))
3411
3461
  ]);
3412
3462
  if (ready) {
3413
- logToFile('INFO', 'Serve process ready (primary instance)');
3414
- console.error('Serve process ready (primary instance)');
3415
- // Start socket proxy so other instances can connect
3463
+ logToFile('INFO', 'Serve process ready (primary)');
3464
+ console.error('Serve process ready (primary)');
3416
3465
  startSocketProxy();
3417
3466
  } else {
3418
3467
  logToFile('WARN', 'Serve process not ready in time, will use fallback');
@@ -3423,6 +3472,21 @@ async function main() {
3423
3472
  // Non-fatal: falls back to execFileSync per query
3424
3473
  }
3425
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
+ }
3426
3490
  }
3427
3491
 
3428
3492
  await loadDescriptions();
@@ -3451,6 +3515,7 @@ function cleanup(reason) {
3451
3515
  try { if (existsSync(SOCK_PATH)) unlinkSync(SOCK_PATH); } catch {}
3452
3516
  socketServer = null;
3453
3517
  }
3518
+ releasePrimaryLock();
3454
3519
  removePidFile();
3455
3520
  removeReindexPidFile();
3456
3521
  }