Merge pull request #1705 from TriTechAI/fix/git-pager-cpu-spin

Disable pagers in non-interactive code execution shells
This commit is contained in:
Alessandro Frau 2026-06-24 11:14:27 +02:00 committed by GitHub
commit e2a23aafe6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 70 additions and 4 deletions

View file

@ -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)

View file

@ -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())

View file

@ -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 ────────────────────────────

View 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