Merge pull request #1135 from ispyisail/fix/qet-mcp-windows-launch

Fix the MCP server failing to start QElectroTech on Windows
This commit is contained in:
ispyisail
2026-09-29 18:52:10 +13:00
committed by GitHub
2 changed files with 79 additions and 7 deletions
+40 -7
View File
@@ -884,6 +884,44 @@ def tool_element_info(path: str) -> dict:
}
def _launch_executable(src: Path, sandbox: Path, windows: bool) -> Path:
"""The executable _run_qet() starts: a private copy, except on Windows.
The copy gives each run its own SingleApplication key, which is derived
from the executable's path. On Windows a program loads its DLLs from its
own folder, so a copy on its own dies before main() (0xC0000135, DLL not
found) and nothing could ever be exported or edited there. Run the
original instead: every flag this server passes is a CLI export flag or
--run, and QElectroTech handles both and returns before it constructs
SingleApplication (main.cpp), so there is no instance to be handed to.
The copy stays elsewhere for builds from before that early return.
"""
if windows:
return src
exe = sandbox / f"qet-mcp-{os.getpid()}"
shutil.copy2(src, exe)
return exe
def _launch_env(env: dict, home: Path, windows: bool) -> dict:
"""The environment _run_qet() starts QElectroTech in.
A private HOME and XDG directories, and, except on Windows, Qt's
offscreen platform so no display is needed. The Windows packages ship
only the qwindows platform plugin: asked for "offscreen", Qt finds no
plugin and stops at a message box nobody can close, so every call hung
until its timeout. The export flags and --run open no window there, so
the default platform is what they need.
"""
env = dict(env,
HOME=str(home),
XDG_CONFIG_HOME=str(home / ".config"),
XDG_DATA_HOME=str(home / ".local" / "share"))
if not windows:
env["QT_QPA_PLATFORM"] = "offscreen"
return env
def _run_qet(binary: str, args: list[str], timeout: int = 180,
elements_dir: str | None = None,
script: str | None = None, tail: int = 4000) -> dict:
@@ -925,8 +963,7 @@ def _run_qet(binary: str, args: list[str], timeout: int = 180,
raise ValueError(f"not an executable: {src}")
with tempfile.TemporaryDirectory(prefix="qet-mcp-") as tmp:
sandbox = Path(tmp)
exe = sandbox / f"qet-mcp-{os.getpid()}"
shutil.copy2(src, exe)
exe = _launch_executable(src, sandbox, os.name == "nt")
home = sandbox / "home"
(home / ".config").mkdir(parents=True)
(home / ".local" / "share").mkdir(parents=True)
@@ -943,11 +980,7 @@ def _run_qet(binary: str, args: list[str], timeout: int = 180,
script_path = sandbox / "qet-mcp-edit.js"
script_path.write_text(script, encoding="utf-8")
args = ["--run", str(script_path), *args]
env = dict(os.environ,
HOME=str(home),
XDG_CONFIG_HOME=str(home / ".config"),
XDG_DATA_HOME=str(home / ".local" / "share"),
QT_QPA_PLATFORM="offscreen")
env = _launch_env(dict(os.environ), home, os.name == "nt")
try:
p = subprocess.run([str(exe), *args], env=env, timeout=timeout,
capture_output=True, text=True)
+39
View File
@@ -2563,6 +2563,45 @@ class BinaryPolicy(unittest.TestCase):
str((prefix / "share" / "qelectrotech" / "elements").resolve()))
class LaunchExecutable(unittest.TestCase):
"""Windows cannot run a lone copy of QElectroTech (F065): its DLLs sit
beside the original. Everywhere else the private copy stays."""
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
base = Path(self.tmp.name)
self.src = base / "bin" / "qelectrotech"
self.src.parent.mkdir()
self.src.write_text("#!/bin/sh\nexit 0\n")
self.sandbox = base / "sandbox"
self.sandbox.mkdir()
def tearDown(self):
self.tmp.cleanup()
def test_windows_runs_the_original(self):
self.assertEqual(m._launch_executable(self.src, self.sandbox, True), self.src)
self.assertEqual(list(self.sandbox.iterdir()), [], "nothing is copied on Windows")
def test_windows_keeps_its_own_qt_platform(self):
"""The Windows packages have no offscreen plugin; asking for it
leaves QElectroTech stuck at a message box."""
env = m._launch_env({"PATH": "x"}, self.sandbox, True)
self.assertNotIn("QT_QPA_PLATFORM", env)
self.assertEqual(env["PATH"], "x")
def test_elsewhere_runs_offscreen(self):
env = m._launch_env({}, self.sandbox, False)
self.assertEqual(env["QT_QPA_PLATFORM"], "offscreen")
self.assertEqual(env["HOME"], str(self.sandbox))
def test_elsewhere_runs_a_private_copy(self):
exe = m._launch_executable(self.src, self.sandbox, False)
self.assertEqual(exe.parent, self.sandbox)
self.assertNotEqual(exe, self.src)
self.assertEqual(exe.read_text(), self.src.read_text())
class ScriptingDisabledHint(unittest.TestCase):
"""QElectroTech may refuse to run scripts at all, and says so in French.