main
py 93 lines 2.97 KB
Raw
1 import os
2 import platform
3 import select
4 import subprocess
5 import time
6 import sys
7 from typing import Optional, Tuple
8 from helpers import runtime
9 from plugins._code_execution.helpers import tty_session
10 from plugins._code_execution.helpers.shell_ssh import clean_string
11
12
13 def disable_pagers_in_env(env: dict | None = None) -> dict:
14 """Return a copy of ``env`` with terminal pagers disabled.
15
16 Commands such as ``git diff``/``git log`` detect a TTY and pipe their output
17 through a pager (``more``/``less``). The non-interactive shells created by
18 the code execution tool never receive any user input, so the pager blocks
19 forever and spins at 100% CPU per process. Pointing the pager variables at
20 ``cat`` lets the output stream through instead. See issue #1697.
21 """
22 env = dict(env if env is not None else os.environ)
23 env["PAGER"] = "cat"
24 env["GIT_PAGER"] = "cat"
25 return env
26
27
28 class LocalInteractiveSession:
29 def __init__(self, cwd: str|None = None):
30 self.session: tty_session.TTYSession|None = None
31 self.full_output = ''
32 self.cwd = cwd
33
34 def __del__(self):
35 try:
36 if self.session:
37 self.session.kill()
38 except Exception:
39 pass
40
41 async def connect(self):
42 self.session = tty_session.TTYSession(
43 runtime.get_terminal_executable(),
44 cwd=self.cwd,
45 env=disable_pagers_in_env(),
46 )
47 await self.session.start()
48 await self.session.read_full_until_idle(idle_timeout=1, total_timeout=1)
49
50 async def close(self):
51 if self.session:
52 session = self.session
53 self.session = None
54 try:
55 await session.close()
56 except Exception:
57 try:
58 session.kill()
59 except Exception:
60 pass
61
62 async def send_command(self, command: str):
63 if not self.session:
64 raise Exception("Shell not connected")
65 self.full_output = ""
66 await self.session.sendline(command)
67
68 def is_terminated(self) -> bool:
69 return self.session is None or self.session.is_terminated()
70
71 def get_exit_code(self) -> int | None:
72 if not self.session:
73 return None
74 return self.session.get_exit_code()
75
76 async def read_output(self, timeout: float = 0, reset_full_output: bool = False) -> Tuple[str, Optional[str]]:
77 if not self.session:
78 raise Exception("Shell not connected")
79
80 if reset_full_output:
81 self.full_output = ""
82
83 # get output from terminal
84 partial_output = await self.session.read_full_until_idle(idle_timeout=0.01, total_timeout=timeout)
85 self.full_output += partial_output
86
87 # clean output
88 partial_output = clean_string(partial_output)
89 clean_full_output = clean_string(self.full_output)
90
91 if not partial_output:
92 return clean_full_output, None
93 return clean_full_output, partial_output