157 lines
5.4 KiB
Python
157 lines
5.4 KiB
Python
# modules/toolbox_container.py - 专用工具容器管理器
|
|
|
|
import asyncio
|
|
import time
|
|
import uuid
|
|
import shlex
|
|
from pathlib import Path
|
|
from typing import Optional, Dict
|
|
|
|
from modules.persistent_terminal import PersistentTerminal
|
|
from config import (
|
|
TERMINAL_SANDBOX_MODE,
|
|
TERMINAL_SANDBOX_IMAGE,
|
|
TERMINAL_SANDBOX_MOUNT_PATH,
|
|
TERMINAL_SANDBOX_SHELL,
|
|
TERMINAL_SANDBOX_NETWORK,
|
|
TERMINAL_SANDBOX_CPUS,
|
|
TERMINAL_SANDBOX_MEMORY,
|
|
TERMINAL_SANDBOX_BINDS,
|
|
TERMINAL_SANDBOX_BIN,
|
|
TERMINAL_SANDBOX_NAME_PREFIX,
|
|
TERMINAL_SANDBOX_ENV,
|
|
TERMINAL_SANDBOX_REQUIRE,
|
|
TOOLBOX_TERMINAL_IDLE_SECONDS,
|
|
)
|
|
|
|
|
|
def _build_sandbox_options() -> Dict:
|
|
"""构造与终端一致的沙箱配置."""
|
|
name_prefix = TERMINAL_SANDBOX_NAME_PREFIX
|
|
if name_prefix:
|
|
name_prefix = f"{name_prefix}-toolbox"
|
|
else:
|
|
name_prefix = "toolbox-term"
|
|
return {
|
|
"image": TERMINAL_SANDBOX_IMAGE,
|
|
"mount_path": TERMINAL_SANDBOX_MOUNT_PATH,
|
|
"shell": TERMINAL_SANDBOX_SHELL,
|
|
"network": TERMINAL_SANDBOX_NETWORK,
|
|
"cpus": TERMINAL_SANDBOX_CPUS,
|
|
"memory": TERMINAL_SANDBOX_MEMORY,
|
|
"binds": list(TERMINAL_SANDBOX_BINDS),
|
|
"bin": TERMINAL_SANDBOX_BIN,
|
|
"name_prefix": name_prefix,
|
|
"env": dict(TERMINAL_SANDBOX_ENV),
|
|
"require": TERMINAL_SANDBOX_REQUIRE,
|
|
}
|
|
|
|
|
|
class ToolboxContainer:
|
|
"""为 run_command/run_python 提供的专用容器."""
|
|
|
|
def __init__(
|
|
self,
|
|
project_path: str,
|
|
sandbox_mode: Optional[str] = None,
|
|
sandbox_options: Optional[Dict] = None,
|
|
idle_timeout: int = TOOLBOX_TERMINAL_IDLE_SECONDS,
|
|
):
|
|
self.project_path = Path(project_path).resolve()
|
|
self.sandbox_mode = (sandbox_mode or TERMINAL_SANDBOX_MODE or "host").lower()
|
|
options = _build_sandbox_options()
|
|
if sandbox_options:
|
|
for key, value in sandbox_options.items():
|
|
if key == "binds" and isinstance(value, list):
|
|
options[key] = list(value)
|
|
elif key == "env" and isinstance(value, dict):
|
|
options[key] = dict(value)
|
|
else:
|
|
options[key] = value
|
|
self.sandbox_options = options
|
|
self.idle_timeout = max(0, int(idle_timeout)) if idle_timeout is not None else 0
|
|
|
|
self._terminal: Optional[PersistentTerminal] = None
|
|
self._lock = asyncio.Lock()
|
|
self._session_name = f"toolbox-{uuid.uuid4().hex[:10]}"
|
|
self._last_used = 0.0
|
|
|
|
async def _ensure_terminal(self) -> PersistentTerminal:
|
|
"""确保容器已启动。"""
|
|
async with self._lock:
|
|
if self._terminal and self._terminal.is_running:
|
|
return self._terminal
|
|
|
|
terminal = PersistentTerminal(
|
|
session_name=self._session_name,
|
|
working_dir=str(self.project_path),
|
|
broadcast_callback=None,
|
|
project_path=str(self.project_path),
|
|
sandbox_mode=self.sandbox_mode,
|
|
sandbox_options=self.sandbox_options,
|
|
)
|
|
|
|
if not terminal.start():
|
|
raise RuntimeError("工具容器启动失败,请检查 Docker 或本地 shell 环境。")
|
|
|
|
self._terminal = terminal
|
|
self._last_used = time.time()
|
|
print(f"[Toolbox] 工具容器已启动 (session={self._session_name}, mode={terminal.execution_mode})")
|
|
return terminal
|
|
|
|
def _wrap_command(self, command: str, work_path: Path, execution_mode: Optional[str]) -> str:
|
|
"""根据执行模式动态拼接 cd 指令。"""
|
|
if work_path == self.project_path:
|
|
return command
|
|
|
|
try:
|
|
relative = work_path.relative_to(self.project_path).as_posix()
|
|
except ValueError:
|
|
relative = ""
|
|
|
|
if not relative:
|
|
return command
|
|
|
|
if execution_mode == "docker":
|
|
mount_path = self.sandbox_options.get("mount_path", "/workspace").rstrip("/")
|
|
target = f"{mount_path}/{relative}"
|
|
else:
|
|
target = str(work_path)
|
|
|
|
return f"cd {shlex.quote(target)} && {command}"
|
|
|
|
async def run(self, command: str, work_path: Path, timeout: Optional[float] = None) -> Dict:
|
|
"""在容器/主机终端中执行命令。"""
|
|
terminal = await self._ensure_terminal()
|
|
shell_command = self._wrap_command(command, work_path, terminal.execution_mode)
|
|
result = await asyncio.to_thread(
|
|
terminal.send_command,
|
|
shell_command,
|
|
True,
|
|
timeout,
|
|
)
|
|
self._last_used = time.time()
|
|
self._cleanup_if_idle()
|
|
return result
|
|
|
|
def _cleanup_if_idle(self):
|
|
if self.idle_timeout <= 0 or not self._terminal:
|
|
return
|
|
if time.time() - self._last_used >= self.idle_timeout:
|
|
self._terminal.close()
|
|
print(f"[Toolbox] 工具容器空闲超时,已释放 (session={self._session_name})")
|
|
self._terminal = None
|
|
|
|
def shutdown(self):
|
|
"""立即关闭容器。"""
|
|
if self._terminal:
|
|
self._terminal.close()
|
|
print(f"[Toolbox] 工具容器已关闭 (session={self._session_name})")
|
|
self._terminal = None
|
|
|
|
@property
|
|
def execution_mode(self) -> Optional[str]:
|
|
if self._terminal:
|
|
return self._terminal.execution_mode
|
|
return None
|