mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-29 14:14:15 +02:00
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:
+40
-7
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user