genexus-mcp 3.8.0 → 3.9.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/cli/commands/axi.js +65 -0
- package/cli/index.js +19 -1
- package/cli/lib/client-adapters.test.js +1 -0
- package/cli/lib/config.js +11 -0
- package/cli/lib/runtime-stager.js +645 -0
- package/cli/lib/runtime-stager.test.js +507 -0
- package/cli/run.test.js +18 -3
- package/docs/llm_cli_mcp_playbook.md +28 -0
- package/package.json +2 -2
- package/publish/GxMcp.Gateway.deps.json +2 -2
- package/publish/GxMcp.Gateway.dll +0 -0
- package/publish/GxMcp.Gateway.exe +0 -0
- package/publish/config.json +13 -13
- package/publish/gxmcp-manifest.json +15 -10
- package/publish/gxmcp-sbom.json +6 -6
- package/publish/nexus-ide.vsix +0 -0
- package/publish/tool_definitions.json +56 -56
- package/publish/worker/GxMcp.Worker.exe +0 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const { execFileSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
function resolveDefaultRuntimeRoot() {
|
|
10
|
+
if (process.env.GENEXUS_MCP_RUNTIME_DIR) {
|
|
11
|
+
return process.env.GENEXUS_MCP_RUNTIME_DIR;
|
|
12
|
+
}
|
|
13
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
14
|
+
return path.join(localAppData, 'GenexusMCP', 'runtime');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isCheckoutMode(packageRoot) {
|
|
18
|
+
try {
|
|
19
|
+
const gitMarker = fs.statSync(path.join(packageRoot, '.git'));
|
|
20
|
+
return gitMarker.isDirectory() || gitMarker.isFile();
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function shouldStage(packageRoot = path.resolve(__dirname, '..', '..')) {
|
|
27
|
+
if (process.env.GENEXUS_MCP_GATEWAY_EXE) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if (process.env.GENEXUS_MCP_NO_STAGING === '1') {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (process.env.GENEXUS_MCP_FORCE_STAGING === '1') {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
return !isCheckoutMode(packageRoot);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function computeFileSha256(filePath) {
|
|
40
|
+
const hash = crypto.createHash('sha256');
|
|
41
|
+
const buffer = fs.readFileSync(filePath);
|
|
42
|
+
hash.update(buffer);
|
|
43
|
+
return hash.digest('hex');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function computePublishFingerprint(publishDir) {
|
|
47
|
+
const manifestPath = path.join(publishDir, 'gxmcp-manifest.json');
|
|
48
|
+
if (fs.existsSync(manifestPath)) {
|
|
49
|
+
try {
|
|
50
|
+
return computeFileSha256(manifestPath).slice(0, 8);
|
|
51
|
+
} catch {
|
|
52
|
+
// fallback
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const gatewayExe = path.join(publishDir, 'GxMcp.Gateway.exe');
|
|
56
|
+
if (fs.existsSync(gatewayExe)) {
|
|
57
|
+
try {
|
|
58
|
+
return computeFileSha256(gatewayExe).slice(0, 8);
|
|
59
|
+
} catch {
|
|
60
|
+
// fallback
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return '00000000';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function verifyManifest(stagedDir, manifestPath) {
|
|
67
|
+
if (!fs.existsSync(manifestPath)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
let manifest;
|
|
71
|
+
try {
|
|
72
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
73
|
+
} catch (err) {
|
|
74
|
+
throw new Error(`Failed to parse manifest at ${manifestPath}: ${err.message}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const artifacts = Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
|
|
78
|
+
for (const artifact of artifacts) {
|
|
79
|
+
if (!artifact || !artifact.path) continue;
|
|
80
|
+
const fullPath = path.join(stagedDir, artifact.path);
|
|
81
|
+
if (!fs.existsSync(fullPath)) {
|
|
82
|
+
throw new Error(`Manifest verification failed: missing artifact '${artifact.path}'`);
|
|
83
|
+
}
|
|
84
|
+
if (typeof artifact.size === 'number') {
|
|
85
|
+
const stat = fs.statSync(fullPath);
|
|
86
|
+
if (stat.size !== artifact.size) {
|
|
87
|
+
throw new Error(`Manifest verification failed: size mismatch for '${artifact.path}' (expected ${artifact.size}, got ${stat.size})`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (artifact.sha256) {
|
|
91
|
+
const actualSha = computeFileSha256(fullPath);
|
|
92
|
+
if (actualSha.toLowerCase() !== artifact.sha256.toLowerCase()) {
|
|
93
|
+
throw new Error(`Manifest verification failed: sha256 mismatch for '${artifact.path}'`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isPathWithin(candidatePath, rootPath) {
|
|
100
|
+
if (!candidatePath || !rootPath) return false;
|
|
101
|
+
const relative = path.relative(path.resolve(rootPath), path.resolve(candidatePath));
|
|
102
|
+
return relative === '' || (
|
|
103
|
+
relative !== '..'
|
|
104
|
+
&& !relative.startsWith(`..${path.sep}`)
|
|
105
|
+
&& !path.isAbsolute(relative)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getRuntimeProcessUsage(runtimeRoot, processes, runtimeDirectories = null) {
|
|
110
|
+
const runtimes = Array.isArray(runtimeDirectories)
|
|
111
|
+
? runtimeDirectories
|
|
112
|
+
: listStagedRuntimes(runtimeRoot);
|
|
113
|
+
const usage = new Map();
|
|
114
|
+
|
|
115
|
+
for (const runtime of runtimes) {
|
|
116
|
+
const pids = (Array.isArray(processes) ? processes : [])
|
|
117
|
+
.filter((proc) => proc && isPathWithin(proc.exePath, runtime.path))
|
|
118
|
+
.map((proc) => Number(proc.pid))
|
|
119
|
+
.filter((pid) => Number.isInteger(pid) && pid > 0)
|
|
120
|
+
.sort((a, b) => a - b);
|
|
121
|
+
if (pids.length > 0) {
|
|
122
|
+
usage.set(path.resolve(runtime.path).toLowerCase(), {
|
|
123
|
+
name: runtime.name,
|
|
124
|
+
path: runtime.path,
|
|
125
|
+
pids
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return Array.from(usage.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function createRuntimeCleanupResult() {
|
|
134
|
+
return {
|
|
135
|
+
deleted: [],
|
|
136
|
+
skipped: [],
|
|
137
|
+
failed: [],
|
|
138
|
+
processProbeAvailable: true
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function readRuntimeProcessSnapshot(processProvider) {
|
|
143
|
+
let snapshot = processProvider();
|
|
144
|
+
if (snapshot && !Array.isArray(snapshot) && Array.isArray(snapshot.processes)) {
|
|
145
|
+
if (snapshot.ok === false) throw new Error(snapshot.error || 'runtime process probe failed');
|
|
146
|
+
snapshot = snapshot.processes;
|
|
147
|
+
}
|
|
148
|
+
if (!Array.isArray(snapshot)) throw new Error('runtime process probe returned no process list');
|
|
149
|
+
if (snapshot.some((proc) => !proc || !proc.exePath)) {
|
|
150
|
+
throw new Error('runtime process probe returned a process without an executable path');
|
|
151
|
+
}
|
|
152
|
+
return snapshot;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function processPidsUnder(directory, processes) {
|
|
156
|
+
return (Array.isArray(processes) ? processes : [])
|
|
157
|
+
.filter((proc) => proc && isPathWithin(proc.exePath, directory))
|
|
158
|
+
.map((proc) => Number(proc.pid))
|
|
159
|
+
.filter((pid) => Number.isInteger(pid) && pid > 0)
|
|
160
|
+
.sort((a, b) => a - b);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function processPidsUnderAny(directories, processes) {
|
|
164
|
+
const paths = Array.isArray(directories) ? directories.filter(Boolean) : [directories].filter(Boolean);
|
|
165
|
+
return [...new Set(paths.flatMap((directory) => processPidsUnder(directory, processes)))]
|
|
166
|
+
.sort((a, b) => a - b);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function originalPathForTombstone(tombstonePath) {
|
|
170
|
+
if (typeof tombstonePath !== 'string') return null;
|
|
171
|
+
const marker = tombstonePath.lastIndexOf('.deleting-');
|
|
172
|
+
return marker > 0 ? tombstonePath.slice(0, marker) : null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function cleanupRuntimeDirectory(directory, options = {}) {
|
|
176
|
+
const result = createRuntimeCleanupResult();
|
|
177
|
+
if (!directory || !fs.existsSync(directory)) return result;
|
|
178
|
+
|
|
179
|
+
const processProvider = typeof options.getRunningProcesses === 'function'
|
|
180
|
+
? options.getRunningProcesses
|
|
181
|
+
: probeRunningGxMcpProcesses;
|
|
182
|
+
const rmSync = typeof options.rmSync === 'function' ? options.rmSync : fs.rmSync;
|
|
183
|
+
const removeOptions = { recursive: true, force: true, maxRetries: 3, retryDelay: 100 };
|
|
184
|
+
const probePaths = [
|
|
185
|
+
directory,
|
|
186
|
+
...(Array.isArray(options.additionalProbePaths) ? options.additionalProbePaths : [])
|
|
187
|
+
].filter(Boolean);
|
|
188
|
+
|
|
189
|
+
let initialSnapshot;
|
|
190
|
+
try {
|
|
191
|
+
initialSnapshot = readRuntimeProcessSnapshot(processProvider);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
result.processProbeAvailable = false;
|
|
194
|
+
result.skipped.push({
|
|
195
|
+
path: directory,
|
|
196
|
+
reason: 'process-probe-unavailable',
|
|
197
|
+
pids: [],
|
|
198
|
+
error: err && err.message ? err.message : String(err)
|
|
199
|
+
});
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const initialPids = processPidsUnderAny(probePaths, initialSnapshot);
|
|
204
|
+
if (initialPids.length > 0) {
|
|
205
|
+
result.skipped.push({ path: directory, reason: 'in-use', pids: initialPids });
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let finalSnapshot;
|
|
210
|
+
try {
|
|
211
|
+
finalSnapshot = readRuntimeProcessSnapshot(processProvider);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
result.processProbeAvailable = false;
|
|
214
|
+
result.skipped.push({
|
|
215
|
+
path: directory,
|
|
216
|
+
reason: 'process-probe-unavailable-before-delete',
|
|
217
|
+
pids: [],
|
|
218
|
+
error: err && err.message ? err.message : String(err)
|
|
219
|
+
});
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const finalPids = processPidsUnderAny(probePaths, finalSnapshot);
|
|
224
|
+
if (finalPids.length > 0) {
|
|
225
|
+
result.skipped.push({ path: directory, reason: 'in-use-before-delete', pids: finalPids });
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
rmSync(directory, removeOptions);
|
|
231
|
+
result.deleted.push(directory);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
result.failed.push({ path: directory, error: err && err.message ? err.message : String(err) });
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function mergeRuntimeCleanupResults(target, additional) {
|
|
239
|
+
if (!additional) return target;
|
|
240
|
+
target.deleted.push(...additional.deleted);
|
|
241
|
+
target.skipped.push(...additional.skipped);
|
|
242
|
+
target.failed.push(...additional.failed);
|
|
243
|
+
target.processProbeAvailable = target.processProbeAvailable && additional.processProbeAvailable;
|
|
244
|
+
return target;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function cleanOldRuntimes(runtimeRoot, currentTargetDir, keepCount = 2, options = {}) {
|
|
248
|
+
const result = createRuntimeCleanupResult();
|
|
249
|
+
|
|
250
|
+
const processProvider = typeof options.getRunningProcesses === 'function'
|
|
251
|
+
? options.getRunningProcesses
|
|
252
|
+
: probeRunningGxMcpProcesses;
|
|
253
|
+
const renameSync = typeof options.renameSync === 'function' ? options.renameSync : fs.renameSync;
|
|
254
|
+
const rmSync = typeof options.rmSync === 'function' ? options.rmSync : fs.rmSync;
|
|
255
|
+
const removeOptions = { recursive: true, force: true, maxRetries: 3, retryDelay: 100 };
|
|
256
|
+
|
|
257
|
+
const readProcessSnapshot = () => readRuntimeProcessSnapshot(processProvider);
|
|
258
|
+
|
|
259
|
+
const probeImmediatelyBeforeDelete = (directory) => {
|
|
260
|
+
try {
|
|
261
|
+
const snapshot = readProcessSnapshot();
|
|
262
|
+
return { ok: true, pids: processPidsUnder(directory, snapshot) };
|
|
263
|
+
} catch (err) {
|
|
264
|
+
return { ok: false, pids: [], error: err && err.message ? err.message : String(err) };
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
if (!fs.existsSync(runtimeRoot)) return result;
|
|
270
|
+
const entries = fs.readdirSync(runtimeRoot, { withFileTypes: true });
|
|
271
|
+
const now = Date.now();
|
|
272
|
+
const versionDirs = [];
|
|
273
|
+
const tmpDirs = [];
|
|
274
|
+
const tombstoneDirs = [];
|
|
275
|
+
|
|
276
|
+
for (const entry of entries) {
|
|
277
|
+
if (!entry.isDirectory()) continue;
|
|
278
|
+
const fullPath = path.join(runtimeRoot, entry.name);
|
|
279
|
+
if (entry.name.includes('.tmp-')) {
|
|
280
|
+
tmpDirs.push(fullPath);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (entry.name.includes('.deleting-')) {
|
|
284
|
+
tombstoneDirs.push(fullPath);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
const stat = fs.statSync(fullPath);
|
|
289
|
+
versionDirs.push({ name: entry.name, path: fullPath, mtimeMs: stat.mtimeMs });
|
|
290
|
+
} catch {
|
|
291
|
+
// Ignore an entry that disappeared during enumeration.
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
versionDirs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
296
|
+
const normalizedCurrent = path.resolve(currentTargetDir).toLowerCase();
|
|
297
|
+
const keep = Number.isFinite(Number(keepCount)) ? Math.max(0, Number(keepCount)) : 2;
|
|
298
|
+
let kept = 0;
|
|
299
|
+
const toDelete = [];
|
|
300
|
+
for (const vdir of versionDirs) {
|
|
301
|
+
if (path.resolve(vdir.path).toLowerCase() === normalizedCurrent) continue;
|
|
302
|
+
if (kept < keep) kept++;
|
|
303
|
+
else toDelete.push(vdir.path);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const staleTmpDirs = tmpDirs.filter((dir) => {
|
|
307
|
+
try { return now - fs.statSync(dir).mtimeMs > 10 * 60 * 1000; } catch { return false; }
|
|
308
|
+
});
|
|
309
|
+
if (toDelete.length === 0 && staleTmpDirs.length === 0 && tombstoneDirs.length === 0) {
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Finish tombstones and stale staging directories with the same
|
|
314
|
+
// fail-closed process protection used by immediate staging cleanup.
|
|
315
|
+
for (const tombstone of tombstoneDirs) {
|
|
316
|
+
const originalPath = originalPathForTombstone(tombstone);
|
|
317
|
+
mergeRuntimeCleanupResults(result, cleanupRuntimeDirectory(tombstone, {
|
|
318
|
+
...options,
|
|
319
|
+
additionalProbePaths: originalPath ? [originalPath] : []
|
|
320
|
+
}));
|
|
321
|
+
}
|
|
322
|
+
for (const tmpDir of staleTmpDirs) {
|
|
323
|
+
mergeRuntimeCleanupResults(result, cleanupRuntimeDirectory(tmpDir, options));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (toDelete.length === 0) return result;
|
|
327
|
+
let processSnapshot;
|
|
328
|
+
try {
|
|
329
|
+
processSnapshot = readProcessSnapshot();
|
|
330
|
+
} catch (err) {
|
|
331
|
+
result.processProbeAvailable = false;
|
|
332
|
+
const error = err && err.message ? err.message : String(err);
|
|
333
|
+
for (const dir of toDelete) {
|
|
334
|
+
result.skipped.push({ path: dir, reason: 'process-probe-unavailable', pids: [], error });
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const initialUsage = new Map(
|
|
340
|
+
getRuntimeProcessUsage(runtimeRoot, processSnapshot, versionDirs)
|
|
341
|
+
.map((entry) => [path.resolve(entry.path).toLowerCase(), entry])
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
for (const dirToDelete of toDelete) {
|
|
345
|
+
let freshSnapshot;
|
|
346
|
+
try {
|
|
347
|
+
freshSnapshot = readProcessSnapshot();
|
|
348
|
+
} catch (err) {
|
|
349
|
+
result.skipped.push({
|
|
350
|
+
path: dirToDelete,
|
|
351
|
+
reason: 'process-probe-unavailable',
|
|
352
|
+
pids: [],
|
|
353
|
+
error: err && err.message ? err.message : String(err)
|
|
354
|
+
});
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const freshUsage = getRuntimeProcessUsage(runtimeRoot, freshSnapshot, versionDirs)
|
|
359
|
+
.find((entry) => path.resolve(entry.path).toLowerCase() === path.resolve(dirToDelete).toLowerCase());
|
|
360
|
+
if (freshUsage || initialUsage.has(path.resolve(dirToDelete).toLowerCase())) {
|
|
361
|
+
const usage = freshUsage || initialUsage.get(path.resolve(dirToDelete).toLowerCase());
|
|
362
|
+
result.skipped.push({ path: dirToDelete, reason: 'in-use', pids: usage.pids });
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Rename first. The post-rename probe below closes the normal
|
|
367
|
+
// TOCTOU window; a process that appears while retiring is restored
|
|
368
|
+
// to its original name before any recursive removal is attempted.
|
|
369
|
+
const deletingPath = `${dirToDelete}.deleting-${process.pid}-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
370
|
+
try {
|
|
371
|
+
renameSync(dirToDelete, deletingPath);
|
|
372
|
+
} catch (err) {
|
|
373
|
+
result.failed.push({ path: dirToDelete, error: err && err.message ? err.message : String(err) });
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
let postSnapshot;
|
|
378
|
+
try {
|
|
379
|
+
postSnapshot = readProcessSnapshot();
|
|
380
|
+
} catch (err) {
|
|
381
|
+
try { renameSync(deletingPath, dirToDelete); } catch { }
|
|
382
|
+
result.skipped.push({
|
|
383
|
+
path: dirToDelete,
|
|
384
|
+
reason: 'process-probe-unavailable-after-rename',
|
|
385
|
+
pids: [],
|
|
386
|
+
error: err && err.message ? err.message : String(err)
|
|
387
|
+
});
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const postPids = [
|
|
391
|
+
...processPidsUnder(dirToDelete, postSnapshot),
|
|
392
|
+
...processPidsUnder(deletingPath, postSnapshot)
|
|
393
|
+
].filter((pid, index, all) => all.indexOf(pid) === index).sort((a, b) => a - b);
|
|
394
|
+
if (postPids.length > 0) {
|
|
395
|
+
let restored = true;
|
|
396
|
+
try { renameSync(deletingPath, dirToDelete); } catch { restored = false; }
|
|
397
|
+
result.skipped.push({
|
|
398
|
+
path: dirToDelete,
|
|
399
|
+
reason: 'in-use-after-rename',
|
|
400
|
+
pids: postPids,
|
|
401
|
+
restored
|
|
402
|
+
});
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const finalDeletingProbe = probeImmediatelyBeforeDelete(deletingPath);
|
|
406
|
+
if (!finalDeletingProbe.ok) {
|
|
407
|
+
let restored = true;
|
|
408
|
+
try { renameSync(deletingPath, dirToDelete); } catch { restored = false; }
|
|
409
|
+
result.skipped.push({
|
|
410
|
+
path: dirToDelete,
|
|
411
|
+
reason: 'process-probe-unavailable-before-delete',
|
|
412
|
+
pids: [],
|
|
413
|
+
error: finalDeletingProbe.error,
|
|
414
|
+
restored
|
|
415
|
+
});
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (finalDeletingProbe.pids.length > 0) {
|
|
419
|
+
let restored = true;
|
|
420
|
+
try { renameSync(deletingPath, dirToDelete); } catch { restored = false; }
|
|
421
|
+
result.skipped.push({
|
|
422
|
+
path: dirToDelete,
|
|
423
|
+
reason: 'in-use-before-delete',
|
|
424
|
+
pids: finalDeletingProbe.pids,
|
|
425
|
+
restored
|
|
426
|
+
});
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
rmSync(deletingPath, removeOptions);
|
|
432
|
+
result.deleted.push(dirToDelete);
|
|
433
|
+
} catch (err) {
|
|
434
|
+
result.failed.push({
|
|
435
|
+
path: dirToDelete,
|
|
436
|
+
deletingPath,
|
|
437
|
+
error: err && err.message ? err.message : String(err)
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
} catch (err) {
|
|
442
|
+
result.failed.push({ path: runtimeRoot, error: err && err.message ? err.message : String(err) });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return result;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function ensureStagedGateway(options = {}) {
|
|
449
|
+
const packageRoot = options.packageRoot || path.resolve(__dirname, '..', '..');
|
|
450
|
+
const publishDir = options.publishDir || path.join(packageRoot, 'publish');
|
|
451
|
+
const defaultGatewayExe = options.defaultGatewayExe || path.join(publishDir, 'GxMcp.Gateway.exe');
|
|
452
|
+
|
|
453
|
+
if (!options.forceStaging && !shouldStage(packageRoot)) {
|
|
454
|
+
return {
|
|
455
|
+
staged: false,
|
|
456
|
+
gatewayExePath: process.env.GENEXUS_MCP_GATEWAY_EXE || defaultGatewayExe,
|
|
457
|
+
runtimeDir: path.dirname(process.env.GENEXUS_MCP_GATEWAY_EXE || defaultGatewayExe),
|
|
458
|
+
reason: process.env.GENEXUS_MCP_GATEWAY_EXE ? 'GENEXUS_MCP_GATEWAY_EXE' : (process.env.GENEXUS_MCP_NO_STAGING === '1' ? 'NO_STAGING' : 'checkout'),
|
|
459
|
+
cleanup: null
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
let version = '0.0.0';
|
|
464
|
+
try {
|
|
465
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
466
|
+
if (pkg.version) version = pkg.version;
|
|
467
|
+
} catch {
|
|
468
|
+
// Fallback
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const sha8 = computePublishFingerprint(publishDir);
|
|
472
|
+
const runtimeRoot = options.runtimeRoot || resolveDefaultRuntimeRoot();
|
|
473
|
+
const targetDir = path.join(runtimeRoot, `${version}-${sha8}`);
|
|
474
|
+
const stagedGatewayExe = path.join(targetDir, 'GxMcp.Gateway.exe');
|
|
475
|
+
|
|
476
|
+
if (fs.existsSync(stagedGatewayExe)) {
|
|
477
|
+
const cleanup = cleanOldRuntimes(runtimeRoot, targetDir, 2, options);
|
|
478
|
+
return {
|
|
479
|
+
staged: true,
|
|
480
|
+
gatewayExePath: stagedGatewayExe,
|
|
481
|
+
runtimeDir: targetDir,
|
|
482
|
+
fresh: false,
|
|
483
|
+
cleanup
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (!fs.existsSync(publishDir)) {
|
|
488
|
+
throw new Error(`Cannot stage runtime: publish directory not found at ${publishDir}`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
fs.mkdirSync(runtimeRoot, { recursive: true });
|
|
492
|
+
const tmpDir = `${targetDir}.tmp-${process.pid}-${Date.now()}`;
|
|
493
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
494
|
+
let temporaryCleanup = null;
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
fs.cpSync(publishDir, tmpDir, { recursive: true });
|
|
498
|
+
const manifestPath = path.join(tmpDir, 'gxmcp-manifest.json');
|
|
499
|
+
verifyManifest(tmpDir, manifestPath);
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
fs.renameSync(tmpDir, targetDir);
|
|
503
|
+
} catch (renameErr) {
|
|
504
|
+
// Concurrent launcher might have completed the rename first
|
|
505
|
+
if (fs.existsSync(stagedGatewayExe)) {
|
|
506
|
+
temporaryCleanup = cleanupRuntimeDirectory(tmpDir, options);
|
|
507
|
+
} else {
|
|
508
|
+
throw renameErr;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
} catch (err) {
|
|
512
|
+
const failure = err instanceof Error ? err : new Error(String(err));
|
|
513
|
+
const cleanup = cleanupRuntimeDirectory(tmpDir, options);
|
|
514
|
+
failure.runtimeCleanup = cleanup;
|
|
515
|
+
const notes = [];
|
|
516
|
+
if (cleanup.skipped.length > 0) {
|
|
517
|
+
const reasons = cleanup.skipped
|
|
518
|
+
.map((entry) => `${entry.reason}${entry.pids?.length ? ` (PIDs: ${entry.pids.join(', ')})` : ''}`)
|
|
519
|
+
.join(', ');
|
|
520
|
+
notes.push(`skipped ${cleanup.skipped.length} path(s): ${reasons}`);
|
|
521
|
+
}
|
|
522
|
+
if (cleanup.failed.length > 0) {
|
|
523
|
+
notes.push(`failed ${cleanup.failed.length} path(s)`);
|
|
524
|
+
}
|
|
525
|
+
if (!cleanup.processProbeAvailable) {
|
|
526
|
+
notes.push('process probe unavailable');
|
|
527
|
+
}
|
|
528
|
+
if (notes.length > 0) {
|
|
529
|
+
failure.message = `${failure.message} Runtime cleanup ${notes.join('; ')}.`;
|
|
530
|
+
}
|
|
531
|
+
throw failure;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const cleanup = mergeRuntimeCleanupResults(
|
|
535
|
+
cleanOldRuntimes(runtimeRoot, targetDir, 2, options),
|
|
536
|
+
temporaryCleanup
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
staged: true,
|
|
541
|
+
gatewayExePath: stagedGatewayExe,
|
|
542
|
+
runtimeDir: targetDir,
|
|
543
|
+
fresh: true,
|
|
544
|
+
cleanup
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function listStagedRuntimes(runtimeRoot = resolveDefaultRuntimeRoot()) {
|
|
549
|
+
if (!fs.existsSync(runtimeRoot)) return [];
|
|
550
|
+
try {
|
|
551
|
+
const entries = fs.readdirSync(runtimeRoot, { withFileTypes: true });
|
|
552
|
+
return entries
|
|
553
|
+
.filter((e) => e.isDirectory() && !e.name.includes('.tmp-') && !e.name.includes('.deleting-'))
|
|
554
|
+
.map((e) => {
|
|
555
|
+
const dirPath = path.join(runtimeRoot, e.name);
|
|
556
|
+
let stat = null;
|
|
557
|
+
try {
|
|
558
|
+
stat = fs.statSync(dirPath);
|
|
559
|
+
} catch {
|
|
560
|
+
// Ignore
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
name: e.name,
|
|
564
|
+
path: dirPath,
|
|
565
|
+
hasGateway: fs.existsSync(path.join(dirPath, 'GxMcp.Gateway.exe')),
|
|
566
|
+
mtime: stat ? stat.mtime : null
|
|
567
|
+
};
|
|
568
|
+
});
|
|
569
|
+
} catch {
|
|
570
|
+
return [];
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function probeRunningGxMcpProcesses() {
|
|
575
|
+
if (process.platform !== 'win32') {
|
|
576
|
+
return { ok: true, processes: [] };
|
|
577
|
+
}
|
|
578
|
+
try {
|
|
579
|
+
// Query CIM Win32_Process for GxMcp processes. Keep the probe result
|
|
580
|
+
// distinguishable from a successful empty result: GC must fail closed
|
|
581
|
+
// when Windows denies the query.
|
|
582
|
+
const output = execFileSync('powershell.exe', [
|
|
583
|
+
'-NoProfile',
|
|
584
|
+
'-NonInteractive',
|
|
585
|
+
'-Command',
|
|
586
|
+
`Get-CimInstance Win32_Process -Filter "name like 'GxMcp%'" | Select-Object ProcessId, ExecutablePath, CommandLine | ConvertTo-Json -Compress`
|
|
587
|
+
], { encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
588
|
+
|
|
589
|
+
if (!output) return { ok: true, processes: [] };
|
|
590
|
+
let data;
|
|
591
|
+
try {
|
|
592
|
+
data = JSON.parse(output);
|
|
593
|
+
} catch (err) {
|
|
594
|
+
return { ok: false, processes: [], error: `invalid process probe JSON: ${err.message}` };
|
|
595
|
+
}
|
|
596
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
597
|
+
const processes = rows.filter(Boolean).map((r) => ({
|
|
598
|
+
pid: r.ProcessId,
|
|
599
|
+
exePath: r.ExecutablePath || '',
|
|
600
|
+
commandLine: r.CommandLine || ''
|
|
601
|
+
}));
|
|
602
|
+
const unresolved = processes.filter((proc) => !proc.exePath);
|
|
603
|
+
if (unresolved.length > 0) {
|
|
604
|
+
return {
|
|
605
|
+
ok: false,
|
|
606
|
+
processes,
|
|
607
|
+
error: `${unresolved.length} GxMcp process record(s) had no executable path; runtime GC is fail-closed.`
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
return { ok: true, processes };
|
|
611
|
+
} catch (err) {
|
|
612
|
+
return { ok: false, processes: [], error: err && err.message ? err.message : String(err) };
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function getRunningGxMcpProcesses() {
|
|
617
|
+
return probeRunningGxMcpProcesses().processes;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function classifyProcessRuntime(exePath, runtimeRoot = resolveDefaultRuntimeRoot()) {
|
|
621
|
+
if (!exePath) return 'unknown';
|
|
622
|
+
if (isPathWithin(exePath, runtimeRoot)) {
|
|
623
|
+
return 'staged';
|
|
624
|
+
}
|
|
625
|
+
const norm = path.resolve(exePath).toLowerCase();
|
|
626
|
+
if (norm.includes('npm-cache') || norm.includes('_npx') || norm.includes('node_modules')) {
|
|
627
|
+
return 'npx-cache';
|
|
628
|
+
}
|
|
629
|
+
return 'other';
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
module.exports = {
|
|
633
|
+
resolveDefaultRuntimeRoot,
|
|
634
|
+
isCheckoutMode,
|
|
635
|
+
shouldStage,
|
|
636
|
+
computePublishFingerprint,
|
|
637
|
+
verifyManifest,
|
|
638
|
+
cleanOldRuntimes,
|
|
639
|
+
ensureStagedGateway,
|
|
640
|
+
listStagedRuntimes,
|
|
641
|
+
getRunningGxMcpProcesses,
|
|
642
|
+
probeRunningGxMcpProcesses,
|
|
643
|
+
getRuntimeProcessUsage,
|
|
644
|
+
classifyProcessRuntime
|
|
645
|
+
};
|