fix: fallback timeout wrapper for terminal commands
This commit is contained in:
parent
88dc7e02a4
commit
aacdfc78eb
@ -3,6 +3,7 @@
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import shlex
|
import shlex
|
||||||
|
import shutil
|
||||||
from typing import Dict, List, Optional, Callable, TYPE_CHECKING
|
from typing import Dict, List, Optional, Callable, TYPE_CHECKING
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -122,6 +123,8 @@ class TerminalManager:
|
|||||||
if sandbox_options and sandbox_options.get("container_name"):
|
if sandbox_options and sandbox_options.get("container_name"):
|
||||||
self.sandbox_mode = "docker"
|
self.sandbox_mode = "docker"
|
||||||
self._apply_container_session(container_session)
|
self._apply_container_session(container_session)
|
||||||
|
# 终端命令超时工具(兼容 macOS 无 coreutils 的情况)
|
||||||
|
self._timeout_bin = shutil.which("timeout") or shutil.which("gtimeout")
|
||||||
|
|
||||||
# 终端会话字典
|
# 终端会话字典
|
||||||
self.terminals: Dict[str, PersistentTerminal] = {}
|
self.terminals: Dict[str, PersistentTerminal] = {}
|
||||||
@ -532,11 +535,7 @@ class TerminalManager:
|
|||||||
base_timeout = timeout
|
base_timeout = timeout
|
||||||
marker = f"__CMD_DONE__{int(time.time()*1000)}__"
|
marker = f"__CMD_DONE__{int(time.time()*1000)}__"
|
||||||
|
|
||||||
# 1) timeout -k 2 : 超时后先发 SIGTERM,再 2 秒后 SIGKILL,只结束命令不关终端
|
wrapped_command, wait_timeout = self._build_wrapped_command(command, marker, timeout)
|
||||||
# 2) sh -c 'cmd; echo marker' 用标记区分正常结束;在终端侧过滤掉回显中的误判
|
|
||||||
wrapped_inner = f"{command} ; echo {marker}"
|
|
||||||
wrapped_command = f"timeout -k 2 {int(timeout)}s sh -c {shlex.quote(wrapped_inner)}"
|
|
||||||
wait_timeout = timeout + 3 # 留出终止与收尾输出
|
|
||||||
|
|
||||||
result = terminal.send_command(
|
result = terminal.send_command(
|
||||||
wrapped_command,
|
wrapped_command,
|
||||||
@ -548,6 +547,33 @@ class TerminalManager:
|
|||||||
result["timeout"] = base_timeout
|
result["timeout"] = base_timeout
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def _build_wrapped_command(self, command: str, marker: str, timeout: int) -> (str, int):
|
||||||
|
"""
|
||||||
|
构造带超时与完成标记的包装命令。
|
||||||
|
|
||||||
|
- 优先使用 coreutils timeout / gtimeout
|
||||||
|
- 若不可用(如 macOS 默认环境),退化为 sleep+kill 方案
|
||||||
|
"""
|
||||||
|
safe_timeout = max(1, int(timeout))
|
||||||
|
wait_timeout = safe_timeout + 3
|
||||||
|
|
||||||
|
if self._timeout_bin:
|
||||||
|
wrapped_inner = f"{command} ; echo {marker}"
|
||||||
|
wrapped_command = f"{self._timeout_bin} -k 2 {safe_timeout}s sh -c {shlex.quote(wrapped_inner)}"
|
||||||
|
return wrapped_command, wait_timeout
|
||||||
|
|
||||||
|
# fallback: 使用 sh + sleep/kill 实现超时(适用于缺少 timeout 的环境)
|
||||||
|
fallback_inner = (
|
||||||
|
f"{command} & CMD_PID=$!; "
|
||||||
|
f"(sleep {safe_timeout} && kill -s INT $CMD_PID >/dev/null 2>&1 && sleep 2 && kill -s KILL $CMD_PID >/dev/null 2>&1) & "
|
||||||
|
f"WAITER=$!; "
|
||||||
|
f"wait $CMD_PID; CMD_STATUS=$?; kill $WAITER >/dev/null 2>&1; "
|
||||||
|
f"echo {marker}; exit $CMD_STATUS"
|
||||||
|
)
|
||||||
|
wrapped_command = f"sh -c {shlex.quote(fallback_inner)}"
|
||||||
|
# 额外留 1s 收尾
|
||||||
|
return wrapped_command, safe_timeout + 4
|
||||||
|
|
||||||
def get_terminal_output(
|
def get_terminal_output(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@ -40,9 +40,11 @@ class TerminalOperator:
|
|||||||
def __init__(self, project_path: str, container_session: Optional["ContainerHandle"] = None):
|
def __init__(self, project_path: str, container_session: Optional["ContainerHandle"] = None):
|
||||||
self.project_path = Path(project_path).resolve()
|
self.project_path = Path(project_path).resolve()
|
||||||
self.process = None
|
self.process = None
|
||||||
# 自动检测Python命令,并记录虚拟环境变量(若存在)
|
# 自动检测Python命令,并记录虚拟环境变量(仅宿主机使用)
|
||||||
self._python_env: Dict[str, str] = {}
|
self._python_env: Dict[str, str] = {}
|
||||||
self.python_cmd = self._detect_python_runtime()
|
self.python_cmd = self._detect_python_runtime()
|
||||||
|
# Docker 容器内的 Python 命令(镜像内已将 /opt/agent-venv/bin 置于 PATH)
|
||||||
|
self.container_python_cmd = os.environ.get("CONTAINER_PYTHON_CMD", "python3")
|
||||||
print(f"{OUTPUT_FORMATS['info']} 检测到Python命令: {self.python_cmd}")
|
print(f"{OUTPUT_FORMATS['info']} 检测到Python命令: {self.python_cmd}")
|
||||||
self._toolbox: Optional[ToolboxContainer] = None
|
self._toolbox: Optional[ToolboxContainer] = None
|
||||||
self.container_session: Optional["ContainerHandle"] = container_session
|
self.container_session: Optional["ContainerHandle"] = container_session
|
||||||
@ -220,9 +222,11 @@ class TerminalOperator:
|
|||||||
}
|
}
|
||||||
# 每次执行前重置工具容器(保持隔离),但下面改用一次性子进程执行,仍保留重置以兼容后续逻辑
|
# 每次执行前重置工具容器(保持隔离),但下面改用一次性子进程执行,仍保留重置以兼容后续逻辑
|
||||||
self._reset_toolbox()
|
self._reset_toolbox()
|
||||||
|
execution_in_container = bool(self.container_session and getattr(self.container_session, "mode", None) == "docker")
|
||||||
|
python_rewrite = self.container_python_cmd if execution_in_container else self.python_cmd
|
||||||
# 统一替换 python/python3,为保障虚拟环境命中只替换独立单词
|
# 统一替换 python/python3,为保障虚拟环境命中只替换独立单词
|
||||||
if re.search(r"\bpython3?\b", command):
|
if re.search(r"\bpython3?\b", command):
|
||||||
command = re.sub(r"\bpython3?\b", self.python_cmd, command)
|
command = re.sub(r"\bpython3?\b", python_rewrite, command)
|
||||||
|
|
||||||
# 验证命令
|
# 验证命令
|
||||||
valid, error = self._validate_command(command)
|
valid, error = self._validate_command(command)
|
||||||
@ -507,9 +511,9 @@ class TerminalOperator:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
relative_temp = temp_file.as_posix()
|
relative_temp = temp_file.as_posix()
|
||||||
|
|
||||||
# 使用检测到的Python命令执行文件(相对路径可兼容容器挂载路径)
|
# 使用通用 python3 占位,由 run_command 根据执行环境重写
|
||||||
result = await self.run_command(
|
result = await self.run_command(
|
||||||
f'{self.python_cmd} -u "{relative_temp}"',
|
f'python3 -u "{relative_temp}"',
|
||||||
timeout=timeout
|
timeout=timeout
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -579,8 +583,8 @@ class TerminalOperator:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
relative_path = full_path.as_posix()
|
relative_path = full_path.as_posix()
|
||||||
|
|
||||||
# 使用检测到的Python命令构建命令
|
# 使用通用 python3 占位,由 run_command 根据执行环境重写
|
||||||
command = f'{self.python_cmd} "{relative_path}"'
|
command = f'python3 "{relative_path}"'
|
||||||
if args:
|
if args:
|
||||||
command += f" {args}"
|
command += f" {args}"
|
||||||
|
|
||||||
@ -599,8 +603,8 @@ class TerminalOperator:
|
|||||||
"""
|
"""
|
||||||
print(f"{OUTPUT_FORMATS['terminal']} 安装包: {package}")
|
print(f"{OUTPUT_FORMATS['terminal']} 安装包: {package}")
|
||||||
|
|
||||||
# 使用检测到的Python命令安装
|
# 使用通用 python3 占位,由 run_command 根据执行环境重写
|
||||||
command = f'{self.python_cmd} -m pip install {package}'
|
command = f'python3 -m pip install {package}'
|
||||||
|
|
||||||
result = await self.run_command(command, timeout=120)
|
result = await self.run_command(command, timeout=120)
|
||||||
|
|
||||||
@ -625,7 +629,7 @@ class TerminalOperator:
|
|||||||
|
|
||||||
# 获取Python版本
|
# 获取Python版本
|
||||||
version_result = await self.run_command(
|
version_result = await self.run_command(
|
||||||
f'{self.python_cmd} --version',
|
'python3 --version',
|
||||||
timeout=5
|
timeout=5
|
||||||
)
|
)
|
||||||
if version_result["success"]:
|
if version_result["success"]:
|
||||||
@ -633,7 +637,7 @@ class TerminalOperator:
|
|||||||
|
|
||||||
# 获取pip版本
|
# 获取pip版本
|
||||||
pip_result = await self.run_command(
|
pip_result = await self.run_command(
|
||||||
f'{self.python_cmd} -m pip --version',
|
'python3 -m pip --version',
|
||||||
timeout=5
|
timeout=5
|
||||||
)
|
)
|
||||||
if pip_result["success"]:
|
if pip_result["success"]:
|
||||||
@ -641,7 +645,7 @@ class TerminalOperator:
|
|||||||
|
|
||||||
# 获取已安装的包
|
# 获取已安装的包
|
||||||
packages_result = await self.run_command(
|
packages_result = await self.run_command(
|
||||||
f'{self.python_cmd} -m pip list --format=json',
|
'python3 -m pip list --format=json',
|
||||||
timeout=10
|
timeout=10
|
||||||
)
|
)
|
||||||
if packages_result["success"]:
|
if packages_result["success"]:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user