qet-mcp: run the original QElectroTech on macOS, not a private copy (#1178)

Every headless call failed with the macOS package: _launch_executable()
copied the program out of the signed .app bundle, and macOS kills such a
copy at launch (exit 137). The copy has also lost the path to the bundle's
Qt frameworks. Windows already ran the original for the same kind of
reason (DLLs beside the program); macOS now does too.

That is safe for the same reason as on Windows: the export flags and
--run return before QElectroTech constructs SingleApplication, so there
is no running instance to hand the call to. Linux keeps the private copy.

Reported and tested by DarkBiscuits on macOS 26.7 with the r10796 DMG:
with this one change and QT_QPA_PLATFORM=offscreen, every tool works
headless with no Dock icon.

test_qet_mcp: 421 tests pass on Linux against a master build with
QET_BINARY/QET_ELEMENTS/QET_EXAMPLES and scripting on, none skipped;
test_which_systems_copy fails with darwin removed from the rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-09 12:49:23 +13:00
parent 36cfa938e9
commit d895be6569
2 changed files with 35 additions and 16 deletions
+24 -11
View File
@@ -896,19 +896,32 @@ 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.
def _copies_executable(platform: str = sys.platform) -> bool:
"""Whether _run_qet() runs a private copy of QElectroTech on @p platform:
not on Windows or macOS (see _launch_executable())."""
return platform not in ("win32", "darwin")
def _launch_executable(src: Path, sandbox: Path, copy: bool) -> Path:
"""The executable _run_qet() starts: a private copy when @p copy is set,
otherwise the original.
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.
from the executable's path. Two systems cannot run a copy:
- Windows: a program loads its DLLs from its own folder, so a copy on
its own dies before main() (0xC0000135, DLL not found).
- macOS: a copy taken out of the signed .app bundle is killed at launch
(exit 137) and has lost the path to the bundle's Qt frameworks
(qelectrotech-source-mirror#1178).
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:
if not copy:
return src
exe = sandbox / f"qet-mcp-{os.getpid()}"
shutil.copy2(src, exe)
@@ -996,7 +1009,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 = _launch_executable(src, sandbox, os.name == "nt")
exe = _launch_executable(src, sandbox, _copies_executable())
home = sandbox / "home"
(home / ".config").mkdir(parents=True)
(home / ".local" / "share").mkdir(parents=True)
+11 -5
View File
@@ -3045,7 +3045,8 @@ class BinaryPolicy(unittest.TestCase):
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."""
beside the original. Nor can macOS: a copy out of the signed .app is
killed at launch (#1178). Everywhere else the private copy stays."""
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
@@ -3059,9 +3060,14 @@ class LaunchExecutable(unittest.TestCase):
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_and_macos_run_the_original(self):
self.assertEqual(m._launch_executable(self.src, self.sandbox, False), self.src)
self.assertEqual(list(self.sandbox.iterdir()), [], "nothing is copied")
def test_which_systems_copy(self):
self.assertFalse(m._copies_executable("win32"))
self.assertFalse(m._copies_executable("darwin"))
self.assertTrue(m._copies_executable("linux"))
def test_windows_keeps_its_own_qt_platform(self):
"""The Windows packages have no offscreen plugin; asking for it
@@ -3083,7 +3089,7 @@ class LaunchExecutable(unittest.TestCase):
self.assertEqual(env["QET_SETTINGS_DIR"], str(self.sandbox / ".config"))
def test_elsewhere_runs_a_private_copy(self):
exe = m._launch_executable(self.src, self.sandbox, False)
exe = m._launch_executable(self.src, self.sandbox, True)
self.assertEqual(exe.parent, self.sandbox)
self.assertNotEqual(exe, self.src)
self.assertEqual(exe.read_text(), self.src.read_text())