mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-15 01:54:14 +02:00
Keep headless runs' settings in a folder of their own (QET_SETTINGS_DIR)
The MCP server gives each QElectroTech run a private HOME and writes the element collection path for it (elements_dir) into ~/.config/QElectroTech/QElectroTech.conf there. Only Linux reads that file: Qt keeps settings in the registry on Windows and in the system preferences on macOS. So on those systems elements_dir did nothing, and every run read the user's own settings (#1178). When QET_SETTINGS_DIR names a folder, QElectroTech now keeps its settings in <folder>/QElectroTech/QElectroTech.ini on every system. It is set in main() before the first setting is read. Without the variable nothing changes. The server sets it for each run and writes the collection path to the .ini as well as the .conf, so an older QElectroTech keeps working on Linux. The path is written with forward slashes: Qt reads a backslash in these files as an escape. The README notes that elements_dir needs this on Windows and macOS, and that long arguments go on stdin with "-" (Windows refuses a command line over 32,767 characters). tst_settingsdir: a script places a symbol that only the folder's settings can resolve, while the usual settings file points at an empty collection. It fails on master and passes here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -161,6 +161,10 @@ python3 qet_mcp.py --call qet_elements '{"path": "drawing.qet"}'
|
||||
echo '{"path": "drawing.qet"}' | python3 qet_mcp.py --call qet_check -
|
||||
```
|
||||
|
||||
Pass long arguments, such as a large `qet_edit` operation list, on stdin
|
||||
with `-`: Windows refuses a command line over 32,767 characters
|
||||
(`WinError 206`).
|
||||
|
||||
It prints the tool's JSON result and exits 0, or 1 if the tool reported an
|
||||
error, or 2 if the call itself was malformed. The workspace rule applies as
|
||||
it does in a server. The sandbox has no QElectroTech in it, so only the
|
||||
@@ -472,7 +476,10 @@ Python, plus the hang guard on `addConductor` and the database refresh in
|
||||
collection path, which on a machine that never ran `make install` does not
|
||||
exist. The only symptom is `addElement` reporting that a file plainly
|
||||
present "does not resolve to an element". An absolute `.elmt` path works
|
||||
without it.
|
||||
without it. On Windows and macOS, `elements_dir` needs a QElectroTech that
|
||||
reads `QET_SETTINGS_DIR` (#1178): an older one keeps its settings in the
|
||||
registry or the system preferences, never sees the path written for the
|
||||
run, and uses the collection it was installed with.
|
||||
- **`set_conductor` changes the whole potential, not one segment.** That is
|
||||
what the application does — a wire number describes a potential — so name
|
||||
a terminal carrying exactly one conductor and the change reaches every
|
||||
|
||||
+32
-13
@@ -64,7 +64,7 @@ import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
from pathlib import Path, PurePath
|
||||
|
||||
SERVER_NAME = "qet-mcp"
|
||||
SERVER_VERSION = "0.1.0"
|
||||
@@ -906,22 +906,40 @@ def _launch_executable(src: Path, sandbox: Path, windows: bool) -> Path:
|
||||
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.
|
||||
A private HOME and XDG directories, and QET_SETTINGS_DIR pointing into
|
||||
them. HOME and XDG move QElectroTech's settings only on Linux: Qt keeps
|
||||
them in the registry on Windows and in the user's preferences on
|
||||
macOS, so without QET_SETTINGS_DIR every run there read the user's own
|
||||
settings and never saw a collection path written for it
|
||||
(qelectrotech-source-mirror#1178). A QElectroTech that knows the
|
||||
variable keeps its settings in an INI file in that folder instead.
|
||||
|
||||
Except on Windows, also 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"))
|
||||
XDG_DATA_HOME=str(home / ".local" / "share"),
|
||||
QET_SETTINGS_DIR=str(home / ".config"))
|
||||
if not windows:
|
||||
env["QT_QPA_PLATFORM"] = "offscreen"
|
||||
return env
|
||||
|
||||
|
||||
def _collection_setting(collection: PurePath) -> str:
|
||||
"""The settings file that points QElectroTech at @p collection.
|
||||
|
||||
Forward slashes: Qt reads a backslash in these files as an escape, so a
|
||||
Windows path written as-is arrives mangled."""
|
||||
return ("[elements-collections]\n"
|
||||
f"common-collection-path={collection.as_posix()}\n")
|
||||
|
||||
|
||||
def _run_qet(binary: str, args: list[str], timeout: int = 180,
|
||||
elements_dir: str | None = None,
|
||||
script: str | None = None, tail: int = 4000) -> dict:
|
||||
@@ -941,8 +959,10 @@ def _run_qet(binary: str, args: list[str], timeout: int = 180,
|
||||
reporting "does not resolve to an element" for a file that is plainly
|
||||
there. elements_dir writes the one setting that fixes it. The file name
|
||||
is not free-choice: QSettings derives it from the organisation and
|
||||
application names main.cpp sets before this branch runs, so it must be
|
||||
QElectroTech/QElectroTech.conf and nothing else.
|
||||
application names main.cpp sets before this branch runs. It is written
|
||||
twice: QElectroTech/QElectroTech.ini is what a QElectroTech that knows
|
||||
QET_SETTINGS_DIR reads, on every system (see _launch_env()), and
|
||||
QElectroTech/QElectroTech.conf is what an older one reads on Linux.
|
||||
|
||||
script, when given, is written into the sandbox and passed to --run.
|
||||
It lives inside the temporary directory so it cannot collide with a
|
||||
@@ -973,9 +993,8 @@ def _run_qet(binary: str, args: list[str], timeout: int = 180,
|
||||
raise ValueError(f"no such elements directory: {coll}")
|
||||
cfg = home / ".config" / "QElectroTech"
|
||||
cfg.mkdir(parents=True, exist_ok=True)
|
||||
(cfg / "QElectroTech.conf").write_text(
|
||||
"[elements-collections]\n"
|
||||
f"common-collection-path={coll}\n", encoding="utf-8")
|
||||
for name in ("QElectroTech.ini", "QElectroTech.conf"):
|
||||
(cfg / name).write_text(_collection_setting(coll), encoding="utf-8")
|
||||
if script is not None:
|
||||
script_path = sandbox / "qet-mcp-edit.js"
|
||||
script_path.write_text(script, encoding="utf-8")
|
||||
|
||||
@@ -2632,6 +2632,13 @@ class LaunchExecutable(unittest.TestCase):
|
||||
self.assertEqual(env["QT_QPA_PLATFORM"], "offscreen")
|
||||
self.assertEqual(env["HOME"], str(self.sandbox))
|
||||
|
||||
def test_settings_stay_in_the_sandbox_everywhere(self):
|
||||
"""HOME and XDG move the settings only on Linux; QET_SETTINGS_DIR
|
||||
moves them on Windows and macOS too (#1178)."""
|
||||
for windows in (True, False):
|
||||
env = m._launch_env({}, self.sandbox, windows)
|
||||
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)
|
||||
self.assertEqual(exe.parent, self.sandbox)
|
||||
@@ -2639,6 +2646,35 @@ class LaunchExecutable(unittest.TestCase):
|
||||
self.assertEqual(exe.read_text(), self.src.read_text())
|
||||
|
||||
|
||||
class ElementsDirSetting(unittest.TestCase):
|
||||
"""elements_dir reaches QElectroTech as a settings file in the sandbox
|
||||
(#1178): the .ini that a QElectroTech knowing QET_SETTINGS_DIR reads on
|
||||
every system, and the .conf an older one reads on Linux."""
|
||||
|
||||
def test_both_files_carry_the_collection(self):
|
||||
seen = {}
|
||||
|
||||
def run(argv, **kwargs):
|
||||
cfg = Path(kwargs["env"]["QET_SETTINGS_DIR"]) / "QElectroTech"
|
||||
for f in cfg.iterdir():
|
||||
seen[f.name] = f.read_text(encoding="utf-8")
|
||||
return subprocess.CompletedProcess(argv, 0, "", "")
|
||||
|
||||
with tempfile.TemporaryDirectory() as coll, \
|
||||
mock.patch.object(m.subprocess, "run", run):
|
||||
m._run_qet("/bin/true", ["x.qet"], elements_dir=coll)
|
||||
want = m._collection_setting(Path(coll))
|
||||
self.assertEqual(seen, {"QElectroTech.ini": want, "QElectroTech.conf": want})
|
||||
self.assertIn(f"common-collection-path={Path(coll).as_posix()}", want)
|
||||
|
||||
def test_a_windows_path_has_forward_slashes(self):
|
||||
"""Qt reads a backslash in the file as an escape."""
|
||||
from pathlib import PureWindowsPath
|
||||
text = m._collection_setting(PureWindowsPath(r"C:\x\custom"))
|
||||
self.assertIn("common-collection-path=C:/x/custom\n", text)
|
||||
self.assertNotIn("\\", 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