fix(update): add heartbeat during dependency install

This commit is contained in:
adybag14-cyber 2026-05-07 18:12:44 +01:00 committed by Teknium
parent 04193cf71c
commit 54c0b10d14
2 changed files with 74 additions and 19 deletions

View file

@ -230,6 +230,7 @@ except Exception:
pass # best-effort — don't crash if config isn't available yet pass # best-effort — don't crash if config isn't available yet
import logging import logging
import threading
import time as _time import time as _time
from datetime import datetime from datetime import datetime
@ -6445,6 +6446,45 @@ def _load_installable_optional_extras() -> list[str]:
return referenced return referenced
def _run_install_with_heartbeat(
cmd: list[str],
*,
env: dict[str, str] | None = None,
heartbeat_interval_seconds: int = 30,
) -> None:
"""Run dependency install command with periodic heartbeat output.
Some resolvers/build backends (especially when compiling Rust/C extensions)
can stay quiet for minutes. Emit a simple elapsed-time heartbeat so users
know ``hermes update`` is still progressing even if pip/uv itself is silent.
"""
done = threading.Event()
start = _time.time()
def _heartbeat() -> None:
# Wait first, then print, so short installs don't emit noise.
while not done.wait(heartbeat_interval_seconds):
elapsed = int(_time.time() - start)
print(
f" … still installing dependencies ({elapsed}s elapsed)"
" — compiling Rust/C extensions can take several minutes",
flush=True,
)
t = threading.Thread(target=_heartbeat, daemon=True)
t.start()
try:
subprocess.run(
cmd,
cwd=PROJECT_ROOT,
check=True,
env=env,
)
finally:
done.set()
t.join(timeout=0.2)
def _install_python_dependencies_with_optional_fallback( def _install_python_dependencies_with_optional_fallback(
install_cmd_prefix: list[str], install_cmd_prefix: list[str],
*, *,
@ -6461,12 +6501,13 @@ def _install_python_dependencies_with_optional_fallback(
Collecting/Building/Installing step), so keeping it visible costs Collecting/Building/Installing step), so keeping it visible costs
nothing on fast hardware and prevents the "hermes update hangs" reports nothing on fast hardware and prevents the "hermes update hangs" reports
on slow hardware. on slow hardware.
We also add periodic heartbeat lines in case the resolver/build backend is
itself silent for long stretches.
""" """
try: try:
subprocess.run( _run_install_with_heartbeat(
install_cmd_prefix + ["install", "-e", ".[all]"], install_cmd_prefix + ["install", "-e", ".[all]"],
cwd=PROJECT_ROOT,
check=True,
env=env, env=env,
) )
return return
@ -6475,10 +6516,8 @@ def _install_python_dependencies_with_optional_fallback(
" ⚠ Optional extras failed, reinstalling base dependencies and retrying extras individually..." " ⚠ Optional extras failed, reinstalling base dependencies and retrying extras individually..."
) )
subprocess.run( _run_install_with_heartbeat(
install_cmd_prefix + ["install", "-e", "."], install_cmd_prefix + ["install", "-e", "."],
cwd=PROJECT_ROOT,
check=True,
env=env, env=env,
) )
@ -6486,10 +6525,8 @@ def _install_python_dependencies_with_optional_fallback(
installed_extras: list[str] = [] installed_extras: list[str] = []
for extra in _load_installable_optional_extras(): for extra in _load_installable_optional_extras():
try: try:
subprocess.run( _run_install_with_heartbeat(
install_cmd_prefix + ["install", "-e", f".[{extra}]"], install_cmd_prefix + ["install", "-e", f".[{extra}]"],
cwd=PROJECT_ROOT,
check=True,
env=env, env=env,
) )
installed_extras.append(extra) installed_extras.append(extra)

View file

@ -323,15 +323,15 @@ def test_cmd_update_retries_optional_extras_individually_when_all_fails(monkeypa
return SimpleNamespace(stdout="main\n", stderr="", returncode=0) return SimpleNamespace(stdout="main\n", stderr="", returncode=0)
if cmd == ["git", "rev-list", "HEAD..origin/main", "--count"]: if cmd == ["git", "rev-list", "HEAD..origin/main", "--count"]:
return SimpleNamespace(stdout="1\n", stderr="", returncode=0) return SimpleNamespace(stdout="1\n", stderr="", returncode=0)
if cmd == ["git", "pull", "origin", "main"]: if cmd == ["git", "pull", "--ff-only", "origin", "main"]:
return SimpleNamespace(stdout="Updating\n", stderr="", returncode=0) return SimpleNamespace(stdout="Updating\n", stderr="", returncode=0)
if cmd == ["/usr/bin/uv", "pip", "install", "-e", ".[all]", "--quiet"]: if cmd == ["/usr/bin/uv", "pip", "install", "-e", ".[all]"]:
raise CalledProcessError(returncode=1, cmd=cmd) raise CalledProcessError(returncode=1, cmd=cmd)
if cmd == ["/usr/bin/uv", "pip", "install", "-e", ".", "--quiet"]: if cmd == ["/usr/bin/uv", "pip", "install", "-e", "."]:
return SimpleNamespace(returncode=0) return SimpleNamespace(returncode=0)
if cmd == ["/usr/bin/uv", "pip", "install", "-e", ".[matrix]", "--quiet"]: if cmd == ["/usr/bin/uv", "pip", "install", "-e", ".[matrix]"]:
raise CalledProcessError(returncode=1, cmd=cmd) raise CalledProcessError(returncode=1, cmd=cmd)
if cmd == ["/usr/bin/uv", "pip", "install", "-e", ".[mcp]", "--quiet"]: if cmd == ["/usr/bin/uv", "pip", "install", "-e", ".[mcp]"]:
return SimpleNamespace(returncode=0) return SimpleNamespace(returncode=0)
# Catch-all must include stdout/stderr so consumers that parse # Catch-all must include stdout/stderr so consumers that parse
# output (e.g. the dashboard-restart `ps -A` scan added in the # output (e.g. the dashboard-restart `ps -A` scan added in the
@ -344,10 +344,10 @@ def test_cmd_update_retries_optional_extras_individually_when_all_fails(monkeypa
install_cmds = [c for c in recorded if "pip" in c and "install" in c] install_cmds = [c for c in recorded if "pip" in c and "install" in c]
assert install_cmds == [ assert install_cmds == [
["/usr/bin/uv", "pip", "install", "-e", ".[all]", "--quiet"], ["/usr/bin/uv", "pip", "install", "-e", ".[all]"],
["/usr/bin/uv", "pip", "install", "-e", ".", "--quiet"], ["/usr/bin/uv", "pip", "install", "-e", "."],
["/usr/bin/uv", "pip", "install", "-e", ".[matrix]", "--quiet"], ["/usr/bin/uv", "pip", "install", "-e", ".[matrix]"],
["/usr/bin/uv", "pip", "install", "-e", ".[mcp]", "--quiet"], ["/usr/bin/uv", "pip", "install", "-e", ".[mcp]"],
] ]
out = capsys.readouterr().out out = capsys.readouterr().out
@ -371,7 +371,7 @@ def test_cmd_update_succeeds_with_extras(monkeypatch, tmp_path):
return SimpleNamespace(stdout="main\n", stderr="", returncode=0) return SimpleNamespace(stdout="main\n", stderr="", returncode=0)
if cmd == ["git", "rev-list", "HEAD..origin/main", "--count"]: if cmd == ["git", "rev-list", "HEAD..origin/main", "--count"]:
return SimpleNamespace(stdout="1\n", stderr="", returncode=0) return SimpleNamespace(stdout="1\n", stderr="", returncode=0)
if cmd == ["git", "pull", "origin", "main"]: if cmd == ["git", "pull", "--ff-only", "origin", "main"]:
return SimpleNamespace(stdout="Updating\n", stderr="", returncode=0) return SimpleNamespace(stdout="Updating\n", stderr="", returncode=0)
return SimpleNamespace(returncode=0, stdout="", stderr="") return SimpleNamespace(returncode=0, stdout="", stderr="")
@ -384,6 +384,24 @@ def test_cmd_update_succeeds_with_extras(monkeypatch, tmp_path):
assert ".[all]" in install_cmds[0] assert ".[all]" in install_cmds[0]
def test_install_heartbeat_prints_when_dependency_install_is_silent(monkeypatch, capsys):
"""Long quiet installs should emit periodic heartbeat lines."""
def fake_run(cmd, **kwargs):
hermes_main._time.sleep(1.2)
return SimpleNamespace(returncode=0)
monkeypatch.setattr(hermes_main.subprocess, "run", fake_run)
hermes_main._run_install_with_heartbeat(
["uv", "pip", "install", "-e", "."],
heartbeat_interval_seconds=1,
)
out = capsys.readouterr().out
assert "still installing dependencies" in out
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# ff-only fallback to reset --hard on diverged history # ff-only fallback to reset --hard on diverged history
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------