mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-29 14:14:15 +02:00
qet-mcp: make QElectroTech launches work on Windows
On Windows every tool that starts QElectroTech (qet_export, qet_edit, qet_query, qet_check, qet_continuity, qet_project_new) failed, for two reasons: - The server ran a copy of the executable from a temporary folder, to get its own SingleApplication key. A Windows program loads its DLLs from its own folder, so the copy died before main() with 0xC0000135 (DLL not found). Every flag the server passes is a CLI export flag or --run, and main.cpp handles both before it constructs SingleApplication, so on Windows the original is now run. The copy stays elsewhere. - It set QT_QPA_PLATFORM=offscreen. The Windows packages ship only the qwindows platform plugin, so Qt found none and stopped at a message box nobody could close: every call hung until its timeout. Windows now keeps its default platform; the export flags and --run open no window. Checked under Wine (qet-wine-smoke) on the fork's CI Windows build, run through python.org's embeddable Python: before, qet_export ended with exit 3221225781; after, a PDF export, a qet_query (98 elements, as on Linux) and a qet_edit placing a common:// element all succeed. The hang was isolated by launching the same export from bash (works) and from Python with one change at a time: only dropping QT_QPA_PLATFORM made it work. Four unit tests pin both choices per platform; each fails with its fix removed. Suite 262/262 none skipped on Linux. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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)
|
||||
|
||||
@@ -1902,6 +1902,45 @@ class PathPolicy(unittest.TestCase):
|
||||
f"{name} has no {arg!r} argument to guard")
|
||||
|
||||
|
||||
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