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:
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user