Starts ``tracedecay dashboard`` and returns its base URL.
()
| 207 | |
| 208 | |
| 209 | def _spawn_dashboard() -> str: |
| 210 | """Starts ``tracedecay dashboard`` and returns its base URL.""" |
| 211 | binary = _find_tracedecay_bin() |
| 212 | if not binary: |
| 213 | raise HTTPException( |
| 214 | status_code=503, |
| 215 | detail=( |
| 216 | "tracedecay binary not found. Install tracedecay or set " |
| 217 | "TRACEDECAY_BIN / TRACEDECAY_DASHBOARD_URL." |
| 218 | ), |
| 219 | ) |
| 220 | project = _project_root() |
| 221 | cmd = [ |
| 222 | binary, |
| 223 | "dashboard", |
| 224 | "--host", |
| 225 | "127.0.0.1", |
| 226 | "--port", |
| 227 | "0", |
| 228 | "--path", |
| 229 | project, |
| 230 | ] |
| 231 | # Spawned on the dedicated long-lived thread so PDEATHSIG binds the |
| 232 | # child's lifetime to the Hermes process, not a transient request thread. |
| 233 | process = _spawn_pool.submit( |
| 234 | subprocess.Popen, |
| 235 | cmd, |
| 236 | stdout=subprocess.PIPE, |
| 237 | stderr=subprocess.PIPE, |
| 238 | text=True, |
| 239 | env=_dashboard_env(), |
| 240 | preexec_fn=_child_preexec if _libc is not None else None, # noqa: PLW1509 — minimal prctl-only hook |
| 241 | ).result(timeout=_SPAWN_TIMEOUT_SECONDS) |
| 242 | |
| 243 | # Single reader per pipe, for the child's whole lifetime: the stderr |
| 244 | # drain keeps a bounded tail for error detail; the stdout reader parses |
| 245 | # the URL line then KEEPS draining (a stopped reader would eventually |
| 246 | # block the server on a full pipe buffer and 502 every proxied request). |
| 247 | stderr_tail: deque = deque(maxlen=_STDERR_TAIL_LINES) |
| 248 | threading.Thread( |
| 249 | target=_drain_pipe, args=(process.stderr, stderr_tail), daemon=True |
| 250 | ).start() |
| 251 | |
| 252 | # The first stdout line is stable: "tracedecay dashboard listening on <url>". |
| 253 | # Extract the URL itself rather than depending on any surrounding text. |
| 254 | url_ready = threading.Event() |
| 255 | url_holder: dict[str, str] = {} |
| 256 | |
| 257 | def _read_stdout() -> None: |
| 258 | if process.stdout is None: |
| 259 | url_ready.set() |
| 260 | return |
| 261 | for line in process.stdout: |
| 262 | stripped = line.strip() |
| 263 | if not url_ready.is_set() and "listening on" in stripped: |
| 264 | match = _LISTENING_URL_RE.search(stripped) |
| 265 | if match: |
| 266 | url_holder["url"] = match.group(0).rstrip("/") |
no test coverage detected