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:
shisan 2026-06-13 21:17:57 +08:00
parent f9d8167a00
commit 528c33b7ef
3 changed files with 61 additions and 2 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

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