145 lines
5.0 KiB
Python
Executable File
145 lines
5.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""boot-smoke.py — start hermes-webui + plugin, curl every plugin endpoint.
|
|
|
|
Exit 0 if every endpoint returns its expected status, 1 otherwise. Used by
|
|
upstream-sync.py and as a one-shot manual check after install.
|
|
|
|
Usage:
|
|
python3 boot-smoke.py # start webui + smoke + stop
|
|
python3 boot-smoke.py --no-start # webui already running; just smoke
|
|
python3 boot-smoke.py --base http://... # smoke against custom base URL
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import signal
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
from urllib.request import Request, urlopen
|
|
from urllib.error import URLError
|
|
|
|
PLUGIN_REPO = Path(__file__).resolve().parent.parent
|
|
|
|
# Endpoints we expect after the plugin is loaded. Status codes and content
|
|
# checks are minimal — this is "did it boot", not "is it correct".
|
|
SMOKE = [
|
|
{"path": "/health", "expect": [200], "kind": "vanilla"},
|
|
{"path": "/api/vault/status", "expect": [200, 401, 403], "kind": "plugin"},
|
|
{"path": "/api/umbrella", "expect": [200, 401, 403], "kind": "plugin"},
|
|
{"path": "/api/umbrella/doc?path=sot/README.md", "expect": [200, 401, 403], "kind": "plugin"},
|
|
{"path": "/plugins/svrnty/app.css", "expect": [200, 302, 401, 403], "kind": "plugin-static"},
|
|
{"path": "/plugins/svrnty/app.js", "expect": [200, 302, 401, 403], "kind": "plugin-static"},
|
|
{"path": "/plugins/svrnty/umbrella.html", "expect": [200, 302, 401, 403], "kind": "plugin-static"},
|
|
{"path": "/plugins/svrnty/umbrella.css", "expect": [200, 302, 401, 403], "kind": "plugin-static"},
|
|
{"path": "/plugins/svrnty/umbrella.js", "expect": [200, 302, 401, 403], "kind": "plugin-static"},
|
|
]
|
|
|
|
|
|
def _free_port():
|
|
s = socket.socket()
|
|
s.bind(("127.0.0.1", 0))
|
|
port = s.getsockname()[1]
|
|
s.close()
|
|
return port
|
|
|
|
|
|
def _wait_for(url, timeout=20):
|
|
deadline = time.time() + timeout
|
|
while time.time() < deadline:
|
|
try:
|
|
with urlopen(url, timeout=1) as r:
|
|
if r.status < 500:
|
|
return True
|
|
except URLError:
|
|
pass
|
|
except Exception:
|
|
pass
|
|
time.sleep(0.3)
|
|
return False
|
|
|
|
|
|
def _hit(base, path):
|
|
url = base.rstrip("/") + path
|
|
try:
|
|
with urlopen(url, timeout=5) as r:
|
|
return r.status, r.read()[:200]
|
|
except URLError as e:
|
|
if hasattr(e, "code"):
|
|
return e.code, b""
|
|
return None, str(e).encode()
|
|
except Exception as e:
|
|
return None, str(e).encode()
|
|
|
|
|
|
def smoke(base):
|
|
rows = []
|
|
failed = 0
|
|
for s in SMOKE:
|
|
status, _body = _hit(base, s["path"])
|
|
ok = status in s["expect"]
|
|
rows.append({"path": s["path"], "status": status, "kind": s["kind"], "ok": ok})
|
|
if not ok:
|
|
failed += 1
|
|
return rows, failed
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
ap.add_argument("--no-start", action="store_true",
|
|
help="Assume hermes-webui is already running; just curl")
|
|
ap.add_argument("--base", default=None,
|
|
help="Base URL (default http://127.0.0.1:<port>)")
|
|
ap.add_argument("--webui-dir",
|
|
default=str(PLUGIN_REPO.parent / "hermes-webui"),
|
|
help="Path to hermes-webui repo")
|
|
args = ap.parse_args()
|
|
|
|
proc = None
|
|
base = args.base or "http://127.0.0.1:8787"
|
|
|
|
if not args.no_start:
|
|
port = _free_port()
|
|
base = f"http://127.0.0.1:{port}"
|
|
env = os.environ.copy()
|
|
env["HERMES_WEBUI_PYTHON_PLUGIN"] = "svrnty_hermes_webui_plugin"
|
|
env["HERMES_WEBUI_PORT"] = str(port)
|
|
env["HERMES_REPO_ROOT"] = str(PLUGIN_REPO.parent)
|
|
env["PYTHONPATH"] = (
|
|
str(PLUGIN_REPO)
|
|
if not env.get("PYTHONPATH")
|
|
else f"{PLUGIN_REPO}{os.pathsep}{env['PYTHONPATH']}"
|
|
)
|
|
# Best-effort: start under the agent venv if it exists; else system python.
|
|
py = Path(args.webui_dir) / "venv" / "bin" / "python"
|
|
cmd = [str(py) if py.exists() else "python3", "bootstrap.py", str(port), "--foreground"]
|
|
proc = subprocess.Popen(
|
|
cmd, cwd=args.webui_dir, env=env,
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
preexec_fn=os.setsid,
|
|
)
|
|
if not _wait_for(f"{base}/health", timeout=30):
|
|
print(f"FAIL: webui did not respond at {base}/health within 30s",
|
|
file=sys.stderr)
|
|
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
|
sys.exit(1)
|
|
|
|
try:
|
|
rows, failed = smoke(base)
|
|
finally:
|
|
if proc is not None:
|
|
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
|
try:
|
|
proc.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
|
|
|
print(json.dumps({"base": base, "rows": rows, "failed": failed}, indent=2))
|
|
sys.exit(0 if failed == 0 else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|