fix: align toolbox python env for run_command
This commit is contained in:
parent
e256182304
commit
026588bba3
@ -7,6 +7,7 @@ import subprocess
|
||||
import shutil
|
||||
import time
|
||||
import signal
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Tuple, TYPE_CHECKING
|
||||
try:
|
||||
@ -39,8 +40,9 @@ class TerminalOperator:
|
||||
def __init__(self, project_path: str, container_session: Optional["ContainerHandle"] = None):
|
||||
self.project_path = Path(project_path).resolve()
|
||||
self.process = None
|
||||
# 自动检测Python命令
|
||||
self.python_cmd = self._detect_python_command()
|
||||
# 自动检测Python命令,并记录虚拟环境变量(若存在)
|
||||
self._python_env: Dict[str, str] = {}
|
||||
self.python_cmd = self._detect_python_runtime()
|
||||
print(f"{OUTPUT_FORMATS['info']} 检测到Python命令: {self.python_cmd}")
|
||||
self._toolbox: Optional[ToolboxContainer] = None
|
||||
self.container_session: Optional["ContainerHandle"] = container_session
|
||||
@ -54,28 +56,64 @@ class TerminalOperator:
|
||||
pass
|
||||
self._toolbox = None
|
||||
|
||||
def _detect_python_command(self) -> str:
|
||||
def _detect_python_runtime(self) -> str:
|
||||
"""
|
||||
自动检测可用的Python命令
|
||||
|
||||
Returns:
|
||||
可用的Python命令(python、python3、py)
|
||||
自动检测可用的 Python 命令,优先使用预装虚拟环境。
|
||||
|
||||
1) 优先查找 AGENT_TOOLBOX_VENV /opt/agent-venv / 当前进程 VIRTUAL_ENV。
|
||||
2) 找不到预装虚拟环境时,回退到系统可执行文件。
|
||||
"""
|
||||
# 按优先级尝试不同的Python命令
|
||||
preferred = self._detect_preinstalled_python()
|
||||
if preferred:
|
||||
# 记录虚拟环境相关环境变量,供宿主机子进程复用
|
||||
self._python_env = self._build_python_env(preferred)
|
||||
return preferred
|
||||
|
||||
return self._detect_system_python()
|
||||
|
||||
def _detect_preinstalled_python(self) -> Optional[str]:
|
||||
"""尝试定位预装虚拟环境的 python 可执行文件。"""
|
||||
candidates = []
|
||||
|
||||
# 环境变量优先(Dockerfile 已设置 AGENT_TOOLBOX_VENV=/opt/agent-venv)
|
||||
env_venv = os.environ.get("AGENT_TOOLBOX_VENV")
|
||||
if env_venv:
|
||||
candidates.append(env_venv)
|
||||
|
||||
# 默认预装路径
|
||||
candidates.append("/opt/agent-venv")
|
||||
|
||||
# 当前进程若已激活虚拟环境,也纳入候选
|
||||
current_venv = os.environ.get("VIRTUAL_ENV")
|
||||
if current_venv:
|
||||
candidates.append(current_venv)
|
||||
|
||||
# 去重并依次检查
|
||||
seen = set()
|
||||
for raw in candidates:
|
||||
if not raw or raw in seen:
|
||||
continue
|
||||
seen.add(raw)
|
||||
root = Path(raw).expanduser()
|
||||
bin_dir = root / ("Scripts" if sys.platform == "win32" else "bin")
|
||||
for name in ("python3", "python"):
|
||||
candidate = bin_dir / name
|
||||
if candidate.exists() and os.access(candidate, os.X_OK):
|
||||
return str(candidate.resolve())
|
||||
return None
|
||||
|
||||
def _detect_system_python(self) -> str:
|
||||
"""在系统 PATH 中探测可用的 Python3 可执行文件。"""
|
||||
commands_to_try = []
|
||||
|
||||
|
||||
if sys.platform == "win32":
|
||||
# Windows优先顺序
|
||||
commands_to_try = ["python", "py", "python3"]
|
||||
else:
|
||||
# Unix-like系统优先顺序
|
||||
commands_to_try = ["python3", "python"]
|
||||
|
||||
# 检测哪个命令可用
|
||||
|
||||
for cmd in commands_to_try:
|
||||
if shutil.which(cmd):
|
||||
try:
|
||||
# 验证是否真的可以运行
|
||||
result = subprocess.run(
|
||||
[cmd, "--version"],
|
||||
capture_output=True,
|
||||
@ -83,15 +121,32 @@ class TerminalOperator:
|
||||
timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
# 检查版本是否为Python 3
|
||||
output = result.stdout + result.stderr
|
||||
if "Python 3" in output or "Python 2" not in output:
|
||||
return cmd
|
||||
except:
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# 如果都没找到,根据平台返回默认值
|
||||
|
||||
return "python" if sys.platform == "win32" else "python3"
|
||||
|
||||
def _build_python_env(self, python_path: str) -> Dict[str, str]:
|
||||
"""构造与预装虚拟环境匹配的环境变量(仅作用于宿主机子进程)。"""
|
||||
env: Dict[str, str] = {}
|
||||
try:
|
||||
py_path = Path(python_path).resolve()
|
||||
bin_dir = py_path.parent
|
||||
venv_dir = bin_dir.parent
|
||||
env["VIRTUAL_ENV"] = str(venv_dir)
|
||||
current_path = os.environ.get("PATH", "")
|
||||
# 避免重复添加
|
||||
prefix = str(bin_dir)
|
||||
if current_path.startswith(prefix + os.pathsep) or current_path == prefix:
|
||||
env["PATH"] = current_path
|
||||
else:
|
||||
env["PATH"] = f"{prefix}{os.pathsep}{current_path}" if current_path else prefix
|
||||
except Exception:
|
||||
pass
|
||||
return env
|
||||
|
||||
def _get_toolbox(self) -> ToolboxContainer:
|
||||
if self._toolbox is None:
|
||||
@ -165,12 +220,9 @@ class TerminalOperator:
|
||||
}
|
||||
# 每次执行前重置工具容器(保持隔离),但下面改用一次性子进程执行,仍保留重置以兼容后续逻辑
|
||||
self._reset_toolbox()
|
||||
# 替换命令中的python3为实际可用的命令
|
||||
if "python3" in command and self.python_cmd != "python3":
|
||||
command = command.replace("python3", self.python_cmd)
|
||||
elif "python" in command and "python3" not in command and self.python_cmd == "python3":
|
||||
# 如果命令中有python(但不是python3),而系统使用python3
|
||||
command = command.replace("python", self.python_cmd)
|
||||
# 统一替换 python/python3,为保障虚拟环境命中只替换独立单词
|
||||
if re.search(r"\bpython3?\b", command):
|
||||
command = re.sub(r"\bpython3?\b", self.python_cmd, command)
|
||||
|
||||
# 验证命令
|
||||
valid, error = self._validate_command(command)
|
||||
@ -290,6 +342,8 @@ class TerminalOperator:
|
||||
# 统一环境,确保 Python 输出无缓冲
|
||||
env = os.environ.copy()
|
||||
env.setdefault("PYTHONUNBUFFERED", "1")
|
||||
if self._python_env:
|
||||
env.update(self._python_env)
|
||||
|
||||
if use_shell:
|
||||
process = await asyncio.create_subprocess_shell(
|
||||
|
||||
@ -5,6 +5,7 @@ import sys
|
||||
import asyncio
|
||||
import subprocess
|
||||
import shutil
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Tuple
|
||||
try:
|
||||
@ -29,32 +30,49 @@ class TerminalOperator:
|
||||
def __init__(self, project_path: str):
|
||||
self.project_path = Path(project_path).resolve()
|
||||
self.process = None
|
||||
# 自动检测Python命令
|
||||
self.python_cmd = self._detect_python_command()
|
||||
# 自动检测Python命令,并尝试复用预装虚拟环境
|
||||
self._python_env: Dict[str, str] = {}
|
||||
self.python_cmd = self._detect_python_runtime()
|
||||
print(f"{OUTPUT_FORMATS['info']} 检测到Python命令: {self.python_cmd}")
|
||||
|
||||
def _detect_python_command(self) -> str:
|
||||
"""
|
||||
自动检测可用的Python命令
|
||||
|
||||
Returns:
|
||||
可用的Python命令(python、python3、py)
|
||||
"""
|
||||
# 按优先级尝试不同的Python命令
|
||||
commands_to_try = []
|
||||
|
||||
def _detect_python_runtime(self) -> str:
|
||||
"""优先选择预装虚拟环境的 Python,其次回退系统可执行文件。"""
|
||||
preferred = self._detect_preinstalled_python()
|
||||
if preferred:
|
||||
self._python_env = self._build_python_env(preferred)
|
||||
return preferred
|
||||
return self._detect_system_python()
|
||||
|
||||
def _detect_preinstalled_python(self) -> Optional[str]:
|
||||
candidates = []
|
||||
env_venv = os.environ.get("AGENT_TOOLBOX_VENV") or os.environ.get("VIRTUAL_ENV")
|
||||
if env_venv:
|
||||
candidates.append(env_venv)
|
||||
|
||||
candidates.append("/opt/agent-venv")
|
||||
|
||||
seen = set()
|
||||
for raw in candidates:
|
||||
if not raw or raw in seen:
|
||||
continue
|
||||
seen.add(raw)
|
||||
root = Path(raw).expanduser()
|
||||
bin_dir = root / ("Scripts" if sys.platform == "win32" else "bin")
|
||||
for name in ("python3", "python"):
|
||||
cand = bin_dir / name
|
||||
if cand.exists() and os.access(cand, os.X_OK):
|
||||
return str(cand.resolve())
|
||||
return None
|
||||
|
||||
def _detect_system_python(self) -> str:
|
||||
if sys.platform == "win32":
|
||||
# Windows优先顺序
|
||||
commands_to_try = ["python", "py", "python3"]
|
||||
else:
|
||||
# Unix-like系统优先顺序
|
||||
commands_to_try = ["python3", "python"]
|
||||
|
||||
# 检测哪个命令可用
|
||||
|
||||
for cmd in commands_to_try:
|
||||
if shutil.which(cmd):
|
||||
try:
|
||||
# 验证是否真的可以运行
|
||||
result = subprocess.run(
|
||||
[cmd, "--version"],
|
||||
capture_output=True,
|
||||
@ -62,15 +80,29 @@ class TerminalOperator:
|
||||
timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
# 检查版本是否为Python 3
|
||||
output = result.stdout + result.stderr
|
||||
if "Python 3" in output or "Python 2" not in output:
|
||||
return cmd
|
||||
except:
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# 如果都没找到,根据平台返回默认值
|
||||
return "python" if sys.platform == "win32" else "python3"
|
||||
|
||||
def _build_python_env(self, python_path: str) -> Dict[str, str]:
|
||||
env: Dict[str, str] = {}
|
||||
try:
|
||||
py_path = Path(python_path).resolve()
|
||||
bin_dir = py_path.parent
|
||||
venv_dir = bin_dir.parent
|
||||
env["VIRTUAL_ENV"] = str(venv_dir)
|
||||
current_path = os.environ.get("PATH", "")
|
||||
prefix = str(bin_dir)
|
||||
if current_path.startswith(prefix + os.pathsep) or current_path == prefix:
|
||||
env["PATH"] = current_path
|
||||
else:
|
||||
env["PATH"] = f"{prefix}{os.pathsep}{current_path}" if current_path else prefix
|
||||
except Exception:
|
||||
pass
|
||||
return env
|
||||
|
||||
def _validate_command(self, command: str) -> Tuple[bool, str]:
|
||||
"""验证命令安全性"""
|
||||
@ -111,12 +143,9 @@ class TerminalOperator:
|
||||
Returns:
|
||||
执行结果字典
|
||||
"""
|
||||
# 替换命令中的python3为实际可用的命令
|
||||
if "python3" in command and self.python_cmd != "python3":
|
||||
command = command.replace("python3", self.python_cmd)
|
||||
elif "python" in command and "python3" not in command and self.python_cmd == "python3":
|
||||
# 如果命令中有python(但不是python3),而系统使用python3
|
||||
command = command.replace("python", self.python_cmd)
|
||||
# 替换 python/python3,确保命中预装虚拟环境
|
||||
if re.search(r"\bpython3?\b", command):
|
||||
command = re.sub(r"\bpython3?\b", self.python_cmd, command)
|
||||
|
||||
# 验证命令
|
||||
valid, error = self._validate_command(command)
|
||||
@ -151,12 +180,17 @@ class TerminalOperator:
|
||||
|
||||
try:
|
||||
# 创建进程
|
||||
env = os.environ.copy()
|
||||
if self._python_env:
|
||||
env.update(self._python_env)
|
||||
|
||||
process = await asyncio.create_subprocess_shell(
|
||||
command,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=str(work_path),
|
||||
shell=True
|
||||
shell=True,
|
||||
env=env
|
||||
)
|
||||
|
||||
# 等待执行完成
|
||||
|
||||
Loading…
Reference in New Issue
Block a user