mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-07-09 17:08:29 +00:00
Merge pull request #1705 from TriTechAI/fix/git-pager-cpu-spin
Disable pagers in non-interactive code execution shells
This commit is contained in:
commit
e2a23aafe6
4 changed files with 70 additions and 4 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())
|
||||
|
|
|
|||
|
|
@ -6,9 +6,16 @@ if _IS_WIN:
|
|||
import msvcrt
|
||||
|
||||
|
||||
def _reconfigure_stream_errors(stream) -> None:
|
||||
reconfigure = getattr(stream, "reconfigure", None)
|
||||
if not callable(reconfigure):
|
||||
return
|
||||
reconfigure(errors="replace")
|
||||
|
||||
|
||||
# Make stdin / stdout tolerant to broken UTF-8 so input() never aborts
|
||||
sys.stdin.reconfigure(errors="replace") # type: ignore
|
||||
sys.stdout.reconfigure(errors="replace") # type: ignore
|
||||
_reconfigure_stream_errors(sys.stdin)
|
||||
_reconfigure_stream_errors(sys.stdout)
|
||||
|
||||
|
||||
# ──────────────────────────── PUBLIC CLASS ────────────────────────────
|
||||
|
|
|
|||
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