diff --git a/misc/qet-mcp/qet_mcp.py b/misc/qet-mcp/qet_mcp.py index eae28f5f1..cfff04efc 100755 --- a/misc/qet-mcp/qet_mcp.py +++ b/misc/qet-mcp/qet_mcp.py @@ -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) diff --git a/misc/qet-mcp/test_qet_mcp.py b/misc/qet-mcp/test_qet_mcp.py index 8e876c2c3..701ae5189 100644 --- a/misc/qet-mcp/test_qet_mcp.py +++ b/misc/qet-mcp/test_qet_mcp.py @@ -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())