Files
hermes-agent/scripts/termux/validate_installed.py

143 lines
5.8 KiB
Python

"""Exercise a freshly installed Termux bundle with networking disabled."""
from __future__ import annotations
import fcntl
import json
import os
from pathlib import Path
import pty
import re
import select
import struct
import subprocess
import termios
import tempfile
import time
def run(argv: list[str], env: dict[str, str], cwd: Path) -> subprocess.CompletedProcess:
result = subprocess.run(argv, env=env, cwd=cwd, capture_output=True, text=True, timeout=90)
print("+", " ".join(argv), flush=True)
print(result.stdout, result.stderr, flush=True)
result.check_returncode()
return result
def stop_child_tree(child: subprocess.Popen) -> None:
import psutil
from agent.deadline import kill_process_tree
if child.poll() is not None:
return
descendants = psutil.Process(child.pid).children(recursive=True)
kill_process_tree(child.pid)
child.wait(timeout=10)
deadline = time.monotonic() + 10
while descendants and time.monotonic() < deadline:
descendants = [p for p in descendants if p.is_running() and p.status() != psutil.STATUS_ZOMBIE]
if descendants:
time.sleep(0.05)
if descendants:
raise RuntimeError(f"TUI children still alive: {[p.pid for p in descendants]}")
def tui_smoke(launcher: Path, env: dict[str, str], cwd: Path) -> None:
master, slave = pty.openpty()
fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack("HHHH", 40, 120, 0, 0))
child = subprocess.Popen(
[str(launcher), "--tui"], stdin=slave, stdout=slave, stderr=slave,
cwd=cwd, env={**env, "TERM": "xterm-256color"}, start_new_session=True,
)
os.close(slave)
captured = bytearray()
ready = False
try:
deadline = time.monotonic() + 90
while time.monotonic() < deadline:
if select.select([master], [], [], 0.5)[0]:
try:
chunk = os.read(master, 65536)
except OSError:
break
if not chunk:
break
captured.extend(chunk)
plain = re.sub(rb"\x1b\[[0-?]*[ -/]*[@-~]", b"", bytes(captured))
if b"Setup Required" in plain and b"/model" in plain:
ready = True
break
if child.poll() is not None:
break
if not ready:
raise RuntimeError("TUI never reached its real setup screen:\n" + captured.decode(errors="replace"))
print("TUI_SETUP_SCREEN_OK", flush=True)
finally:
try:
stop_child_tree(child)
finally:
os.close(master)
def main() -> None:
prefix = Path(os.environ["PREFIX"])
root = prefix / "lib/hermes-agent"
with tempfile.TemporaryDirectory(prefix="hermes-install-proof-", dir=prefix / "tmp") as tmp:
home = Path(tmp)
env = {
"PREFIX": str(prefix), "HOME": str(home), "HERMES_HOME": str(home / "state"),
"PATH": str(prefix / "bin"), "TERM": "xterm-256color", "LANG": "C.UTF-8",
"LD_LIBRARY_PATH": ":".join((
str(root / "tools/python" / prefix.relative_to("/") / "lib"),
str(root / "tools/node" / prefix.relative_to("/") / "lib"),
str(root / "tools/ffmpeg" / prefix.relative_to("/") / "lib"),
str(root / "runtime-libs/lib"), str(prefix / "lib"),
)),
"HERMES_RUNTIME_DIR": str(root / "tools"),
"PYTHONPATH": str(root / "app"),
"PYTHONPYCACHEPREFIX": str(home / "pycache"),
}
launcher = prefix / "bin/hermes"
run([str(launcher), "--version"], env, home)
run([str(launcher), "chat", "--help"], env, home)
run([str(prefix / "bin/hermes-acp"), "--check"], env, home)
python = root / "venv/bin/python"
run([
str(python), "-c",
"import ctypes, ssl, sqlite3, bz2, lzma, zlib, hashlib, readline; "
"import cli, run_agent, tui_gateway.server; "
"from hermes_cli.config import detect_install_method; "
"assert detect_install_method() == 'apt'; "
"print('CLI_AND_STDLIB_IMPORTS_OK')",
], env, home)
natives = json.loads((root / "native-wheels.json").read_text(encoding="utf-8"))
run([
str(python), str(root / "app/scripts/termux/build_wheels.py"),
"--import-modules", *natives,
], env, home)
node = root / "tools/node" / prefix.relative_to("/") / "bin/node"
run([str(node), "--version"], env, home)
run([str(node), str(root / "tools/npm/lib/node_modules/npm/bin/npm-cli.js"), "--version"], env, home)
run([str(root / "tools/ripgrep/rg"), "--version"], env, home)
ffmpeg = root / "tools/ffmpeg" / prefix.relative_to("/") / "bin/ffmpeg"
output = home / "silence.wav"
run([str(ffmpeg), "-hide_banner", "-f", "lavfi", "-i", "anullsrc=r=16000:cl=mono", "-t", "0.1", str(output)], env, home)
import wave
with wave.open(str(output), "rb") as audio:
assert audio.getnframes() > 0 and audio.getframerate() == 16000
print("FFMPEG_MEDIA_CONVERSION_OK", flush=True)
run([
str(python), "-c",
"import pm; issues = pm.check(); "
"assert not issues, issues; print('PM_RUNTIME_TOOLS_OK')",
], env, home)
result = subprocess.run([str(launcher), "update"], env=env, cwd=home, capture_output=True, text=True, timeout=60)
if result.returncode == 0 or "pkg upgrade hermes-agent" not in result.stdout + result.stderr:
raise RuntimeError(f"wrong updater refusal ({result.returncode}): {result.stdout}\n{result.stderr}")
print("APT_UPDATE_REFUSAL_OK", flush=True)
tui_smoke(launcher, env, home)
print("INSTALLED_BUNDLE_VALIDATION_OK", flush=True)
if __name__ == "__main__":
main()