fix(web): bound dashboard action log tails

(cherry picked from commit a4188a3e24)
This commit is contained in:
luyifan 2026-07-05 01:04:31 +08:00 committed by kshitij
parent 7d0ddbb2ff
commit 492be28e50
2 changed files with 73 additions and 7 deletions

View file

@ -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]: