doctor: detect editor↔project mismatch, robust probe path

The editor probe wrote to a project-relative path and assumed the Remote Control
editor was the same project the server resolved from disk. When they differ (a
different editor is the live RC target), the probe file never matched and the
doctor wrongly reported "enable the Python plugin".

Now the probe writes to a machine-local temp path (RC is always localhost) and
reports the editor's own project, so the doctor:
- correctly confirms Python + C++ regardless of which project is resolved
- flags a clear "Editor ↔ project" warning when the live editor differs

Also use process.exitCode instead of process.exit() in the CLI to avoid a
Windows libuv teardown assertion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Bonchellon
2026-06-24 01:24:58 +03:00
parent 04c1e92a63
commit b04ba768b4

View File

@ -12,6 +12,7 @@
import { ping, executePythonInUE } from "./ueBridge.js";
import * as PP from "./projectPaths.js";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
@ -68,6 +69,23 @@ export async function runDiagnostics({ deep = true } = {}) {
} else {
checks.push({ id: "python", label: "Python plugin", status: STATUS.OK,
detail: probe.engine ? `UE ${probe.engine}` : "ExecutePythonCommand works" });
// The Remote Control editor might be a DIFFERENT project than the one the
// plugin resolved from disk — in which case every project-relative tool
// would target the wrong project. Flag it loudly.
const resolved = PP.projectName();
const editorName = probe.project
? path.basename(probe.project.replace(/\\/g, "/")).replace(/\.uproject$/i, "")
: null;
if (resolved && editorName && editorName.toLowerCase() !== resolved.toLowerCase()) {
checks.push({ id: "project_match", label: "Editor ↔ project", status: STATUS.WARN,
detail: `Remote Control is connected to "${editorName}", but this plugin resolves to "${resolved}".`,
fix: `Open the ${resolved} editor (with Remote Control on), or set UE_PROJECT_DIR / run setup so the server targets "${editorName}".` });
} else if (editorName) {
checks.push({ id: "project_match", label: "Editor ↔ project", status: STATUS.OK,
detail: `live editor = ${editorName}` });
}
checks.push(probe.has_mcp
? { id: "cpp", label: "C++ module", status: STATUS.OK, detail: "UMCPGraphLibrary is loaded" }
: { id: "cpp", label: "C++ module", status: STATUS.FAIL,
@ -107,22 +125,25 @@ async function checkNodeDeps() {
}
// Ask the live editor to write a probe file we can read back (RC doesn't echo
// Python stdout, so we tee to <Project>/Saved/MCP/doctor.json).
// Python stdout). We tee to a MACHINE-LOCAL temp path — not a project-relative
// one — because Remote Control is always localhost, and because the live editor
// may be a different project than the one we resolved from disk (which we want
// to detect, not trip over). The probe reports the editor's own project path.
async function probeEditor() {
const outFile = path.join(PP.projectDir(), "Saved", "MCP", "doctor.json").replace(/\\/g, "/");
const outFile = path.join(os.tmpdir(), "ue_mcp_doctor.json").replace(/\\/g, "/");
try { fs.rmSync(outFile, { force: true }); } catch { /* ignore */ }
const py = [
"import unreal, json, os",
"import unreal, json",
"d = {'ok': True}",
"try: d['has_mcp'] = hasattr(unreal, 'MCPGraphLibrary')",
"except Exception as e: d['has_mcp'] = False",
"try: d['has_voxel'] = hasattr(unreal, 'MCPVoxelGraphLibrary') and unreal.MCPVoxelGraphLibrary.is_voxel_graph_asset is not None",
"except Exception: d['has_mcp'] = False",
"try: d['has_voxel'] = hasattr(unreal, 'MCPVoxelGraphLibrary')",
"except Exception: d['has_voxel'] = False",
"try: d['engine'] = unreal.SystemLibrary.get_engine_version()",
"except Exception: pass",
"p = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_saved_dir()) + 'MCP/'",
"os.makedirs(p, exist_ok=True)",
"open(p + 'doctor.json', 'w', encoding='utf-8').write(json.dumps(d))",
"try: d['project'] = unreal.Paths.get_project_file_path()",
"except Exception: pass",
`open(r'${outFile}', 'w', encoding='utf-8').write(json.dumps(d))`,
].join("\n");
const r = await executePythonInUE(py, { timeoutMs: 8000 });
if (!r.ok) return { ok: false, error: r.error };
@ -133,7 +154,7 @@ async function probeEditor() {
return { ok: true, ...JSON.parse(txt) };
} catch { await sleep(100); }
}
return { ok: false, error: "probe file was not written (Python ran but produced no output)" };
return { ok: false, error: "Python ran but wrote no probe file — the 'Python Editor Script Plugin' may be disabled." };
}
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
@ -166,5 +187,7 @@ if (isMain) {
console.log("\nUnreal Engine MCP — diagnostics\n");
console.log(formatReport(res));
console.log(res.ok ? "\nAll good. ✓\n" : "\nSome checks failed — run `npm run setup` to fix interactively.\n");
process.exit(res.ok ? 0 : 1);
// Set the code and let the event loop drain naturally — calling process.exit()
// here can abort mid-socket-teardown on Windows (libuv assertion).
process.exitCode = res.ok ? 0 : 1;
}