Files
hermes-webui/api/gateway_restart.py

155 lines
5.2 KiB
Python

"""Helpers for restarting the active-profile Hermes gateway."""
from __future__ import annotations
import logging
import os
import shutil
import subprocess
import sys
import threading
from pathlib import Path
from api.profiles import get_active_hermes_home
logger = logging.getLogger(__name__)
_GATEWAY_RESTART_LOCK = threading.Lock()
def _resolve_hermes_command() -> str:
"""Resolve the CLI path used for active-profile gateway restarts."""
hermes_cmd = shutil.which("hermes")
if hermes_cmd:
return hermes_cmd
sibling = Path(sys.executable).parent / "hermes"
if sibling.exists():
return str(sibling)
return "hermes"
def _consume_stream(stream) -> None:
"""Drain a subprocess stream to prevent stdout/stderr pipe deadlocks."""
try:
while stream and stream.read(4096):
pass
except Exception:
pass
def _release_lock() -> None:
try:
_GATEWAY_RESTART_LOCK.release()
except RuntimeError:
# The lock may already have been released by another path.
pass
def restart_active_profile_gateway(
*,
quick_timeout_seconds: float = 2.0,
background_wait_seconds: float = 240.0,
) -> dict:
"""Run a non-blocking ``hermes gateway restart`` for the active profile.
Returns a short status dict with these values:
- completed: command finished quickly and succeeded.
- in_progress: command did not finish within ``quick_timeout_seconds``.
- failed: command finished quickly with non-zero exit status.
- busy: restart already in progress from another caller.
"""
if not _GATEWAY_RESTART_LOCK.acquire(blocking=False):
return {
"status": "busy",
"message": "Restart already in progress. Please wait a moment and try again.",
}
try:
active_home = get_active_hermes_home()
env = os.environ.copy()
env["HERMES_HOME"] = str(active_home)
hermes_cmd = _resolve_hermes_command()
logger.info(
"Restarting gateway service via CLI command: %s gateway restart (HERMES_HOME=%s)",
hermes_cmd,
active_home,
)
proc = subprocess.Popen(
[hermes_cmd, "gateway", "restart"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
env=env,
)
try:
stdout, stderr = proc.communicate(timeout=quick_timeout_seconds)
_release_lock()
stdout = (stdout or "").strip()
stderr = (stderr or "").strip()
if proc.returncode == 0:
logger.info("Gateway service restarted successfully: %s", stdout)
return {
"status": "completed",
"message": "Gateway service restarted successfully",
"detail": stdout or stderr,
}
logger.error("Gateway service restart failed with code %s: %s", proc.returncode, stderr)
return {
"status": "failed",
"message": f"Restart failed: {stderr or stdout}",
"detail": stdout or stderr,
"returncode": proc.returncode,
}
except subprocess.TimeoutExpired:
logger.info(
"Gateway restart is taking longer than %.1fs (likely draining in-flight runs);"
" continuing in background",
quick_timeout_seconds,
)
threading.Thread(target=_consume_stream, args=(proc.stdout,), daemon=True).start()
threading.Thread(target=_consume_stream, args=(proc.stderr,), daemon=True).start()
def _wait_and_release() -> None:
try:
proc.wait(timeout=background_wait_seconds)
except subprocess.TimeoutExpired:
logger.error(
"Gateway restart process timed out after %.1fs. Terminating process.",
background_wait_seconds,
)
try:
proc.terminate()
try:
proc.wait(timeout=5.0)
except subprocess.TimeoutExpired:
proc.kill()
try:
proc.wait(timeout=5.0)
except subprocess.TimeoutExpired:
logger.error(
"Gateway restart process refused to die even after SIGKILL.",
)
except Exception:
logger.exception("Failed to terminate timed out gateway restart process.")
finally:
_release_lock()
threading.Thread(target=_wait_and_release, daemon=True).start()
return {
"status": "in_progress",
"message": "Gateway service restart initiated (in progress)",
}
except Exception as exc:
_release_lock()
logger.exception("Failed to run gateway restart command")
return {
"status": "failed",
"message": f"Internal error running restart: {type(exc).__name__}: {exc}",
}