mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-15 14:22:43 +00:00
fix(web): bound dashboard action log tails
(cherry picked from commit a4188a3e24)
This commit is contained in:
parent
7d0ddbb2ff
commit
492be28e50
2 changed files with 73 additions and 7 deletions
|
|
@ -3041,6 +3041,9 @@ async def run_debug_share_endpoint(body: DebugShareRequest | None = None):
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
_ACTION_LOG_DIR: Path = get_hermes_home() / "logs"
|
||||
_ACTION_LOG_TAIL_MAX_BYTES = 256 * 1024
|
||||
_ACTION_LOG_TAIL_INITIAL_CHUNK_BYTES = 8 * 1024
|
||||
_ACTION_LOG_TAIL_MAX_CHUNK_BYTES = 64 * 1024
|
||||
|
||||
# Short ``name`` (from the URL) → absolute log file path.
|
||||
_ACTION_LOG_FILES: Dict[str, str] = {
|
||||
|
|
@ -3142,17 +3145,50 @@ def _spawn_hermes_action(subcommand: List[str], name: str) -> subprocess.Popen:
|
|||
|
||||
|
||||
def _tail_lines(path: Path, n: int) -> List[str]:
|
||||
"""Return the last ``n`` lines of ``path``. Reads the whole file — fine
|
||||
for our small per-action logs. Binary-decoded with ``errors='replace'``
|
||||
so log corruption doesn't 500 the endpoint."""
|
||||
if not path.exists():
|
||||
"""Return the last ``n`` lines of ``path`` without loading huge logs."""
|
||||
if n <= 0 or not path.exists():
|
||||
return []
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
size = path.stat().st_size
|
||||
except OSError:
|
||||
return []
|
||||
lines = text.splitlines()
|
||||
return lines[-n:] if n > 0 else lines
|
||||
if size <= 0:
|
||||
return []
|
||||
|
||||
min_offset = max(0, size - _ACTION_LOG_TAIL_MAX_BYTES)
|
||||
offset = size
|
||||
chunk_size = _ACTION_LOG_TAIL_INITIAL_CHUNK_BYTES
|
||||
newline_count = 0
|
||||
chunks: List[bytes] = []
|
||||
drop_partial_first_line = False
|
||||
|
||||
try:
|
||||
with path.open("rb") as handle:
|
||||
while offset > min_offset and newline_count <= n:
|
||||
read_size = min(chunk_size, offset - min_offset)
|
||||
offset -= read_size
|
||||
handle.seek(offset)
|
||||
chunk = handle.read(read_size)
|
||||
chunks.append(chunk)
|
||||
newline_count += chunk.count(b"\n")
|
||||
chunk_size = min(
|
||||
chunk_size * 2,
|
||||
_ACTION_LOG_TAIL_MAX_CHUNK_BYTES,
|
||||
)
|
||||
if offset > 0:
|
||||
handle.seek(offset - 1)
|
||||
drop_partial_first_line = handle.read(1) != b"\n"
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
lines = (
|
||||
b"".join(reversed(chunks))
|
||||
.decode("utf-8", errors="replace")
|
||||
.splitlines()
|
||||
)
|
||||
if drop_partial_first_line and lines:
|
||||
lines = lines[1:]
|
||||
return lines[-n:]
|
||||
|
||||
|
||||
def _gateway_subcommand(profile: Optional[str], verb: str) -> List[str]:
|
||||
|
|
|
|||
|
|
@ -1620,6 +1620,36 @@ class TestWebServerEndpoints:
|
|||
"pid": 99,
|
||||
}
|
||||
|
||||
def test_action_status_tails_large_log_without_read_text(self, tmp_path, monkeypatch):
|
||||
import hermes_cli.web_server as web_server
|
||||
|
||||
monkeypatch.setattr(web_server, "_ACTION_LOG_DIR", tmp_path)
|
||||
web_server._ACTION_PROCS.pop("hermes-update", None)
|
||||
web_server._ACTION_RESULTS.pop("hermes-update", None)
|
||||
|
||||
log_path = tmp_path / web_server._ACTION_LOG_FILES["hermes-update"]
|
||||
log_path.write_text(
|
||||
"stale-start\n"
|
||||
+ ("x" * (web_server._ACTION_LOG_TAIL_MAX_BYTES + 1024))
|
||||
+ "\ntail-one\ntail-two\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert log_path.stat().st_size > web_server._ACTION_LOG_TAIL_MAX_BYTES
|
||||
|
||||
original_read_text = Path.read_text
|
||||
|
||||
def fail_if_status_reads_whole_log(path, *args, **kwargs):
|
||||
if path == log_path:
|
||||
raise AssertionError("action status must not read the entire log")
|
||||
return original_read_text(path, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(Path, "read_text", fail_if_status_reads_whole_log)
|
||||
|
||||
resp = self.client.get("/api/actions/hermes-update/status?lines=3")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["lines"] == ["tail-one", "tail-two"]
|
||||
|
||||
|
||||
def test_get_status_filters_unconfigured_gateway_platforms(self, monkeypatch):
|
||||
import gateway.config as gateway_config
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue