genexus-mcp 3.9.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 +19 -6
- package/cli/index.js +9 -0
- package/cli/lib/runtime-stager.js +384 -71
- package/cli/lib/runtime-stager.test.js +289 -3
- package/package.json +1 -1
- 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 +6 -6
- 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
package/cli/commands/axi.js
CHANGED
|
@@ -818,42 +818,55 @@ function buildRuntimeStagingCheck() {
|
|
|
818
818
|
const {
|
|
819
819
|
resolveDefaultRuntimeRoot,
|
|
820
820
|
listStagedRuntimes,
|
|
821
|
-
|
|
821
|
+
probeRunningGxMcpProcesses,
|
|
822
|
+
getRuntimeProcessUsage,
|
|
822
823
|
classifyProcessRuntime,
|
|
823
824
|
shouldStage
|
|
824
825
|
} = require('../lib/runtime-stager');
|
|
825
826
|
|
|
826
827
|
const runtimeRoot = resolveDefaultRuntimeRoot();
|
|
827
828
|
const stagedList = listStagedRuntimes(runtimeRoot);
|
|
828
|
-
const
|
|
829
|
+
const processProbe = probeRunningGxMcpProcesses();
|
|
830
|
+
const runningProcs = processProbe.processes || [];
|
|
831
|
+
if (processProbe.ok === false) {
|
|
832
|
+
return {
|
|
833
|
+
id: 'runtime_staging',
|
|
834
|
+
status: 'warn',
|
|
835
|
+
detail: `Runtime process inspection was incomplete; GC is fail-closed. ${processProbe.error || 'The process probe did not return executable paths.'}`
|
|
836
|
+
};
|
|
837
|
+
}
|
|
829
838
|
const npxProcs = runningProcs.filter((p) => classifyProcessRuntime(p.exePath, runtimeRoot) === 'npx-cache');
|
|
830
839
|
const stagedProcs = runningProcs.filter((p) => classifyProcessRuntime(p.exePath, runtimeRoot) === 'staged');
|
|
840
|
+
const inUseRuntimes = getRuntimeProcessUsage(runtimeRoot, runningProcs);
|
|
841
|
+
const inUseDetail = inUseRuntimes.length > 0
|
|
842
|
+
? ` Retained in-use runtime(s): ${inUseRuntimes.map((entry) => `${entry.name} (PIDs: ${entry.pids.join(', ')})`).join('; ')}.`
|
|
843
|
+
: '';
|
|
831
844
|
|
|
832
845
|
if (npxProcs.length > 0) {
|
|
833
846
|
return {
|
|
834
847
|
id: 'runtime_staging',
|
|
835
848
|
status: 'warn',
|
|
836
|
-
detail: `${npxProcs.length} process(es) running directly from npx cache (PIDs: ${npxProcs.map((p) => p.pid).join(', ')}). Terminate them to prevent EBUSY during upgrades
|
|
849
|
+
detail: `${npxProcs.length} process(es) running directly from npx cache (PIDs: ${npxProcs.map((p) => p.pid).join(', ')}). Terminate them to prevent EBUSY during upgrades.${inUseDetail}`
|
|
837
850
|
};
|
|
838
851
|
}
|
|
839
852
|
if (shouldStage()) {
|
|
840
853
|
return {
|
|
841
854
|
id: 'runtime_staging',
|
|
842
855
|
status: 'pass',
|
|
843
|
-
detail: `Staging active at ${runtimeRoot}. ${stagedList.length} staged version(s) found, ${stagedProcs.length} active process(es)
|
|
856
|
+
detail: `Staging active at ${runtimeRoot}. ${stagedList.length} staged version(s) found, ${stagedProcs.length} active process(es).${inUseDetail}`
|
|
844
857
|
};
|
|
845
858
|
}
|
|
846
859
|
if (process.env.GENEXUS_MCP_GATEWAY_EXE) {
|
|
847
860
|
return {
|
|
848
861
|
id: 'runtime_staging',
|
|
849
862
|
status: 'pass',
|
|
850
|
-
detail: `Fixed-path runtime override active via GENEXUS_MCP_GATEWAY_EXE (${process.env.GENEXUS_MCP_GATEWAY_EXE}). Staging bypassed
|
|
863
|
+
detail: `Fixed-path runtime override active via GENEXUS_MCP_GATEWAY_EXE (${process.env.GENEXUS_MCP_GATEWAY_EXE}). Staging bypassed.${inUseDetail}`
|
|
851
864
|
};
|
|
852
865
|
}
|
|
853
866
|
return {
|
|
854
867
|
id: 'runtime_staging',
|
|
855
868
|
status: 'pass',
|
|
856
|
-
detail:
|
|
869
|
+
detail: `Development checkout active. Runtime staging bypassed.${inUseDetail}`
|
|
857
870
|
};
|
|
858
871
|
} catch {
|
|
859
872
|
return {
|
package/cli/index.js
CHANGED
|
@@ -455,6 +455,15 @@ async function launchGateway(passthroughArgs, options) {
|
|
|
455
455
|
const { ensureStagedGateway } = require('./lib/runtime-stager');
|
|
456
456
|
const staged = ensureStagedGateway();
|
|
457
457
|
gatewayExePath = staged.gatewayExePath;
|
|
458
|
+
const cleanup = staged.cleanup;
|
|
459
|
+
if (cleanup && (cleanup.skipped?.length || cleanup.failed?.length || cleanup.processProbeAvailable === false)) {
|
|
460
|
+
const summary = [
|
|
461
|
+
cleanup.processProbeAvailable === false ? 'process probe unavailable' : null,
|
|
462
|
+
cleanup.skipped?.length ? `${cleanup.skipped.length} skipped` : null,
|
|
463
|
+
cleanup.failed?.length ? `${cleanup.failed.length} failed` : null
|
|
464
|
+
].filter(Boolean).join(', ');
|
|
465
|
+
if (!options.quiet) launcherStderr.write(`[genexus-mcp] Runtime cleanup incomplete: ${summary}.\n`);
|
|
466
|
+
}
|
|
458
467
|
} catch (stageErr) {
|
|
459
468
|
if (!options.quiet) {
|
|
460
469
|
launcherStderr.write(`[genexus-mcp] Warning: Runtime staging failed (${stageErr.message}), falling back to direct binary.\n`);
|
|
@@ -96,75 +96,353 @@ function verifyManifest(stagedDir, manifestPath) {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
function
|
|
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;
|
|
100
190
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
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
|
+
}
|
|
103
202
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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) };
|
|
118
265
|
}
|
|
266
|
+
};
|
|
119
267
|
|
|
120
|
-
|
|
268
|
+
try {
|
|
269
|
+
if (!fs.existsSync(runtimeRoot)) return result;
|
|
270
|
+
const entries = fs.readdirSync(runtimeRoot, { withFileTypes: true });
|
|
271
|
+
const now = Date.now();
|
|
121
272
|
const versionDirs = [];
|
|
273
|
+
const tmpDirs = [];
|
|
274
|
+
const tombstoneDirs = [];
|
|
275
|
+
|
|
122
276
|
for (const entry of entries) {
|
|
123
|
-
if (!entry.isDirectory()
|
|
277
|
+
if (!entry.isDirectory()) continue;
|
|
124
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
|
+
}
|
|
125
287
|
try {
|
|
126
288
|
const stat = fs.statSync(fullPath);
|
|
127
|
-
versionDirs.push({
|
|
128
|
-
name: entry.name,
|
|
129
|
-
path: fullPath,
|
|
130
|
-
mtimeMs: stat.mtimeMs
|
|
131
|
-
});
|
|
289
|
+
versionDirs.push({ name: entry.name, path: fullPath, mtimeMs: stat.mtimeMs });
|
|
132
290
|
} catch {
|
|
133
|
-
// Ignore
|
|
291
|
+
// Ignore an entry that disappeared during enumeration.
|
|
134
292
|
}
|
|
135
293
|
}
|
|
136
294
|
|
|
137
|
-
// Sort newest first
|
|
138
295
|
versionDirs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
139
|
-
|
|
140
|
-
// Keep current targetDir always, plus up to keepCount newest
|
|
141
296
|
const normalizedCurrent = path.resolve(currentTargetDir).toLowerCase();
|
|
297
|
+
const keep = Number.isFinite(Number(keepCount)) ? Math.max(0, Number(keepCount)) : 2;
|
|
142
298
|
let kept = 0;
|
|
143
299
|
const toDelete = [];
|
|
144
|
-
|
|
145
300
|
for (const vdir of versionDirs) {
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
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 });
|
|
154
335
|
}
|
|
336
|
+
return result;
|
|
155
337
|
}
|
|
156
338
|
|
|
339
|
+
const initialUsage = new Map(
|
|
340
|
+
getRuntimeProcessUsage(runtimeRoot, processSnapshot, versionDirs)
|
|
341
|
+
.map((entry) => [path.resolve(entry.path).toLowerCase(), entry])
|
|
342
|
+
);
|
|
343
|
+
|
|
157
344
|
for (const dirToDelete of toDelete) {
|
|
345
|
+
let freshSnapshot;
|
|
158
346
|
try {
|
|
159
|
-
|
|
160
|
-
} catch {
|
|
161
|
-
|
|
162
|
-
|
|
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
|
+
});
|
|
163
439
|
}
|
|
164
440
|
}
|
|
165
|
-
} catch {
|
|
166
|
-
|
|
441
|
+
} catch (err) {
|
|
442
|
+
result.failed.push({ path: runtimeRoot, error: err && err.message ? err.message : String(err) });
|
|
167
443
|
}
|
|
444
|
+
|
|
445
|
+
return result;
|
|
168
446
|
}
|
|
169
447
|
|
|
170
448
|
function ensureStagedGateway(options = {}) {
|
|
@@ -177,7 +455,8 @@ function ensureStagedGateway(options = {}) {
|
|
|
177
455
|
staged: false,
|
|
178
456
|
gatewayExePath: process.env.GENEXUS_MCP_GATEWAY_EXE || defaultGatewayExe,
|
|
179
457
|
runtimeDir: path.dirname(process.env.GENEXUS_MCP_GATEWAY_EXE || defaultGatewayExe),
|
|
180
|
-
reason: process.env.GENEXUS_MCP_GATEWAY_EXE ? 'GENEXUS_MCP_GATEWAY_EXE' : (process.env.GENEXUS_MCP_NO_STAGING === '1' ? 'NO_STAGING' : 'checkout')
|
|
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
|
|
181
460
|
};
|
|
182
461
|
}
|
|
183
462
|
|
|
@@ -195,12 +474,13 @@ function ensureStagedGateway(options = {}) {
|
|
|
195
474
|
const stagedGatewayExe = path.join(targetDir, 'GxMcp.Gateway.exe');
|
|
196
475
|
|
|
197
476
|
if (fs.existsSync(stagedGatewayExe)) {
|
|
198
|
-
cleanOldRuntimes(runtimeRoot, targetDir);
|
|
477
|
+
const cleanup = cleanOldRuntimes(runtimeRoot, targetDir, 2, options);
|
|
199
478
|
return {
|
|
200
479
|
staged: true,
|
|
201
480
|
gatewayExePath: stagedGatewayExe,
|
|
202
481
|
runtimeDir: targetDir,
|
|
203
|
-
fresh: false
|
|
482
|
+
fresh: false,
|
|
483
|
+
cleanup
|
|
204
484
|
};
|
|
205
485
|
}
|
|
206
486
|
|
|
@@ -211,6 +491,7 @@ function ensureStagedGateway(options = {}) {
|
|
|
211
491
|
fs.mkdirSync(runtimeRoot, { recursive: true });
|
|
212
492
|
const tmpDir = `${targetDir}.tmp-${process.pid}-${Date.now()}`;
|
|
213
493
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
494
|
+
let temporaryCleanup = null;
|
|
214
495
|
|
|
215
496
|
try {
|
|
216
497
|
fs.cpSync(publishDir, tmpDir, { recursive: true });
|
|
@@ -222,31 +503,45 @@ function ensureStagedGateway(options = {}) {
|
|
|
222
503
|
} catch (renameErr) {
|
|
223
504
|
// Concurrent launcher might have completed the rename first
|
|
224
505
|
if (fs.existsSync(stagedGatewayExe)) {
|
|
225
|
-
|
|
226
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
227
|
-
} catch {
|
|
228
|
-
// Ignore tmp cleanup error
|
|
229
|
-
}
|
|
506
|
+
temporaryCleanup = cleanupRuntimeDirectory(tmpDir, options);
|
|
230
507
|
} else {
|
|
231
508
|
throw renameErr;
|
|
232
509
|
}
|
|
233
510
|
}
|
|
234
511
|
} catch (err) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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('; ')}.`;
|
|
239
530
|
}
|
|
240
|
-
throw
|
|
531
|
+
throw failure;
|
|
241
532
|
}
|
|
242
533
|
|
|
243
|
-
|
|
534
|
+
const cleanup = mergeRuntimeCleanupResults(
|
|
535
|
+
cleanOldRuntimes(runtimeRoot, targetDir, 2, options),
|
|
536
|
+
temporaryCleanup
|
|
537
|
+
);
|
|
244
538
|
|
|
245
539
|
return {
|
|
246
540
|
staged: true,
|
|
247
541
|
gatewayExePath: stagedGatewayExe,
|
|
248
542
|
runtimeDir: targetDir,
|
|
249
|
-
fresh: true
|
|
543
|
+
fresh: true,
|
|
544
|
+
cleanup
|
|
250
545
|
};
|
|
251
546
|
}
|
|
252
547
|
|
|
@@ -255,7 +550,7 @@ function listStagedRuntimes(runtimeRoot = resolveDefaultRuntimeRoot()) {
|
|
|
255
550
|
try {
|
|
256
551
|
const entries = fs.readdirSync(runtimeRoot, { withFileTypes: true });
|
|
257
552
|
return entries
|
|
258
|
-
.filter((e) => e.isDirectory() && !e.name.includes('.tmp-'))
|
|
553
|
+
.filter((e) => e.isDirectory() && !e.name.includes('.tmp-') && !e.name.includes('.deleting-'))
|
|
259
554
|
.map((e) => {
|
|
260
555
|
const dirPath = path.join(runtimeRoot, e.name);
|
|
261
556
|
let stat = null;
|
|
@@ -276,10 +571,14 @@ function listStagedRuntimes(runtimeRoot = resolveDefaultRuntimeRoot()) {
|
|
|
276
571
|
}
|
|
277
572
|
}
|
|
278
573
|
|
|
279
|
-
function
|
|
280
|
-
if (process.platform !== 'win32')
|
|
574
|
+
function probeRunningGxMcpProcesses() {
|
|
575
|
+
if (process.platform !== 'win32') {
|
|
576
|
+
return { ok: true, processes: [] };
|
|
577
|
+
}
|
|
281
578
|
try {
|
|
282
|
-
// Query CIM Win32_Process for GxMcp processes
|
|
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.
|
|
283
582
|
const output = execFileSync('powershell.exe', [
|
|
284
583
|
'-NoProfile',
|
|
285
584
|
'-NonInteractive',
|
|
@@ -287,31 +586,43 @@ function getRunningGxMcpProcesses() {
|
|
|
287
586
|
`Get-CimInstance Win32_Process -Filter "name like 'GxMcp%'" | Select-Object ProcessId, ExecutablePath, CommandLine | ConvertTo-Json -Compress`
|
|
288
587
|
], { encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
289
588
|
|
|
290
|
-
if (!output) return [];
|
|
589
|
+
if (!output) return { ok: true, processes: [] };
|
|
291
590
|
let data;
|
|
292
591
|
try {
|
|
293
592
|
data = JSON.parse(output);
|
|
294
|
-
} catch {
|
|
295
|
-
return [];
|
|
593
|
+
} catch (err) {
|
|
594
|
+
return { ok: false, processes: [], error: `invalid process probe JSON: ${err.message}` };
|
|
296
595
|
}
|
|
297
596
|
const rows = Array.isArray(data) ? data : [data];
|
|
298
|
-
|
|
597
|
+
const processes = rows.filter(Boolean).map((r) => ({
|
|
299
598
|
pid: r.ProcessId,
|
|
300
599
|
exePath: r.ExecutablePath || '',
|
|
301
600
|
commandLine: r.CommandLine || ''
|
|
302
601
|
}));
|
|
303
|
-
|
|
304
|
-
|
|
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) };
|
|
305
613
|
}
|
|
306
614
|
}
|
|
307
615
|
|
|
616
|
+
function getRunningGxMcpProcesses() {
|
|
617
|
+
return probeRunningGxMcpProcesses().processes;
|
|
618
|
+
}
|
|
619
|
+
|
|
308
620
|
function classifyProcessRuntime(exePath, runtimeRoot = resolveDefaultRuntimeRoot()) {
|
|
309
621
|
if (!exePath) return 'unknown';
|
|
310
|
-
|
|
311
|
-
const normRuntime = path.resolve(runtimeRoot).toLowerCase();
|
|
312
|
-
if (norm.startsWith(normRuntime)) {
|
|
622
|
+
if (isPathWithin(exePath, runtimeRoot)) {
|
|
313
623
|
return 'staged';
|
|
314
624
|
}
|
|
625
|
+
const norm = path.resolve(exePath).toLowerCase();
|
|
315
626
|
if (norm.includes('npm-cache') || norm.includes('_npx') || norm.includes('node_modules')) {
|
|
316
627
|
return 'npx-cache';
|
|
317
628
|
}
|
|
@@ -328,5 +639,7 @@ module.exports = {
|
|
|
328
639
|
ensureStagedGateway,
|
|
329
640
|
listStagedRuntimes,
|
|
330
641
|
getRunningGxMcpProcesses,
|
|
642
|
+
probeRunningGxMcpProcesses,
|
|
643
|
+
getRuntimeProcessUsage,
|
|
331
644
|
classifyProcessRuntime
|
|
332
645
|
};
|