134 lines
5.2 KiB
Python
134 lines
5.2 KiB
Python
"""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, check=False).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, check=False)
|
|
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, check=False)
|
|
return proc.returncode, proc.stdout, proc.stderr
|
|
|
|
|
|
def _log(home: Path) -> str:
|
|
"""The hand-off log so far. windows.ps1 appends with Add-Content, which holds the file without
|
|
read sharing for the length of each write: a read landing in that window is a sharing
|
|
violation (PermissionError), not a missing line, so it is retried."""
|
|
path = home / 'logs/desktop-update-handoff.log'
|
|
for _ in range(50):
|
|
try:
|
|
return path.read_text(encoding='utf-8-sig') if path.exists() else ''
|
|
except PermissionError:
|
|
time.sleep(0.1)
|
|
return path.read_text(encoding='utf-8-sig')
|
|
|
|
|
|
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') # 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)
|