- pm.environment owns _RESOLVER_MARKERS: the streaming uv runner imported
pm.workspace, whose tomllib import fails on the 3.10 system python that
bootstraps the Docker arm64 image (No module named 'tomllib'). Invariant test
proves runtime staging needs neither tomllib nor pm.workspace.
- run_tests.sh forwards the MSVC/SDK/Rust/OpenSSL toolchain variables through
its env -i scrub so PM tests that compile ruamel-yaml-clib on Windows arm64
find cl.exe (previously 'Visual C++ 14.0 or greater is required').
- nix desktop-backend check: the spawned backend outlives cage's process group
and kept writing under the temp HERMES_HOME during rmtree; stop every
process bound to the throwaway HOME before cleanup.
- windows: test_launcher_runtime_selection imports runtime_state from
hermes_cli (moved in bbec973514); the ' spaced ' suffix row loses its
trailing space on win32 (the filesystem strips it).
- macOS: test_sealed_worker_command copies the interpreter into the payload
(the escape guard resolves symlinks) and links the host lib tree.
111 lines
4.5 KiB
Python
111 lines
4.5 KiB
Python
"""Launch the packaged desktop with a competing mutable install."""
|
|
import contextlib
|
|
import os
|
|
from pathlib import Path
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
|
|
|
|
def _processes_under(home: Path) -> list[int]:
|
|
"""PIDs whose environment or cwd binds them to this throwaway HOME (Linux /proc)."""
|
|
needle = str(home).encode()
|
|
found = []
|
|
for entry in os.listdir("/proc"):
|
|
if not entry.isdigit() or int(entry) == os.getpid():
|
|
continue
|
|
try:
|
|
environ = Path("/proc", entry, "environ").read_bytes()
|
|
cwd = os.readlink(f"/proc/{entry}/cwd").encode()
|
|
except OSError:
|
|
continue
|
|
if needle in environ or cwd.startswith(needle):
|
|
found.append(int(entry))
|
|
return found
|
|
|
|
|
|
desktop, expected = sys.argv[1:]
|
|
with tempfile.TemporaryDirectory(prefix="hermes-desktop-backend-") as temporary:
|
|
home = Path(temporary)
|
|
hermes_home = home / ".hermes"
|
|
legacy = hermes_home / "hermes-agent"
|
|
(legacy / "hermes_cli").mkdir(parents=True)
|
|
(legacy / "hermes_cli" / "main.py").touch()
|
|
launcher = legacy / "venv" / "bin" / "hermes"
|
|
launcher.parent.mkdir(parents=True)
|
|
# The old runtime passes discovery but cannot initialize a session. It
|
|
# must not win merely because it was installed before the Nix desktop.
|
|
launcher.write_text(
|
|
f"#!{sys.executable}\n"
|
|
"import sys\n"
|
|
"if '--version' in sys.argv:\n"
|
|
" print('Hermes legacy fixture')\n"
|
|
" sys.exit(0)\n"
|
|
"print('WRONG_BACKEND_SELECTED', flush=True)\n"
|
|
"sys.exit(73)\n"
|
|
)
|
|
launcher.chmod(0o755)
|
|
runtime = home / "runtime"
|
|
runtime.mkdir(mode=0o700)
|
|
env = {
|
|
"PATH": os.environ["PATH"],
|
|
"HOME": str(home),
|
|
"HERMES_HOME": str(hermes_home),
|
|
"HERMES_DESKTOP_USER_DATA_DIR": str(home / "electron"),
|
|
"XDG_RUNTIME_DIR": str(runtime),
|
|
"XDG_CONFIG_HOME": str(home / ".config"),
|
|
"XDG_CACHE_HOME": str(home / ".cache"),
|
|
"WLR_BACKENDS": "headless",
|
|
"WLR_RENDERER": "pixman",
|
|
"WLR_NO_HARDWARE_CURSORS": "1",
|
|
"LANG": "C.UTF-8",
|
|
"TZ": "UTC",
|
|
}
|
|
log_path = home / "desktop-output.log"
|
|
with log_path.open("w") as log:
|
|
child = subprocess.Popen(
|
|
["cage", "--", desktop, "--no-sandbox", "--disable-gpu", "--ozone-platform=wayland"],
|
|
cwd=home, env=env, stdout=log, stderr=subprocess.STDOUT,
|
|
start_new_session=True,
|
|
)
|
|
try:
|
|
deadline = time.monotonic() + 90
|
|
while time.monotonic() < deadline:
|
|
output = log_path.read_text()
|
|
desktop_log = hermes_home / "logs" / "desktop.log"
|
|
if desktop_log.exists():
|
|
output += desktop_log.read_text()
|
|
assert "WRONG_BACKEND_SELECTED" not in output, output
|
|
assert child.poll() is None, output
|
|
if "HERMES_BACKEND_READY port=" in output:
|
|
assert f"existing Hermes CLI at {expected}" in output, output
|
|
print("PASS: packaged desktop selected its pinned backend over the mutable install")
|
|
break
|
|
time.sleep(0.1)
|
|
else:
|
|
raise AssertionError(f"Desktop did not start its backend:\n{log_path.read_text()}")
|
|
finally:
|
|
os.killpg(child.pid, signal.SIGTERM)
|
|
try:
|
|
child.wait(timeout=15)
|
|
except subprocess.TimeoutExpired:
|
|
os.killpg(child.pid, signal.SIGKILL)
|
|
child.wait(timeout=5)
|
|
# The desktop spawns its backend in its own session (hermes serve outlives a
|
|
# window close on purpose), so killing cage's group leaves that gateway writing
|
|
# under HERMES_HOME while the tempdir is removed. Stop everything still rooted
|
|
# in this home before cleanup; the sandbox has no other processes to confuse.
|
|
survivors = _processes_under(home)
|
|
for pid in survivors:
|
|
with contextlib.suppress(ProcessLookupError, PermissionError):
|
|
os.kill(pid, signal.SIGTERM)
|
|
deadline = time.monotonic() + 15
|
|
while survivors and time.monotonic() < deadline:
|
|
time.sleep(0.2)
|
|
survivors = _processes_under(home)
|
|
for pid in survivors:
|
|
with contextlib.suppress(ProcessLookupError, PermissionError):
|
|
os.kill(pid, signal.SIGKILL)
|