"""Shared helpers for the real-process Windows hand-off protocol tests (not collected).""" from __future__ import annotations import os from pathlib import Path import re import subprocess import sys import time import pytest ROOT = Path(__file__).resolve().parent.parent.parent.parent SCRIPT = ROOT / 'scripts/desktop-update/windows.ps1' MARKER_PS1 = ROOT / 'scripts/desktop-update/marker.ps1' MARKER = '.hermes-update-in-progress' POWERSHELL = os.path.join(os.environ.get('SystemRoot', r'C:\Windows'), 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe') HOLD_CLI = """ import os, sys, time from pathlib import Path def main(): if '--version' in sys.argv: print('Install directory: ' + str(Path(__file__).resolve().parents[1])); return 0 if '--help' in sys.argv: print('--keep-stash'); return 0 if sys.argv[1:2] == ['update']: # the update's first act: say so, then wait to be released hold = Path(os.environ['HANDOFF_HOLD']) Path(str(hold) + '.pid').write_text(str(os.getpid()), encoding='utf-8') while not hold.exists(): time.sleep(0.05) return 0 if __name__ == '__main__': sys.exit(main()) """ def _ps(command: str, timeout: int = 60) -> str: return subprocess.run([POWERSHELL, '-NoProfile', '-Command', command], capture_output=True, text=True, timeout=timeout, check=True).stdout.strip() def _creation_time(pid: int) -> str: out = _ps(f"$c = (Get-CimInstance Win32_Process -Filter 'ProcessId={pid}').CreationDate; " "[DateTimeOffset]::new($c.ToUniversalTime()).ToUnixTimeMilliseconds().ToString()") return f'{int(out) / 1000:.3f}' def _alive(pid: int) -> bool: return str(pid) in subprocess.run(['tasklist', '/FI', f'PID eq {pid}', '/NH'], capture_output=True, text=True).stdout def _dead_pid() -> int: proc = subprocess.Popen([sys.executable, '-c', 'pass']) proc.wait() return proc.pid def _env(home: Path, **extra: str) -> dict[str, str]: return {**os.environ, 'HERMES_HOME': str(home), 'HERMES_RUNTIME_DIR': str(home / 'empty-store'), **extra} def _script(home: Path, *args: str, install: Path | None = None, **env: str) -> subprocess.Popen: return subprocess.Popen( [POWERSHELL, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', str(SCRIPT), '-InstallRoot', str(install or home / 'hermes-agent'), '-NoUi', *args], cwd=home, env=_env(home, **env), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) def _finish(proc: subprocess.Popen, timeout: int = 120) -> tuple[int, str]: try: out, _ = proc.communicate(timeout=timeout) except subprocess.TimeoutExpired: subprocess.run(['taskkill', '/T', '/F', '/PID', str(proc.pid)], capture_output=True) out, _ = proc.communicate() pytest.fail(f'hand-off did not finish within {timeout}s: {out}') return proc.returncode, out def _op(home: Path, *args: str) -> tuple[int, str, str]: proc = subprocess.run( [POWERSHELL, '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', str(SCRIPT), '-InstallRoot', str(home / 'hermes-agent'), *args], cwd=home, env=_env(home), capture_output=True, text=True, timeout=120) return proc.returncode, proc.stdout, proc.stderr def _log(home: Path) -> str: path = home / 'logs/desktop-update-handoff.log' return path.read_text(encoding='utf-8-sig') if path.exists() else '' def _custodian(home: Path, handoff: int) -> str: """The pid windows.ps1 names on line 1 before any update work starts: its custodian, which outlives the hand-off so an old Desktop never reads a dead owner ('' until it is named).""" found = re.search(rf'names its custodian pid (\d+) \(hand-off pid {handoff}\)', _log(home)) return found.group(1) if found else '' class _HeldLock: """Hold the A7 sidecar lock from another process (this one): an open handle conflicts with the script's FileShare.None open.""" def __init__(self, home: Path) -> None: deadline = time.monotonic() + 30 while True: try: self._f = open(home / (MARKER + '.lock'), 'a+b') # noqa: SIM115 # windows-footgun: ok — binary mode return except PermissionError: # the script holds it right now assert time.monotonic() < deadline, 'never got the marker lock' time.sleep(0.02) def release(self) -> None: self._f.close() def _wait_for_log(home: Path, needle: str, timeout: int = 90) -> str: deadline = time.monotonic() + timeout while needle not in _log(home): assert time.monotonic() < deadline, f'{needle!r} never logged: {_log(home)}' time.sleep(0.1) return _log(home)