mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-07-09 17:08:29 +00:00
Disable pagers in non-interactive code execution shells
The code execution tool runs commands inside TTY-backed shells (local PTY and remote SSH). Commands like `git diff`/`git log` detect the TTY and pipe output through a pager (more/less). These shells never receive interactive input, so the pager blocks forever and spins at 100% CPU per process — on a 16-core host 5 pager processes pegged 5 cores for 8+ hours (#1697). Disable pagers in both session types: - LocalInteractiveSession: inject PAGER=cat / GIT_PAGER=cat into the TTY env - SSHInteractiveSession: export the same in the initial shell command `cat` streams the output through instead of blocking, and also covers other pager-using tools (man, systemctl, journalctl). Adds regression tests. Fixes #1697
This commit is contained in:
parent
f9d8167a00
commit
528c33b7ef
3 changed files with 61 additions and 2 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import os
|
||||
import platform
|
||||
import select
|
||||
import subprocess
|
||||
|
|
@ -8,6 +9,22 @@ from helpers import runtime
|
|||
from plugins._code_execution.helpers import tty_session
|
||||
from plugins._code_execution.helpers.shell_ssh import clean_string
|
||||
|
||||
|
||||
def disable_pagers_in_env(env: dict | None = None) -> dict:
|
||||
"""Return a copy of ``env`` with terminal pagers disabled.
|
||||
|
||||
Commands such as ``git diff``/``git log`` detect a TTY and pipe their output
|
||||
through a pager (``more``/``less``). The non-interactive shells created by
|
||||
the code execution tool never receive any user input, so the pager blocks
|
||||
forever and spins at 100% CPU per process. Pointing the pager variables at
|
||||
``cat`` lets the output stream through instead. See issue #1697.
|
||||
"""
|
||||
env = dict(env if env is not None else os.environ)
|
||||
env["PAGER"] = "cat"
|
||||
env["GIT_PAGER"] = "cat"
|
||||
return env
|
||||
|
||||
|
||||
class LocalInteractiveSession:
|
||||
def __init__(self, cwd: str|None = None):
|
||||
self.session: tty_session.TTYSession|None = None
|
||||
|
|
@ -15,7 +32,11 @@ class LocalInteractiveSession:
|
|||
self.cwd = cwd
|
||||
|
||||
async def connect(self):
|
||||
self.session = tty_session.TTYSession(runtime.get_terminal_executable(), cwd=self.cwd)
|
||||
self.session = tty_session.TTYSession(
|
||||
runtime.get_terminal_executable(),
|
||||
cwd=self.cwd,
|
||||
env=disable_pagers_in_env(),
|
||||
)
|
||||
await self.session.start()
|
||||
await self.session.read_full_until_idle(idle_timeout=1, total_timeout=1)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ from helpers.print_style import PrintStyle
|
|||
# from helpers.strings import calculate_valid_match_lengths
|
||||
|
||||
|
||||
# Injected into every new SSH shell to keep it safe for non-interactive use.
|
||||
# Pagers (more/less) would otherwise block forever waiting for input that
|
||||
# never arrives and spin at 100% CPU; see issue #1697.
|
||||
PAGER_DISABLE_COMMAND = "export GIT_PAGER=cat; export PAGER=cat"
|
||||
|
||||
|
||||
class SSHInteractiveSession:
|
||||
|
||||
# end_comment = "# @@==>> SSHInteractiveSession End-of-Command <<==@@"
|
||||
|
|
@ -63,7 +69,7 @@ class SSHInteractiveSession:
|
|||
self.shell = self.client.invoke_shell(width=100, height=50)
|
||||
|
||||
# disable systemd/OSC prompt metadata and disable local echo
|
||||
initial_command = "unset PROMPT_COMMAND PS0; stty -echo"
|
||||
initial_command = f"unset PROMPT_COMMAND PS0; stty -echo; {PAGER_DISABLE_COMMAND}"
|
||||
if self.cwd:
|
||||
initial_command = f"cd {self.cwd}; {initial_command}"
|
||||
self.shell.send(f"{initial_command}\n".encode())
|
||||
|
|
|
|||
32
tests/test_code_execution_pager.py
Normal file
32
tests/test_code_execution_pager.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
"""Regression tests for issue #1697.
|
||||
|
||||
Pagers (more/less) must be disabled in the non-interactive shells created by the
|
||||
code execution tool: without user input they block forever and spin at 100% CPU.
|
||||
"""
|
||||
|
||||
from plugins._code_execution.helpers import shell_local, shell_ssh
|
||||
|
||||
|
||||
def test_local_env_disables_pagers_and_preserves_existing():
|
||||
env = shell_local.disable_pagers_in_env({"PATH": "/usr/bin", "PAGER": "less"})
|
||||
assert env["PAGER"] == "cat"
|
||||
assert env["GIT_PAGER"] == "cat"
|
||||
# pre-existing keys are preserved
|
||||
assert env["PATH"] == "/usr/bin"
|
||||
|
||||
|
||||
def test_local_env_defaults_to_environ():
|
||||
env = shell_local.disable_pagers_in_env()
|
||||
assert env["PAGER"] == "cat"
|
||||
assert env["GIT_PAGER"] == "cat"
|
||||
|
||||
|
||||
def test_local_env_does_not_mutate_input():
|
||||
src = {"PATH": "/usr/bin"}
|
||||
shell_local.disable_pagers_in_env(src)
|
||||
assert src == {"PATH": "/usr/bin"}
|
||||
|
||||
|
||||
def test_ssh_command_disables_pagers():
|
||||
assert "GIT_PAGER=cat" in shell_ssh.PAGER_DISABLE_COMMAND
|
||||
assert "PAGER=cat" in shell_ssh.PAGER_DISABLE_COMMAND
|
||||
Loading…
Add table
Add a link
Reference in a new issue