File size: 5,646 Bytes
5655a42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
"""Endpoint resolution for llamacpp-alias requests (provider integration).

``provider: llamacpp`` with no explicit base_url resolves, in order, to the managed server (state
file), a detected external llama-server, or — during a backend boot race — the managed server once
its state file appears.
"""

from __future__ import annotations

from contextlib import suppress
import json
import logging
import threading
import time
import urllib.request

LLAMACPP_ALIASES = frozenset({"llamacpp", "llama.cpp", "llama-cpp"})

logger = logging.getLogger(__name__)


def _pid_alive(pid: int) -> bool:
    """Liveness for the state file's supervisor-child pid: psutil when available, else True
    (optimistic). On Windows ``os.kill(pid, 0)`` TERMINATES the process — never use it as a probe."""
    if not pid or pid < 0:
        return False
    with suppress(Exception):
        import psutil  # type: ignore

        return psutil.pid_exists(pid)
    return True


def _state_endpoint() -> dict | None:
    from hermes_cli.local_runtime.recovery import is_modern, read_state, recorded_process

    state = read_state()
    base_url = state.get("base_url", "")
    if not isinstance(base_url, str) or not base_url:
        return None
    if is_modern(state):
        if recorded_process(state) is None:
            return None
    else:
        # Preserve the legacy endpoint shape, with malformed PID values rejected.
        try:
            pid = state.get("pid")
            if isinstance(pid, bool) or not _pid_alive(int(pid or 0)):
                return None
        except (TypeError, ValueError, OverflowError):
            return None
    return {"base_url": base_url, "api_key": state.get("api_key", "")}


def managed_root() -> "tuple[str, str] | None":
    """(base_root, api_key) of the managed router, or None. Resolved through the
    ownership-guarded reader, not a raw state-file read: on the shared stable port a foreign
    install's server answers /health for anyone, and a raw read would attach callers to someone
    else's server."""
    with suppress(Exception):
        state = _state_endpoint()
        if state is None:
            return None
        base = str(state.get("base_url", "")).rsplit("/v1", 1)[0]
        return (base, str(state.get("api_key", ""))) if base else None
    return None


def managed_get_json(base: str, api_key: str, route: str, timeout_s: float) -> object:
    """Authenticated GET against the managed router; raises on any transport/decode failure."""
    req = urllib.request.Request(f"{base}{route}",
                                 headers={"Authorization": f"Bearer {api_key}"})
    with urllib.request.urlopen(req, timeout=timeout_s) as r:
        return json.loads(r.read())


def resolve_llamacpp_endpoint(config: dict | None = None,
                              wait_for_boot_s: float = 8.0) -> dict | None:
    """Managed-first, detection-second endpoint for llamacpp aliases.

    Boot-race rung: on a fresh backend start there is NO state file yet — the lifespan boot thread
    is still spawning the server (≈1-3 s) while the desktop's readiness probe fires the moment the
    WebSocket connects.
    """
    managed = _state_endpoint()
    if managed:
        return managed

    from hermes_cli.local_runtime.detect import detect_server

    ports = ((config or {}).get("local_runtime") or {}).get("detect_ports") or []
    hit = detect_server(extra_ports=tuple(int(p) for p in ports))
    if hit and not hit.auth_required:
        return {"base_url": hit.base_url, "api_key": ""}

    if wait_for_boot_s > 0 and _boot_in_flight(config):
        _kick_managed_boot(config)
        deadline = time.monotonic() + wait_for_boot_s
        while time.monotonic() < deadline:
            time.sleep(0.25)
            managed = _state_endpoint()
            if managed:
                return managed
    return None


_KICK_LOCK = threading.Lock()


def _load_config_if_none(config: dict | None) -> dict | None:
    if config is not None:
        return config
    from hermes_cli.config import load_config

    return load_config()


def _kick_managed_boot(config: dict | None) -> None:
    """Actively start the managed server when resolution finds it missing — the wait loop assumes
    some OTHER thread is bringing it up, which is true only at backend start."""
    if not _KICK_LOCK.acquire(blocking=False):
        return  # a kick is already in flight

    def _boot() -> None:
        try:
            from hermes_cli.local_runtime.bootstrap import ensure_local_runtime

            ensure_local_runtime(_load_config_if_none(config))
        except Exception:  # noqa: BLE001 — best-effort; resolution falls back
            logger.warning("on-demand managed-server boot failed", exc_info=True)
        finally:
            _KICK_LOCK.release()

    threading.Thread(target=_boot, daemon=True,
                     name="lr-on-demand-boot").start()


def _boot_in_flight(config: dict | None) -> bool:
    """True when the managed runtime is enabled and installed (a verified-manifest scan under
    runtimes_root(), NOT a bare ``server_binary()`` call — that needs an install_dir, and calling
    it bare once made this gate throw-and-return False forever, disabling the boot wait)."""
    with suppress(Exception):
        config = _load_config_if_none(config)
        if not ((config or {}).get("local_runtime") or {}).get("enabled"):
            return False
        from hermes_cli.local_runtime.binaries import manifest_verified, runtimes_root

        return any(manifest_verified(m) for m in runtimes_root().glob("*/*/manifest.json"))
    return False