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
import logging
import threading
import time as _time
from datetime import datetime
@ -6445,6 +6446,45 @@ def _load_installable_optional_extras() -> list[str]:
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(
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
nothing on fast hardware and prevents the "hermes update hangs" reports
on slow hardware.
We also add periodic heartbeat lines in case the resolver/build backend is
itself silent for long stretches.
"""
try:
subprocess.run(
_run_install_with_heartbeat(
install_cmd_prefix + ["install", "-e", ".[all]"],
cwd=PROJECT_ROOT,
check=True,
env=env,
)
return
@ -6475,10 +6516,8 @@ def _install_python_dependencies_with_optional_fallback(
" ⚠ Optional extras failed, reinstalling base dependencies and retrying extras individually..."
)
subprocess.run(
_run_install_with_heartbeat(
install_cmd_prefix + ["install", "-e", "."],
cwd=PROJECT_ROOT,
check=True,
env=env,
)
@ -6486,10 +6525,8 @@ def _install_python_dependencies_with_optional_fallback(
installed_extras: list[str] = []
for extra in _load_installable_optional_extras():
try:
subprocess.run(
_run_install_with_heartbeat(
install_cmd_prefix + ["install", "-e", f".[{extra}]"],
cwd=PROJECT_ROOT,
check=True,
env=env,
)
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)
if cmd == ["git", "rev-list", "HEAD..origin/main", "--count"]:
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)
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)
if cmd == ["/usr/bin/uv", "pip", "install", "-e", ".", "--quiet"]:
if cmd == ["/usr/bin/uv", "pip", "install", "-e", "."]:
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)
if cmd == ["/usr/bin/uv", "pip", "install", "-e", ".[mcp]", "--quiet"]:
if cmd == ["/usr/bin/uv", "pip", "install", "-e", ".[mcp]"]:
return SimpleNamespace(returncode=0)
# Catch-all must include stdout/stderr so consumers that parse
# 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]
assert install_cmds == [
["/usr/bin/uv", "pip", "install", "-e", ".[all]", "--quiet"],
["/usr/bin/uv", "pip", "install", "-e", ".", "--quiet"],
["/usr/bin/uv", "pip", "install", "-e", ".[matrix]", "--quiet"],
["/usr/bin/uv", "pip", "install", "-e", ".[mcp]", "--quiet"],
["/usr/bin/uv", "pip", "install", "-e", ".[all]"],
["/usr/bin/uv", "pip", "install", "-e", "."],
["/usr/bin/uv", "pip", "install", "-e", ".[matrix]"],
["/usr/bin/uv", "pip", "install", "-e", ".[mcp]"],
]
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)
if cmd == ["git", "rev-list", "HEAD..origin/main", "--count"]:
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(returncode=0, stdout="", stderr="")
@ -384,6 +384,24 @@ def test_cmd_update_succeeds_with_extras(monkeypatch, tmp_path):
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
# ---------------------------------------------------------------------------