comfyui-mcp 0.52.111 → 0.52.113
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/dist/services/live-interpreter.js +118 -5
- package/dist/services/live-interpreter.js.map +1 -1
- package/dist/services/process-control.js +387 -11
- package/dist/services/process-control.js.map +1 -1
- package/dist/services/workspace-env.js +123 -15
- package/dist/services/workspace-env.js.map +1 -1
- package/package.json +1 -1
|
@@ -40,14 +40,16 @@ const IS_WIN = platform() === "win32";
|
|
|
40
40
|
// live-interpreter resolver can use them without importing this module (which would
|
|
41
41
|
// cycle through workspace-env). Re-exported here: this is still their public home.
|
|
42
42
|
export { parseListenerPidFromNetstat, findPidByPort };
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
function isExpectedPgrepNoMatch(error) {
|
|
44
|
+
return (!IS_WIN &&
|
|
45
|
+
typeof error === "object" &&
|
|
46
|
+
error !== null &&
|
|
47
|
+
"status" in error &&
|
|
48
|
+
error.status === 1);
|
|
49
|
+
}
|
|
50
|
+
function findDesktopAppPidsWithStatus() {
|
|
50
51
|
const pids = [];
|
|
52
|
+
let complete = true;
|
|
51
53
|
if (IS_WIN) {
|
|
52
54
|
for (const exe of ["ComfyUI.exe", "Comfy Desktop.exe"]) {
|
|
53
55
|
try {
|
|
@@ -61,7 +63,7 @@ function findDesktopAppPids() {
|
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
catch {
|
|
64
|
-
|
|
66
|
+
complete = false;
|
|
65
67
|
}
|
|
66
68
|
}
|
|
67
69
|
}
|
|
@@ -77,11 +79,50 @@ function findDesktopAppPids() {
|
|
|
77
79
|
pids.push(pid);
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
|
-
catch {
|
|
81
|
-
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (!isExpectedPgrepNoMatch(error))
|
|
84
|
+
complete = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { pids, complete };
|
|
88
|
+
}
|
|
89
|
+
function findDesktopAppPids() {
|
|
90
|
+
return findDesktopAppPidsWithStatus().pids;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Find Python processes that may be the Desktop server when the port-owner
|
|
94
|
+
* lookup is unavailable. This is only a candidate list: every candidate is
|
|
95
|
+
* re-read through the process identity reader and must still pass the exact
|
|
96
|
+
* command and authenticated-ancestry checks below.
|
|
97
|
+
*/
|
|
98
|
+
function findPythonProcessPids() {
|
|
99
|
+
const pids = new Set();
|
|
100
|
+
let complete = true;
|
|
101
|
+
const names = IS_WIN ? ["python.exe", "pythonw.exe"] : ["python", "python3", "pythonw"];
|
|
102
|
+
for (const name of names) {
|
|
103
|
+
try {
|
|
104
|
+
const out = IS_WIN
|
|
105
|
+
? execSync(`tasklist /FI "IMAGENAME eq ${name}" /FO CSV /NH`, {
|
|
106
|
+
encoding: "utf-8",
|
|
107
|
+
timeout: 5000,
|
|
108
|
+
})
|
|
109
|
+
: execSync(`pgrep -x "${name}"`, {
|
|
110
|
+
encoding: "utf-8",
|
|
111
|
+
timeout: 5000,
|
|
112
|
+
});
|
|
113
|
+
for (const line of String(out).split(/\r?\n/)) {
|
|
114
|
+
const match = IS_WIN ? line.match(/^"[^"]+","(\d+)"/) : line.match(/^\s*(\d+)\s*$/);
|
|
115
|
+
const pid = match ? Number.parseInt(match[1], 10) : NaN;
|
|
116
|
+
if (Number.isInteger(pid) && pid > 0)
|
|
117
|
+
pids.add(pid);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (!isExpectedPgrepNoMatch(error))
|
|
122
|
+
complete = false;
|
|
82
123
|
}
|
|
83
124
|
}
|
|
84
|
-
return pids;
|
|
125
|
+
return { pids: [...pids], complete };
|
|
85
126
|
}
|
|
86
127
|
function killProcessTree(pid) {
|
|
87
128
|
try {
|
|
@@ -236,6 +277,235 @@ function isDesktopApp(argv) {
|
|
|
236
277
|
joined.includes("comfy desktop") ||
|
|
237
278
|
joined.includes("@comfyorgcomfyui-electron"));
|
|
238
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* Return a path without command-line quoting and with the host's path rules
|
|
282
|
+
* applied. POSIX paths are intentionally not case-folded: unlike Windows,
|
|
283
|
+
* `/ComfyUI/main.py` and `/comfyui/main.py` are different paths.
|
|
284
|
+
*/
|
|
285
|
+
function normalizeRecoveryPath(path) {
|
|
286
|
+
const unquoted = path.trim().replace(/^['"]+|['"]+$/g, "");
|
|
287
|
+
if (!IS_WIN)
|
|
288
|
+
return unquoted;
|
|
289
|
+
return unquoted.replace(/\\/g, "/").toLowerCase();
|
|
290
|
+
}
|
|
291
|
+
function isAbsoluteRecoveryPath(path) {
|
|
292
|
+
return IS_WIN
|
|
293
|
+
? /^[a-z]:[\\/]/i.test(path) || /^\\\\/.test(path)
|
|
294
|
+
: path.startsWith("/");
|
|
295
|
+
}
|
|
296
|
+
function isComfyUIServerScript(path) {
|
|
297
|
+
const normalized = normalizeRecoveryPath(path);
|
|
298
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
299
|
+
if (segments.pop() !== "main.py")
|
|
300
|
+
return false;
|
|
301
|
+
// Desktop's H3 launch names the ComfyUI install explicitly. A broad
|
|
302
|
+
// "Desktop" marker elsewhere in argv must not make an arbitrary helper.py
|
|
303
|
+
// authoritative.
|
|
304
|
+
return segments.some((segment) => segment === (IS_WIN ? "comfyui" : "ComfyUI"));
|
|
305
|
+
}
|
|
306
|
+
function isDesktopConfigPath(path) {
|
|
307
|
+
const normalized = normalizeRecoveryPath(path);
|
|
308
|
+
if (!isAbsoluteRecoveryPath(path))
|
|
309
|
+
return false;
|
|
310
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
311
|
+
return segments.some((segment) => segment === (IS_WIN ? "comfy desktop" : "Comfy Desktop"));
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Is this the exact H3 server command Desktop uses for its backend?
|
|
315
|
+
*
|
|
316
|
+
* The old check only looked for `main.py` plus any Desktop-looking token. That
|
|
317
|
+
* allowed an unrelated helper launched from the Desktop install to become a
|
|
318
|
+
* restart target. Keep the accepted shape deliberately narrow: script, Manager
|
|
319
|
+
* flag, one absolute Desktop model-paths config, and the requested port, in that
|
|
320
|
+
* order and with no extra arguments.
|
|
321
|
+
*/
|
|
322
|
+
function isDesktopServerArgv(argv, port) {
|
|
323
|
+
if (argv.length !== 6)
|
|
324
|
+
return false;
|
|
325
|
+
const [script, managerFlag, configFlag, configPath, portFlag, reportedPort] = argv;
|
|
326
|
+
return Boolean(script &&
|
|
327
|
+
managerFlag === "--enable-manager" &&
|
|
328
|
+
configFlag === "--extra-model-paths-config" &&
|
|
329
|
+
configPath &&
|
|
330
|
+
isComfyUIServerScript(script) &&
|
|
331
|
+
isDesktopConfigPath(configPath) &&
|
|
332
|
+
portFlag === "--port" &&
|
|
333
|
+
reportedPort === String(port));
|
|
334
|
+
}
|
|
335
|
+
function normalizeRecoveryToken(token) {
|
|
336
|
+
const unquoted = token.trim().replace(/^['"]+|['"]+$/g, "");
|
|
337
|
+
return IS_WIN ? unquoted.replace(/\\/g, "/").toLowerCase() : unquoted;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Match the server's argv to an OS process command without the permissive
|
|
341
|
+
* substring semantics used for ordinary corroboration. Requiring equal lengths
|
|
342
|
+
* rejects an injected extra command or flag. Windows permits the process table to
|
|
343
|
+
* report an absolute spelling for Desktop's relative `ComfyUI\\main.py`; POSIX
|
|
344
|
+
* does not get that suffix fallback because its path and case rules are exact.
|
|
345
|
+
*/
|
|
346
|
+
function exactDesktopServerCommand(identity, serverArgv) {
|
|
347
|
+
if (identity.argvFidelity !== "exact" ||
|
|
348
|
+
!identity.commandLine ||
|
|
349
|
+
!identity.argv ||
|
|
350
|
+
identity.argv.length < 2) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
const observed = identity.argv.slice(1);
|
|
354
|
+
if (observed.length !== serverArgv.length)
|
|
355
|
+
return false;
|
|
356
|
+
return observed.every((token, index) => {
|
|
357
|
+
const actual = normalizeRecoveryToken(token);
|
|
358
|
+
const expected = normalizeRecoveryToken(serverArgv[index] ?? "");
|
|
359
|
+
if (actual === expected)
|
|
360
|
+
return true;
|
|
361
|
+
// Only Windows may reconcile Desktop's relative sys.argv[0] with the
|
|
362
|
+
// absolute script path exposed by the process table.
|
|
363
|
+
const expectedRaw = (serverArgv[index] ?? "").trim().replace(/^['"]+|['"]+$/g, "");
|
|
364
|
+
if (!IS_WIN ||
|
|
365
|
+
index !== 0 ||
|
|
366
|
+
/^[a-z]:[\\/]/i.test(expectedRaw) ||
|
|
367
|
+
/^\\\\/.test(expectedRaw)) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
return actual.endsWith(`/${expected}`);
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
function isVerifiedExecutablePath(path) {
|
|
374
|
+
return Boolean(path && isAbsoluteRecoveryPath(path));
|
|
375
|
+
}
|
|
376
|
+
function sameRecoveryPath(a, b) {
|
|
377
|
+
return normalizeRecoveryPath(a) === normalizeRecoveryPath(b);
|
|
378
|
+
}
|
|
379
|
+
function hasExactProcessIdentity(identity) {
|
|
380
|
+
return Boolean(identity?.startedAt &&
|
|
381
|
+
identity.executablePath &&
|
|
382
|
+
identity.commandLine &&
|
|
383
|
+
identity.argvFidelity === "exact" &&
|
|
384
|
+
identity.argv?.length);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* The process reader exposes two different Python paths on Windows/Linux for a
|
|
388
|
+
* venv: `executablePath` is the base interpreter image, while argv[0] is the venv
|
|
389
|
+
* interpreter that owns the server's site-packages. Either path alone is not
|
|
390
|
+
* enough; the pair must have the shape the reader documents, and the server's
|
|
391
|
+
* exact H3 command must tie the venv path to the ComfyUI install.
|
|
392
|
+
*/
|
|
393
|
+
function isPythonInterpreterPath(path) {
|
|
394
|
+
if (!isVerifiedExecutablePath(path))
|
|
395
|
+
return false;
|
|
396
|
+
const basename = path.replace(/\\/g, "/").split("/").pop()?.toLowerCase() ?? "";
|
|
397
|
+
return /^python(?:w)?(?:\d+(?:\.\d+)*)?(?:\.exe)?$/.test(basename);
|
|
398
|
+
}
|
|
399
|
+
function isAuthenticatedPython(identity, serverArgv) {
|
|
400
|
+
const executable = identity.executablePath;
|
|
401
|
+
const argv0 = identity.argv?.[0];
|
|
402
|
+
if (!hasExactProcessIdentity(identity) ||
|
|
403
|
+
!isPythonInterpreterPath(executable) ||
|
|
404
|
+
!isPythonInterpreterPath(argv0)) {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
// A normal process has the same path in both fields. A venv process is the
|
|
408
|
+
// documented exception: the kernel image resolves to base Python, while
|
|
409
|
+
// argv[0] remains the venv trampoline. Do not accept arbitrary two-Python
|
|
410
|
+
// paths; only that one-way base -> venv relationship is reader-proven.
|
|
411
|
+
const samePath = sameRecoveryPath(executable, argv0);
|
|
412
|
+
if (!samePath && !(isVenvInterpreterPath(argv0) && !isVenvInterpreterPath(executable))) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
if (!serverArgv)
|
|
416
|
+
return true;
|
|
417
|
+
// A recovered Desktop server must use a venv interpreter under the same
|
|
418
|
+
// ComfyUI install named by its H3 script. This rejects an arbitrary absolute
|
|
419
|
+
// `C:\\attacker\\venv\\Scripts\\python.exe` even when its basename is valid.
|
|
420
|
+
if (!isVenvInterpreterPath(argv0))
|
|
421
|
+
return false;
|
|
422
|
+
const script = normalizeRecoveryPath(serverArgv[0] ?? "");
|
|
423
|
+
const pythonPath = normalizeRecoveryPath(argv0);
|
|
424
|
+
const comfySegment = IS_WIN ? "comfyui" : "ComfyUI";
|
|
425
|
+
const scriptSegments = script.split("/").filter(Boolean);
|
|
426
|
+
const pythonSegments = pythonPath.split("/").filter(Boolean);
|
|
427
|
+
const scriptRootIndex = scriptSegments.lastIndexOf(comfySegment);
|
|
428
|
+
const pythonRootIndex = pythonSegments.lastIndexOf(comfySegment);
|
|
429
|
+
if (scriptRootIndex < 0 || pythonRootIndex < 0)
|
|
430
|
+
return false;
|
|
431
|
+
if (isAbsoluteRecoveryPath(serverArgv[0] ?? "")) {
|
|
432
|
+
const expectedRoot = scriptSegments.slice(0, scriptRootIndex + 1);
|
|
433
|
+
if (!expectedRoot.every((segment, index) => pythonSegments[index] === segment)) {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
function expectedDesktopExecutablePaths() {
|
|
440
|
+
if (IS_WIN) {
|
|
441
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
442
|
+
const userProfile = process.env.USERPROFILE;
|
|
443
|
+
return [
|
|
444
|
+
"C:/Program Files/Comfy Desktop/Comfy Desktop.exe",
|
|
445
|
+
"C:/Program Files/ComfyUI/ComfyUI.exe",
|
|
446
|
+
...(localAppData
|
|
447
|
+
? [
|
|
448
|
+
`${localAppData}/Programs/Comfy Desktop/Comfy Desktop.exe`,
|
|
449
|
+
`${localAppData}/Programs/@comfyorgcomfyui-electron/ComfyUI.exe`,
|
|
450
|
+
`${localAppData}/Programs/ComfyUI/ComfyUI.exe`,
|
|
451
|
+
]
|
|
452
|
+
: []),
|
|
453
|
+
...(userProfile ? [`${userProfile}/Programs/ComfyUI/ComfyUI.exe`] : []),
|
|
454
|
+
];
|
|
455
|
+
}
|
|
456
|
+
const home = homedir();
|
|
457
|
+
return [
|
|
458
|
+
"/Applications/Comfy Desktop.app/Contents/MacOS/Comfy Desktop",
|
|
459
|
+
"/Applications/Comfy Desktop.app/Contents/MacOS/ComfyUI",
|
|
460
|
+
"/Applications/ComfyUI.app/Contents/MacOS/Comfy Desktop",
|
|
461
|
+
"/Applications/ComfyUI.app/Contents/MacOS/ComfyUI",
|
|
462
|
+
`${home}/Applications/Comfy Desktop.app/Contents/MacOS/Comfy Desktop`,
|
|
463
|
+
`${home}/Applications/Comfy Desktop.app/Contents/MacOS/ComfyUI`,
|
|
464
|
+
`${home}/Applications/ComfyUI.app/Contents/MacOS/Comfy Desktop`,
|
|
465
|
+
`${home}/Applications/ComfyUI.app/Contents/MacOS/ComfyUI`,
|
|
466
|
+
];
|
|
467
|
+
}
|
|
468
|
+
function isExpectedDesktopExecutablePath(path) {
|
|
469
|
+
const normalized = normalizeRecoveryPath(path);
|
|
470
|
+
return expectedDesktopExecutablePaths().some((candidate) => sameRecoveryPath(normalized, candidate));
|
|
471
|
+
}
|
|
472
|
+
function isAuthenticatedDesktopSupervisor(identity) {
|
|
473
|
+
return Boolean(hasExactProcessIdentity(identity) &&
|
|
474
|
+
identity.executablePath &&
|
|
475
|
+
identity.argv?.[0] &&
|
|
476
|
+
isExpectedDesktopExecutablePath(identity.executablePath) &&
|
|
477
|
+
sameRecoveryPath(identity.executablePath, identity.argv[0]) &&
|
|
478
|
+
isDesktopSupervisorProcess(identity));
|
|
479
|
+
}
|
|
480
|
+
function directDesktopPythonAncestry(pid, knownPids, readParent, readIdentity) {
|
|
481
|
+
// The issue's accepted shape is specifically Desktop.exe -> python.exe ->
|
|
482
|
+
// server python.exe. Do not walk through an arbitrary wrapper and turn a
|
|
483
|
+
// merely shared Desktop ancestor into restart authority.
|
|
484
|
+
const pythonPid = readParent(pid);
|
|
485
|
+
if (pythonPid == null)
|
|
486
|
+
return "unavailable";
|
|
487
|
+
if (pythonPid <= 1 || pythonPid === pid)
|
|
488
|
+
return undefined;
|
|
489
|
+
const python = readIdentity(pythonPid);
|
|
490
|
+
if (!python || !isAuthenticatedPython(python))
|
|
491
|
+
return "unavailable";
|
|
492
|
+
const desktopPid = readParent(pythonPid);
|
|
493
|
+
if (desktopPid == null)
|
|
494
|
+
return "unavailable";
|
|
495
|
+
if (desktopPid <= 1 || desktopPid === pythonPid)
|
|
496
|
+
return undefined;
|
|
497
|
+
if (!knownPids.has(desktopPid))
|
|
498
|
+
return undefined;
|
|
499
|
+
const desktop = readIdentity(desktopPid);
|
|
500
|
+
if (!desktop ||
|
|
501
|
+
!hasExactProcessIdentity(desktop) ||
|
|
502
|
+
!isVerifiedExecutablePath(desktop.executablePath)) {
|
|
503
|
+
return "unavailable";
|
|
504
|
+
}
|
|
505
|
+
if (!isAuthenticatedDesktopSupervisor(desktop))
|
|
506
|
+
return undefined;
|
|
507
|
+
return { desktopPid, desktopIdentity: desktop };
|
|
508
|
+
}
|
|
239
509
|
/**
|
|
240
510
|
* Try to find the ComfyUI Desktop exe from common install locations.
|
|
241
511
|
* Used as a fallback when no process info was previously captured.
|
|
@@ -2334,6 +2604,101 @@ async function reconfirmAnsweringServer(argv, pid, port) {
|
|
|
2334
2604
|
return "unknown";
|
|
2335
2605
|
return ownerNow === pid ? "confirmed" : "changed";
|
|
2336
2606
|
}
|
|
2607
|
+
/**
|
|
2608
|
+
* Recover a Desktop backend when the listener cannot be mapped to a PID.
|
|
2609
|
+
*
|
|
2610
|
+
* This is deliberately narrower than the old "find a Desktop process by name"
|
|
2611
|
+
* fallback. The server has to have supplied a Desktop-shaped `sys.argv`; the OS
|
|
2612
|
+
* process table has to expose one exact, spawn-faithful Python command with the
|
|
2613
|
+
* same arguments; its direct parent has to be Python and that process's direct
|
|
2614
|
+
* parent has to be a name-discovered Desktop PID; and the normal supervision
|
|
2615
|
+
* classifier must authenticate every causality link. Missing or ambiguous evidence
|
|
2616
|
+
* returns undefined so the caller keeps its existing refusal.
|
|
2617
|
+
*/
|
|
2618
|
+
function recoverDesktopProcessFromAncestry(port, serverArgv) {
|
|
2619
|
+
if (!isDesktopServerArgv(serverArgv, port))
|
|
2620
|
+
return undefined;
|
|
2621
|
+
const desktopProbe = findDesktopAppPidsWithStatus();
|
|
2622
|
+
if (!desktopProbe.complete)
|
|
2623
|
+
return undefined;
|
|
2624
|
+
const desktopPids = new Set(desktopProbe.pids);
|
|
2625
|
+
if (desktopPids.size === 0)
|
|
2626
|
+
return undefined;
|
|
2627
|
+
const pythonProbe = findPythonProcessPids();
|
|
2628
|
+
if (!pythonProbe.complete)
|
|
2629
|
+
return undefined;
|
|
2630
|
+
const identities = new Map();
|
|
2631
|
+
const readIdentity = (pid) => {
|
|
2632
|
+
if (!identities.has(pid))
|
|
2633
|
+
identities.set(pid, resolveProcessIdentity(pid));
|
|
2634
|
+
return identities.get(pid);
|
|
2635
|
+
};
|
|
2636
|
+
const readParent = (pid) => {
|
|
2637
|
+
// The normal reader and the test seam both represent the same OS fact. Use
|
|
2638
|
+
// the seam when installed, otherwise retain the identity snapshot in the
|
|
2639
|
+
// cache so one recovery decision does not mix process generations.
|
|
2640
|
+
if (parentPidResolverOverride)
|
|
2641
|
+
return readParentPid(pid);
|
|
2642
|
+
return readIdentity(pid)?.parentPid;
|
|
2643
|
+
};
|
|
2644
|
+
const matches = [];
|
|
2645
|
+
let evidenceUnavailable = false;
|
|
2646
|
+
for (const pid of pythonProbe.pids) {
|
|
2647
|
+
const identity = readIdentity(pid);
|
|
2648
|
+
if (!hasExactProcessIdentity(identity)) {
|
|
2649
|
+
evidenceUnavailable = true;
|
|
2650
|
+
continue;
|
|
2651
|
+
}
|
|
2652
|
+
// Fully observed non-matching Python processes are ordinary background
|
|
2653
|
+
// processes and can be ignored. Once the exact H3 command matches, however,
|
|
2654
|
+
// an untrusted executable path is an ambiguous target and must fail closed.
|
|
2655
|
+
if (!exactDesktopServerCommand(identity, serverArgv))
|
|
2656
|
+
continue;
|
|
2657
|
+
if (!isAuthenticatedPython(identity, serverArgv)) {
|
|
2658
|
+
evidenceUnavailable = true;
|
|
2659
|
+
continue;
|
|
2660
|
+
}
|
|
2661
|
+
const ancestry = directDesktopPythonAncestry(pid, desktopPids, readParent, readIdentity);
|
|
2662
|
+
if (ancestry === "unavailable") {
|
|
2663
|
+
evidenceUnavailable = true;
|
|
2664
|
+
continue;
|
|
2665
|
+
}
|
|
2666
|
+
if (!ancestry)
|
|
2667
|
+
continue;
|
|
2668
|
+
const { verdict } = classifyDesktopSupervision({
|
|
2669
|
+
pid,
|
|
2670
|
+
readParentPid: readParent,
|
|
2671
|
+
readIdentity,
|
|
2672
|
+
processExists,
|
|
2673
|
+
isSupervisorProcess: isDesktopSupervisorProcess,
|
|
2674
|
+
});
|
|
2675
|
+
if (verdict === "supervised") {
|
|
2676
|
+
matches.push({ pid, identity, ...ancestry });
|
|
2677
|
+
}
|
|
2678
|
+
else if (verdict === "unconfirmed") {
|
|
2679
|
+
evidenceUnavailable = true;
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
// Two identical H3 launches under Desktop are not distinguishable once the
|
|
2683
|
+
// port table is unavailable. Choosing one would turn ambiguity into authority.
|
|
2684
|
+
if (evidenceUnavailable || matches.length !== 1)
|
|
2685
|
+
return undefined;
|
|
2686
|
+
const match = matches[0];
|
|
2687
|
+
if (!match)
|
|
2688
|
+
return undefined;
|
|
2689
|
+
return {
|
|
2690
|
+
pid: match.pid,
|
|
2691
|
+
port,
|
|
2692
|
+
argv: [...serverArgv],
|
|
2693
|
+
osArgv: match.identity.argv,
|
|
2694
|
+
osArgvExact: match.identity.argvFidelity === "exact",
|
|
2695
|
+
osCommandLine: match.identity.commandLine,
|
|
2696
|
+
observedInterpreter: observedInterpreterFromIdentity(match.identity),
|
|
2697
|
+
isDesktopApp: true,
|
|
2698
|
+
desktopExePath: match.desktopIdentity.executablePath,
|
|
2699
|
+
startedAt: match.identity.startedAt,
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2337
2702
|
async function gatherProcessInfo() {
|
|
2338
2703
|
const port = config.resolvedPort;
|
|
2339
2704
|
// 1. Get argv from /system_stats, BRACKETED by port-owner lookups.
|
|
@@ -2490,6 +2855,17 @@ async function gatherProcessInfo() {
|
|
|
2490
2855
|
throw err;
|
|
2491
2856
|
}
|
|
2492
2857
|
if (!pid) {
|
|
2858
|
+
// A broken Windows listener can leave the server alive and still answering
|
|
2859
|
+
// through an already-established panel connection, while every port-owner
|
|
2860
|
+
// lookup returns unknown. Bind that server through the authenticated
|
|
2861
|
+
// Desktop→Python ancestry instead of treating a Desktop name scan as proof.
|
|
2862
|
+
// The recovery helper returns nothing unless the command and ancestry are
|
|
2863
|
+
// independently verified, so the ordinary fail-closed error remains intact.
|
|
2864
|
+
if (argv.length > 0) {
|
|
2865
|
+
const recovered = recoverDesktopProcessFromAncestry(port, argv);
|
|
2866
|
+
if (recovered)
|
|
2867
|
+
return recovered;
|
|
2868
|
+
}
|
|
2493
2869
|
// Liveness is the reachable SERVER, not only a local PID scan. If
|
|
2494
2870
|
// /system_stats just answered (argv populated) yet we still can't map the
|
|
2495
2871
|
// listening socket to a PID, say so precisely instead of claiming the
|