@noleemits/vision-builder-control-mcp 4.47.0 → 4.48.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/index.js +64 -8
- package/package.json +1 -1
- package/parent-watchdog.js +266 -94
package/index.js
CHANGED
|
@@ -111,7 +111,7 @@ process.on('SIGINT', () => {
|
|
|
111
111
|
// CONFIG
|
|
112
112
|
// ================================================================
|
|
113
113
|
|
|
114
|
-
const VERSION = '4.
|
|
114
|
+
const VERSION = '4.48.1';
|
|
115
115
|
const MIN_PLUGIN_VERSION = '4.13.0'; // Minimum WP plugin version required by this MCP server
|
|
116
116
|
|
|
117
117
|
// ================================================================
|
|
@@ -3887,6 +3887,18 @@ function getToolDefinitions() {
|
|
|
3887
3887
|
},
|
|
3888
3888
|
required: ['page_id']
|
|
3889
3889
|
}
|
|
3890
|
+
},
|
|
3891
|
+
// ── Class Registry ──
|
|
3892
|
+
{
|
|
3893
|
+
name: 'list_classes',
|
|
3894
|
+
description: 'List all registered CSS classes in the NVBC class registry. Returns classes with name, description, category, and which element types they apply to. Use to discover available classes before applying them via update_element. Optionally filter by category or element type.',
|
|
3895
|
+
inputSchema: {
|
|
3896
|
+
type: 'object',
|
|
3897
|
+
properties: {
|
|
3898
|
+
category: { type: 'string', enum: ['layout', 'cards', 'typography', 'hero', 'contact', 'decorative', 'utility'], description: 'Filter to a specific category.' },
|
|
3899
|
+
applies_to: { type: 'string', enum: ['container', 'widget', 'heading', 'button', 'image', 'any'], description: 'Filter to classes that apply to this element type.' }
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3890
3902
|
}
|
|
3891
3903
|
];
|
|
3892
3904
|
}
|
|
@@ -7258,6 +7270,37 @@ async function handleToolCall(name, args) {
|
|
|
7258
7270
|
return ok(`Front page set to "${r.front_page_title}" (ID: ${r.front_page_id})${r.blog_page_id ? `\nBlog page: ${r.blog_page_id}` : ''}`);
|
|
7259
7271
|
}
|
|
7260
7272
|
|
|
7273
|
+
case 'list_classes': {
|
|
7274
|
+
const params = new URLSearchParams();
|
|
7275
|
+
if (args.category) params.set('category', args.category);
|
|
7276
|
+
if (args.applies_to) params.set('applies_to', args.applies_to);
|
|
7277
|
+
|
|
7278
|
+
const r = await apiCall(`/class-registry?${params}`);
|
|
7279
|
+
|
|
7280
|
+
let msg = `=== CSS CLASS REGISTRY (${r.total} classes) ===\n`;
|
|
7281
|
+
msg += `Categories: ${r.categories.join(', ')}\n\n`;
|
|
7282
|
+
|
|
7283
|
+
const byCategory = {};
|
|
7284
|
+
for (const c of r.classes) {
|
|
7285
|
+
if (!byCategory[c.category]) byCategory[c.category] = [];
|
|
7286
|
+
byCategory[c.category].push(c);
|
|
7287
|
+
}
|
|
7288
|
+
|
|
7289
|
+
for (const [cat, items] of Object.entries(byCategory)) {
|
|
7290
|
+
msg += `── ${cat.toUpperCase()} ──\n`;
|
|
7291
|
+
for (const c of items) {
|
|
7292
|
+
const tags = c.applies_to.join(', ');
|
|
7293
|
+
const src = c.source === 'plugin' ? '[plugin]' : '[site]';
|
|
7294
|
+
msg += ` .${c.name} ${src}\n`;
|
|
7295
|
+
msg += ` ${c.description}\n`;
|
|
7296
|
+
msg += ` Applies to: ${tags}\n`;
|
|
7297
|
+
}
|
|
7298
|
+
msg += '\n';
|
|
7299
|
+
}
|
|
7300
|
+
|
|
7301
|
+
return ok(msg);
|
|
7302
|
+
}
|
|
7303
|
+
|
|
7261
7304
|
default:
|
|
7262
7305
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
7263
7306
|
}
|
|
@@ -7319,18 +7362,31 @@ async function startStdio() {
|
|
|
7319
7362
|
// NVBC_MCP_PARENT_WATCH=0. No-op in HTTP mode (only called from stdio).
|
|
7320
7363
|
startParentWatchdog();
|
|
7321
7364
|
|
|
7322
|
-
//
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7365
|
+
// Exit when the Claude session closes its end of the stdin pipe.
|
|
7366
|
+
// This is the primary self-destruct mechanism — the parent-watchdog is backup
|
|
7367
|
+
// for cases where Windows handle inheritance keeps stdin open after the parent dies.
|
|
7368
|
+
// Grace period: 5s covers any brief reconnects, then we shut down cleanly.
|
|
7369
|
+
let _exitScheduled = false;
|
|
7370
|
+
const scheduleExit = (reason) => {
|
|
7371
|
+
if (_exitScheduled) return;
|
|
7372
|
+
_exitScheduled = true;
|
|
7373
|
+
console.error(`[LIFECYCLE] ${reason} — exiting in 5s`);
|
|
7374
|
+
clearInterval(global._mcpKeepAlive);
|
|
7375
|
+
setTimeout(() => {
|
|
7376
|
+
console.error('[LIFECYCLE] grace period elapsed, exiting');
|
|
7377
|
+
process.exit(0);
|
|
7378
|
+
}, 5000);
|
|
7379
|
+
};
|
|
7380
|
+
|
|
7381
|
+
process.stdin.on('end', () => scheduleExit('stdin ended'));
|
|
7382
|
+
process.stdin.on('close', () => scheduleExit('stdin closed'));
|
|
7329
7383
|
process.stdin.on('error', (err) => {
|
|
7330
7384
|
console.error('[LIFECYCLE] stdin error:', err.message);
|
|
7385
|
+
scheduleExit('stdin error');
|
|
7331
7386
|
});
|
|
7332
7387
|
process.stdout.on('error', (err) => {
|
|
7333
7388
|
console.error('[LIFECYCLE] stdout error:', err.message);
|
|
7389
|
+
scheduleExit('stdout error');
|
|
7334
7390
|
});
|
|
7335
7391
|
|
|
7336
7392
|
_server = createMcpServer();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noleemits/vision-builder-control-mcp",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.48.1",
|
|
4
4
|
"description": "Vision Builder Control MCP server - design token-driven page builder tools for WordPress/Elementor websites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
package/parent-watchdog.js
CHANGED
|
@@ -1,120 +1,228 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parent-watchdog for the stdio MCP server.
|
|
3
3
|
*
|
|
4
|
-
* Walks the parent
|
|
5
|
-
* (
|
|
6
|
-
*
|
|
7
|
-
* or via SIGKILL / taskkill /F / OS crash — the watchdog terminates this
|
|
8
|
-
* process. Solves the orphan-accumulation leak where the global keepalive
|
|
9
|
-
* interval in index.js was preventing exit after the host crashed.
|
|
4
|
+
* Walks the parent process chain at startup to find a stable "session anchor"
|
|
5
|
+
* (the process that represents the owning Claude session). Watches that
|
|
6
|
+
* anchor by PID + creation-time fingerprint. When it dies, this process exits.
|
|
10
7
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* Anchor selection (in priority order):
|
|
9
|
+
* 1. `claude.exe` — Claude Desktop per-window process
|
|
10
|
+
* 2. A `node.exe` whose command line references claude-code / @anthropic CLI
|
|
11
|
+
* 3. First non-shell ancestor (skip `cmd.exe`, `conhost.exe`, `sh`, `bash`)
|
|
12
|
+
* 4. Direct parent (process.ppid) — last-resort fallback
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* Why anchor selection matters:
|
|
15
|
+
* - Watching `process.ppid` alone fails on Windows when invoked via `npx -y`.
|
|
16
|
+
* The chain is: claude.exe → node(npx) → cmd.exe → node(MCP). `process.ppid`
|
|
17
|
+
* points to the `cmd.exe` shim, which dies within milliseconds of startup.
|
|
18
|
+
* Windows then recycles that PID, eventually assigning it to some unrelated
|
|
19
|
+
* long-lived process. The watchdog then sees "parent alive" forever.
|
|
20
|
+
* - Walking to the chain-top fails when running inside VS Code / Cursor: those
|
|
21
|
+
* processes outlive individual Claude sessions, so the watchdog never fires.
|
|
22
|
+
* - The fix is to pick a session-scoped anchor (claude.exe per window, or the
|
|
23
|
+
* claude-code node in VS Code) and validate by creation-time to defeat
|
|
24
|
+
* PID recycling.
|
|
25
|
+
*
|
|
26
|
+
* Env controls:
|
|
27
|
+
* NVBC_MCP_PARENT_WATCH=0 Disable watchdog entirely
|
|
28
|
+
* NVBC_MCP_WATCHDOG_OWNER_PID=N Override anchor selection (use PID N)
|
|
29
|
+
* NVBC_MCP_WATCHDOG_TRACE=1 Verbose logging of chain walk + ticks
|
|
15
30
|
*/
|
|
16
31
|
|
|
17
32
|
import { execSync } from 'node:child_process';
|
|
18
|
-
import { readFileSync } from 'node:fs';
|
|
19
|
-
import { platform } from 'node:os';
|
|
20
33
|
|
|
21
34
|
const CHECK_INTERVAL_MS = 7000;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
'
|
|
26
|
-
'smss.exe', 'csrss.exe', 'lsass.exe', 'winlogon.exe',
|
|
27
|
-
'system', 'system idle process', 'registry', 'memory compression',
|
|
35
|
+
// Re-verify creation time every N ticks (catches PID recycling within ~N*7s).
|
|
36
|
+
const RECYCLE_CHECK_EVERY_N_TICKS = 8;
|
|
37
|
+
const SHELL_NAMES = new Set([
|
|
38
|
+
'cmd.exe', 'conhost.exe', 'cmd', 'sh', 'bash', 'zsh', 'fish', 'dash', 'ksh'
|
|
28
39
|
]);
|
|
40
|
+
const TRACE = process.env.NVBC_MCP_WATCHDOG_TRACE === '1';
|
|
29
41
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
function trace(msg) {
|
|
43
|
+
if (TRACE) console.error(`[WATCHDOG:TRACE] ${msg}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isAlive(pid) {
|
|
47
|
+
try {
|
|
48
|
+
process.kill(pid, 0);
|
|
49
|
+
return true;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
if (e.code === 'ESRCH') return false;
|
|
52
|
+
// EPERM: process exists but we can't signal it — still alive.
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Process-snapshot helpers (one-shot at startup, then cheap PID checks)
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
function parseCimDate(v) {
|
|
62
|
+
if (v == null) return null;
|
|
63
|
+
if (typeof v === 'number') return v;
|
|
64
|
+
if (typeof v === 'object' && v.value != null) return parseCimDate(v.value);
|
|
65
|
+
const s = String(v);
|
|
66
|
+
// PowerShell ConvertTo-Json on a DateTime emits /Date(milliseconds)/
|
|
67
|
+
const m = s.match(/\/Date\((-?\d+)\)\//);
|
|
68
|
+
if (m) return parseInt(m[1], 10);
|
|
69
|
+
// Or ISO 8601
|
|
70
|
+
const t = Date.parse(s);
|
|
71
|
+
return Number.isFinite(t) ? t : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function snapshotWin32() {
|
|
75
|
+
// Single PowerShell call. ConvertTo-Json -Depth 2 keeps CreationDate as
|
|
76
|
+
// a /Date(...)/ string. Cap rows defensively in case of huge process tables.
|
|
77
|
+
const ps = [
|
|
78
|
+
'Get-CimInstance Win32_Process',
|
|
79
|
+
'Select-Object ProcessId,ParentProcessId,Name,CommandLine,CreationDate',
|
|
80
|
+
'ConvertTo-Json -Compress -Depth 2'
|
|
81
|
+
].join(' | ');
|
|
82
|
+
const cmd = `powershell -NoProfile -NonInteractive -Command "${ps}"`;
|
|
83
|
+
const out = execSync(cmd, {
|
|
84
|
+
encoding: 'utf8',
|
|
85
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
86
|
+
timeout: 15000,
|
|
87
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
88
|
+
});
|
|
89
|
+
const arr = JSON.parse(out);
|
|
90
|
+
const list = Array.isArray(arr) ? arr : [arr];
|
|
42
91
|
const map = new Map();
|
|
43
|
-
for (const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
ppid:
|
|
47
|
-
name:
|
|
92
|
+
for (const p of list) {
|
|
93
|
+
if (!p || typeof p.ProcessId !== 'number') continue;
|
|
94
|
+
map.set(p.ProcessId, {
|
|
95
|
+
ppid: typeof p.ParentProcessId === 'number' ? p.ParentProcessId : 0,
|
|
96
|
+
name: p.Name || '',
|
|
97
|
+
cmdline: p.CommandLine || '',
|
|
98
|
+
createDate: parseCimDate(p.CreationDate),
|
|
48
99
|
});
|
|
49
100
|
}
|
|
50
101
|
return map;
|
|
51
102
|
}
|
|
52
103
|
|
|
53
104
|
function snapshotPosix() {
|
|
54
|
-
// ps
|
|
55
|
-
|
|
56
|
-
|
|
105
|
+
// `ps -axo pid=,ppid=,lstart=,command=` is portable across Linux/macOS.
|
|
106
|
+
// lstart is 5 whitespace-separated tokens (Day Mon DD HH:MM:SS YYYY).
|
|
107
|
+
const out = execSync('ps -axo pid=,ppid=,lstart=,command=', {
|
|
108
|
+
encoding: 'utf8',
|
|
109
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
110
|
+
timeout: 5000,
|
|
111
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
57
112
|
});
|
|
58
113
|
const map = new Map();
|
|
59
114
|
for (const line of out.split('\n')) {
|
|
60
|
-
const m = line.match(/^\s*(\d+)\s+(\d+)\s+(
|
|
115
|
+
const m = line.match(/^\s*(\d+)\s+(\d+)\s+(\S+\s+\S+\s+\S+\s+\S+\s+\S+)\s+(.+)$/);
|
|
61
116
|
if (!m) continue;
|
|
62
117
|
const pid = parseInt(m[1], 10);
|
|
63
118
|
const ppid = parseInt(m[2], 10);
|
|
64
|
-
|
|
119
|
+
const lstart = m[3];
|
|
120
|
+
const cmdline = m[4];
|
|
121
|
+
const name = (cmdline.split(/\s+/)[0] || '').split('/').pop() || '';
|
|
122
|
+
const ts = Date.parse(lstart);
|
|
123
|
+
map.set(pid, {
|
|
124
|
+
ppid,
|
|
125
|
+
name,
|
|
126
|
+
cmdline,
|
|
127
|
+
createDate: Number.isFinite(ts) ? ts : null,
|
|
128
|
+
});
|
|
65
129
|
}
|
|
66
130
|
return map;
|
|
67
131
|
}
|
|
68
132
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
133
|
+
function snapshotProcesses() {
|
|
134
|
+
return process.platform === 'win32' ? snapshotWin32() : snapshotPosix();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Single-PID creation-time refresh (used during recycle checks).
|
|
138
|
+
function fetchCreateDate(pid) {
|
|
139
|
+
if (process.platform === 'win32') {
|
|
140
|
+
const cmd = `powershell -NoProfile -NonInteractive -Command "(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}').CreationDate"`;
|
|
141
|
+
const out = execSync(cmd, {
|
|
142
|
+
encoding: 'utf8',
|
|
143
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
144
|
+
timeout: 5000,
|
|
145
|
+
}).trim();
|
|
146
|
+
if (!out) return null;
|
|
147
|
+
return parseCimDate(out);
|
|
148
|
+
}
|
|
149
|
+
// POSIX: use `ps -o lstart= -p <pid>` so the value matches snapshotPosix.
|
|
76
150
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
151
|
+
const out = execSync(`ps -o lstart= -p ${pid}`, {
|
|
152
|
+
encoding: 'utf8',
|
|
153
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
154
|
+
timeout: 3000,
|
|
155
|
+
}).trim();
|
|
156
|
+
if (!out) return null;
|
|
157
|
+
const t = Date.parse(out);
|
|
158
|
+
return Number.isFinite(t) ? t : null;
|
|
159
|
+
} catch (e) {
|
|
160
|
+
if (e.status === 1) return null; // ps returns 1 when no such pid
|
|
161
|
+
throw e;
|
|
80
162
|
}
|
|
163
|
+
}
|
|
81
164
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Chain walking and anchor selection
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
85
168
|
|
|
86
|
-
|
|
169
|
+
function walkChain(snapshot, startPid, maxDepth = 30) {
|
|
170
|
+
const chain = [];
|
|
171
|
+
const seen = new Set();
|
|
172
|
+
let cur = startPid;
|
|
173
|
+
for (let i = 0; i < maxDepth; i++) {
|
|
174
|
+
if (!cur || cur <= 1 || seen.has(cur)) break;
|
|
175
|
+
seen.add(cur);
|
|
87
176
|
const info = snapshot.get(cur);
|
|
88
|
-
if (!info
|
|
177
|
+
if (!info) break;
|
|
178
|
+
chain.push({ pid: cur, ...info });
|
|
179
|
+
cur = info.ppid;
|
|
180
|
+
}
|
|
181
|
+
return chain;
|
|
182
|
+
}
|
|
89
183
|
|
|
90
|
-
|
|
91
|
-
if (ppid === 0) break; // Windows pseudo-root.
|
|
92
|
-
if (os !== 'win32' && ppid === 1) break; // POSIX init/systemd/launchd.
|
|
184
|
+
function lower(s) { return (s || '').toLowerCase(); }
|
|
93
185
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
186
|
+
function looksLikeClaudeCodeNode(entry) {
|
|
187
|
+
const name = lower(entry.name);
|
|
188
|
+
if (!/^node(\.exe)?$/.test(name)) return false;
|
|
189
|
+
const cmd = lower(entry.cmdline);
|
|
190
|
+
if (!cmd) return false;
|
|
191
|
+
// Match common ways the Claude Code CLI appears in the command line.
|
|
192
|
+
return (
|
|
193
|
+
cmd.includes('claude-code') ||
|
|
194
|
+
cmd.includes('@anthropic') ||
|
|
195
|
+
/[\\/]claude(?:\.cmd|\.js)?(?:\s|$)/.test(cmd) ||
|
|
196
|
+
cmd.includes('claude/cli.js') ||
|
|
197
|
+
cmd.includes('claude\\cli.js')
|
|
198
|
+
);
|
|
199
|
+
}
|
|
99
200
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
201
|
+
function findAnchor(chain) {
|
|
202
|
+
if (chain.length === 0) return null;
|
|
103
203
|
|
|
104
|
-
|
|
105
|
-
|
|
204
|
+
// 1. Claude Desktop per-window process.
|
|
205
|
+
const claudeDesktop = chain.find(x => /^claude\.exe$/i.test(x.name));
|
|
206
|
+
if (claudeDesktop) return { ...claudeDesktop, reason: 'claude_desktop' };
|
|
106
207
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
208
|
+
// 2. Claude Code CLI node process (VS Code / terminal).
|
|
209
|
+
const claudeCli = chain.find(looksLikeClaudeCodeNode);
|
|
210
|
+
if (claudeCli) return { ...claudeCli, reason: 'claude_code_cli' };
|
|
211
|
+
|
|
212
|
+
// 3. First non-shell ancestor (skips cmd.exe between npx and the real MCP).
|
|
213
|
+
// chain[0] is ourselves — start the search at index 1.
|
|
214
|
+
const nonShell = chain.slice(1).find(x => !SHELL_NAMES.has(lower(x.name)));
|
|
215
|
+
if (nonShell) return { ...nonShell, reason: 'first_non_shell_ancestor' };
|
|
216
|
+
|
|
217
|
+
// 4. Last-resort: direct parent.
|
|
218
|
+
if (chain[1]) return { ...chain[1], reason: 'direct_parent_fallback' };
|
|
219
|
+
return null;
|
|
116
220
|
}
|
|
117
221
|
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// Public API
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
|
|
118
226
|
/**
|
|
119
227
|
* Start the parent-watchdog. Should only be called from stdio mode.
|
|
120
228
|
*
|
|
@@ -126,38 +234,102 @@ export function startParentWatchdog() {
|
|
|
126
234
|
return { ownerPid: null, enabled: false, reason: 'env_disabled' };
|
|
127
235
|
}
|
|
128
236
|
|
|
129
|
-
let
|
|
237
|
+
let snapshot;
|
|
238
|
+
try {
|
|
239
|
+
snapshot = snapshotProcesses();
|
|
240
|
+
} catch (e) {
|
|
241
|
+
console.error(`[WATCHDOG] snapshot failed: ${e.message} — falling back to ppid`);
|
|
242
|
+
return startFallbackPpidWatch();
|
|
243
|
+
}
|
|
130
244
|
|
|
131
|
-
//
|
|
132
|
-
|
|
245
|
+
// Walk our chain.
|
|
246
|
+
const chain = walkChain(snapshot, process.pid);
|
|
247
|
+
if (TRACE) {
|
|
248
|
+
for (const entry of chain) {
|
|
249
|
+
trace(`chain pid=${entry.pid} ppid=${entry.ppid} name=${entry.name} createDate=${entry.createDate}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Optional override via env var.
|
|
254
|
+
let anchor = null;
|
|
133
255
|
const override = parseInt(process.env.NVBC_MCP_WATCHDOG_OWNER_PID || '', 10);
|
|
134
256
|
if (Number.isFinite(override) && override > 0) {
|
|
135
|
-
|
|
257
|
+
const info = snapshot.get(override);
|
|
258
|
+
anchor = info
|
|
259
|
+
? { pid: override, ...info, reason: 'env_override' }
|
|
260
|
+
: { pid: override, name: 'unknown', cmdline: '', createDate: null, ppid: 0, reason: 'env_override_unsnapshot' };
|
|
136
261
|
} else {
|
|
137
|
-
|
|
138
|
-
ownerPid = findOwningAncestor();
|
|
139
|
-
} catch (err) {
|
|
140
|
-
console.error(`[WATCHDOG] ancestry walk failed: ${err.message} — watchdog disabled`);
|
|
141
|
-
return { ownerPid: null, enabled: false, reason: 'walk_failed' };
|
|
142
|
-
}
|
|
262
|
+
anchor = findAnchor(chain);
|
|
143
263
|
}
|
|
144
264
|
|
|
145
|
-
if (!
|
|
146
|
-
console.error('[WATCHDOG]
|
|
147
|
-
return { ownerPid: null, enabled: false, reason: '
|
|
265
|
+
if (!anchor || !anchor.pid || anchor.pid <= 1) {
|
|
266
|
+
console.error('[WATCHDOG] could not identify anchor process — watchdog disabled');
|
|
267
|
+
return { ownerPid: null, enabled: false, reason: 'no_anchor' };
|
|
148
268
|
}
|
|
149
269
|
|
|
150
|
-
|
|
270
|
+
const cachedCreateDate = anchor.createDate;
|
|
271
|
+
console.error(
|
|
272
|
+
`[WATCHDOG] anchor pid=${anchor.pid} name=${anchor.name} reason=${anchor.reason}` +
|
|
273
|
+
` createDate=${cachedCreateDate ?? 'unknown'} (interval=${CHECK_INTERVAL_MS}ms)`
|
|
274
|
+
);
|
|
151
275
|
|
|
276
|
+
let tick = 0;
|
|
152
277
|
const interval = setInterval(() => {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
278
|
+
tick++;
|
|
279
|
+
// Fast existence check.
|
|
280
|
+
if (!isAlive(anchor.pid)) {
|
|
281
|
+
console.error(`[WATCHDOG] anchor pid=${anchor.pid} (${anchor.name}) is gone — shutting down`);
|
|
156
282
|
process.exit(0);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Periodic creation-time re-verification to catch PID recycling.
|
|
287
|
+
if (cachedCreateDate && tick % RECYCLE_CHECK_EVERY_N_TICKS === 0) {
|
|
288
|
+
try {
|
|
289
|
+
const current = fetchCreateDate(anchor.pid);
|
|
290
|
+
if (current == null) {
|
|
291
|
+
console.error(`[WATCHDOG] anchor pid=${anchor.pid} creation-date lookup returned empty — assuming gone, shutting down`);
|
|
292
|
+
process.exit(0);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
// Allow 2s slop for clock formatting differences.
|
|
296
|
+
if (Math.abs(current - cachedCreateDate) > 2000) {
|
|
297
|
+
console.error(
|
|
298
|
+
`[WATCHDOG] anchor pid=${anchor.pid} was recycled` +
|
|
299
|
+
` (createDate cached=${cachedCreateDate} current=${current}) — shutting down`
|
|
300
|
+
);
|
|
301
|
+
process.exit(0);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
trace(`recycle-check ok (tick=${tick}, pid=${anchor.pid})`);
|
|
305
|
+
} catch (e) {
|
|
306
|
+
console.error(`[WATCHDOG] recycle-check failed (${e.message}) — shutting down to be safe`);
|
|
307
|
+
process.exit(0);
|
|
308
|
+
}
|
|
157
309
|
}
|
|
158
310
|
}, CHECK_INTERVAL_MS);
|
|
159
311
|
|
|
312
|
+
// unref so this interval doesn't prevent exit if everything else finishes.
|
|
160
313
|
interval.unref();
|
|
161
314
|
|
|
162
|
-
return { ownerPid, enabled: true };
|
|
315
|
+
return { ownerPid: anchor.pid, enabled: true, reason: anchor.reason };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Fallback used when the process snapshot itself fails (PowerShell missing,
|
|
319
|
+
// ps unavailable, etc.). Watches process.ppid only — the pre-v4.48.1 behavior.
|
|
320
|
+
function startFallbackPpidWatch() {
|
|
321
|
+
const ownerPid = process.ppid;
|
|
322
|
+
if (!ownerPid || ownerPid <= 1) {
|
|
323
|
+
console.error('[WATCHDOG] no valid parent pid — disabled');
|
|
324
|
+
return { ownerPid: null, enabled: false, reason: 'no_parent' };
|
|
325
|
+
}
|
|
326
|
+
console.error(`[WATCHDOG] fallback: watching direct parent pid=${ownerPid} (interval=${CHECK_INTERVAL_MS}ms)`);
|
|
327
|
+
const interval = setInterval(() => {
|
|
328
|
+
if (!isAlive(ownerPid)) {
|
|
329
|
+
console.error(`[WATCHDOG] parent pid=${ownerPid} is gone — shutting down`);
|
|
330
|
+
process.exit(0);
|
|
331
|
+
}
|
|
332
|
+
}, CHECK_INTERVAL_MS);
|
|
333
|
+
interval.unref();
|
|
334
|
+
return { ownerPid, enabled: true, reason: 'fallback_ppid' };
|
|
163
335
|
}
|