mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-07 08:10:45 +00:00
155 lines
5.2 KiB
Python
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}",
|
|
}
|