test(install-e2e/windows): capture the packaged launch of a PM-launched hermes desktop

`windows: * -> hermes-desktop-app-update (HEAD -> NEXT)` failed with
"a launch was actually captured (exit 0 without a launch must not pass)":
`hermes desktop` built the app and launched the real Hermes.exe, and the
driver's hook never saw it. There were two gaps, and either one alone is
enough to miss the launch:

- HEAD installs publish the PM launcher (.hermes\bin\hermes.exe), which
  runs its interpreter with -I and drops PYTHONPATH, so sitecustomize never
  loads. windows-e2e.ps1 now does what installer-script-e2e.sh already does:
  run launch-capture/pm-launch.py for a PM launcher, and keep PYTHONPATH
  for pre-PM venv console scripts.
- Since 3ddf82fe24 the Windows packaged launch is a detached
  subprocess.Popen, not subprocess.run. sitecustomize now also intercepts a
  launch-shaped Popen: it records the same spec and stands in for a child
  that already exited with 0. Every other Popen, including the one inside
  subprocess.run, spawns for real.
This commit is contained in:
teknium1
2026-09-26 18:32:12 -07:00
committed by Teknium
parent 5f9c5a2c38
commit 9f78424dbb
2 changed files with 70 additions and 9 deletions

View File

@@ -30,9 +30,14 @@ tag's own hermes_cli/main.py):
where launch_command[0] is the packaged app executable
under apps/desktop/release/
Both go through ``subprocess.run`` with an explicit ``env=`` kwarg. npm
BUILD calls (``npm run build`` / ``npm run pack``) carry no ``electron``
token in argv and pass through untouched -- they must run for real.
3ddf82fe249+ on Windows: the packaged launch is a detached
subprocess.Popen(launch_command, creationflags=...) and
``hermes desktop`` exits without waiting for it
Every shape passes an explicit ``env=`` kwarg, so both ``subprocess.run``
and ``subprocess.Popen`` are intercepted. npm BUILD calls (``npm run
build`` / ``npm run pack``) carry no ``electron`` token in argv and pass
through untouched -- they must run for real.
"""
import os
@@ -74,14 +79,14 @@ if _SPEC_PATH:
return "packaged"
return ""
def _capturing_run(*args, **kwargs):
def _launch_shape(args, kwargs) -> "tuple[list[str], str]":
argv = args[0] if args else kwargs.get("args")
if not isinstance(argv, (list, tuple)):
return _real_run(*args, **kwargs)
return [], ""
tokens = [str(t) for t in argv]
shape = _match_shape(tokens)
if not shape:
return _real_run(*args, **kwargs)
return tokens, _match_shape(tokens)
def _capture(tokens: "list[str]", kwargs: dict, shape: str) -> None:
env = kwargs.get("env")
spec = {
"argv": tokens,
@@ -98,6 +103,54 @@ if _SPEC_PATH:
with open(_SPEC + ".captured", "w", encoding="utf-8") as fh:
fh.write(shape)
print(f"[e2e launch-capture] captured {shape} launch -> {_SPEC} (not spawning)")
def _capturing_run(*args, **kwargs):
tokens, shape = _launch_shape(args, kwargs)
if not shape:
return _real_run(*args, **kwargs)
_capture(tokens, kwargs, shape)
return subprocess.CompletedProcess(tokens, 0, stdout=None, stderr=None)
_RealPopen = subprocess.Popen
class _CapturingPopen(_RealPopen): # type: ignore[misc, valid-type]
"""A launch-shaped Popen records the spec and stands in for an exited child."""
_e2e_stub = False
def __init__(self, *args, **kwargs):
tokens, shape = _launch_shape(args, kwargs)
if not shape:
super().__init__(*args, **kwargs)
return
_capture(tokens, kwargs, shape)
self._e2e_stub = True
self._child_created = False # Popen.__del__ must not reap a pid we never made.
self.args = tokens
self.stdin = self.stdout = self.stderr = None
self.pid = 0
self.returncode = 0
def poll(self):
return 0 if self._e2e_stub else super().poll()
def wait(self, timeout=None):
return 0 if self._e2e_stub else super().wait(timeout)
def communicate(self, input=None, timeout=None):
return (None, None) if self._e2e_stub else super().communicate(input, timeout)
def send_signal(self, sig):
if not self._e2e_stub:
super().send_signal(sig)
def terminate(self):
if not self._e2e_stub:
super().terminate()
def kill(self):
if not self._e2e_stub:
super().kill()
subprocess.run = _capturing_run
subprocess.Popen = _CapturingPopen # type: ignore[misc]

View File

@@ -582,7 +582,15 @@ function Invoke-HermesDesktopAppUpdate([string]$TargetSha) {
$prevEap = $ErrorActionPreference; $ErrorActionPreference = "Continue"
Push-Location $InstallDir
try {
& $hermesExe desktop 2>&1 | Add-TsPrefix | Out-File -Encoding UTF8 $log
if ($hermesExe.StartsWith((Join-Path $InstallDir '.hermes'), [StringComparison]::OrdinalIgnoreCase)) {
# The PM launcher runs its interpreter with -I, so PYTHONPATH never
# imports sitecustomize. Same as installer-script-e2e.sh: ask the
# launcher for its own isolated command and inject the hook into it.
& python -I (Join-Path $capDir 'pm-launch.py') $hermesExe $spec 2>&1 | Add-TsPrefix | Out-File -Encoding UTF8 $log
} else {
# Pre-PM venv console scripts load sitecustomize from PYTHONPATH.
& $hermesExe desktop 2>&1 | Add-TsPrefix | Out-File -Encoding UTF8 $log
}
$capExit = $LASTEXITCODE
} finally {
Pop-Location